@coldtea/pr-lens-schema

中文文档
0.10.0发布于 2026/11/08MITTypeScriptES Module近 30 天下载 29.9万

The PR Lens contract: versioned schemas for architecture / data-flow / data-model graph documents, baseline patches, config and render manifests.

原始使用文档
文档目录12 节
  1. The four documents
  2. Views and the render they imply
  3. Three lenses
  4. The data model
  5. Walkthroughs
  6. Deltas
  7. What validation covers
  8. Evolving a stored graph
  9. Repository config
  10. JSON Schema
  11. Goldens
  12. Versioning

@coldtea/pr-lens-schema

The PR Lens contract. Extraction produces these documents, the renderer consumes them, and every other surface (CLI, GitHub Action, agent skill, hosted app) passes them around. If a document validates here, it is safe to render.

MIT © Coldtea AI.

pnpm add @coldtea/pr-lens-schema
import { parseGraphDoc, safeParseGraphDoc } from "@coldtea/pr-lens-schema";

const graph = parseGraphDoc(json);          // throws PrLensSchemaError
const result = safeParseGraphDoc(json);     // { ok: true, value } | { ok: false, error }

The four documents

DocumentPurposeParser
GraphDocLanes, nodes, edges, flows, stats, the drill-down tree and an optional walkthrough for one pull request, or for a stored baseline mapparseGraphDoc
PatchDocOrdered operations that move a stored graph forward as pull requests mergeparsePatchDoc
ConfigWhat a repository commits as .github/pr-lens.ymlparseConfig
RenderManifestThe SVGs a render produced, and where they liveparseRenderManifest

Every document carries a schemaVersion, config included: a repository's corrections have to keep their meaning as the contract moves.

Views and the render they imply

A manifest also lists the walkthrough embed under walkthrough, one entry per half of the theme pair, when the document carries a tour; it sits beside the view assets rather than among them, so it never counts against the view cap below.

A render is one asset per view per theme, so the size of a drill-down tree and the size of a render manifest are one rule, not two numbers:

MAX_VIEWS * THEMES.length === MAX_RENDER_ASSETS; // 128 * 2 === 256

Each array in the view tree is capped, but its depth is not, so the total is bounded by graphIntegrityIssues: a document carrying more views than a manifest could describe is rejected here rather than at the renderer, which would otherwise be left holding a document it was told was fine. Import MAX_VIEWS, MAX_RENDER_ASSETS and THEMES rather than restating any of them: raise the budget or add a theme and the other end moves with it.

The cap is the worst case, every theme rendered, not what a particular render would emit. A single-theme render could describe twice as many views, but then whether a document is renderable would depend on how it was asked to be rendered, and if it parses, it renders is the promise this package exists to make.

Three lenses

architecture shows blast radius against the existing system. data-flow animates an ordered pipeline. data-model draws the tables a change touches and the keys between them. There is deliberately no security lens and no findings field: PR Lens is the comprehension layer, and a document that carries findings is rejected rather than quietly stripped. The Lens enum is additive: a future contract version may add lenses, so treat one you do not recognise as a view to skip, not as a failure.

The data model

A document whose change touched a schema may carry a dataModel, and then declares the data-model lens.

dataModel: {
  stores: [{ id: "billing", label: "Billing", platform: "Postgres" }],
  tables: [
    {
      id: "subscription",
      label: "Subscription",
      store: "billing",
      name: "subscriptions",
      delta: "modified",
      badges: ["key moved"],
      columns: [
        { name: "id", type: "uuid", key: "primary", delta: "unchanged" },
        { name: "user_id", type: "uuid", key: "foreign", delta: "removed" },
        { name: "org_id", type: "uuid", key: "foreign", delta: "added" },
        { name: "amount_cents", type: "bigint", previousType: "int", delta: "modified" },
      ],
      files: [{ path: "migrations/0042_billing_by_org.sql", startLine: 36, endLine: 48 }],
    },
  ],
  relations: [
    {
      id: "subscription-user",
      from: { table: "subscription", column: "user_id" },
      to: { table: "user", column: "id" },
      delta: "removed",
      label: "billed to the user until now",
    },
  ],
}

A store is a database, a schema or a namespace: the lane a table sits in, with the platform in its header. A table is a table, a collection or a key family, and lists its columns with a delta each, so a dropped column is drawn struck through and an arrived one in green. A column whose type changed carries previousType, so the picture says int → bigint. A type is a name, as the schema spells it: uuid, text[], numeric(5,4), timestamp with time zone. A quote, an equals sign, a slash, a colon or a -- has no place in a type name and every place in a default, a literal, a connection string or a comment, so a type carrying one is refused: the picture shows the shape of the data, never the data. key marks the table's own key, a column that holds another table's key, or an index row. A relation runs from the column that holds a key to the column it points at; enforced: false draws a reference nothing enforces, such as one across stores, as a dotted line.

The deltas are the same four as everywhere else. A relation marked removed is one the change deleted, and its label is where to say what it used to mean.

A view may scope tables, a walkthrough step may focus them, and a cite may name a table or a column: { "kind": "table", "table": "subscription" } or { "kind": "column", "table": "subscription", "column": "org_id" }.

The validator checks that every table sits in a declared store, that no table lists a column twice, that every relation ends on a column its table has, and that a document carrying a data model declares the lens. A stored map never carries one: a data model shows the tables one change touched, and a map describes a system.

Walkthroughs

A document may carry a walkthrough.

walkthrough: {
  steps: [
    {
      id: "four-batch-calls",
      heading: "Postmark now gets 500 emails per call",
      body: "One call per batch, and Postmark answers with a result for each message.",
      stage: { kind: "flow", flow: "send-pipeline" },
      focus: { kind: "selection", messages: ["batch-post", "batch-results"] },
      detail: {
        text: "sendBroadcastBulk posts each batch to Postmark and reads the results back.",
        cites: [
          { text: "sendBroadcastBulk", ref: { kind: "node", node: "send-broadcast-bulk" } },
          { text: "posts each batch", ref: { kind: "message", flow: "send-pipeline", message: "batch-post" } },
          { text: "Postmark", ref: { kind: "node", node: "postmark" } },
        ],
      },
    },
    {
      id: "old-path-goes-dark",
      heading: "processBroadcast and sendSingleEmail removed",
      body: "sendBroadcastBulk does their job for whole batches.",
      stage: { kind: "view", view: "retired-path" },
      focus: { kind: "selection", nodes: ["process-broadcast", "send-single-email"] },
    },
  ],
}

A walkthrough is a short guided tour of the diagrams. It has two to twelve steps. Each step shows one diagram, points at one part of it, and says a few words about it.

Each step has:

  • heading: the thing and what happened to it, up to 48 characters, in sentence case. For example "Postmark now gets 500 emails per call".

  • body: one line under the heading, up to 140 characters, on what the change means for behaviour. For example "One call per batch instead of one call per person". A heading with no body reads as unfinished, so the parser requires one.

  • stage: which diagram to show. A document can have several diagrams: its views (the drill-down diagrams) and its flows (the sequence diagrams). { "kind": "view", "view": "overview" } shows the view called overview. { "kind": "flow", "flow": "send-pipeline" } shows the flow called send-pipeline. Leave stage out and the step uses the diagram the reader is already on.

  • focus: what to zoom in on inside that diagram. { "kind": "all" } means the whole diagram. A selection means "just these things": name any lanes, nodes, edges or flow steps (messages) by id, and the camera zooms to them while everything else dims. A selection must name at least one thing.

  • detail: optional. One or two sentences under the body, up to 240 characters, that name the nodes, flow steps and files the step is about. For example "sendBroadcastBulk posts each batch to Postmark and reads the results back". The body carries no links, so they go here. text is the sentence. cites lists the words in it that name a place, and what each one points to, and those words are drawn as links. A cite's text is copied exactly from the detail's text, and its ref is one of:

    • { "kind": "node", "node": "postmark" } for a node,
    • { "kind": "message", "flow": "send-pipeline", "message": "batch-post" } for a flow step,
    • { "kind": "view", "view": "overview" } or { "kind": "flow", "flow": "send-pipeline" } for a diagram,
    • { "kind": "file", "path": "functions/src/broadcast/send.ts" } for a file in the repository.

    List cites in the order the sentence says them. A detail takes one to eight. Leave detail out of a step that has nothing to link.

The validator checks:

  • Every id you name exists in the document. A flow step you name must belong to the flow the stage shows, because flow step ids are only unique inside their own flow.
  • messages needs a stage that shows a flow. Leave it out when the stage is an architecture view.
  • Every cite's words appear in its detail's text, in the order the cites are listed, and no two cites share words. A cited node, flow step, view or flow exists in the document. A cited file only has to be a path inside the repository, because the document does not hold the files and cannot check that one exists.
  • Step ids are unique within the walkthrough. Two steps minimum, twelve maximum.
  • A stored map never carries a walkthrough. A map describes the system; a walkthrough tells the story of one change.

When a patch or a correction removes something from the document, the walkthrough follows: a step loses the names that are gone, a step with nothing left to point at or whose diagram is gone is dropped, and if fewer than two steps remain the walkthrough is dropped. A cite of something that is gone is dropped as well, and its words stay in the detail as plain text. A detail left with no cites is dropped, and the step keeps its body. pruneWalkthrough does this and is exported for anything else that removes parts of a document.

detailParts splits a detail's text into plain runs and cited runs, in reading order. Each cited run carries its ref.

Deltas

Every node, edge and flow step declares how it relates to the base commit: added, modified, removed or unchanged. unchanged elements are the context a reviewer needs to judge blast radius, so they belong in the document rather than being filtered out.

What validation covers

Parsing runs four things in one pass, and reports every problem it finds rather than only the first:

  1. Structure: types, lengths, enums, no unknown keys, and file paths that can actually become a diff permalink (repository-relative, POSIX, no .. segment).
  2. Contract version: the same major, and a minor from the first the contract shipped (0.1) up to this package's own. A newer minor is refused, since it may carry a field this parser would take for an invented one.
  3. Referential integrity: every node sits in a declared lane, every edge joins declared nodes, every flow step runs between declared participants, every drill-down view and layout hint names elements that exist, every walkthrough step stages a diagram the document has, and focuses and cites elements it has, and a document carrying flows declares the data-flow lens.
  4. That a render could describe the document: see below.

A document nested deeper than the stack can walk is reported as a document too deep to read, not thrown: safeParse* returns a result whatever it is handed.

Failures arrive as a PrLensSchemaError with a machine-readable code (INVALID_DOCUMENT, BROKEN_REFERENCE, DUPLICATE_ID, UNSUPPORTED_SCHEMA_VERSION, PATCH_CONFLICT, NOT_A_SNAPSHOT) and an issues array carrying a path and message each.

Evolving a stored graph

applyPatchDoc(graph, patch) is the executable definition of the patch operations:

import { applyPatchDoc, parseGraphDoc, parsePatchDoc } from "@coldtea/pr-lens-schema";

const result = applyPatchDoc(parseGraphDoc(baseline), parsePatchDoc(patch));
  • A patch names the map it targets and the commits it carries it between: graphId, fromSha and toSha are all required, they are full 40-character commit names, and they must differ. Abbreviations are fine for something a human reads but not for deciding whether two records mean the same commit, and a patch that does not move the map is a patch that can be replayed.
  • The target is checked before anything is applied: a patch aimed at another map, or written against a commit the map has already moved past, is a PATCH_CONFLICT rather than a merge.
  • A stored map is a snapshot rather than a diff: it has an id, it records the single commit it reflects in full on both base and head, and nothing in it is annotated as a change. graphSnapshotIssues is that rule, exported for anything that stores a map, and applyPatchDoc runs it on the way in and on the way out, so a patch can neither leave an annotation behind nor launder a map that already carries one. Failures are NOT_A_SNAPSHOT and name the offender, down to the step of a flow.
  • The target rules are re-checked inside applyPatchDoc rather than trusted from parsing, because a zod refinement does not survive into the inferred type: a PatchDoc a caller assembled itself is held to the same rules as one that came from parsePatchDoc.
  • Operations apply in array order, and the first conflict stops the batch, because a later operation was written against the state an earlier one was supposed to produce.
  • add_* refuses an id that is taken; update_* and remove_* refuse an id that is absent; update_* writes only the fields it names.
  • Removing a node takes its edges and flow steps with it, and drops a flow left with fewer than two participants or no steps. Removed ids are pruned out of the drill-down tree and the layout hints, and a view whose selection loses its last element is dropped rather than widened.
  • A walkthrough is pruned once, against the document the operations produced rather than alongside each one. A step names a diagram as well as elements, so it can name a view the tree prune has just dropped, and an update that rewrites a flow's steps takes focus members away without removing anything the other prunes would notice. A step whose stage is gone, or whose focus loses its last element, goes with it, and a tour left with fewer than two steps goes whole. Focused flow steps are measured against the flow the step's own stage draws, never against the document at large.
  • Removing a lane that still holds nodes is a conflict, not a cascade: move its nodes first.
  • remove_* deletes an element from the stored graph. That is a different statement from delta: "removed", which says an element still exists but is being deleted by the change under review.
  • A successful result is always a document that would pass parseGraphDoc: the candidate is validated as a whole before it is returned.

The input document is never mutated. applyPatch(graph, ops) is the same thing without the target check, for callers that already know which document they hold.

A stored map describes a system rather than a change, so everything in it is unchanged and its base equals its head; the deltas belong to pull-request documents.

Repository config

# .github/pr-lens.yml
schemaVersion: 0.1.0
lenses: [architecture, data-flow, data-model]
branding: true
glossary: docs/CONTEXT.md#Language
github:
  draw: auto
  description: true
  comment:
    collapsed: false
    notice: true
map:
  rename:
    - match: functions/src/broadcast/sendBroadcastBulk.ts
      to: Broadcast sender
  exclude:
    - "**/*.test.ts"
  lane:
    - match: packages/broadcast-lib/**
      lane: functions
  group:
    - match: id:build-bulk-payload
      group: broadcast-lib

A match beginning with id: addresses one node exactly; anything else is a path glob matched against a node's file paths, so a correction survives the model renaming the node between runs. Corrections are an overlay applied over fresh inference on every run. Inference never writes back into this file.

glossary names a markdown file in the repository that explains the team's own terms for the system, so drawings, walkthroughs and the canvas use those words rather than the code's. A heading after # narrows it to one section. The hosted App reads it from the PR's head commit.

The hosted App reads github settings from the PR's head commit. Other options apply to the CLI.

SettingDefaultEffect
github.comment.collapsedfalseStart diagrams and details closed. Drawing still runs automatically.
github.drawautoon-demand leaves a pull request undrawn, with a short notice, until someone comments @pr-lens draw.
github.comment.noticetruefalse drops that notice, so an on-demand repository hears nothing until someone asks.
github.descriptiontrueOnce a pull request is drawn, the walkthrough and data flow buttons go at the end of its description. false leaves descriptions alone.

JSON Schema

json-schema/*.json (draft 2020-12) is generated from the zod schemas and published with the package, for producers that do not run TypeScript and for editors validating .github/pr-lens.yml:

{ "$ref": "node_modules/@coldtea/pr-lens-schema/json-schema/graph-doc.schema.json" }

They describe what an author may write: a field with a default is one you may leave out. Rules are carried across wherever JSON Schema can state them: the supported contract versions, the repository-relative path rule, endLine requiring startLine, an asset needing a url or a path, a selection view having to select something.

Exactly seven rules cannot be stated in JSON Schema and stay the parser's job, each of them a comparison the shape alone cannot make:

  1. referential integrity between elements, which includes a walkthrough cite pointing at words its detail does not say,
  2. a line range that ends before it starts,
  3. the agreement between a self message's endpoints,
  4. a patch whose two commits are the same,
  5. more views than a render manifest could describe,
  6. a walkthrough step focusing flow steps the diagram on its stage does not draw,
  7. sample traffic on a flow step deeper than 8 levels or over 4096 bytes once serialised (and a shape over 2048 bytes: JSON Schema counts characters, not bytes).

The tests run a table of documents through both representations and assert the same verdict, accept and reject alike, including a case per divergence above, so they stay deliberate and cannot quietly grow an eighth.

Goldens

examples/ holds the reference documents, also importable pre-parsed:

import { postmarkRefactorGraph } from "@coldtea/pr-lens-schema/examples";

They tell one story: a real refactor that moved broadcast sending from one Postmark request per recipient to batches of 500:

  • postmark-refactor.graph.json is the canonical document: the pull request itself, across three lanes, exercising all four delta states, a hero edge, a seven-step data flow with returns and a repeated batch step, a nested drill-down tree, and a six-step walkthrough that stages both a view and a flow. Downstream renderer goldens are measured against it.
  • broadcast-baseline.graph.json is the stored map of that subsystem as main stood before the change, and broadcast-baseline.patch.json carries it to the merged state, the transition applyPatchDoc performs.
  • payload.graph.json is the pull-request document with sample traffic on six of its seven flow steps: both sides on the batch call, a before that differs from its sample, a void response, and one step left bare.
  • billing-org.graph.json is the data-model reference: billing moves from the person to the organization, with two tables arriving, a key leaving one table for another, a dropped column, a widened type, and a walkthrough that stages the data-model view.
  • postmark-refactor.render-manifest.json is what rendering the pull-request document produces, pr-lens.config.json a repository's corrections, and minimal.graph.json the smallest document that validates.

Versioning

SCHEMA_VERSION is the contract version, and it moves independently of this package's version. A release that changes no contract, a documentation fix or another patch, ships a new package version and leaves SCHEMA_VERSION where it stands, so a document written against the older string keeps parsing. A minor release only adds optional fields or widens an enum, and a parser accepts every minor from 0.1 up to its own: a document stored against 0.1.1 still opens on a 0.2.0 parser, and a 0.3.0 document is refused by one until the package that reads it ships. A major release may remove or retype a field.

Contract 0.2.0 added payload on a flow step: sample traffic, as JSON values, for a step that moves data.

Contract 0.3.0 added the data-model lens and dataModel on a document: the tables a change touched and the keys between them, with tables in view scopes and step focuses and table and column cites.

Contract 0.3.1 added walkthrough to a render manifest: the walkthrough embed, one entry per half of the theme pair, beside the view assets.


Part of PR Lens. Review what actually matters.

版本与发布记录

@coldtea/pr-lens-schema 共发布 15 个版本,首次发布于 2026/08/23;过去 12 个月发布了 15 个版本。

版本发布日期距今
0.10.0latest2026/11/082 天前
0.9.02026/10/2714 天前
0.8.02026/10/2417 天前
0.7.12026/10/2219 天前
0.7.02026/10/021 个月前
0.6.02026/09/271 个月前

关于 @coldtea/pr-lens-schema 的常见问题

@coldtea/pr-lens-schema 是什么?

The PR Lens contract: versioned schemas for architecture / data-flow / data-model graph documents, baseline patches, config and render manifests.

如何安装 @coldtea/pr-lens-schema?

在项目目录运行 npm install @coldtea/pr-lens-schema,或使用 pnpm add / yarn add / bun add。

@coldtea/pr-lens-schema 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@coldtea/pr-lens-schema 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@coldtea/pr-lens-schema 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.11。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@coldtea/pr-lens-schema 可以商用吗?

@coldtea/pr-lens-schema 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@coldtea/pr-lens-schema 的下载量有多少?

根据 npm 官方下载统计,@coldtea/pr-lens-schema 在 2026/10/11 – 2026/11/09 这 30 天内被下载 299,403 次,日均约 9,980 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@coldtea/pr-lens-schema 还在维护吗?

最新版本 0.10.0 发布于 2026/11/08(2 天前);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/08/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。