PR Lens skill

WHAT: Draws a code change or part of a codebase as an animated architecture or data-flow diagram, on its own or in a pull request.

by coldteadotai·MIT license·★ 1,850 Stars on the repo·GitHub ↗

Use now

Files of PR Lens

coldteadotai/main1 file shown
SKILL.md
Show the full text410 lines

PR Lens

PR Lens draws code as visually rich animated diagrams. It can represent diffs, architecture, data flows, and more.

The diff or code is represented as one JSON document (lanes, nodes, edges, ordered flows) and it renders the JSON as an animated SVG

Operating manual

Decide where the diagram lands before you write it: a canvas, or an SVG and a pull request comment. Only a canvas draws payload, the sample request and response on a flow step. A late decision costs another pass through steps 2 and 3.

  1. Read the diff. When asked to represent a code change: git diff --find-renames <base>...<head>. The base is the merge base, not the tip of the base branch.

    If not expressing a code diff, read the code to be visually represented

  2. Write the document to .pr-lens/graph.json, following references/graph-document.md. references/example.graph.json is valid reference with three lanes, all four delta states, a hero edge, a seven-step flow, a nested drill-down tree and a six-step walkthrough. Read it before you write your first one. It is quicker than reading the reference. If it is going to a canvas, give every flow step (messages) that moves data a payload as you write it. "Sample traffic on a flow step" below says what goes in one. Only a flow step carries one. Flows need the data-flow lens, so an architecture view draws none.

  3. Validate, and fix

    npx @coldtea/pr-lens-cli@latest validate .pr-lens/graph.json
    

    Fix every failure and run it again. Do not render an invalid document; do not "work around" a failure by deleting the element it names.

  4. Render.

    npx @coldtea/pr-lens-cli@latest render .pr-lens/graph.json --theme light
    

    Render light by default unless the user requests another theme. The SVGs, the manifest and drawn.graph.json land in a directory of their own under .pr-lens/, named after the document's title. The render prints that path, so read it from there. The CLI adds .pr-lens/ to the repository's .gitignore. Do not commit any of it. These files are rebuilt from the diff whenever anyone wants them again. Each SVG is named after its view, the theme and a content hash; manifest.json lists them by lens and view, so read the names from there or from the directory.

    If the user asked for a diagram, an explanation or a picture of the architecture and nothing more, put it on a canvas and hand back the link:

    npx @coldtea/pr-lens-cli@latest canvas push .pr-lens/<drawing>/drawn.graph.json
    

    Pass the path the render printed. A bare canvas push finds the drawing when the checkout holds only one; with more than one it lists them and asks which, so always pass the path.

    It prints three links. Give the user the view link, https://prlens.dev/c/{id}: that is the diagram, full screen, every view on one page, and it opens without a login. The edit link, the one ending in #w=…, lets its holder push over the canvas, so leave it out of the reply unless they ask, and never paste it anywhere public. The embed link serves the top view as an SVG for a README.

    Pushing the same document again updates the same canvas, so a follow-up such as "rename that node" or "add the queue" is: edit the document, validate, render, push. While the title stays the same, it renders to the same directory and pushes to the same canvas, so the link does not change. If the push fails, say so and tell them where the SVGs are and which one is the top view.

    A different diagram only needs a different title. It renders into its own directory and pushes to its own canvas, and the first one stays as it was. canvas list shows them all.

  5. Attach, when there is a pull request to attach to. That means the user asked you to open a PR, asked for a diagram on one that exists, or you are opening a PR as part of changes made. Otherwise skip this step.

How the diagram gets there depends on the forge. Run git remote get-url origin to see the host before you write a body around a flag that forge may not have.

GitHub. GitHub CLI uploads the diagram with the pull request. Write the body with a Markdown image pointing at the local file, then pass the same path to --attach. gh rewrites the reference to the uploaded asset and keeps the alt text you wrote:

Moves bulk sending off the per-recipient trigger and onto a batch endpoint.

![Architecture after this change: the queue route, the new bulk sender and the retired per-recipient path](.pr-lens/overview-light-4f9bd6c1.svg)
gh pr create --title "Batch broadcast sends" --body-file .pr-lens/body.md \
  --attach .pr-lens/overview-light-4f9bd6c1.svg

On a pull request that already exists, gh pr edit <number> with the same two flags puts the diagram in the description, and gh pr comment <number> puts it in a comment. Repeat --attach for each diagram the body references.

gh has three rules:

  • The reference has to be a Markdown image, ![alt](path). An HTML <img> or <picture> is left as written, and the file is appended at the bottom of the body instead.
  • The alt text is the caption a reader without images gets. Say what the diagram shows, in one line.
  • --attach arrived in GitHub CLI 2.99. Check with gh --version before you write a body around it.

Attach the views a reviewer needs and leave the rest in .pr-lens/: the top architecture view first, then a data flow if the change has a sequence worth following. A body with four diagrams reads worse than one with two, except the four are really needed to understand the change e.g., in the case of a complex feature or refactor.

GitLab. Nothing uploads the file with the description for you, so upload each SVG to the project first. The response includes the Markdown to paste into the description:

curl -sf --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
  --form "[email protected]/overview-light-4f9bd6c1.svg" \
  "https://gitlab.com/api/v4/projects/<url-encoded-path>/uploads"
# → {"markdown":"![overview-light-4f9bd6c1](/uploads/…/overview-light-4f9bd6c1.svg)", …}

glab mr create --title "Batch broadcast sends" --description "$(cat .pr-lens/body.md)"

An uploaded image loads for every reader of the merge request. A raw file URL on a private project does not. The catch is that the attachment URL is the permission: it is unguessable, but anyone who has it can see the diagram, member or not. Tell the user this if the project is private. GitLab strips <picture>, so render with --theme neutral and reference that single SVG. The neutral render has its own background and reads in both light and dark mode. Either half of the light/dark pair looks wrong in one of them.

Bitbucket. Comments and descriptions are plain Markdown with no HTML, so there are no collapsible sections or theme pairs. Render with --theme neutral here too. Publish the SVGs somewhere durable, such as the repository's Downloads or a canvas, and reference them as ordinary Markdown images.

On any forge where --attach is not an option, publish the SVGs somewhere durable and let the CLI compose the comment instead:

npx @coldtea/pr-lens-cli@latest comment \
  --graph .pr-lens/<drawing>/drawn.graph.json \
  --manifest .pr-lens/<drawing>/manifest.json \
  --asset-base-url https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<dir>

--graph takes the drawing's own drawn.graph.json, not the document you wrote, because corrections change what the diagrams show and the CLI refuses a document its manifest does not describe. --asset-base-url is where you published the SVGs; leave it out and the markdown points at local paths no reader can fetch. The markdown goes to stdout, with each diagram as a <picture> pair; posting it is your business.

Add --target gitlab or --target bitbucket when the comment is not for GitHub, so the composer writes markup that forge can render. GitHub gets <picture> theme pairs and collapsible drill-downs. GitLab gets the same HTML with the single neutral render. Bitbucket gets plain Markdown with the views flattened. With the wrong target, the comment shows its tags as raw text.

If you would rather not author the document yourself, npx @coldtea/pr-lens-cli@latest analyze --base <ref> does steps 1 and 2 by asking a provider — Gemini, OpenAI, or any endpoint speaking /chat/completions — with a key of your own. That is the only path here that needs one.

Answering beside an open canvas

Once a canvas is pushed, you can answer questions about it on the canvas itself. The reader keeps the page open, asks you in the terminal, and your answer plays there: the camera moves step by step, what you name lights up, and each name is a link.

Run this once, when the user wants to talk about a canvas they have open or are about to open. Name the drawing you pushed, the same path as the push:

npx @coldtea/pr-lens-cli@latest canvas open .pr-lens/<drawing>/drawn.graph.json

It opens one browser tab that follows you. Only that tab moves. Anyone else reading the same link sees the canvas as it was. Every command below talks to that tab, and takes the same path as --drawing. Always pass it: a checkout can hold several canvases, and the path says which one you mean.

When the user gives you a link to a canvas you did not push, like https://prlens.dev/c/<id>, open it with the link. This works from any folder, as long as the canvas is not private:

npx @coldtea/pr-lens-cli@latest canvas open --canvas https://prlens.dev/c/<id>

The CLI saves a copy of the drawing to .pr-lens/canvases/<id>.graph.json. Take the ids for your answer from that file. In the commands below, use --canvas <id> in place of --drawing. A private canvas opens only for its owner, after they run npx @coldtea/pr-lens-cli@latest auth login.

When the user says "this", "here" or "what I selected", look first. They clicked a component, dragged a box or picked a part of a drawing in the tab, and you cannot see it:

npx @coldtea/pr-lens-cli@latest canvas look --drawing .pr-lens/<drawing>/drawn.graph.json

It prints JSON: the diagram they are on (diagram.stage, ready to paste into a step), what is on screen (inFrame), what they selected (scope), the answer they have open, and the drawing hung under the canvas (fork), if there is one. Answer about scope when it is set. following: false means they left agent mode: tell them the answer is waiting rather than saying you moved their canvas.

Answer with a JSON file, or - to pipe it in:

npx @coldtea/pr-lens-cli@latest canvas answer .pr-lens/answer.json --drawing .pr-lens/<drawing>/drawn.graph.json
{
  "question": "What does push check first?",
  "steps": [
    {
      "heading": "Push checks the token first",
      "stage": { "kind": "view", "view": "overview" },
      "focus": { "kind": "selection", "nodes": ["canvas-api"] },
      "paragraphs": [
        {
          "parts": [
            { "text": "The " },
            { "text": "canvas API", "ref": { "kind": "component", "id": "canvas-api" } },
            { "text": " refuses a push without the write token, before it draws anything." }
          ]
        }
      ]
    }
  ]
}
  • One to four steps. Two is usual. Each step stops on one diagram, in the order a reader should follow.
  • heading is a sentence of at most six words: who or what, a verb, what happens. The first step's heading is the answer. To a yes or no question it says yes or no.
  • stage and focus work exactly as they do in a walkthrough. Leave stage out for the opening diagram. Light one or two things, not half the diagram.
  • A paragraph is one or two sentences and at most 30 words. A second paragraph is for a failure or a risk the first did not name.
  • Every place you name carries a ref: component is a node id, message is flowId/messageId, diagram is a view or flow id. Copy ids from the document exactly. Never make one up. The CLI checks every id against the drawing before sending, and the app checks again; a wrong one comes back with the ids that exist.
  • When the canvas cannot answer part of the question, say what is missing in cannotTell rather than guessing.

"Take me to X" is a camera move, not an answer:

npx @coldtea/pr-lens-cli@latest canvas show --drawing .pr-lens/<drawing>/drawn.graph.json \
  --diagram send-pipeline --focus send-pipeline/enqueue --open send-pipeline/enqueue

--focus takes node ids or flow/message and repeats. --open opens that message's sample payload.

"What's inside X", "expand on X" or "break X down" is a drawing. Write a small graph document of X's insides (up to ten nodes) and hang it under X:

npx @coldtea/pr-lens-cli@latest canvas fork .pr-lens/inside-x.json --from x --drawing .pr-lens/<drawing>/drawn.graph.json

The sketch can leave out schemaVersion, kind and provenance; they come from the canvas.

When the question needs the diagram itself changed (a missing component, a wrong arrow), edit the document, validate, render and canvas push as usual. The open tab reloads onto the new revision by itself.

After answer, show and fork, the CLI says where the reader is. If it says they stepped out, tell the user the answer is waiting in their Questions list, and that / opens it.

The pull request body, when there is one

A reviewer should understand the change before reading the diff, so the diagram goes where they look first: the description, not a trailing comment. Open with one sentence on why the change exists, then the architecture diagram, then whatever proves the change works, such as a screenshot of the result or a recording of the interaction. Use one visual per idea. A diagram that needs a paragraph of explanation has a document problem; go back to step 2.

What makes a document worth reading

  • Include what did not change. A diagram of only the changed nodes says nothing about blast radius. The unchanged neighbours a change touches are the context; mark them delta: "unchanged".
  • Lanes are the reader's mental model (a runtime, a tier, a boundary), not the folder tree.
  • One hero edge, two at the outside: the connection the change is really about.
  • Add a flow only when there is a sequence worth animating. One good flow beats three thin ones.
  • Attach file refs: they become the permalinks a reviewer clicks.
  • There is no findings lens. PR Lens is the comprehension layer, not another review bot. There is no field for a bug, a risk or a security note, and a document that invents one is rejected rather than trimmed.

Choosing architecture views

Treat architecture views as a C4-inspired decision tree, not a checklist. One useful view is enough for a small change. Start with system context when the change affects a user, an external system or a system boundary. Use a container view for the affected applications, services, jobs, data stores and runtimes. Add a component child only when an affected container's internals matter. Do not add code-level views by default.

Every child moves down one level and covers a materially narrower scope. Skip empty, repetitive or speculative levels, and do not infer architecture from folder names alone. Two views should not carry substantially the same nodes and edges. Keep the unchanged direct neighbours that explain blast radius.

Keep data-flow views as separate roots rather than nesting them in the architecture tree. Set defaultOpen: true on the highest useful architecture view. Lower levels should normally keep the default, false.

Writing a walkthrough

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. A canvas plays it, and the reader scrolls through it.

The contract leaves a walkthrough optional. Write one anyway for anything that is not trivial: more than one diagram, a diagram with several changed parts, or any flow. Skip it only when the document is one small diagram whose single step would just repeat the title.

Aim for three to seven steps.

A walkthrough is the fastest read of a pull request. Each step is one change: something added, changed, removed or moved, in the order a reviewer needs it. A step is never a description of the diagram.

What counts as a step: a behaviour change, an API change, an architecture change, a data-flow change, or an addition. Unchanged parts appear only where a step needs them to make sense. The headline change is step one. An overview of everything touched, if there is one, is the last step.

"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": "blast-radius",
      "heading": "4 parts added, 2 removed, across 3 lanes",
      "body": "A 2,000-person broadcast used to make 2,000 calls to Postmark. It now makes 4.",
      "stage": { "kind": "view", "view": "overview" }
    }
  ]
}

Each step has:

  • heading: the thing and what happened to it, up to 48 characters, in sentence case. Build it from change words: added, removed, replaced, now, moved, split. If a heading could have been true before the pull request, it is not a change heading.
  • body: one line under the heading, up to 140 characters, on what the change means for behaviour: what happens now that did not before, or what stops happening, with the numbers when they matter. Not a restatement of the heading, and not a description of the code. A heading with no body reads as unfinished, so the body is required.
  • 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. Open on the widest view with the focus left out, so the reader sees the whole thing before it narrows.
  • focus: what to zoom in on inside that diagram. { "kind": "all" }, the default, 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. Focus the elements the step's change touched, so the veil lights the change. Point at two or three of them. A step that lights half the diagram has not said anything.
  • detail: optional. A second sentence under the body, with links into the diagram. See "Links under a step" below.

Write every word for a smart twelve-year-old: short common words, one idea per line, active voice, things named as the diagram names them, numbers as digits. If a line needs a second read, rewrite it. Words like leverages, orchestrates, asynchronous pipeline and fan-out never belong in a step. This holds in whatever language the document is written in.

The same three steps, written well and written badly. Heading first, then the body after the slash:

Write this Not this
Route now queues the job instead of sending / The API call finishes at once. A worker sends the mail later. Broadcast fan-out moves behind the queue / The API route now enqueues broadcast jobs for asynchronous batch processing instead of sending emails inline.
Postmark now gets 500 emails per call / One call per batch instead of one call per person. Batched delivery replaces single sends / The worker leverages the shared library to send emails in chunks of 500 via Postmark's batch endpoint.
processBroadcast and sendSingleEmail removed / sendBroadcastBulk does their job for whole batches. Single send functions are retired / sendBroadcastBulk replaces processBroadcast and sendSingleEmail to handle bulk deliveries in chunks.

The body has no links. To let the reader jump to a part of the diagram, add a detail under the body.

A detail is one or two sentences, up to 240 characters. It says which parts do what the body describes, and it uses the names on the diagram. Its cites turn words in that sentence into links.

  • A cite's text is copied letter for letter from the detail's text.
  • A cite's ref says where the link goes. A node is { "kind": "node", "node": "postmark" }. A flow step is { "kind": "message", "flow": "send-pipeline", "message": "batch-post" }. A whole diagram is { "kind": "view", "view": "overview" } or { "kind": "flow", "flow": "send-pipeline" }.
  • List the cites in the order the sentence says them. Two cites cannot use the same words. A detail has one to eight cites.
  • Copy every id from the document. Never make one up.

A click on a node or a diagram moves the camera there. A click on a flow step also opens its sample payload, when it has one.

Do not cite files. A canvas you push has no pull request behind it, so a file link has nothing to open and shows as plain words.

Leave the detail off the overview step, and off any step where it would only say the body again.

Keep consecutive steps on the same stage together. Every change of stage flies the camera across the canvas, so a tour that alternates between two diagrams spends its time travelling.

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.
  • Step ids are unique within the walkthrough. Two steps minimum, twelve maximum.
  • Every cite's words are in its detail's text, in the order the cites are listed. The node, flow step, view or flow it points to exists in the document.
  • A stored map never carries a walkthrough. A map describes the system; a walkthrough tells the story of one change.

The field arrived with contract 0.1.1. A CLI older than 0.4.0 does not know it and rejects the whole document as an invented field, so validate with a current one. detail is newer still. If validate rejects it as an unknown field, the CLI is out of date.

Sample traffic on a flow step

A flow step can carry a payload: what travels on it. Only the canvas draws it, in the rail that opens when a reader clicks a step. Nothing in an SVG or a pull request comment changes. Write it when the document is going to a canvas (step 4, canvas push) and leave it out otherwise. Six payloads on the reference document add half its length again, so this is not a field to fill by default.

On a canvas document, add it to a step that moves data: a request body, a job record, a query, a result. Leave it off a step that only signals, such as a trigger with nothing attached.

{
  "id": "batch-post",
  "from": "send-broadcast-bulk",
  "to": "postmark",
  "label": "POST /email/batch",
  "kind": "sync",
  "delta": "added",
  "repeat": 4,
  "payload": {
    "request": {
      "type": "EmailBatch[500]",
      "shape": "Email[]  // max 500\nEmail = { From: string; To: string; Subject: string; HtmlBody: string; MessageStream: \"broadcast\"; Metadata: { campaignId: string; batchId: string } }",
      "sample": [
        {
          "From": "[email protected]",
          "To": "[email protected]",
          "Subject": "The batching issue, fixed",
          "HtmlBody": "<!doctype html><html><body>…",
          "MessageStream": "broadcast",
          "Metadata": { "campaignId": "cmp_0001", "batchId": "b_0001" }
        }
      ],
      "before": [
        {
          "From": "[email protected]",
          "To": "[email protected]",
          "Subject": "The batching issue, fixed",
          "HtmlBody": "<!doctype html><html><body>…",
          "Metadata": { "campaignId": "cmp_0001" }
        }
      ],
      "source": { "path": "tests/fixtures/postmark-batch.json" }
    },
    "response": {
      "type": "BatchResult[500]",
      "shape": "SendResult[]  // one per Email, same order",
      "sample": [{ "ErrorCode": 0, "Message": "OK", "To": "[email protected]", "MessageID": "b7fa5c1e-…" }]
    }
  }
}

A payload has a request side, a response side, or both. Each side has:

  • type: the name a reader of the code would recognise. Put the count in it when the step carries a collection: EmailBatch[500], not EmailBatch. Write { "type": "void" } for a side that carries nothing, such as the answer to a fire and forget call.
  • shape: the type signature as text, taken from the code's own types. Up to 2048 bytes.
  • sample: one exemplar instance after the change, written inline as JSON. It is a JSON value, not a JSON string: "sample": [{ "To": "[email protected]" }], never "sample": "[{\"To\": ...}]". A string here is rejected. Every key once, one element in any array, long strings cut with an ellipsis. At most 8 levels deep and 4096 bytes once serialised. The parser refuses a sample over either cap rather than trimming it.
  • before: the same exemplar as it was before the change, when it differs. Same rules as sample, and it needs a sample to differ from.
  • source: the fixture or type the shape and sample came from, as a file reference. It becomes the permalink.

Use placeholder values: [email protected], cmp_0001. Never copy a value from a fixture that could belong to a real person or unlock something, even in test data.

Do not write changedPaths. The paths that differ between before and sample are worked out when the document is stored. A list you write is discarded.

The field arrived with contract 0.2.0. A CLI built before it rejects the whole document as an invented field, so validate with a current one.

What the validator will catch

Read references/graph-document.md before writing. The four failures that account for nearly everything:

Code What you did
BROKEN_REFERENCE an edge, a flow step, a view or a walkthrough step names an id you never declared
INVALID_DOCUMENT an invented field; the schemas are strict, unknown keys are rejected
DUPLICATE_ID two nodes, edges or views sharing an id
UNSUPPORTED_SCHEMA_VERSION schemaVersion is not the contract version installed

Seven rules cannot be expressed in JSON Schema and are checked only by the parser, so structured output alone does not make a document valid: referential integrity, 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 render manifest could describe, a walkthrough step focusing flow steps the diagram on its stage does not draw, and sample traffic past its depth or byte caps. Always validate.

Fixing a map instead of writing one

When someone says the diagram is wrong (a node is misnamed, a folder should not be on it, something sits in the wrong lane), do not edit the generated document. It is regenerated on every run. Write the correction into .github/pr-lens.yml, which is an overlay applied over fresh inference every time:

schemaVersion: 0.2.0
map:
  rename:
    - match: functions/src/broadcast/sendBroadcastBulk.ts
      to: Broadcast sender
  exclude:
    - "**/*.test.ts"
  lane:
    - match: packages/broadcast-lib/**
      lane: functions

references/config.md has the full format and the recipes. Validate it the same way: npx @coldtea/pr-lens-cli@latest validate .github/pr-lens.yml.

A match beginning with id: addresses one node exactly; anything else is a path glob matched against a node's file paths. Prefer the glob, because it keeps holding when the next run names the node differently. A lane pin may name a lane the document never declared: the band is created, and takes the id for its label, so give it one a reader would want to see.

pr-lens render says so when a correction matched nothing, which is how a config that has drifted, because the file it named moved or was deleted, becomes visible instead of quietly doing nothing.

What ships with this skill

Everything you need is beside this page. Nothing here asks you to install a package first.

references/graph-document.md the document, field by field: enums, limits, and where documents actually go wrong
references/config.md .github/pr-lens.yml, the correction overlay, in full
references/example.graph.json one complete document that validates, to read and to copy the shape of

The same document ships as postmark-refactor.graph.json in @coldtea/pr-lens-schema, and the JSON Schema the validator enforces is published at https://unpkg.com/@coldtea/pr-lens-schema/json-schema/graph-doc.schema.json. Neither is something you need to fetch to write a document.

1---
2name: pr-lens
3description: "WHAT: Draws a code change or part of a codebase as an animated architecture or data-flow diagram, on its own or in a pull request. WHEN: asked to diagram, visualise or explain a change or a system, or when a pull request should carry a diagram. KEYWORDS: PR Lens, diagram, architecture, data flow, visualise, visualize, pull request"
4---
5 
6# PR Lens
7 
8PR Lens draws code as visually rich animated diagrams. It can represent diffs, architecture, data flows, and more.
9 
10The diff or code is represented as one JSON document (lanes, nodes, edges, ordered flows) and it renders the JSON as an animated SVG
11 
12## Operating manual
13 
14Decide where the diagram lands before you write it: a canvas, or an SVG and a pull request comment. Only a canvas draws `payload`, the sample request and response on a flow step. A late decision costs another pass through steps 2 and 3.
15 
161. **Read the diff.** When asked to represent a code change: `git diff --find-renames <base>...<head>`. The base is the merge base, not the tip of the base branch.
17 
18 If not expressing a code diff, read the code to be visually represented
19 
202. **Write the document** to `.pr-lens/graph.json`, following `references/graph-document.md`. `references/example.graph.json` is valid reference with three lanes, all four delta states, a hero edge, a seven-step flow, a nested drill-down tree and a six-step walkthrough. Read it before you write your first one. It is quicker than reading the reference. If it is going to a canvas, give every flow step (`messages`) that moves data a `payload` as you write it. "Sample traffic on a flow step" below says what goes in one. Only a flow step carries one. Flows need the `data-flow` lens, so an architecture view draws none.
21 
223. **Validate, and fix**
23 
24 ```bash
25 npx @coldtea/pr-lens-cli@latest validate .pr-lens/graph.json
26 ```
27 
28 Fix every failure and run it again. Do not render an invalid document; do not "work around" a failure by deleting the element it names.
29 
304. **Render.**
31 
32 ```bash
33 npx @coldtea/pr-lens-cli@latest render .pr-lens/graph.json --theme light
34 ```
35 
36 Render light by default unless the user requests another theme. The SVGs, the manifest and `drawn.graph.json` land in a directory of their own under `.pr-lens/`, named after the document's title. The render prints that path, so read it from there. The CLI adds `.pr-lens/` to the repository's .gitignore. Do not commit any of it. These files are rebuilt from the diff whenever anyone wants them again. Each SVG is named after its view, the theme and a content hash; `manifest.json` lists them by lens and view, so read the names from there or from the directory.
37 
38 If the user asked for a diagram, an explanation or a picture of the architecture and nothing more, put it on a canvas and hand back the link:
39 
40 ```bash
41 npx @coldtea/pr-lens-cli@latest canvas push .pr-lens/<drawing>/drawn.graph.json
42 ```
43 
44 Pass the path the render printed. A bare `canvas push` finds the drawing when the checkout holds only one; with more than one it lists them and asks which, so always pass the path.
45 
46 It prints three links. Give the user the view link, `https://prlens.dev/c/{id}`: that is the diagram, full screen, every view on one page, and it opens without a login. The edit link, the one ending in `#w=…`, lets its holder push over the canvas, so leave it out of the reply unless they ask, and never paste it anywhere public. The embed link serves the top view as an SVG for a README.
47 
48 Pushing the same document again updates the same canvas, so a follow-up such as "rename that node" or "add the queue" is: edit the document, validate, render, push. While the title stays the same, it renders to the same directory and pushes to the same canvas, so the link does not change. If the push fails, say so and tell them where the SVGs are and which one is the top view.
49 
50 A different diagram only needs a different title. It renders into its own directory and pushes to its own canvas, and the first one stays as it was. `canvas list` shows them all.
51 
525. **Attach, when there is a pull request to attach to.** That means the user asked you to open a PR, asked for a diagram on one that exists, or you are opening a PR as part of changes made. Otherwise skip this step.
53 
54How the diagram gets there depends on the forge. Run `git remote get-url origin` to see the host before you write a body around a flag that forge may not have.
55 
56 **GitHub.** GitHub CLI uploads the diagram with the pull request. Write the body with a Markdown image pointing at the local file, then pass the same path to `--attach`. `gh` rewrites the reference to the uploaded asset and keeps the alt text you wrote:
57 
58 ```markdown
59 Moves bulk sending off the per-recipient trigger and onto a batch endpoint.
60 
61 ![Architecture after this change: the queue route, the new bulk sender and the retired per-recipient path](.pr-lens/overview-light-4f9bd6c1.svg)
62 ```
63 
64 ```bash
65 gh pr create --title "Batch broadcast sends" --body-file .pr-lens/body.md \
66 --attach .pr-lens/overview-light-4f9bd6c1.svg
67 ```
68 
69 On a pull request that already exists, `gh pr edit <number>` with the same two flags puts the diagram in the description, and `gh pr comment <number>` puts it in a comment. Repeat `--attach` for each diagram the body references.
70 
71 gh has three rules:
72 - The reference has to be a Markdown image, `![alt](path)`. An HTML `<img>` or `<picture>` is left as written, and the file is appended at the bottom of the body instead.
73 - The alt text is the caption a reader without images gets. Say what the diagram shows, in one line.
74 - `--attach` arrived in GitHub CLI 2.99. Check with `gh --version` before you write a body around it.
75 
76 Attach the views a reviewer needs and leave the rest in `.pr-lens/`: the top architecture view first, then a data flow if the change has a sequence worth following. A body with four diagrams reads worse than one with two, except the four are really needed to understand the change e.g., in the case of a complex feature or refactor.
77 
78 **GitLab.** Nothing uploads the file with the description for you, so upload each SVG to the project first. The response includes the Markdown to paste into the description:
79 
80 ```bash
81 curl -sf --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
82 --form "[email protected]/overview-light-4f9bd6c1.svg" \
83 "https://gitlab.com/api/v4/projects/<url-encoded-path>/uploads"
84 # → {"markdown":"![overview-light-4f9bd6c1](/uploads/…/overview-light-4f9bd6c1.svg)", …}
85 
86 glab mr create --title "Batch broadcast sends" --description "$(cat .pr-lens/body.md)"
87 ```
88 
89An uploaded image loads for every reader of the merge request. A raw file URL on a private project does not. The catch is that the attachment URL is the permission: it is unguessable, but anyone who has it can see the diagram, member or not. Tell the user this if the project is private. GitLab strips `<picture>`, so render with `--theme neutral` and reference that single SVG. The neutral render has its own background and reads in both light and dark mode. Either half of the light/dark pair looks wrong in one of them.
90 
91 **Bitbucket.** Comments and descriptions are plain Markdown with no HTML, so there are no collapsible sections or theme pairs. Render with `--theme neutral` here too. Publish the SVGs somewhere durable, such as the repository's Downloads or a canvas, and reference them as ordinary Markdown images.
92 
93 On any forge where `--attach` is not an option, publish the SVGs somewhere durable and let the CLI compose the comment instead:
94 
95 ```bash
96 npx @coldtea/pr-lens-cli@latest comment \
97 --graph .pr-lens/<drawing>/drawn.graph.json \
98 --manifest .pr-lens/<drawing>/manifest.json \
99 --asset-base-url https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<dir>
100 ```
101 
102 `--graph` takes the drawing's own `drawn.graph.json`, not the document you wrote, because corrections change what the diagrams show and the CLI refuses a document its manifest does not describe. `--asset-base-url` is where you published the SVGs; leave it out and the markdown points at local paths no reader can fetch. The markdown goes to stdout, with each diagram as a `<picture>` pair; posting it is your business.
103 
104 Add `--target gitlab` or `--target bitbucket` when the comment is not for GitHub, so the composer writes markup that forge can render. GitHub gets `<picture>` theme pairs and collapsible drill-downs. GitLab gets the same HTML with the single neutral render. Bitbucket gets plain Markdown with the views flattened. With the wrong target, the comment shows its tags as raw text.
105 
106If you would rather not author the document yourself, `npx @coldtea/pr-lens-cli@latest analyze --base <ref>` does steps 1 and 2 by asking a provider — Gemini, OpenAI, or any endpoint speaking `/chat/completions` — with a key of your own. That is the only path here that needs one.
107 
108## Answering beside an open canvas
109 
110Once a canvas is pushed, you can answer questions about it on the canvas itself. The reader keeps the page open, asks you in the terminal, and your answer plays there: the camera moves step by step, what you name lights up, and each name is a link.
111 
112Run this once, when the user wants to talk about a canvas they have open or are about to open. Name the drawing you pushed, the same path as the push:
113 
114```bash
115npx @coldtea/pr-lens-cli@latest canvas open .pr-lens/<drawing>/drawn.graph.json
116```
117 
118It opens one browser tab that follows you. Only that tab moves. Anyone else reading the same link sees the canvas as it was. Every command below talks to that tab, and takes the same path as `--drawing`. Always pass it: a checkout can hold several canvases, and the path says which one you mean.
119 
120**When the user gives you a link to a canvas you did not push**, like `https://prlens.dev/c/<id>`, open it with the link. This works from any folder, as long as the canvas is not private:
121 
122```bash
123npx @coldtea/pr-lens-cli@latest canvas open --canvas https://prlens.dev/c/<id>
124```
125 
126The CLI saves a copy of the drawing to `.pr-lens/canvases/<id>.graph.json`. Take the ids for your answer from that file. In the commands below, use `--canvas <id>` in place of `--drawing`. A private canvas opens only for its owner, after they run `npx @coldtea/pr-lens-cli@latest auth login`.
127 
128**When the user says "this", "here" or "what I selected", look first.** They clicked a component, dragged a box or picked a part of a drawing in the tab, and you cannot see it:
129 
130```bash
131npx @coldtea/pr-lens-cli@latest canvas look --drawing .pr-lens/<drawing>/drawn.graph.json
132```
133 
134It prints JSON: the diagram they are on (`diagram.stage`, ready to paste into a step), what is on screen (`inFrame`), what they selected (`scope`), the answer they have open, and the drawing hung under the canvas (`fork`), if there is one. Answer about `scope` when it is set. `following: false` means they left agent mode: tell them the answer is waiting rather than saying you moved their canvas.
135 
136**Answer** with a JSON file, or `-` to pipe it in:
137 
138```bash
139npx @coldtea/pr-lens-cli@latest canvas answer .pr-lens/answer.json --drawing .pr-lens/<drawing>/drawn.graph.json
140```
141 
142```json
143{
144 "question": "What does push check first?",
145 "steps": [
146 {
147 "heading": "Push checks the token first",
148 "stage": { "kind": "view", "view": "overview" },
149 "focus": { "kind": "selection", "nodes": ["canvas-api"] },
150 "paragraphs": [
151 {
152 "parts": [
153 { "text": "The " },
154 { "text": "canvas API", "ref": { "kind": "component", "id": "canvas-api" } },
155 { "text": " refuses a push without the write token, before it draws anything." }
156 ]
157 }
158 ]
159 }
160 ]
161}
162```
163 
164- One to four steps. Two is usual. Each step stops on one diagram, in the order a reader should follow.
165- `heading` is a sentence of at most six words: who or what, a verb, what happens. The first step's heading is the answer. To a yes or no question it says yes or no.
166- `stage` and `focus` work exactly as they do in a walkthrough. Leave `stage` out for the opening diagram. Light one or two things, not half the diagram.
167- A paragraph is one or two sentences and at most 30 words. A second paragraph is for a failure or a risk the first did not name.
168- Every place you name carries a `ref`: `component` is a node id, `message` is `flowId/messageId`, `diagram` is a view or flow id. Copy ids from the document exactly. Never make one up. The CLI checks every id against the drawing before sending, and the app checks again; a wrong one comes back with the ids that exist.
169- When the canvas cannot answer part of the question, say what is missing in `cannotTell` rather than guessing.
170 
171**"Take me to X"** is a camera move, not an answer:
172 
173```bash
174npx @coldtea/pr-lens-cli@latest canvas show --drawing .pr-lens/<drawing>/drawn.graph.json \
175 --diagram send-pipeline --focus send-pipeline/enqueue --open send-pipeline/enqueue
176```
177 
178`--focus` takes node ids or `flow/message` and repeats. `--open` opens that message's sample payload.
179 
180**"What's inside X", "expand on X" or "break X down"** is a drawing. Write a small graph document of X's insides (up to ten nodes) and hang it under X:
181 
182```bash
183npx @coldtea/pr-lens-cli@latest canvas fork .pr-lens/inside-x.json --from x --drawing .pr-lens/<drawing>/drawn.graph.json
184```
185 
186The sketch can leave out `schemaVersion`, `kind` and `provenance`; they come from the canvas.
187 
188**When the question needs the diagram itself changed** (a missing component, a wrong arrow), edit the document, validate, render and `canvas push` as usual. The open tab reloads onto the new revision by itself.
189 
190After `answer`, `show` and `fork`, the CLI says where the reader is. If it says they stepped out, tell the user the answer is waiting in their Questions list, and that `/` opens it.
191 
192## The pull request body, when there is one
193 
194A reviewer should understand the change before reading the diff, so the diagram goes where they look first: the description, not a trailing comment. Open with one sentence on why the change exists, then the architecture diagram, then whatever proves the change works, such as a screenshot of the result or a recording of the interaction. Use one visual per idea. A diagram that needs a paragraph of explanation has a document problem; go back to step 2.
195 
196## What makes a document worth reading
197 
198- **Include what did not change.** A diagram of only the changed nodes says nothing about blast radius. The unchanged neighbours a change touches are the context; mark them `delta: "unchanged"`.
199- **Lanes are the reader's mental model** (a runtime, a tier, a boundary), not the folder tree.
200- **One hero edge**, two at the outside: the connection the change is really about.
201- **Add a flow only when there is a sequence** worth animating. One good flow beats three thin ones.
202- **Attach file refs**: they become the permalinks a reviewer clicks.
203- **There is no findings lens.** PR Lens is the comprehension layer, not another review bot. There is no field for a bug, a risk or a security note, and a document that invents one is rejected rather than trimmed.
204 
205## Choosing architecture views
206 
207Treat architecture views as a C4-inspired decision tree, not a checklist. One useful view is enough for a small change. Start with system context when the change affects a user, an external system or a system boundary. Use a container view for the affected applications, services, jobs, data stores and runtimes. Add a component child only when an affected container's internals matter. Do not add code-level views by default.
208 
209Every child moves down one level and covers a materially narrower scope. Skip empty, repetitive or speculative levels, and do not infer architecture from folder names alone. Two views should not carry substantially the same nodes and edges. Keep the unchanged direct neighbours that explain blast radius.
210 
211Keep data-flow views as separate roots rather than nesting them in the architecture tree. Set `defaultOpen: true` on the highest useful architecture view. Lower levels should normally keep the default, `false`.
212 
213## Writing a walkthrough
214 
215A 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. A canvas plays it, and the reader scrolls through it.
216 
217The contract leaves a walkthrough optional. Write one anyway for anything that is not trivial: more than one diagram, a diagram with several changed parts, or any flow. Skip it only when the document is one small diagram whose single step would just repeat the title.
218 
219Aim for three to seven steps.
220 
221A walkthrough is the fastest read of a pull request. Each step is one change: something added, changed, removed or moved, in the order a reviewer needs it. A step is never a description of the diagram.
222 
223What counts as a step: a behaviour change, an API change, an architecture change, a data-flow change, or an addition. Unchanged parts appear only where a step needs them to make sense. The headline change is step one. An overview of everything touched, if there is one, is the last step.
224 
225```json
226"walkthrough": {
227 "steps": [
228 {
229 "id": "four-batch-calls",
230 "heading": "Postmark now gets 500 emails per call",
231 "body": "One call per batch, and Postmark answers with a result for each message.",
232 "stage": { "kind": "flow", "flow": "send-pipeline" },
233 "focus": { "kind": "selection", "messages": ["batch-post", "batch-results"] },
234 "detail": {
235 "text": "sendBroadcastBulk posts each batch to Postmark and reads the results back.",
236 "cites": [
237 { "text": "sendBroadcastBulk", "ref": { "kind": "node", "node": "send-broadcast-bulk" } },
238 { "text": "posts each batch", "ref": { "kind": "message", "flow": "send-pipeline", "message": "batch-post" } },
239 { "text": "Postmark", "ref": { "kind": "node", "node": "postmark" } }
240 ]
241 }
242 },
243 {
244 "id": "blast-radius",
245 "heading": "4 parts added, 2 removed, across 3 lanes",
246 "body": "A 2,000-person broadcast used to make 2,000 calls to Postmark. It now makes 4.",
247 "stage": { "kind": "view", "view": "overview" }
248 }
249 ]
250}
251```
252 
253Each step has:
254 
255- `heading`: the thing and what happened to it, up to 48 characters, in sentence case. Build it from change words: added, removed, replaced, now, moved, split. If a heading could have been true before the pull request, it is not a change heading.
256- `body`: one line under the heading, up to 140 characters, on what the change means for behaviour: what happens now that did not before, or what stops happening, with the numbers when they matter. Not a restatement of the heading, and not a description of the code. A heading with no body reads as unfinished, so the body is required.
257- `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. Open on the widest view with the focus left out, so the reader sees the whole thing before it narrows.
258- `focus`: what to zoom in on inside that diagram. `{ "kind": "all" }`, the default, 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. Focus the elements the step's change touched, so the veil lights the change. Point at two or three of them. A step that lights half the diagram has not said anything.
259- `detail`: optional. A second sentence under the body, with links into the diagram. See "Links under a step" below.
260 
261Write every word for a smart twelve-year-old: short common words, one idea per line, active voice, things named as the diagram names them, numbers as digits. If a line needs a second read, rewrite it. Words like leverages, orchestrates, asynchronous pipeline and fan-out never belong in a step. This holds in whatever language the document is written in.
262 
263The same three steps, written well and written badly. Heading first, then the body after the slash:
264 
265| Write this | Not this |
266| -------- | -------- |
267| Route now queues the job instead of sending / The API call finishes at once. A worker sends the mail later. | Broadcast fan-out moves behind the queue / The API route now enqueues broadcast jobs for asynchronous batch processing instead of sending emails inline. |
268| Postmark now gets 500 emails per call / One call per batch instead of one call per person. | Batched delivery replaces single sends / The worker leverages the shared library to send emails in chunks of 500 via Postmark's batch endpoint. |
269| processBroadcast and sendSingleEmail removed / sendBroadcastBulk does their job for whole batches. | Single send functions are retired / sendBroadcastBulk replaces processBroadcast and sendSingleEmail to handle bulk deliveries in chunks. |
270 
271### Links under a step
272 
273The body has no links. To let the reader jump to a part of the diagram, add a `detail` under the body.
274 
275A detail is one or two sentences, up to 240 characters. It says which parts do what the body describes, and it uses the names on the diagram. Its `cites` turn words in that sentence into links.
276 
277- A cite's `text` is copied letter for letter from the detail's `text`.
278- A cite's `ref` says where the link goes. A node is `{ "kind": "node", "node": "postmark" }`. A flow step is `{ "kind": "message", "flow": "send-pipeline", "message": "batch-post" }`. A whole diagram is `{ "kind": "view", "view": "overview" }` or `{ "kind": "flow", "flow": "send-pipeline" }`.
279- List the cites in the order the sentence says them. Two cites cannot use the same words. A detail has one to eight cites.
280- Copy every id from the document. Never make one up.
281 
282A click on a node or a diagram moves the camera there. A click on a flow step also opens its sample payload, when it has one.
283 
284Do not cite files. A canvas you push has no pull request behind it, so a file link has nothing to open and shows as plain words.
285 
286Leave the detail off the overview step, and off any step where it would only say the body again.
287 
288Keep consecutive steps on the same stage together. Every change of stage flies the camera across the canvas, so a tour that alternates between two diagrams spends its time travelling.
289 
290The validator checks:
291 
292- 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.
293- `messages` needs a stage that shows a flow. Leave it out when the stage is an architecture view.
294- Step ids are unique within the walkthrough. Two steps minimum, twelve maximum.
295- Every cite's words are in its detail's `text`, in the order the cites are listed. The node, flow step, view or flow it points to exists in the document.
296- A stored map never carries a walkthrough. A map describes the system; a walkthrough tells the story of one change.
297 
298The field arrived with contract 0.1.1. A CLI older than 0.4.0 does not know it and rejects the whole document as an invented field, so validate with a current one. `detail` is newer still. If `validate` rejects it as an unknown field, the CLI is out of date.
299 
300## Sample traffic on a flow step
301 
302A flow step can carry a `payload`: what travels on it. Only the canvas draws it, in the rail that opens when a reader clicks a step. Nothing in an SVG or a pull request comment changes. Write it when the document is going to a canvas (step 4, `canvas push`) and leave it out otherwise. Six payloads on the reference document add half its length again, so this is not a field to fill by default.
303 
304On a canvas document, add it to a step that moves data: a request body, a job record, a query, a result. Leave it off a step that only signals, such as a trigger with nothing attached.
305 
306```json
307{
308 "id": "batch-post",
309 "from": "send-broadcast-bulk",
310 "to": "postmark",
311 "label": "POST /email/batch",
312 "kind": "sync",
313 "delta": "added",
314 "repeat": 4,
315 "payload": {
316 "request": {
317 "type": "EmailBatch[500]",
318 "shape": "Email[] // max 500\nEmail = { From: string; To: string; Subject: string; HtmlBody: string; MessageStream: \"broadcast\"; Metadata: { campaignId: string; batchId: string } }",
319 "sample": [
320 {
321 "From": "[email protected]",
322 "To": "[email protected]",
323 "Subject": "The batching issue, fixed",
324 "HtmlBody": "<!doctype html><html><body>…",
325 "MessageStream": "broadcast",
326 "Metadata": { "campaignId": "cmp_0001", "batchId": "b_0001" }
327 }
328 ],
329 "before": [
330 {
331 "From": "[email protected]",
332 "To": "[email protected]",
333 "Subject": "The batching issue, fixed",
334 "HtmlBody": "<!doctype html><html><body>…",
335 "Metadata": { "campaignId": "cmp_0001" }
336 }
337 ],
338 "source": { "path": "tests/fixtures/postmark-batch.json" }
339 },
340 "response": {
341 "type": "BatchResult[500]",
342 "shape": "SendResult[] // one per Email, same order",
343 "sample": [{ "ErrorCode": 0, "Message": "OK", "To": "[email protected]", "MessageID": "b7fa5c1e-…" }]
344 }
345 }
346}
347```
348 
349A payload has a `request` side, a `response` side, or both. Each side has:
350 
351- `type`: the name a reader of the code would recognise. Put the count in it when the step carries a collection: `EmailBatch[500]`, not `EmailBatch`. Write `{ "type": "void" }` for a side that carries nothing, such as the answer to a fire and forget call.
352- `shape`: the type signature as text, taken from the code's own types. Up to 2048 bytes.
353- `sample`: one exemplar instance after the change, written inline as JSON. It is a JSON value, not a JSON string: `"sample": [{ "To": "[email protected]" }]`, never `"sample": "[{\"To\": ...}]"`. A string here is rejected. Every key once, one element in any array, long strings cut with an ellipsis. At most 8 levels deep and 4096 bytes once serialised. The parser refuses a sample over either cap rather than trimming it.
354- `before`: the same exemplar as it was before the change, when it differs. Same rules as `sample`, and it needs a `sample` to differ from.
355- `source`: the fixture or type the shape and sample came from, as a file reference. It becomes the permalink.
356 
357Use placeholder values: `[email protected]`, `cmp_0001`. Never copy a value from a fixture that could belong to a real person or unlock something, even in test data.
358 
359Do not write `changedPaths`. The paths that differ between `before` and `sample` are worked out when the document is stored. A list you write is discarded.
360 
361The field arrived with contract 0.2.0. A CLI built before it rejects the whole document as an invented field, so validate with a current one.
362 
363## What the validator will catch
364 
365Read `references/graph-document.md` before writing. The four failures that account for nearly everything:
366 
367| Code | What you did |
368| ---------------------------- | -------------------------------------------------------------------- |
369| `BROKEN_REFERENCE` | an edge, a flow step, a view or a walkthrough step names an id you never declared |
370| `INVALID_DOCUMENT` | an invented field; the schemas are strict, unknown keys are rejected |
371| `DUPLICATE_ID` | two nodes, edges or views sharing an id |
372| `UNSUPPORTED_SCHEMA_VERSION` | `schemaVersion` is not the contract version installed |
373 
374Seven rules cannot be expressed in JSON Schema and are checked only by the parser, so structured output alone does not make a document valid: referential integrity, 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 render manifest could describe, a walkthrough step focusing flow steps the diagram on its stage does not draw, and sample traffic past its depth or byte caps. Always validate.
375 
376## Fixing a map instead of writing one
377 
378When someone says the diagram is wrong (a node is misnamed, a folder should not be on it, something sits in the wrong lane), do not edit the generated document. It is regenerated on every run. Write the correction into `.github/pr-lens.yml`, which is an overlay applied over fresh inference every time:
379 
380```yaml
381schemaVersion: 0.2.0
382map:
383 rename:
384 - match: functions/src/broadcast/sendBroadcastBulk.ts
385 to: Broadcast sender
386 exclude:
387 - "**/*.test.ts"
388 lane:
389 - match: packages/broadcast-lib/**
390 lane: functions
391```
392 
393`references/config.md` has the full format and the recipes. Validate it the same way: `npx @coldtea/pr-lens-cli@latest validate .github/pr-lens.yml`.
394 
395A `match` beginning with `id:` addresses one node exactly; anything else is a path glob matched against a node's file paths. Prefer the glob, because it keeps holding when the next run names the node differently. A lane pin may name a lane the document never declared: the band is created, and takes the id for its label, so give it one a reader would want to see.
396 
397`pr-lens render` says so when a correction matched nothing, which is how a config that has drifted, because the file it named moved or was deleted, becomes visible instead of quietly doing nothing.
398 
399## What ships with this skill
400 
401Everything you need is beside this page. Nothing here asks you to install a package first.
402 
403| | |
404| ------------------------------- | ---------------------------------------------------------------------------------- |
405| `references/graph-document.md` | the document, field by field: enums, limits, and where documents actually go wrong |
406| `references/config.md` | `.github/pr-lens.yml`, the correction overlay, in full |
407| `references/example.graph.json` | one complete document that validates, to read and to copy the shape of |
408 
409The same document ships as `postmark-refactor.graph.json` in `@coldtea/pr-lens-schema`, and the JSON Schema the validator enforces is published at `https://unpkg.com/@coldtea/pr-lens-schema/json-schema/graph-doc.schema.json`. Neither is something you need to fetch to write a document.
410 

Discussion

Alternatives

Visual explainerGenerate self-contained HTML visual explanations for systems, code changes, plans, data, and technical concepts. Use for diagrams, architecture overviews, diff or plan reviews, project recaps, comparison tables, slide decks, animated explainers, and other visual explanations.Design & UI · MITReladrawWrite, render and edit diagrams in reladraw, a text diagram language where you say where things go, so the arrangement can be read back out of the source without looking at the picture. Use when asked to draw, diagram, sketch or visualize an architecture, a system, a data flow, a pipeline, a deployment, a directory layout, or the shape of a change or pull request; when reading or editing a .reladraw file; or when the user says "reladraw", "diagram this", "draw the architecture", "show me how these pieces fit". Use it in place of Mermaid, Graphviz, D2 or hand-drawn ASCII boxes whenever a node-and-line diagram is wanted and reladraw is installed. NOT for charts of data — bar, line, pie, scatter — and NOT for pictures that are not nodes and lines.Design & UI · Apache-2.0Core scenarios (what this skill handles)Design or redesign frontend UI, presentations, and graphics on the Superdesign canvas with a choice of leading AI models. Use whenever the user wants to design a page, feature, flow, slide deck, or brand-new product; improve or reproduce existing UI; compare design results across top models; explore visual variants; set or extract a design system; build reusable components or multi-page flows; create presentations; or create posters and marketing graphics, even if they never say the word 'design tool'. Also supports generating supporting image or video assets when a design needs them.Design & UI · MITAstrologerCustom astrologer built in Gemini Gems. Pulls further data from specific websites for a more tailored experience. Can do: astrology & horoscopy, tarot, runes, crystals Possible additions: lenormand, astrology deck, i ching *it's called "Astrologer" because that was the original function, even though it can do more now.Content & docs · CC0-1.0