PR Lens

Reference

The PR Lens graph document explained

The JSON document PR Lens draws from: lanes, nodes, edges, flows, drill-down views and a walkthrough, with the field limits and the six rules only the parser can check.

One lane, one node. The smallest document that validates draws this.

A PR Lens graph document is one JSON file that describes a code change as a graph: lanes, nodes, edges, ordered flows, a nested drill-down tree, and an optional walkthrough. It carries kind: "graph" and a schemaVersion, currently 0.1.1. A model reads your diff and writes this file. The renderer reads the file and emits an SVG. The document says what changed, and nothing in it says how to draw anything. No coordinates, no colours, no shapes. @coldtea/pr-lens-schema is the parser, and its promise is short. If it parses, it renders.

The CLI writes one to .pr-lens/graph.json. The GitHub App stores one per pull request head commit. With the agent skill, the coding agent you already have open writes one directly. Same file, four producers. If you have not met the product yet, what is PR Lens is the shorter road in, and the PR Lens glossary defines every term used below in a sentence each.

What is inside a graph document

Five fields are required beyond the version and the kind. The rest are optional, and most documents use only some of them.

FieldRequiredWhat it holds
schemaVersionyesContract version, currently 0.1.1
kindyesThe literal "graph"
titleyesUp to 120 characters, the change in a phrase
lensesyesOne to eight of architecture, data-flow
provenanceyesRepo owner and name, base and head shas, the pull request, the generator
lanesyesOne to sixteen bands
nodesyesOne to 256 cards
edgesnoUp to 512 connections
flowsnoUp to sixteen ordered sequences
viewsnoUp to 32 drill-down sections, nesting further
statsnoFiles changed, additions, deletions, and up to eight header chips
walkthroughnoTwo to twelve guided steps
layoutnoDirection, lane order and per-node rank, as hints
summary, id, generatedAtnoProse answer, stored id, timestamp

Every schema in the package is strict. An unknown key is a rejection, not a warning, which is what stops a producer inventing a field and having it silently ignored for a month.

The smallest document that validates

The package ships this as examples/minimal.graph.json, with its empty arrays spelled out. It is the document behind the render at the top of this page.

{
  "schemaVersion": "0.1.1",
  "kind": "graph",
  "title": "Touch the health check",
  "lenses": ["architecture"],
  "provenance": {
    "repo": { "owner": "coldteadotai", "name": "pr-lens", "host": "github.com" },
    "base": { "sha": "1111111" },
    "head": { "sha": "2222222" }
  },
  "lanes": [{ "id": "api", "label": "API" }],
  "nodes": [
    {
      "id": "health-route",
      "label": "GET /health",
      "kind": "route",
      "delta": "modified",
      "lane": "api",
      "files": [{ "path": "src/routes/health.ts" }]
    }
  ]
}

One lane, one node, no edges. The render says a single endpoint changed and stops. That is the floor the whole contract sits on: a document is never allowed to be so small it fails to draw.

Lanes, nodes and edges

A lane is a band of the diagram. It has an id, a label, an optional subtitle for the platform or runtime, an optional order, and an optional delta for the case where the band itself is new or gone. Every node belongs to exactly one lane.

A node has an id, a label, a kind, a delta, and the lane it sits in. kind is coarse on purpose, because it picks the card's icon and shape and never feeds analysis: service, app, module, function, route, job, queue, datastore, cache, external, ui, config, test, package, and other for everything else, which still renders. A node may also carry a subtitle, a summary for its drill-down body, up to 64 files, and up to six badges.

A files entry is a repository-relative POSIX path with an optional startLine and endLine, and that is what becomes the diff permalink a reviewer clicks. The path rule is enforced: no absolute path in any spelling a platform recognises, no drive letter, no backslash, no .. segment. A path that breaks it could not produce a permalink, whatever else it might mean.

An edge joins two nodes with a kind (call, http, rpc, event, queue, data, dependency, render, other), a delta, an optional label, an emphasis of normal, hero or muted, and an animated flag that gives it a travelling pulse. One hero edge per diagram is the connection the change is really about. More than a couple and the emphasis stops meaning anything.

What delta means

Every node, edge and flow step declares one of four values: added, modified, removed, unchanged. The renderer draws added green, modified amber, and removed red, ghosted and struck through. unchanged gets no delta colour, which is how it reads as context.

unchanged is the value people are tempted to filter out, and it is the one that matters most here. The parts a change did not touch are the context a reader needs to judge blast radius, so they are first class in the document rather than filtered out on the way in. A diagram of only the changed nodes tells you nothing about what surrounds them. That argument, at length, is in blast radius diagrams.

Flows, and the data-flow lens

A flow is an ordered sequence: two to twelve participants, each naming a node, and one to 64 messages between them. A message has a kind of sync, async, return or self, a delta, an optional repeat count for a step that happens several times per run, and an animated flag that defaults to true.

Order is the array position. There is no step number, deliberately, because an explicit one would let a producer emit a document whose animation order disagrees with its own message list. A document carrying flows must declare the data-flow lens, and the parser checks it.

Seven messages, one cycle. Each step takes its turn on the arrow.

The steps share one animation cycle and take it in turn, so the next arrow lights as the last dot lands. A repeated step takes consecutive turns.

Views: the drill-down tree

views is the nested set of <details> sections in the pull request comment. Each view has an id, a title, a lens, an optional summary, a defaultOpen flag, up to 32 children, and a scope.

A scope is either { "kind": "all" } or a selection naming lanes, nodes, edges or flows by id. The two are separate states rather than one selection that happens to be empty, so removing the last element a view pointed at can never quietly turn it into a view of everything. A selection must select something.

One nested view of a larger document. Same file, narrower scope.

The tree's total size is capped, and the cap comes from arithmetic rather than taste. A render is one asset per view per theme, the asset budget is 256, there are two themes, so the view ceiling is 128. The package exports MAX_VIEWS, MAX_RENDER_ASSETS and THEMES so nobody restates the numbers. A document carrying more views than a manifest could describe is rejected here rather than at the renderer.

The walkthrough

A document may carry a walkthrough: two to twelve steps, each one stop on a guided tour of its own diagrams.

{
  "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"] }
}

heading is capped at 48 characters and body at 140. Those caps are contract rather than advice: the rail shows one line per step, so a heading long enough to wrap turns a tour into a wall of text, and no producer can pad its way past it. A body is required, because a heading with nothing under it reads as a step someone started and never finished.

stage picks the diagram, either one of the document's views or one of its flows. Leave it out and the step uses whatever the reader is already looking at. focus picks what stays lit inside it while everything else dims, naming lanes, nodes, edges or flow steps by id. What a walkthrough looks like when it is good is the subject of what a good pull request walkthrough looks like.

What validation actually checks

parseGraphDoc throws a PrLensSchemaError; safeParseGraphDoc returns { ok: true, value } or { ok: false, error }. Both run four things in one pass and report every problem they find rather than only the first.

Structure comes first: types, lengths, enums, unknown keys, path rules. Then the contract version. Then referential integrity, which is the pass that catches most real documents: every node in a declared lane, every edge between declared nodes, every flow step between declared participants, every view and layout hint naming elements that exist, every walkthrough step staging a diagram the document has. Last, that a render could describe the document at all.

Failures carry a machine-readable code: INVALID_DOCUMENT, BROKEN_REFERENCE, DUPLICATE_ID, UNSUPPORTED_SCHEMA_VERSION, PATCH_CONFLICT, NOT_A_SNAPSHOT, plus an issues array with a path and a message each.

The JSON Schemas in json-schema/ are generated from the same zod definitions and published with the package, so an editor can validate your file without running TypeScript. Six rules cannot be expressed there and stay the parser's job, each of them a comparison the shape alone cannot make: referential integrity between elements, a line range that ends before it starts, a self message whose endpoints disagree, a patch whose two commits are the same, more views than a manifest could describe, and a walkthrough step focusing flow steps its stage does not draw. Structured output from a model is not enough on its own. Always validate.

Why the model writes this instead of drawing

Everything to the left of the graph document reasons about code. Everything to the right of it draws. A model asked to emit SVG directly produces a picture that rearranges itself between two pushes that barely changed anything, and you cannot address a picture by the hash of itself if the hash moves. Splitting the two at a typed document is what buys determinism, and the longer case for it is in why the model should describe the graph and never draw it. If you want to write a document and draw it on your own machine, the PR Lens CLI is the shortest path there.

Questions people ask

What format does PR Lens use for its diagrams?

A JSON document called a graph document, validated by the @coldtea/pr-lens-schema package. It holds lanes, nodes, edges, ordered flows, a drill-down view tree and an optional walkthrough, each element carrying a delta of added, modified, removed or unchanged. The renderer turns that document into SVG; the document itself has no coordinates, colours or shapes in it.

Can I write a PR Lens graph document by hand?

Yes. Copy examples/minimal.graph.json from the schema package, edit it, and run pr-lens validate on the result. The parser reports every problem it finds in one pass rather than only the first, so a hand-written document converges quickly.

What does schemaVersion mean in a PR Lens document?

It is the contract version the document targets, currently 0.1.1, and it moves independently of the npm package version. Below 1.0.0 a parser accepts only its own exact major and minor, because a minor bump may still break. From 1.0.0 on, minor releases only add optional fields or widen an enum, so a parser accepts any minor at or below its own.

Does a PR Lens document carry bugs or security findings?

No. There is no findings field and no security lens, and a document that invents one is rejected rather than quietly stripped. PR Lens is the comprehension layer, so review bots that hunt defects sit alongside it rather than inside the same document.

Sources

Keep reading

Turn it on

Install the App and every pull request in the repositories you pick gets drawn. Or hand the prompt to the agent you already have open.

Add to GitHub