@coldtea/pr-lens-cli 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +37 -7
  2. package/dist/canvas/api.d.ts +65 -0
  3. package/dist/canvas/api.d.ts.map +1 -0
  4. package/dist/canvas/api.js +175 -0
  5. package/dist/canvas/api.js.map +1 -0
  6. package/dist/canvas/registry.d.ts +45 -0
  7. package/dist/canvas/registry.d.ts.map +1 -0
  8. package/dist/canvas/registry.js +309 -0
  9. package/dist/canvas/registry.js.map +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +17 -5
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/analyze.d.ts.map +1 -1
  14. package/dist/commands/analyze.js +3 -4
  15. package/dist/commands/analyze.js.map +1 -1
  16. package/dist/commands/canvas.d.ts +4 -0
  17. package/dist/commands/canvas.d.ts.map +1 -0
  18. package/dist/commands/canvas.js +358 -0
  19. package/dist/commands/canvas.js.map +1 -0
  20. package/dist/commands/skill.d.ts +4 -0
  21. package/dist/commands/skill.d.ts.map +1 -0
  22. package/dist/commands/skill.js +28 -0
  23. package/dist/commands/skill.js.map +1 -0
  24. package/dist/errors.d.ts +1 -1
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js.map +1 -1
  27. package/dist/io.d.ts +7 -0
  28. package/dist/io.d.ts.map +1 -1
  29. package/dist/io.js +22 -1
  30. package/dist/io.js.map +1 -1
  31. package/dist/skill-content.generated.d.ts +6 -0
  32. package/dist/skill-content.generated.d.ts.map +1 -0
  33. package/dist/skill-content.generated.js +7 -0
  34. package/dist/skill-content.generated.js.map +1 -0
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/dist/workspace.d.ts.map +1 -1
  38. package/dist/workspace.js +8 -3
  39. package/dist/workspace.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/canvas/api.ts +312 -0
  42. package/src/canvas/registry.ts +479 -0
  43. package/src/cli.ts +18 -6
  44. package/src/commands/analyze.ts +3 -8
  45. package/src/commands/canvas.ts +549 -0
  46. package/src/commands/skill.ts +41 -0
  47. package/src/errors.ts +8 -1
  48. package/src/io.ts +23 -1
  49. package/src/skill-content.generated.ts +6 -0
  50. package/src/version.ts +1 -1
  51. package/src/workspace.ts +9 -4
@@ -0,0 +1,6 @@
1
+ // Generated by `pnpm skill:sync`; edit packages/agent-skill instead.
2
+ export const SKILL_MANUAL = "---\nname: pr-lens\ndescription: \"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\"\n---\n\n# PR Lens\n\nPR Lens draws code as visually rich animated diagrams. It can represent diffs, architecture, data flows, and more.\n\nThe diff or code is represented as one JSON document (lanes, nodes, edges, ordered flows) and it renders the JSON as an animated SVG\n\n## Operating manual\n\n1. **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.\n\n If not expressing a code diff, read the code to be visually represented\n\n2. **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. Read it before you write your first one. It is quicker than reading the reference.\n\n3. **Validate, and fix**\n\n ```bash\n pr-lens validate .pr-lens/graph.json\n ```\n\n 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.\n\n4. **Render.**\n\n ```bash\n pr-lens render .pr-lens/graph.json --theme dark\n ```\n\n Render dark as the default theme unless explicitly requested. The SVGs, the manifest and `drawn.graph.json` land in `.pr-lens/`, which the CLI adds 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.\n\n 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:\n\n ```bash\n pr-lens canvas push\n ```\n\n This pushes `.pr-lens/drawn.graph.json` and 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.\n\n Pushing the same file 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. The link stays the same. If the push fails, say so and tell them where the SVGs are and which one is the top view.\n\n5. **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.\n\n 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:\n\n ```markdown\n Moves bulk sending off the per-recipient trigger and onto a batch endpoint.\n\n ![Architecture after this change: the queue route, the new bulk sender and the retired per-recipient path](.pr-lens/overview-dark-4f9bd6c1.svg)\n ```\n\n ```bash\n gh pr create --title \"Batch broadcast sends\" --body-file .pr-lens/body.md \\\n --attach .pr-lens/overview-dark-4f9bd6c1.svg\n ```\n\n 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.\n\n gh has three rules:\n - 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.\n - The alt text is the caption a reader without images gets. Say what the diagram shows, in one line.\n - `--attach` arrived in GitHub CLI 2.99. Check with `gh --version` before you write a body around it.\n\n 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.\n\n When `--attach` is not an option, publish the SVGs somewhere durable and let the CLI compose the comment instead:\n\n ```bash\n pr-lens comment \\\n --graph .pr-lens/drawn.graph.json \\\n --manifest .pr-lens/manifest.json \\\n --asset-base-url https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<dir>\n ```\n\n `--graph` takes `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.\n\nIf you would rather not author the document yourself, `pr-lens 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.\n\n## The pull request body, when there is one\n\nA 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.\n\n## What makes a document worth reading\n\n- **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\"`.\n- **Lanes are the reader's mental model** (a runtime, a tier, a boundary), not the folder tree.\n- **One hero edge**, two at the outside: the connection the change is really about.\n- **Add a flow only when there is a sequence** worth animating. One good flow beats three thin ones.\n- **Attach file refs**: they become the permalinks a reviewer clicks.\n- **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.\n\n## Choosing architecture views\n\nTreat 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.\n\nEvery 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.\n\nKeep 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`.\n\n## What the validator will catch\n\nRead `references/graph-document.md` before writing. The four failures that account for nearly everything:\n\n| Code | What you did |\n| ---------------------------- | -------------------------------------------------------------------- |\n| `BROKEN_REFERENCE` | an edge, a flow step or a view names an id you never declared |\n| `INVALID_DOCUMENT` | an invented field; the schemas are strict, unknown keys are rejected |\n| `DUPLICATE_ID` | two nodes, edges or views sharing an id |\n| `UNSUPPORTED_SCHEMA_VERSION` | `schemaVersion` is not the contract version installed |\n\nFour 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, and a patch whose two commits are the same. Always validate.\n\n## Fixing a map instead of writing one\n\nWhen 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:\n\n```yaml\nschemaVersion: 0.1.0\nmap:\n rename:\n - match: functions/src/broadcast/sendBroadcastBulk.ts\n to: Broadcast sender\n exclude:\n - \"**/*.test.ts\"\n lane:\n - match: packages/broadcast-lib/**\n lane: functions\n```\n\n`references/config.md` has the full format and the recipes. Validate it the same way: `pr-lens validate .github/pr-lens.yml`.\n\nA `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.\n\n`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.\n\n## What ships with this skill\n\nEverything you need is beside this page. Nothing here asks you to install a package first.\n\n| | |\n| ------------------------------- | ---------------------------------------------------------------------------------- |\n| `references/graph-document.md` | the document, field by field: enums, limits, and where documents actually go wrong |\n| `references/config.md` | `.github/pr-lens.yml`, the correction overlay, in full |\n| `references/example.graph.json` | one complete document that validates, to read and to copy the shape of |\n\nThe 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.\n";
3
+ export const CONFIG_REFERENCE = "# Correcting the map: `.github/pr-lens.yml`\n\nThe generated document is regenerated on every run, so editing it is pointless. Corrections live in `.github/pr-lens.yml`, an overlay applied over fresh inference every time. Inference never writes back into this file, which is why a correction keeps holding as the code moves.\n\n```yaml\nschemaVersion: 0.1.0 # required\nlenses: [architecture, data-flow]\nbranding: true\nmap:\n rename:\n - match: functions/src/broadcast/sendBroadcastBulk.ts\n to: Broadcast sender\n exclude:\n - \"**/*.test.ts\"\n - scripts/**\n lane:\n - match: packages/broadcast-lib/**\n lane: functions\n group:\n - match: id:build-bulk-payload\n group: broadcast-lib\n```\n\nEvery field except `schemaVersion` is optional, and the file itself is optional. For editor autocomplete, point at the published JSON Schema — no install needed:\n\n```jsonc\n{ \"$ref\": \"https://unpkg.com/@coldtea/pr-lens-schema/json-schema/config.schema.json\" }\n```\n\n## Selectors\n\nA `match` beginning with `id:` addresses exactly one node, as in `id:build-bulk-payload`. Anything else is a repository-relative path glob matched against the node's file paths.\n\n**Prefer the glob.** Ids come from inference and may change when the code does; a path correction survives that. Reach for `id:` only when no path distinguishes the node, or when the node has no files at all (an external service, a queue).\n\n## The four corrections\n\n| | What it does |\n| --- | --- |\n| `rename` | replaces the inferred label |\n| `exclude` | drops matching nodes, and the edges and flow steps that hung from them |\n| `lane` | moves matching nodes into a lane, **creating it** when the document declares no such id |\n| `group` | clusters matching nodes under a sub-group inside their lane |\n\nUp to 128 of each. They are about intent rather than structure: there is no way to add a node or draw an edge here, and the one thing a correction can bring into existence is a lane, a band a repository wants that inference did not find. It takes the id for its label, because the id is the only name this file carries, so write `lane: infrastructure` rather than `lane: l3`. If the map is wrong in a way corrections cannot express, the fix belongs in the analysis, not in this file.\n\n## Recipes\n\n**\"Stop showing me the test files.\"**\n```yaml\nmap:\n exclude: [\"**/*.test.ts\", \"**/__tests__/**\"]\n```\n\n**\"That node is called the wrong thing.\"** Match the file it comes from, not its id:\n```yaml\nmap:\n rename:\n - match: server/lib/broadcast/createBroadcastSendTask.ts\n to: Send task\n```\n\n**\"These belong in a band of their own.\"** The lane need not exist yet:\n```yaml\nmap:\n lane:\n - match: infra/**\n lane: infrastructure\n```\n\n**\"Keep the shared library together.\"**\n```yaml\nmap:\n group:\n - match: packages/broadcast-lib/**\n group: broadcast-lib\n```\n\n**\"Only draw the architecture.\"**\n```yaml\nlenses: [architecture]\n```\n\n## Check it\n\n```bash\npr-lens validate .github/pr-lens.yml\n```\n\n`pr-lens render` reports any correction that changed nothing about the document it drew. That is a config that has drifted out of date, usually because the file a selector named has moved or gone. It is not an error and nothing stops, but it is worth fixing: a correction that matches nothing is a correction nobody is getting.\n";
4
+ export const GRAPH_DOCUMENT_REFERENCE = "# Authoring a graph document\n\nThis page is the whole shape, and what a schema cannot tell you besides: which parts matter, and where documents actually go wrong. `references/example.graph.json` is one document that validates, if you would rather read than be told.\n\nThe validator enforces the same thing from a JSON Schema, published at `https://unpkg.com/@coldtea/pr-lens-schema/json-schema/graph-doc.schema.json` if you want it machine-readable.\n\nEvery schema here is **strict**: an unknown key is a rejection, not a warning. A field with a default may be left out.\n\n## The document\n\n```json\n{\n \"schemaVersion\": \"0.1.0\",\n \"kind\": \"graph\",\n \"title\": \"Batch broadcast sending through Postmark\",\n \"summary\": \"One paragraph answering: what does this change do?\",\n \"lenses\": [\"architecture\", \"data-flow\"],\n \"provenance\": { \"repo\": { \"owner\": \"…\", \"name\": \"…\" }, \"base\": { \"sha\": \"…\" }, \"head\": { \"sha\": \"…\" } },\n \"lanes\": [],\n \"nodes\": [],\n \"edges\": [],\n \"flows\": [],\n \"stats\": {},\n \"views\": []\n}\n```\n\n`lenses` declares what the document carries enough detail to draw: `architecture`, `data-flow`, or both. A document carrying flows must declare `data-flow`.\n\n`provenance` is where the document came from: the repository, the base and head commit shas (lowercase hex, 7-40 characters), optionally the pull request and the generator. When you produce a document through the CLI these are filled in from the repository, so do not invent them.\n\n## Ids\n\n`^[A-Za-z0-9][A-Za-z0-9._:/-]*$`, at most 128 characters, unique within their own collection. Use readable kebab-case: `broadcast-sender`, not `n1`. An id ends up in an SVG id, a URL fragment and a comment anchor, so nothing else is allowed through.\n\n## Deltas\n\nEvery node, edge, flow and flow step declares one: `added`, `modified`, `removed`, `unchanged`.\n\n`unchanged` is not padding. It is the neighbouring code the change touches, and it is what turns a diagram into a blast radius. A document whose every element is `added` describes a change nobody can place.\n\n## Lanes\n\n1 to 16. Every node belongs to exactly one.\n\n```json\n{ \"id\": \"functions\", \"label\": \"Cloud Functions\", \"subtitle\": \"Node 20\", \"order\": 1 }\n```\n\n`order` (0-64) places lanes left to right; ties fall back to array order. Give a lane a `delta` only when the lane itself is new or gone.\n\n## Nodes\n\n1 to 256.\n\n```json\n{\n \"id\": \"send-broadcast-bulk\",\n \"label\": \"sendBroadcastBulk\",\n \"kind\": \"function\",\n \"delta\": \"added\",\n \"lane\": \"functions\",\n \"group\": \"broadcast-lib\",\n \"subtitle\": \"(broadcastId) => Promise<void>\",\n \"summary\": \"Claims the broadcast, builds one bulk payload and posts it.\",\n \"files\": [{ \"path\": \"functions/src/broadcast/sendBroadcastBulk.ts\", \"startLine\": 1, \"endLine\": 142 }],\n \"badges\": [\"retry\"]\n}\n```\n\n`kind` is one of `service app module function route job queue datastore cache external ui config test package other`. It drives the card's icon and shape and nothing else; when in doubt, `other` still renders.\n\n`group` clusters nodes inside a lane: a package, a folder that means something. `files` (up to 64) become diff permalinks. `badges` (up to 6) are extra chips; the delta badge is drawn for you, so do not restate it.\n\n## Edges\n\nUp to 512.\n\n```json\n{\n \"id\": \"bulk-to-postmark\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"postmark\",\n \"kind\": \"http\",\n \"delta\": \"added\",\n \"label\": \"POST /email/bulk\",\n \"emphasis\": \"hero\",\n \"animated\": true\n}\n```\n\n`kind` is one of `call http rpc event queue data dependency render other`. `emphasis` is `normal` (default), `hero` or `muted`. More than one or two heroes and the emphasis stops meaning anything. `from` and `to` must be node ids you declared. This is the single most common failure.\n\n## Flows\n\nUp to 16, for the data-flow lens.\n\n```json\n{\n \"id\": \"send-pipeline\",\n \"title\": \"Sending a broadcast\",\n \"delta\": \"modified\",\n \"participants\": [{ \"node\": \"queue-route\" }, { \"node\": \"send-broadcast-bulk\" }, { \"node\": \"postmark\" }],\n \"messages\": [\n { \"id\": \"enqueue\", \"from\": \"queue-route\", \"to\": \"send-broadcast-bulk\", \"label\": \"enqueue job\", \"kind\": \"async\", \"delta\": \"modified\" },\n { \"id\": \"send\", \"from\": \"send-broadcast-bulk\", \"to\": \"postmark\", \"label\": \"POST /email/bulk\", \"kind\": \"sync\", \"delta\": \"added\", \"repeat\": 4 },\n { \"id\": \"accepted\", \"from\": \"postmark\", \"to\": \"send-broadcast-bulk\", \"label\": \"200 Accepted\", \"kind\": \"return\", \"delta\": \"added\" }\n ]\n}\n```\n\n- 2 to 12 participants, ordered by array position; each names a node id.\n- 1 to 64 messages. **Step order is array order**: there is no step number field, so a document cannot disagree with its own animation.\n- `kind` is `sync`, `async`, `return` or `self`. `self` requires `from === to`, and no other kind may have them equal.\n- Both endpoints must be participants of that flow, not merely nodes of the document.\n- `repeat` says a step happens more than once per run, e.g. 4 batched requests.\n\n## Stats\n\n```json\n{ \"filesChanged\": 27, \"additions\": 1979, \"deletions\": 1370, \"chips\": [{ \"label\": \"Postmark calls\", \"value\": \"500x fewer\", \"tone\": \"hero\" }] }\n```\n\nUp to 8 chips, `tone` one of `neutral added modified removed hero`. Per-delta element counts are deliberately absent from the schema: they are derivable from the document, and a stored copy can only go stale.\n\n## Views\n\nThe drill-down tree in the comment: up to 32 at the root, nesting up to 32 children each. A document with no views renders as one picture and nothing else.\n\n```json\n{\n \"id\": \"the-new-path\",\n \"title\": \"The new batch path\",\n \"lens\": \"architecture\",\n \"summary\": \"What replaced the per-recipient loop.\",\n \"defaultOpen\": false,\n \"scope\": { \"kind\": \"selection\", \"nodes\": [\"send-broadcast-bulk\", \"postmark\"] },\n \"children\": []\n}\n```\n\n`scope` is either `{ \"kind\": \"all\" }` (the default) or a selection naming at least one lane, node, edge or flow. The two are distinct states on purpose: removing the last element a view pointed at can never quietly turn it into a view of everything. A view's `lens` must be one the document declares.\n\n### Choosing architecture views\n\nTreat the architecture tree as a set of decisions, not a quota:\n\n1. Ask whether the change affects a user, an external system or a system boundary. If it does, start with a system-context view. If it does not, leave that level out.\n2. Show affected applications, services, jobs, data stores and runtimes in a container view. Make it the root when there is no useful context view; otherwise make it a child of that context.\n3. Add a component child only when the internals of an affected container matter to the change. Components may be modules, routes or functions, but the view should explain their responsibilities and relationships rather than mirror folders.\n4. Stop at components unless someone explicitly asks for code-level detail.\n\nOne architecture view may be the right answer for a small change. Each child must move down exactly one level and cover a materially narrower scope. Skip a level when it would be empty, speculative or a repeat of its parent. Do not create two views with substantially the same nodes and edges, and do not infer a boundary from a folder name alone. Keep unchanged direct neighbours when they make the blast radius clear.\n\nSet `defaultOpen: true` on the highest useful architecture view. Lower levels should normally stay collapsed. A data-flow view describes an ordered sequence, so keep it as a separate root instead of placing it inside the architecture hierarchy.\n\nThis compact fragment shows the shape. The selected ids refer to elements declared elsewhere in the document:\n\n```json\n{\n \"views\": [\n {\n \"id\": \"checkout-context\",\n \"title\": \"Checkout in its environment\",\n \"lens\": \"architecture\",\n \"defaultOpen\": true,\n \"scope\": {\n \"kind\": \"selection\",\n \"nodes\": [\"shopper\", \"commerce-platform\", \"payment-provider\", \"fulfilment-system\"],\n \"edges\": [\"shopper-to-commerce\", \"commerce-to-payment\", \"commerce-to-fulfilment\"]\n },\n \"children\": [\n {\n \"id\": \"checkout-containers\",\n \"title\": \"Checkout containers\",\n \"lens\": \"architecture\",\n \"scope\": {\n \"kind\": \"selection\",\n \"nodes\": [\"storefront\", \"checkout-api\", \"orders-db\", \"payment-provider\"],\n \"edges\": [\"storefront-to-checkout\", \"checkout-to-orders\", \"checkout-to-payment\"]\n },\n \"children\": [\n {\n \"id\": \"checkout-components\",\n \"title\": \"Checkout API components\",\n \"lens\": \"architecture\",\n \"scope\": {\n \"kind\": \"selection\",\n \"nodes\": [\"checkout-route\", \"order-service\", \"payment-client\"],\n \"edges\": [\"route-to-orders\", \"orders-to-payment-client\"]\n }\n }\n ]\n }\n ]\n },\n {\n \"id\": \"place-order-flow\",\n \"title\": \"Placing an order\",\n \"lens\": \"data-flow\",\n \"scope\": { \"kind\": \"selection\", \"flows\": [\"place-order\"] }\n }\n ]\n}\n```\n\n## Layout\n\n```json\n{ \"direction\": \"right\", \"laneOrder\": [\"api\", \"functions\", \"external\"], \"rank\": { \"send-broadcast-bulk\": 2 } }\n```\n\nHints, not instructions: the renderer owns final placement, so a diagram stays deterministic and a stale hint cannot break it. Absolute coordinates are not expressible. Omitting `layout` entirely is normal.\n\n## File references\n\n```json\n{ \"path\": \"functions/src/broadcast/sendBroadcastBulk.ts\", \"startLine\": 1, \"endLine\": 142, \"revision\": \"head\" }\n```\n\nRepository-relative POSIX paths: no leading `/`, no drive letter, no backslash, no `..` segment. Lines are 1-based, `endLine` requires `startLine` and may not precede it. `revision` defaults to `head`; use `base` on elements the change removes.\n\n## Length limits\n\nLabels 120 characters, summaries 2000, chip values 32. They are display fields: a label that needs 120 characters is a label the diagram cannot draw.\n\n## Then validate\n\n```bash\npr-lens validate .pr-lens/graph.json\n```\n\nEvery problem is reported at once, with a path into the document. Fix them all and run it again until it is clean.\n";
5
+ export const EXAMPLE_GRAPH_DOCUMENT = "{\n \"schemaVersion\": \"0.1.0\",\n \"kind\": \"graph\",\n \"generatedAt\": \"2026-08-19T18:24:00.000Z\",\n \"title\": \"Batch broadcast sending through Postmark\",\n \"summary\": \"Broadcast delivery moves from one Postmark request per recipient to batched requests of 500, with suppression filtering pulled in front of the send and the payload builder extracted into a shared library.\",\n \"lenses\": [\n \"architecture\",\n \"data-flow\"\n ],\n \"provenance\": {\n \"repo\": {\n \"owner\": \"ohansemmanuel\",\n \"name\": \"bestregards\",\n \"host\": \"github.com\"\n },\n \"base\": {\n \"sha\": \"3f5c1ab9d24e7f08c6b1a5d3e9074c2b8a6f1d40\",\n \"ref\": \"main\"\n },\n \"head\": {\n \"sha\": \"b71e0d4c8a92f5361de7c0b4a8f2593d6c1e8a77\",\n \"ref\": \"batch-broadcast-send\"\n },\n \"pullRequest\": {\n \"number\": 128,\n \"title\": \"Send broadcasts in batches of 500\",\n \"url\": \"https://github.com/ohansemmanuel/bestregards/pull/128\"\n },\n \"generator\": {\n \"name\": \"pr-lens-examples\",\n \"version\": \"0.1.0\"\n }\n },\n \"lanes\": [\n {\n \"id\": \"web\",\n \"label\": \"Next.js\",\n \"subtitle\": \"Vercel\",\n \"order\": 0\n },\n {\n \"id\": \"functions\",\n \"label\": \"Cloud Functions\",\n \"subtitle\": \"Firebase\",\n \"order\": 1\n },\n {\n \"id\": \"external\",\n \"label\": \"External\",\n \"subtitle\": \"Postmark\",\n \"order\": 2\n }\n ],\n \"nodes\": [\n {\n \"id\": \"broadcast-composer\",\n \"label\": \"Broadcast composer\",\n \"kind\": \"ui\",\n \"delta\": \"unchanged\",\n \"lane\": \"web\",\n \"subtitle\": \"app/broadcasts/new\",\n \"summary\": \"Where an author writes a broadcast and hits send. Untouched by this change.\",\n \"files\": [\n {\n \"path\": \"app/broadcasts/new/page.tsx\"\n }\n ],\n \"badges\": []\n },\n {\n \"id\": \"queue-route\",\n \"label\": \"POST /api/broadcasts/queue\",\n \"kind\": \"route\",\n \"delta\": \"modified\",\n \"lane\": \"web\",\n \"summary\": \"Writes the queue document. Now stamps the recipient count and batch size the sender will use instead of leaving batching to the worker.\",\n \"files\": [\n {\n \"path\": \"app/api/broadcasts/queue/route.ts\",\n \"startLine\": 24,\n \"endLine\": 96\n }\n ],\n \"badges\": [\n \"+38 / -12\"\n ]\n },\n {\n \"id\": \"broadcast-queue\",\n \"label\": \"broadcastQueue\",\n \"kind\": \"datastore\",\n \"delta\": \"modified\",\n \"lane\": \"functions\",\n \"subtitle\": \"Firestore collection\",\n \"summary\": \"Queue documents gained batchSize and suppressedCount fields, and results are now written back per batch rather than per recipient.\",\n \"files\": [\n {\n \"path\": \"functions/src/broadcast/schema.ts\",\n \"startLine\": 12,\n \"endLine\": 48\n }\n ],\n \"badges\": []\n },\n {\n \"id\": \"send-broadcast-bulk\",\n \"label\": \"sendBroadcastBulk\",\n \"kind\": \"function\",\n \"delta\": \"added\",\n \"lane\": \"functions\",\n \"subtitle\": \"onWrite trigger\",\n \"summary\": \"New trigger handler. Fetches suppressions once, builds batched payloads, and posts them to Postmark in chunks of 500.\",\n \"files\": [\n {\n \"path\": \"functions/src/broadcast/sendBroadcastBulk.ts\",\n \"startLine\": 1,\n \"endLine\": 142\n }\n ],\n \"badges\": [\n \"new\"\n ]\n },\n {\n \"id\": \"build-bulk-payload\",\n \"label\": \"buildBulkPayload\",\n \"kind\": \"function\",\n \"delta\": \"added\",\n \"lane\": \"functions\",\n \"summary\": \"Turns a broadcast and its recipient slice into a Postmark batch request body.\",\n \"files\": [\n {\n \"path\": \"packages/broadcast-lib/src/buildBulkPayload.ts\",\n \"startLine\": 1,\n \"endLine\": 74\n }\n ],\n \"badges\": []\n },\n {\n \"id\": \"get-suppressed-emails\",\n \"label\": \"getSuppressedEmails\",\n \"kind\": \"function\",\n \"delta\": \"added\",\n \"lane\": \"functions\",\n \"summary\": \"Pulls the Postmark suppression dump once per broadcast so suppressed addresses are filtered before any batch is sent.\",\n \"files\": [\n {\n \"path\": \"packages/broadcast-lib/src/getSuppressedEmails.ts\",\n \"startLine\": 1,\n \"endLine\": 58\n }\n ],\n \"badges\": []\n },\n {\n \"id\": \"broadcast-lib\",\n \"label\": \"broadcast-lib\",\n \"kind\": \"package\",\n \"delta\": \"added\",\n \"lane\": \"functions\",\n \"subtitle\": \"packages/broadcast-lib\",\n \"summary\": \"New shared package so the queue route and the sender agree on payload shape and batch size.\",\n \"files\": [\n {\n \"path\": \"packages/broadcast-lib/src/index.ts\"\n }\n ],\n \"badges\": [\n \"new package\"\n ]\n },\n {\n \"id\": \"process-broadcast\",\n \"label\": \"processBroadcast\",\n \"kind\": \"function\",\n \"delta\": \"removed\",\n \"lane\": \"functions\",\n \"subtitle\": \"onWrite trigger\",\n \"summary\": \"The per-recipient loop this change replaces.\",\n \"files\": [\n {\n \"path\": \"functions/src/broadcast/processBroadcast.ts\",\n \"startLine\": 1,\n \"endLine\": 118,\n \"revision\": \"base\"\n }\n ],\n \"badges\": []\n },\n {\n \"id\": \"send-single-email\",\n \"label\": \"sendSingleEmail\",\n \"kind\": \"function\",\n \"delta\": \"removed\",\n \"lane\": \"functions\",\n \"summary\": \"One Postmark request per recipient. Gone with the loop that called it.\",\n \"files\": [\n {\n \"path\": \"functions/src/broadcast/sendSingleEmail.ts\",\n \"startLine\": 1,\n \"endLine\": 46,\n \"revision\": \"base\"\n }\n ],\n \"badges\": []\n },\n {\n \"id\": \"postmark\",\n \"label\": \"Postmark\",\n \"kind\": \"external\",\n \"delta\": \"modified\",\n \"lane\": \"external\",\n \"subtitle\": \"Email API\",\n \"summary\": \"Same provider, different endpoints: the batch endpoint and the suppression dump replace repeated single sends.\",\n \"files\": [],\n \"badges\": []\n }\n ],\n \"edges\": [\n {\n \"id\": \"composer-to-queue\",\n \"from\": \"broadcast-composer\",\n \"to\": \"queue-route\",\n \"kind\": \"http\",\n \"delta\": \"unchanged\",\n \"label\": \"send broadcast\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"queue-to-firestore\",\n \"from\": \"queue-route\",\n \"to\": \"broadcast-queue\",\n \"kind\": \"data\",\n \"delta\": \"modified\",\n \"label\": \"enqueue job\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"queue-to-lib\",\n \"from\": \"queue-route\",\n \"to\": \"broadcast-lib\",\n \"kind\": \"dependency\",\n \"delta\": \"added\",\n \"label\": \"batch size\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"firestore-to-bulk\",\n \"from\": \"broadcast-queue\",\n \"to\": \"send-broadcast-bulk\",\n \"kind\": \"event\",\n \"delta\": \"added\",\n \"label\": \"onWrite\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"firestore-to-process\",\n \"from\": \"broadcast-queue\",\n \"to\": \"process-broadcast\",\n \"kind\": \"event\",\n \"delta\": \"removed\",\n \"label\": \"onWrite\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"process-to-single\",\n \"from\": \"process-broadcast\",\n \"to\": \"send-single-email\",\n \"kind\": \"call\",\n \"delta\": \"removed\",\n \"label\": \"per recipient\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"single-to-postmark\",\n \"from\": \"send-single-email\",\n \"to\": \"postmark\",\n \"kind\": \"http\",\n \"delta\": \"removed\",\n \"label\": \"POST /email · 1 msg/call\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"bulk-to-payload\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"build-bulk-payload\",\n \"kind\": \"call\",\n \"delta\": \"added\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"bulk-to-suppressions\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"get-suppressed-emails\",\n \"kind\": \"call\",\n \"delta\": \"added\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"bulk-to-lib\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"broadcast-lib\",\n \"kind\": \"dependency\",\n \"delta\": \"added\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n },\n {\n \"id\": \"suppressions-to-postmark\",\n \"from\": \"get-suppressed-emails\",\n \"to\": \"postmark\",\n \"kind\": \"http\",\n \"delta\": \"added\",\n \"label\": \"GET suppression dump\",\n \"emphasis\": \"normal\",\n \"animated\": true,\n \"files\": []\n },\n {\n \"id\": \"bulk-to-postmark\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"postmark\",\n \"kind\": \"http\",\n \"delta\": \"added\",\n \"label\": \"500 msgs/call\",\n \"emphasis\": \"hero\",\n \"animated\": true,\n \"summary\": \"The change in one edge: a broadcast to 10,000 recipients drops from 10,000 requests to 20.\",\n \"files\": []\n },\n {\n \"id\": \"bulk-to-firestore\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"broadcast-queue\",\n \"kind\": \"data\",\n \"delta\": \"added\",\n \"label\": \"write results\",\n \"emphasis\": \"normal\",\n \"animated\": false,\n \"files\": []\n }\n ],\n \"flows\": [\n {\n \"id\": \"send-pipeline\",\n \"title\": \"Sending a broadcast\",\n \"summary\": \"The path a queued broadcast takes now, from enqueue to per-message results.\",\n \"delta\": \"modified\",\n \"participants\": [\n {\n \"node\": \"queue-route\",\n \"label\": \"queue route\"\n },\n {\n \"node\": \"broadcast-queue\",\n \"label\": \"Firestore\"\n },\n {\n \"node\": \"send-broadcast-bulk\",\n \"label\": \"sendBroadcastBulk\"\n },\n {\n \"node\": \"postmark\",\n \"label\": \"Postmark\"\n }\n ],\n \"messages\": [\n {\n \"id\": \"enqueue\",\n \"from\": \"queue-route\",\n \"to\": \"broadcast-queue\",\n \"label\": \"enqueue broadcast job\",\n \"kind\": \"async\",\n \"delta\": \"modified\",\n \"animated\": true,\n \"files\": []\n },\n {\n \"id\": \"trigger\",\n \"from\": \"broadcast-queue\",\n \"to\": \"send-broadcast-bulk\",\n \"label\": \"onWrite trigger\",\n \"kind\": \"async\",\n \"delta\": \"added\",\n \"animated\": true,\n \"files\": []\n },\n {\n \"id\": \"suppressions-request\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"postmark\",\n \"label\": \"GET suppression dump\",\n \"kind\": \"sync\",\n \"delta\": \"added\",\n \"animated\": true,\n \"files\": []\n },\n {\n \"id\": \"suppressions-response\",\n \"from\": \"postmark\",\n \"to\": \"send-broadcast-bulk\",\n \"label\": \"suppressed addresses\",\n \"kind\": \"return\",\n \"delta\": \"added\",\n \"animated\": true,\n \"note\": \"Fetched once per broadcast, not once per recipient.\",\n \"files\": []\n },\n {\n \"id\": \"batch-post\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"postmark\",\n \"label\": \"POST /email/batch · 500 msgs\",\n \"kind\": \"sync\",\n \"delta\": \"added\",\n \"animated\": true,\n \"repeat\": 4,\n \"note\": \"One request per 500 recipients; four for this 2,000-recipient broadcast.\",\n \"files\": []\n },\n {\n \"id\": \"batch-results\",\n \"from\": \"postmark\",\n \"to\": \"send-broadcast-bulk\",\n \"label\": \"per-message results\",\n \"kind\": \"return\",\n \"delta\": \"added\",\n \"animated\": true,\n \"files\": []\n },\n {\n \"id\": \"write-results\",\n \"from\": \"send-broadcast-bulk\",\n \"to\": \"broadcast-queue\",\n \"label\": \"write results\",\n \"kind\": \"async\",\n \"delta\": \"added\",\n \"animated\": true,\n \"files\": []\n }\n ]\n }\n ],\n \"stats\": {\n \"filesChanged\": 14,\n \"additions\": 486,\n \"deletions\": 212,\n \"chips\": [\n {\n \"label\": \"Postmark calls\",\n \"value\": \"500× fewer\",\n \"tone\": \"hero\"\n },\n {\n \"label\": \"New\",\n \"value\": \"4 units\",\n \"tone\": \"added\"\n },\n {\n \"label\": \"Retired\",\n \"value\": \"2 units\",\n \"tone\": \"removed\"\n }\n ]\n },\n \"views\": [\n {\n \"id\": \"overview\",\n \"title\": \"Architecture — blast radius\",\n \"lens\": \"architecture\",\n \"summary\": \"Everything this change touches, across all three lanes.\",\n \"scope\": {\n \"kind\": \"all\"\n },\n \"defaultOpen\": true,\n \"children\": [\n {\n \"id\": \"new-batch-path\",\n \"title\": \"The new batch path\",\n \"lens\": \"architecture\",\n \"summary\": \"What replaced the per-recipient loop.\",\n \"scope\": {\n \"kind\": \"selection\",\n \"lanes\": [],\n \"nodes\": [\n \"send-broadcast-bulk\",\n \"build-bulk-payload\",\n \"get-suppressed-emails\",\n \"broadcast-lib\",\n \"postmark\"\n ],\n \"edges\": [\n \"bulk-to-payload\",\n \"bulk-to-suppressions\",\n \"bulk-to-lib\",\n \"suppressions-to-postmark\",\n \"bulk-to-postmark\",\n \"bulk-to-firestore\"\n ],\n \"flows\": []\n },\n \"defaultOpen\": false,\n \"children\": []\n },\n {\n \"id\": \"retired-path\",\n \"title\": \"What was retired\",\n \"lens\": \"architecture\",\n \"summary\": \"The single-send path, kept visible so a reviewer can confirm nothing else called it.\",\n \"scope\": {\n \"kind\": \"selection\",\n \"lanes\": [],\n \"nodes\": [\n \"process-broadcast\",\n \"send-single-email\"\n ],\n \"edges\": [\n \"firestore-to-process\",\n \"process-to-single\",\n \"single-to-postmark\"\n ],\n \"flows\": []\n },\n \"defaultOpen\": false,\n \"children\": []\n }\n ]\n },\n {\n \"id\": \"send-pipeline-view\",\n \"title\": \"Data flow — sending a broadcast\",\n \"lens\": \"data-flow\",\n \"scope\": {\n \"kind\": \"selection\",\n \"lanes\": [],\n \"nodes\": [],\n \"edges\": [],\n \"flows\": [\n \"send-pipeline\"\n ]\n },\n \"defaultOpen\": false,\n \"children\": []\n }\n ],\n \"layout\": {\n \"direction\": \"right\",\n \"laneOrder\": [\n \"web\",\n \"functions\",\n \"external\"\n ],\n \"rank\": {\n \"queue-route\": 0,\n \"send-broadcast-bulk\": 1,\n \"postmark\": 2\n }\n }\n}\n";
6
+ export const GRAPH_DOCUMENT_JSON_SCHEMA = "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://raw.githubusercontent.com/coldteadotai/pr-lens/main/packages/schema/json-schema/graph-doc.schema.json\",\n \"title\": \"PR Lens graph document\",\n \"version\": \"0.1.0\",\n \"type\": \"object\",\n \"properties\": {\n \"schemaVersion\": {\n \"type\": \"string\",\n \"pattern\": \"^0\\\\.1\\\\.\\\\d+$\",\n \"description\": \"Contract version the document targets. Current: 0.1.0.\"\n },\n \"kind\": {\n \"type\": \"string\",\n \"const\": \"graph\"\n },\n \"id\": {\n \"description\": \"Stable id when the document is stored, e.g. a baseline map.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\"\n },\n \"generatedAt\": {\n \"type\": \"string\",\n \"format\": \"date-time\",\n \"pattern\": \"^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$\"\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120,\n \"description\": \"Short display label.\"\n },\n \"summary\": {\n \"description\": \"The one-paragraph answer to 'what does this change do?'\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 2000\n },\n \"lenses\": {\n \"minItems\": 1,\n \"maxItems\": 8,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"architecture\",\n \"data-flow\"\n ],\n \"description\": \"Rendering lens.\"\n },\n \"description\": \"Lenses this document carries enough detail to render.\"\n },\n \"provenance\": {\n \"type\": \"object\",\n \"properties\": {\n \"repo\": {\n \"type\": \"object\",\n \"properties\": {\n \"owner\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 64\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128\n },\n \"host\": {\n \"default\": \"github.com\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128\n }\n },\n \"required\": [\n \"owner\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n \"base\": {\n \"type\": \"object\",\n \"properties\": {\n \"sha\": {\n \"type\": \"string\",\n \"pattern\": \"^[0-9a-f]{7,40}$\",\n \"description\": \"Git commit sha, abbreviated or full.\"\n },\n \"ref\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 255\n }\n },\n \"required\": [\n \"sha\"\n ],\n \"additionalProperties\": false\n },\n \"head\": {\n \"type\": \"object\",\n \"properties\": {\n \"sha\": {\n \"type\": \"string\",\n \"pattern\": \"^[0-9a-f]{7,40}$\",\n \"description\": \"Git commit sha, abbreviated or full.\"\n },\n \"ref\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 255\n }\n },\n \"required\": [\n \"sha\"\n ],\n \"additionalProperties\": false\n },\n \"pullRequest\": {\n \"type\": \"object\",\n \"properties\": {\n \"number\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 512\n },\n \"url\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n }\n },\n \"required\": [\n \"number\"\n ],\n \"additionalProperties\": false\n },\n \"generator\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 64\n },\n \"version\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 32\n },\n \"model\": {\n \"description\": \"Extraction model, when one was used.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128\n }\n },\n \"required\": [\n \"name\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"repo\",\n \"base\",\n \"head\"\n ],\n \"additionalProperties\": false,\n \"description\": \"Where the document came from.\"\n },\n \"lanes\": {\n \"minItems\": 1,\n \"maxItems\": 16,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n },\n \"label\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120,\n \"description\": \"Short display label.\"\n },\n \"subtitle\": {\n \"description\": \"Secondary line in the lane header, e.g. the platform.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120\n },\n \"order\": {\n \"description\": \"Left-to-right placement. Ties fall back to array order.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 64\n },\n \"delta\": {\n \"description\": \"Set only when the lane itself is new or gone.\",\n \"type\": \"string\",\n \"enum\": [\n \"added\",\n \"modified\",\n \"removed\",\n \"unchanged\"\n ]\n },\n \"summary\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 2000,\n \"description\": \"One or two sentences of plain prose. No markdown headings.\"\n }\n },\n \"required\": [\n \"id\",\n \"label\"\n ],\n \"additionalProperties\": false,\n \"description\": \"A grouping band of the diagram. Every node belongs to exactly one lane.\"\n }\n },\n \"nodes\": {\n \"minItems\": 1,\n \"maxItems\": 256,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n },\n \"label\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120,\n \"description\": \"Short display label.\"\n },\n \"kind\": {\n \"type\": \"string\",\n \"enum\": [\n \"service\",\n \"app\",\n \"module\",\n \"function\",\n \"route\",\n \"job\",\n \"queue\",\n \"datastore\",\n \"cache\",\n \"external\",\n \"ui\",\n \"config\",\n \"test\",\n \"package\",\n \"other\"\n ]\n },\n \"delta\": {\n \"type\": \"string\",\n \"enum\": [\n \"added\",\n \"modified\",\n \"removed\",\n \"unchanged\"\n ],\n \"description\": \"Change state relative to the base commit.\"\n },\n \"lane\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Id of the lane this node sits in.\"\n },\n \"group\": {\n \"description\": \"Optional sub-cluster within the lane, e.g. a package.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\"\n },\n \"subtitle\": {\n \"description\": \"Secondary line on the card, e.g. a symbol signature.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120\n },\n \"summary\": {\n \"description\": \"Body text for this node's drill-down section.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 2000\n },\n \"files\": {\n \"default\": [],\n \"description\": \"Backing source locations, used to build diff permalinks.\",\n \"maxItems\": 64,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 1024,\n \"pattern\": \"^(?!\\\\/)(?![A-Za-z]:)(?!.*\\\\\\\\)(?!.*(?:^|\\\\/)\\\\.\\\\.(?:\\\\/|$)).+$\",\n \"description\": \"Repository-relative path, POSIX separators.\"\n },\n \"startLine\": {\n \"description\": \"1-based first line.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n },\n \"endLine\": {\n \"description\": \"1-based last line, inclusive.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n },\n \"revision\": {\n \"description\": \"Which side of the diff the lines refer to. Defaults to head.\",\n \"type\": \"string\",\n \"enum\": [\n \"head\",\n \"base\"\n ]\n }\n },\n \"required\": [\n \"path\"\n ],\n \"additionalProperties\": false,\n \"dependentRequired\": {\n \"endLine\": [\n \"startLine\"\n ]\n },\n \"description\": \"A file (and optional line range) backing an element.\"\n }\n },\n \"badges\": {\n \"default\": [],\n \"description\": \"Extra chips on the card, beyond the delta badge the renderer adds.\",\n \"maxItems\": 6,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120,\n \"description\": \"Short display label.\"\n }\n }\n },\n \"required\": [\n \"id\",\n \"label\",\n \"kind\",\n \"delta\",\n \"lane\"\n ],\n \"additionalProperties\": false,\n \"description\": \"A node in the architecture graph.\"\n }\n },\n \"edges\": {\n \"default\": [],\n \"maxItems\": 512,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n },\n \"from\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Source node id.\"\n },\n \"to\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Target node id.\"\n },\n \"kind\": {\n \"type\": \"string\",\n \"enum\": [\n \"call\",\n \"http\",\n \"rpc\",\n \"event\",\n \"queue\",\n \"data\",\n \"dependency\",\n \"render\",\n \"other\"\n ]\n },\n \"delta\": {\n \"type\": \"string\",\n \"enum\": [\n \"added\",\n \"modified\",\n \"removed\",\n \"unchanged\"\n ],\n \"description\": \"Change state relative to the base commit.\"\n },\n \"label\": {\n \"description\": \"Text on the edge, e.g. a payload size or protocol.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120\n },\n \"emphasis\": {\n \"default\": \"normal\",\n \"type\": \"string\",\n \"enum\": [\n \"normal\",\n \"hero\",\n \"muted\"\n ]\n },\n \"animated\": {\n \"default\": false,\n \"description\": \"Render a travelling pulse along this edge in the architecture lens.\",\n \"type\": \"boolean\"\n },\n \"summary\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 2000,\n \"description\": \"One or two sentences of plain prose. No markdown headings.\"\n },\n \"files\": {\n \"default\": [],\n \"maxItems\": 32,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 1024,\n \"pattern\": \"^(?!\\\\/)(?![A-Za-z]:)(?!.*\\\\\\\\)(?!.*(?:^|\\\\/)\\\\.\\\\.(?:\\\\/|$)).+$\",\n \"description\": \"Repository-relative path, POSIX separators.\"\n },\n \"startLine\": {\n \"description\": \"1-based first line.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n },\n \"endLine\": {\n \"description\": \"1-based last line, inclusive.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n },\n \"revision\": {\n \"description\": \"Which side of the diff the lines refer to. Defaults to head.\",\n \"type\": \"string\",\n \"enum\": [\n \"head\",\n \"base\"\n ]\n }\n },\n \"required\": [\n \"path\"\n ],\n \"additionalProperties\": false,\n \"dependentRequired\": {\n \"endLine\": [\n \"startLine\"\n ]\n },\n \"description\": \"A file (and optional line range) backing an element.\"\n }\n }\n },\n \"required\": [\n \"id\",\n \"from\",\n \"to\",\n \"kind\",\n \"delta\"\n ],\n \"additionalProperties\": false,\n \"description\": \"A directed connection between two nodes.\"\n }\n },\n \"flows\": {\n \"default\": [],\n \"maxItems\": 16,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120,\n \"description\": \"Short display label.\"\n },\n \"summary\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 2000,\n \"description\": \"One or two sentences of plain prose. No markdown headings.\"\n },\n \"delta\": {\n \"default\": \"modified\",\n \"type\": \"string\",\n \"enum\": [\n \"added\",\n \"modified\",\n \"removed\",\n \"unchanged\"\n ],\n \"description\": \"Change state relative to the base commit.\"\n },\n \"participants\": {\n \"minItems\": 2,\n \"maxItems\": 12,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"node\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Id of the graph node this column represents.\"\n },\n \"label\": {\n \"description\": \"Shorter name for the column when the node label is long.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120\n }\n },\n \"required\": [\n \"node\"\n ],\n \"additionalProperties\": false,\n \"description\": \"A column in the sequence diagram, ordered by array position.\"\n }\n },\n \"messages\": {\n \"minItems\": 1,\n \"maxItems\": 64,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n },\n \"from\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Participant node id the message originates from.\"\n },\n \"to\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Participant node id the message arrives at. Equals `from` when kind is self.\"\n },\n \"label\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120,\n \"description\": \"Short display label.\"\n },\n \"kind\": {\n \"default\": \"sync\",\n \"type\": \"string\",\n \"enum\": [\n \"sync\",\n \"async\",\n \"return\",\n \"self\"\n ]\n },\n \"delta\": {\n \"type\": \"string\",\n \"enum\": [\n \"added\",\n \"modified\",\n \"removed\",\n \"unchanged\"\n ],\n \"description\": \"Change state relative to the base commit.\"\n },\n \"animated\": {\n \"default\": true,\n \"description\": \"Whether the data-flow lens pulses this step.\",\n \"type\": \"boolean\"\n },\n \"repeat\": {\n \"description\": \"Times the step occurs per run, e.g. 4 batched requests.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 1000000\n },\n \"note\": {\n \"description\": \"Aside rendered beside the step in the drill-down.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 2000\n },\n \"files\": {\n \"default\": [],\n \"maxItems\": 32,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 1024,\n \"pattern\": \"^(?!\\\\/)(?![A-Za-z]:)(?!.*\\\\\\\\)(?!.*(?:^|\\\\/)\\\\.\\\\.(?:\\\\/|$)).+$\",\n \"description\": \"Repository-relative path, POSIX separators.\"\n },\n \"startLine\": {\n \"description\": \"1-based first line.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n },\n \"endLine\": {\n \"description\": \"1-based last line, inclusive.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n },\n \"revision\": {\n \"description\": \"Which side of the diff the lines refer to. Defaults to head.\",\n \"type\": \"string\",\n \"enum\": [\n \"head\",\n \"base\"\n ]\n }\n },\n \"required\": [\n \"path\"\n ],\n \"additionalProperties\": false,\n \"dependentRequired\": {\n \"endLine\": [\n \"startLine\"\n ]\n },\n \"description\": \"A file (and optional line range) backing an element.\"\n }\n }\n },\n \"required\": [\n \"id\",\n \"from\",\n \"to\",\n \"label\",\n \"delta\"\n ],\n \"additionalProperties\": false,\n \"description\": \"One ordered step in a flow.\"\n },\n \"description\": \"Ordered by array position.\"\n }\n },\n \"required\": [\n \"id\",\n \"title\",\n \"participants\",\n \"messages\"\n ],\n \"additionalProperties\": false,\n \"description\": \"An ordered message sequence for the data-flow lens.\"\n }\n },\n \"stats\": {\n \"type\": \"object\",\n \"properties\": {\n \"filesChanged\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"additions\": {\n \"description\": \"Lines added across the diff.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"deletions\": {\n \"description\": \"Lines removed across the diff.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"chips\": {\n \"default\": [],\n \"maxItems\": 8,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"label\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120,\n \"description\": \"Short display label.\"\n },\n \"value\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 32\n },\n \"tone\": {\n \"default\": \"neutral\",\n \"type\": \"string\",\n \"enum\": [\n \"neutral\",\n \"added\",\n \"modified\",\n \"removed\",\n \"hero\"\n ]\n }\n },\n \"required\": [\n \"label\",\n \"value\"\n ],\n \"additionalProperties\": false,\n \"description\": \"A headline chip above the diagram.\"\n }\n }\n },\n \"additionalProperties\": false,\n \"description\": \"Headline numbers for the comment header.\"\n },\n \"views\": {\n \"default\": [],\n \"maxItems\": 32,\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/View\"\n }\n },\n \"layout\": {\n \"type\": \"object\",\n \"properties\": {\n \"direction\": {\n \"default\": \"right\",\n \"description\": \"Primary flow direction.\",\n \"type\": \"string\",\n \"enum\": [\n \"right\",\n \"down\"\n ]\n },\n \"laneOrder\": {\n \"default\": [],\n \"description\": \"Explicit left-to-right lane order.\",\n \"maxItems\": 64,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n }\n },\n \"rank\": {\n \"description\": \"Preferred layer index per node id.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n },\n \"additionalProperties\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 256\n }\n }\n },\n \"additionalProperties\": false,\n \"description\": \"Optional, non-binding placement hints.\"\n }\n },\n \"required\": [\n \"schemaVersion\",\n \"kind\",\n \"title\",\n \"lenses\",\n \"provenance\",\n \"lanes\",\n \"nodes\"\n ],\n \"additionalProperties\": false,\n \"description\": \"A PR Lens graph document.\",\n \"$defs\": {\n \"View\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 120,\n \"description\": \"Short display label.\"\n },\n \"lens\": {\n \"type\": \"string\",\n \"enum\": [\n \"architecture\",\n \"data-flow\"\n ],\n \"description\": \"Rendering lens.\"\n },\n \"summary\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 2000,\n \"description\": \"One or two sentences of plain prose. No markdown headings.\"\n },\n \"scope\": {\n \"default\": {\n \"kind\": \"all\"\n },\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"kind\": {\n \"type\": \"string\",\n \"const\": \"all\"\n }\n },\n \"required\": [\n \"kind\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"kind\": {\n \"type\": \"string\",\n \"const\": \"selection\"\n },\n \"lanes\": {\n \"default\": [],\n \"maxItems\": 64,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n }\n },\n \"nodes\": {\n \"default\": [],\n \"maxItems\": 256,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n }\n },\n \"edges\": {\n \"default\": [],\n \"maxItems\": 512,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n }\n },\n \"flows\": {\n \"default\": [],\n \"maxItems\": 32,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 128,\n \"pattern\": \"^[A-Za-z0-9][A-Za-z0-9._:/-]*$\",\n \"description\": \"Stable identifier, unique within its collection in a document.\"\n }\n }\n },\n \"required\": [\n \"kind\"\n ],\n \"additionalProperties\": false,\n \"anyOf\": [\n {\n \"properties\": {\n \"lanes\": {\n \"minItems\": 1\n }\n },\n \"required\": [\n \"lanes\"\n ]\n },\n {\n \"properties\": {\n \"nodes\": {\n \"minItems\": 1\n }\n },\n \"required\": [\n \"nodes\"\n ]\n },\n {\n \"properties\": {\n \"edges\": {\n \"minItems\": 1\n }\n },\n \"required\": [\n \"edges\"\n ]\n },\n {\n \"properties\": {\n \"flows\": {\n \"minItems\": 1\n }\n },\n \"required\": [\n \"flows\"\n ]\n }\n ]\n }\n ],\n \"description\": \"What a drill-down section shows.\"\n },\n \"defaultOpen\": {\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"children\": {\n \"default\": [],\n \"maxItems\": 32,\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/View\"\n }\n }\n },\n \"required\": [\n \"id\",\n \"title\",\n \"lens\"\n ],\n \"additionalProperties\": false,\n \"description\": \"A drill-down section; children nest as further <details> blocks.\"\n }\n }\n}\n";
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** Recorded on every document this CLI produces, as the generator's version. */
2
- export const CLI_VERSION = "0.1.3";
2
+ export const CLI_VERSION = "0.3.0";
3
3
 
4
4
  export const GENERATOR_NAME = "pr-lens-cli";
package/src/workspace.ts CHANGED
@@ -160,13 +160,18 @@ const README = `# .pr-lens
160
160
  PR Lens writes its previews here: the diagrams as light and dark SVGs, the
161
161
  document they were drawn from, and the manifest describing them.
162
162
 
163
- None of it belongs in a commit. Every file is rebuilt from the diff by
163
+ None of that belongs in a commit. Those files are rebuilt from the diff by
164
164
  \`pr-lens analyze\` and \`pr-lens render\`, so a stale copy in the history is
165
165
  worth less than nothing — it is a diagram of a pull request somebody already
166
166
  merged. What readers are meant to see is the comment on the pull request, or
167
- the share page it links to.
168
-
169
- Delete the directory whenever you like. Nothing reads it back.
167
+ the share page it links to. Delete them whenever you like; nothing reads them
168
+ back.
169
+
170
+ \`canvas.json\` is the exception. It holds the write token for every canvas
171
+ this checkout has pushed with \`pr-lens canvas push\`, and nothing can rebuild
172
+ it. Without it the canvases stay readable by everyone, but pushing to them
173
+ again needs the edit link you were given. Keep it out of commits and out of
174
+ other people's hands.
170
175
  `;
171
176
 
172
177
  /**