feat(skills): pr-lens — animated architecture/data-flow diagrams for PRs (port of coldteadotai/pr-lens, 1.1k-star MIT)
Ports the pr-lens agent skill: represent a diff or subsystem as one graph.json document and render it as animated SVG diagrams via the MIT npx CLI (@coldtea/pr-lens-cli), with optional opt-in publishing to a shareable canvas link. Why: PR review and architecture explanation keep producing hand-drawn Mermaid; this gives validated, animated, drill-down diagrams with a deterministic document format. Upstream created Aug 20, 1.1k stars in 3 weeks, GitHub App + Action + CLI + skill. - optional-skills/software-development/pr-lens/: SKILL.md (145 lines), references/ (config, graph document format, valid example) vendored near-verbatim, LICENSE.txt (MIT, Coldtea AI) - gh --attach caveat handled: installed gh 2.97 lacks the flag; skill documents honest fallbacks (gist, canvas link, local path) - Live smoke: validate + render of the vendored example graph passed (4 SVGs + manifest produced) - docs: own catalog row + generated page + sidebar entry only
This commit is contained in:
21
optional-skills/software-development/pr-lens/LICENSE.txt
Normal file
21
optional-skills/software-development/pr-lens/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Coldtea AI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
145
optional-skills/software-development/pr-lens/SKILL.md
Normal file
145
optional-skills/software-development/pr-lens/SKILL.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: pr-lens
|
||||
description: "Draw code changes as animated architecture/data-flow SVGs."
|
||||
version: 1.0.0
|
||||
author: Coldtea AI (adapted by Nous Research)
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [diagrams, pull-requests, code-review, svg]
|
||||
category: software-development
|
||||
related_skills: []
|
||||
upstream: https://github.com/coldteadotai/pr-lens (pinned 0993b4d)
|
||||
---
|
||||
|
||||
# PR Lens Skill
|
||||
|
||||
PR Lens draws code as visually rich animated diagrams: diffs, architecture, data flows. You describe the diff or codebase as one JSON document (lanes, nodes, edges, ordered flows) and the CLI renders it as animated SVGs. There is no findings lens — PR Lens is a comprehension layer, not a review bot. There is no field for a bug, risk, or security note, and a document that invents one is rejected.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Asked to diagram, visualise, or explain a code change or a system.
|
||||
- A pull request should carry an architecture or data-flow diagram.
|
||||
- Keywords: PR Lens, diagram, architecture, data flow, visualise, pull request.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js with `npx` (the CLI runs via `npx @coldtea/pr-lens-cli@latest`; no install step).
|
||||
- `gh` (GitHub CLI) — optional, only for attaching diagrams to PRs.
|
||||
- Optional canvas publishing calls the third-party service prlens.dev (see step 4b).
|
||||
|
||||
## How to Run
|
||||
|
||||
Run all commands with the terminal tool from the repository root.
|
||||
|
||||
1. **Read the diff.** When representing 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 diff, read the code to be visualised.
|
||||
|
||||
2. **Write the document** to `.pr-lens/graph.json`, following `references/graph-document.md`. `references/example.graph.json` is a 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 writing your first document — quicker than reading the reference.
|
||||
|
||||
3. **Validate, and fix.**
|
||||
|
||||
```bash
|
||||
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.**
|
||||
|
||||
```bash
|
||||
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 `.pr-lens/`, which the CLI adds to the repository's .gitignore. Do not commit any of it — these files are rebuilt from the diff on demand. Each SVG is named after its view, theme and content hash; `manifest.json` lists them by lens and view.
|
||||
|
||||
4b. **Canvas push — OPTIONAL, opt-in.** Only when the user explicitly asks for a shareable link. This publishes `.pr-lens/drawn.graph.json` to the third-party service prlens.dev:
|
||||
|
||||
```bash
|
||||
npx @coldtea/pr-lens-cli@latest canvas push
|
||||
```
|
||||
|
||||
It prints three links. Give the user the **view link** (`https://prlens.dev/c/{id}`): the full-screen diagram, every view on one page, no login. The **edit link** (ending in `#w=…`) lets its holder overwrite the canvas — it is a secret: leave it out of the reply unless asked, never paste it anywhere public. The embed link serves the top view as an SVG for a README. Pushing the same file again updates the same canvas, so "rename that node" is: edit, validate, render, push — the link stays the same. If the push fails, say so and tell the user where the local SVGs are and which is the top view.
|
||||
|
||||
5. **Attach to a PR, when there is one.** Upstream documents `gh pr create/edit/comment --attach <path>`, but `--attach` arrived in GitHub CLI 2.99 — check `gh --version` first (e.g. gh 2.97 does NOT have it). With gh ≥ 2.99: write the body with a Markdown image `` (an HTML `<img>` is left as written and the file appended at the bottom instead; alt text is the one-line caption a reader without images gets), then repeat `--attach <path>` per referenced diagram:
|
||||
|
||||
```bash
|
||||
gh pr create --title "…" --body-file .pr-lens/body.md --attach .pr-lens/overview-light-<hash>.svg
|
||||
```
|
||||
|
||||
Without `--attach`, use a commit-free path:
|
||||
- Upload the SVGs to a gist: `gh gist create .pr-lens/<view>.svg`, then reference the raw gist URL in the PR body/comment, or
|
||||
- Publish via the canvas link (step 4b, with user consent) and link the view URL, or
|
||||
- Note the local `.pr-lens/` path in the PR body so reviewers can rebuild.
|
||||
|
||||
Once published somewhere durable, let the CLI compose the comment markdown:
|
||||
|
||||
```bash
|
||||
npx @coldtea/pr-lens-cli@latest comment \
|
||||
--graph .pr-lens/drawn.graph.json \
|
||||
--manifest .pr-lens/manifest.json \
|
||||
--asset-base-url <where you published the SVGs>
|
||||
```
|
||||
|
||||
`--graph` takes `drawn.graph.json`, not the document you wrote — the CLI refuses a document its manifest does not describe. Leave out `--asset-base-url` and the markdown points at local paths no reader can fetch. The markdown goes to stdout; posting it is your business.
|
||||
|
||||
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. Two diagrams usually beat four.
|
||||
|
||||
6. **Optional automation:** `npx @coldtea/pr-lens-cli@latest analyze --base <ref>` does steps 1–2 by asking a provider (Gemini, OpenAI, or any `/chat/completions` endpoint) with a key of your own. That is the only path here that needs one; normally you author the document yourself.
|
||||
|
||||
## What makes a document worth reading
|
||||
|
||||
- **Include what did not change.** 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.
|
||||
- Architecture views are a C4-inspired decision tree: system context → container → component, each child materially narrower. Skip empty or repetitive levels; keep data-flow views as separate roots; set `defaultOpen: true` on the highest useful architecture view.
|
||||
- **Walkthroughs** (2–12 steps, aim 3–7): write one for anything non-trivial. Each step = one change (added/removed/moved), headline change first, overview last. Headings ≤48 chars built from change words; bodies ≤140 chars on behaviour, required. Write for a smart twelve-year-old; no "leverages"/"orchestrates". Keep consecutive steps on the same stage. The walkthrough field needs CLI ≥ 0.4.0 (contract 0.1.1).
|
||||
- **Fixing a wrong map:** never edit the generated document — write corrections into `.github/pr-lens.yml` (see `references/config.md`), then validate it: `npx @coldtea/pr-lens-cli@latest validate .github/pr-lens.yml`. Prefer path globs over `id:` matches.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `npx @coldtea/pr-lens-cli@latest validate .pr-lens/graph.json` | validate the document (also validates `.github/pr-lens.yml`) |
|
||||
| `npx @coldtea/pr-lens-cli@latest render .pr-lens/graph.json --theme light` | render SVGs + manifest into `.pr-lens/` |
|
||||
| `npx @coldtea/pr-lens-cli@latest canvas push` | OPTIONAL: publish to prlens.dev (opt-in only) |
|
||||
| `npx @coldtea/pr-lens-cli@latest comment --graph … --manifest … --asset-base-url …` | compose PR comment markdown to stdout |
|
||||
| `npx @coldtea/pr-lens-cli@latest analyze --base <ref>` | auto-author document via an LLM provider (needs API key) |
|
||||
|
||||
Validator failure codes:
|
||||
|
||||
| Code | What you did |
|
||||
| --- | --- |
|
||||
| `BROKEN_REFERENCE` | an edge, flow step, view or 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 |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Six rules are parser-only, not in the JSON Schema (referential integrity, inverted line ranges, disagreeing `self` endpoints, identical patch commits, too many views for a manifest, flow-step focus on the wrong stage) — always run `validate`, structured output alone is not enough.
|
||||
- Do not commit anything in `.pr-lens/`; it is regenerated and gitignored by the CLI.
|
||||
- The `--attach` gh flag needs gh ≥ 2.99; older gh silently lacks it — check before writing a body around it.
|
||||
- The canvas edit link (`#w=…`) is a write credential — never share it unprompted or paste it publicly.
|
||||
- A stored map never carries a walkthrough; a walkthrough tells the story of one change.
|
||||
- `pr-lens render` reports corrections in `.github/pr-lens.yml` that matched nothing — that is drift worth fixing, not an error.
|
||||
|
||||
## Verification
|
||||
|
||||
Smoke test (live-verified 2026-09-12 with `@coldtea/pr-lens-cli` via npx, node on Linux):
|
||||
|
||||
```bash
|
||||
cp references/example.graph.json /tmp/prlens-smoke/ && cd /tmp/prlens-smoke
|
||||
npx -y @coldtea/pr-lens-cli@latest validate example.graph.json
|
||||
# ✓ example.graph.json — graph document · 3 lanes, 10 nodes, 13 edges, 1 flow · 6 walkthrough steps
|
||||
npx -y @coldtea/pr-lens-cli@latest render example.graph.json --theme light
|
||||
# ✓ .pr-lens/manifest.json — 4 SVGs across 4 diagrams
|
||||
```
|
||||
|
||||
Expect exit 0 on both and four `*-light-<hash>.svg` files plus `manifest.json` and `drawn.graph.json` in `.pr-lens/`.
|
||||
|
||||
---
|
||||
|
||||
Adapted from [coldteadotai/pr-lens](https://github.com/coldteadotai/pr-lens) (packages/agent-skill, pinned 0993b4d), MIT License, Copyright (c) 2026 Coldtea AI. See LICENSE.txt.
|
||||
@@ -0,0 +1,100 @@
|
||||
When to load: fixing or overriding a generated map via the .github/pr-lens.yml correction overlay.
|
||||
|
||||
# Correcting the map: `.github/pr-lens.yml`
|
||||
|
||||
The 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.
|
||||
|
||||
```yaml
|
||||
schemaVersion: 0.1.1 # required
|
||||
lenses: [architecture, data-flow]
|
||||
branding: true
|
||||
map:
|
||||
rename:
|
||||
- match: functions/src/broadcast/sendBroadcastBulk.ts
|
||||
to: Broadcast sender
|
||||
exclude:
|
||||
- "**/*.test.ts"
|
||||
- scripts/**
|
||||
lane:
|
||||
- match: packages/broadcast-lib/**
|
||||
lane: functions
|
||||
group:
|
||||
- match: id:build-bulk-payload
|
||||
group: broadcast-lib
|
||||
```
|
||||
|
||||
Every field except `schemaVersion` is optional, and the file itself is optional. For editor autocomplete, point at the published JSON Schema — no install needed:
|
||||
|
||||
```jsonc
|
||||
{ "$ref": "https://unpkg.com/@coldtea/pr-lens-schema/json-schema/config.schema.json" }
|
||||
```
|
||||
|
||||
## Selectors
|
||||
|
||||
A `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.
|
||||
|
||||
**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).
|
||||
|
||||
## The four corrections
|
||||
|
||||
| | What it does |
|
||||
| --- | --- |
|
||||
| `rename` | replaces the inferred label |
|
||||
| `exclude` | drops matching nodes, and the edges and flow steps that hung from them |
|
||||
| `lane` | moves matching nodes into a lane, **creating it** when the document declares no such id |
|
||||
| `group` | clusters matching nodes under a sub-group inside their lane |
|
||||
|
||||
Up 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.
|
||||
|
||||
## Recipes
|
||||
|
||||
**"Stop showing me the test files."**
|
||||
```yaml
|
||||
map:
|
||||
exclude: ["**/*.test.ts", "**/__tests__/**"]
|
||||
```
|
||||
|
||||
**"That node is called the wrong thing."** Match the file it comes from, not its id:
|
||||
```yaml
|
||||
map:
|
||||
rename:
|
||||
- match: server/lib/broadcast/createBroadcastSendTask.ts
|
||||
to: Send task
|
||||
```
|
||||
|
||||
**"These belong in a band of their own."** The lane need not exist yet:
|
||||
```yaml
|
||||
map:
|
||||
lane:
|
||||
- match: infra/**
|
||||
lane: infrastructure
|
||||
```
|
||||
|
||||
**"Keep the shared library together."**
|
||||
```yaml
|
||||
map:
|
||||
group:
|
||||
- match: packages/broadcast-lib/**
|
||||
group: broadcast-lib
|
||||
```
|
||||
|
||||
**"Only draw the architecture."**
|
||||
```yaml
|
||||
lenses: [architecture]
|
||||
```
|
||||
|
||||
## Hosted GitHub App comments
|
||||
|
||||
The hosted App reads `github` settings from the PR's head commit. Other options apply to the CLI.
|
||||
|
||||
| Setting | Default | Effect |
|
||||
| --- | --- | --- |
|
||||
| `github.comment.collapsed` | `false` | Start diagrams and details closed. Drawing still runs automatically. |
|
||||
|
||||
## Check it
|
||||
|
||||
```bash
|
||||
npx @coldtea/pr-lens-cli@latest validate .github/pr-lens.yml
|
||||
```
|
||||
|
||||
`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.
|
||||
@@ -0,0 +1,685 @@
|
||||
{
|
||||
"schemaVersion": "0.1.1",
|
||||
"kind": "graph",
|
||||
"generatedAt": "2026-08-19T18:24:00.000Z",
|
||||
"title": "Batch broadcast sending through Postmark",
|
||||
"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.",
|
||||
"lenses": [
|
||||
"architecture",
|
||||
"data-flow"
|
||||
],
|
||||
"provenance": {
|
||||
"repo": {
|
||||
"owner": "ohansemmanuel",
|
||||
"name": "bestregards",
|
||||
"host": "github.com"
|
||||
},
|
||||
"base": {
|
||||
"sha": "3f5c1ab9d24e7f08c6b1a5d3e9074c2b8a6f1d40",
|
||||
"ref": "main"
|
||||
},
|
||||
"head": {
|
||||
"sha": "b71e0d4c8a92f5361de7c0b4a8f2593d6c1e8a77",
|
||||
"ref": "batch-broadcast-send"
|
||||
},
|
||||
"pullRequest": {
|
||||
"number": 128,
|
||||
"title": "Send broadcasts in batches of 500",
|
||||
"url": "https://github.com/ohansemmanuel/bestregards/pull/128"
|
||||
},
|
||||
"generator": {
|
||||
"name": "pr-lens-examples",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
"lanes": [
|
||||
{
|
||||
"id": "web",
|
||||
"label": "Next.js",
|
||||
"subtitle": "Vercel",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "functions",
|
||||
"label": "Cloud Functions",
|
||||
"subtitle": "Firebase",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "external",
|
||||
"label": "External",
|
||||
"subtitle": "Postmark",
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "broadcast-composer",
|
||||
"label": "Broadcast composer",
|
||||
"kind": "ui",
|
||||
"delta": "unchanged",
|
||||
"lane": "web",
|
||||
"subtitle": "app/broadcasts/new",
|
||||
"summary": "Where an author writes a broadcast and hits send. Untouched by this change.",
|
||||
"files": [
|
||||
{
|
||||
"path": "app/broadcasts/new/page.tsx"
|
||||
}
|
||||
],
|
||||
"badges": []
|
||||
},
|
||||
{
|
||||
"id": "queue-route",
|
||||
"label": "POST /api/broadcasts/queue",
|
||||
"kind": "route",
|
||||
"delta": "modified",
|
||||
"lane": "web",
|
||||
"summary": "Writes the queue document. Now stamps the recipient count and batch size the sender will use instead of leaving batching to the worker.",
|
||||
"files": [
|
||||
{
|
||||
"path": "app/api/broadcasts/queue/route.ts",
|
||||
"startLine": 24,
|
||||
"endLine": 96
|
||||
}
|
||||
],
|
||||
"badges": [
|
||||
"+38 / -12"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "broadcast-queue",
|
||||
"label": "broadcastQueue",
|
||||
"kind": "datastore",
|
||||
"delta": "modified",
|
||||
"lane": "functions",
|
||||
"subtitle": "Firestore collection",
|
||||
"summary": "Queue documents gained batchSize and suppressedCount fields, and results are now written back per batch rather than per recipient.",
|
||||
"files": [
|
||||
{
|
||||
"path": "functions/src/broadcast/schema.ts",
|
||||
"startLine": 12,
|
||||
"endLine": 48
|
||||
}
|
||||
],
|
||||
"badges": []
|
||||
},
|
||||
{
|
||||
"id": "send-broadcast-bulk",
|
||||
"label": "sendBroadcastBulk",
|
||||
"kind": "function",
|
||||
"delta": "added",
|
||||
"lane": "functions",
|
||||
"subtitle": "onWrite trigger",
|
||||
"summary": "New trigger handler. Fetches suppressions once, builds batched payloads, and posts them to Postmark in chunks of 500.",
|
||||
"files": [
|
||||
{
|
||||
"path": "functions/src/broadcast/sendBroadcastBulk.ts",
|
||||
"startLine": 1,
|
||||
"endLine": 142
|
||||
}
|
||||
],
|
||||
"badges": [
|
||||
"new"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "build-bulk-payload",
|
||||
"label": "buildBulkPayload",
|
||||
"kind": "function",
|
||||
"delta": "added",
|
||||
"lane": "functions",
|
||||
"summary": "Turns a broadcast and its recipient slice into a Postmark batch request body.",
|
||||
"files": [
|
||||
{
|
||||
"path": "packages/broadcast-lib/src/buildBulkPayload.ts",
|
||||
"startLine": 1,
|
||||
"endLine": 74
|
||||
}
|
||||
],
|
||||
"badges": []
|
||||
},
|
||||
{
|
||||
"id": "get-suppressed-emails",
|
||||
"label": "getSuppressedEmails",
|
||||
"kind": "function",
|
||||
"delta": "added",
|
||||
"lane": "functions",
|
||||
"summary": "Pulls the Postmark suppression dump once per broadcast so suppressed addresses are filtered before any batch is sent.",
|
||||
"files": [
|
||||
{
|
||||
"path": "packages/broadcast-lib/src/getSuppressedEmails.ts",
|
||||
"startLine": 1,
|
||||
"endLine": 58
|
||||
}
|
||||
],
|
||||
"badges": []
|
||||
},
|
||||
{
|
||||
"id": "broadcast-lib",
|
||||
"label": "broadcast-lib",
|
||||
"kind": "package",
|
||||
"delta": "added",
|
||||
"lane": "functions",
|
||||
"subtitle": "packages/broadcast-lib",
|
||||
"summary": "New shared package so the queue route and the sender agree on payload shape and batch size.",
|
||||
"files": [
|
||||
{
|
||||
"path": "packages/broadcast-lib/src/index.ts"
|
||||
}
|
||||
],
|
||||
"badges": [
|
||||
"new package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "process-broadcast",
|
||||
"label": "processBroadcast",
|
||||
"kind": "function",
|
||||
"delta": "removed",
|
||||
"lane": "functions",
|
||||
"subtitle": "onWrite trigger",
|
||||
"summary": "The per-recipient loop this change replaces.",
|
||||
"files": [
|
||||
{
|
||||
"path": "functions/src/broadcast/processBroadcast.ts",
|
||||
"startLine": 1,
|
||||
"endLine": 118,
|
||||
"revision": "base"
|
||||
}
|
||||
],
|
||||
"badges": []
|
||||
},
|
||||
{
|
||||
"id": "send-single-email",
|
||||
"label": "sendSingleEmail",
|
||||
"kind": "function",
|
||||
"delta": "removed",
|
||||
"lane": "functions",
|
||||
"summary": "One Postmark request per recipient. Gone with the loop that called it.",
|
||||
"files": [
|
||||
{
|
||||
"path": "functions/src/broadcast/sendSingleEmail.ts",
|
||||
"startLine": 1,
|
||||
"endLine": 46,
|
||||
"revision": "base"
|
||||
}
|
||||
],
|
||||
"badges": []
|
||||
},
|
||||
{
|
||||
"id": "postmark",
|
||||
"label": "Postmark",
|
||||
"kind": "external",
|
||||
"delta": "modified",
|
||||
"lane": "external",
|
||||
"subtitle": "Email API",
|
||||
"summary": "Same provider, different endpoints: the batch endpoint and the suppression dump replace repeated single sends.",
|
||||
"files": [],
|
||||
"badges": []
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "composer-to-queue",
|
||||
"from": "broadcast-composer",
|
||||
"to": "queue-route",
|
||||
"kind": "http",
|
||||
"delta": "unchanged",
|
||||
"label": "send broadcast",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "queue-to-firestore",
|
||||
"from": "queue-route",
|
||||
"to": "broadcast-queue",
|
||||
"kind": "data",
|
||||
"delta": "modified",
|
||||
"label": "enqueue job",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "queue-to-lib",
|
||||
"from": "queue-route",
|
||||
"to": "broadcast-lib",
|
||||
"kind": "dependency",
|
||||
"delta": "added",
|
||||
"label": "batch size",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "firestore-to-bulk",
|
||||
"from": "broadcast-queue",
|
||||
"to": "send-broadcast-bulk",
|
||||
"kind": "event",
|
||||
"delta": "added",
|
||||
"label": "onWrite",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "firestore-to-process",
|
||||
"from": "broadcast-queue",
|
||||
"to": "process-broadcast",
|
||||
"kind": "event",
|
||||
"delta": "removed",
|
||||
"label": "onWrite",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "process-to-single",
|
||||
"from": "process-broadcast",
|
||||
"to": "send-single-email",
|
||||
"kind": "call",
|
||||
"delta": "removed",
|
||||
"label": "per recipient",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "single-to-postmark",
|
||||
"from": "send-single-email",
|
||||
"to": "postmark",
|
||||
"kind": "http",
|
||||
"delta": "removed",
|
||||
"label": "POST /email · 1 msg/call",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "bulk-to-payload",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "build-bulk-payload",
|
||||
"kind": "call",
|
||||
"delta": "added",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "bulk-to-suppressions",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "get-suppressed-emails",
|
||||
"kind": "call",
|
||||
"delta": "added",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "bulk-to-lib",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "broadcast-lib",
|
||||
"kind": "dependency",
|
||||
"delta": "added",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "suppressions-to-postmark",
|
||||
"from": "get-suppressed-emails",
|
||||
"to": "postmark",
|
||||
"kind": "http",
|
||||
"delta": "added",
|
||||
"label": "GET suppression dump",
|
||||
"emphasis": "normal",
|
||||
"animated": true,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "bulk-to-postmark",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "postmark",
|
||||
"kind": "http",
|
||||
"delta": "added",
|
||||
"label": "500 msgs/call",
|
||||
"emphasis": "hero",
|
||||
"animated": true,
|
||||
"summary": "The change in one edge: a broadcast to 10,000 recipients drops from 10,000 requests to 20.",
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "bulk-to-firestore",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "broadcast-queue",
|
||||
"kind": "data",
|
||||
"delta": "added",
|
||||
"label": "write results",
|
||||
"emphasis": "normal",
|
||||
"animated": false,
|
||||
"files": []
|
||||
}
|
||||
],
|
||||
"flows": [
|
||||
{
|
||||
"id": "send-pipeline",
|
||||
"title": "Sending a broadcast",
|
||||
"summary": "The path a queued broadcast takes now, from enqueue to per-message results.",
|
||||
"delta": "modified",
|
||||
"participants": [
|
||||
{
|
||||
"node": "queue-route",
|
||||
"label": "queue route"
|
||||
},
|
||||
{
|
||||
"node": "broadcast-queue",
|
||||
"label": "Firestore"
|
||||
},
|
||||
{
|
||||
"node": "send-broadcast-bulk",
|
||||
"label": "sendBroadcastBulk"
|
||||
},
|
||||
{
|
||||
"node": "postmark",
|
||||
"label": "Postmark"
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
{
|
||||
"id": "enqueue",
|
||||
"from": "queue-route",
|
||||
"to": "broadcast-queue",
|
||||
"label": "enqueue broadcast job",
|
||||
"kind": "async",
|
||||
"delta": "modified",
|
||||
"animated": true,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "trigger",
|
||||
"from": "broadcast-queue",
|
||||
"to": "send-broadcast-bulk",
|
||||
"label": "onWrite trigger",
|
||||
"kind": "async",
|
||||
"delta": "added",
|
||||
"animated": true,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "suppressions-request",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "postmark",
|
||||
"label": "GET suppression dump",
|
||||
"kind": "sync",
|
||||
"delta": "added",
|
||||
"animated": true,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "suppressions-response",
|
||||
"from": "postmark",
|
||||
"to": "send-broadcast-bulk",
|
||||
"label": "suppressed addresses",
|
||||
"kind": "return",
|
||||
"delta": "added",
|
||||
"animated": true,
|
||||
"note": "Fetched once per broadcast, not once per recipient.",
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "batch-post",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "postmark",
|
||||
"label": "POST /email/batch · 500 msgs",
|
||||
"kind": "sync",
|
||||
"delta": "added",
|
||||
"animated": true,
|
||||
"repeat": 4,
|
||||
"note": "One request per 500 recipients; four for this 2,000-recipient broadcast.",
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "batch-results",
|
||||
"from": "postmark",
|
||||
"to": "send-broadcast-bulk",
|
||||
"label": "per-message results",
|
||||
"kind": "return",
|
||||
"delta": "added",
|
||||
"animated": true,
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": "write-results",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "broadcast-queue",
|
||||
"label": "write results",
|
||||
"kind": "async",
|
||||
"delta": "added",
|
||||
"animated": true,
|
||||
"files": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"filesChanged": 14,
|
||||
"additions": 486,
|
||||
"deletions": 212,
|
||||
"chips": [
|
||||
{
|
||||
"label": "Postmark calls",
|
||||
"value": "500× fewer",
|
||||
"tone": "hero"
|
||||
},
|
||||
{
|
||||
"label": "New",
|
||||
"value": "4 units",
|
||||
"tone": "added"
|
||||
},
|
||||
{
|
||||
"label": "Retired",
|
||||
"value": "2 units",
|
||||
"tone": "removed"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "Architecture — blast radius",
|
||||
"lens": "architecture",
|
||||
"summary": "Everything this change touches, across all three lanes.",
|
||||
"scope": {
|
||||
"kind": "all"
|
||||
},
|
||||
"defaultOpen": true,
|
||||
"children": [
|
||||
{
|
||||
"id": "new-batch-path",
|
||||
"title": "The new batch path",
|
||||
"lens": "architecture",
|
||||
"summary": "What replaced the per-recipient loop.",
|
||||
"scope": {
|
||||
"kind": "selection",
|
||||
"lanes": [],
|
||||
"nodes": [
|
||||
"send-broadcast-bulk",
|
||||
"build-bulk-payload",
|
||||
"get-suppressed-emails",
|
||||
"broadcast-lib",
|
||||
"postmark"
|
||||
],
|
||||
"edges": [
|
||||
"bulk-to-payload",
|
||||
"bulk-to-suppressions",
|
||||
"bulk-to-lib",
|
||||
"suppressions-to-postmark",
|
||||
"bulk-to-postmark",
|
||||
"bulk-to-firestore"
|
||||
],
|
||||
"flows": []
|
||||
},
|
||||
"defaultOpen": false,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "retired-path",
|
||||
"title": "What was retired",
|
||||
"lens": "architecture",
|
||||
"summary": "The single-send path, kept visible so a reviewer can confirm nothing else called it.",
|
||||
"scope": {
|
||||
"kind": "selection",
|
||||
"lanes": [],
|
||||
"nodes": [
|
||||
"process-broadcast",
|
||||
"send-single-email"
|
||||
],
|
||||
"edges": [
|
||||
"firestore-to-process",
|
||||
"process-to-single",
|
||||
"single-to-postmark"
|
||||
],
|
||||
"flows": []
|
||||
},
|
||||
"defaultOpen": false,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "send-pipeline-view",
|
||||
"title": "Data flow — sending a broadcast",
|
||||
"lens": "data-flow",
|
||||
"scope": {
|
||||
"kind": "selection",
|
||||
"lanes": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"flows": [
|
||||
"send-pipeline"
|
||||
]
|
||||
},
|
||||
"defaultOpen": false,
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"walkthrough": {
|
||||
"steps": [
|
||||
{
|
||||
"id": "batches-of-500",
|
||||
"heading": "sendBroadcastBulk and buildBulkPayload added",
|
||||
"body": "Nothing loops over recipients any more. The sender works on a whole batch at a time.",
|
||||
"stage": {
|
||||
"kind": "view",
|
||||
"view": "overview"
|
||||
},
|
||||
"focus": {
|
||||
"kind": "selection",
|
||||
"lanes": [],
|
||||
"nodes": [
|
||||
"send-broadcast-bulk",
|
||||
"build-bulk-payload",
|
||||
"postmark"
|
||||
],
|
||||
"edges": [],
|
||||
"messages": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "suppression-first",
|
||||
"heading": "getSuppressedEmails added before the send",
|
||||
"body": "It pulls the blocked addresses once, before any batch is built.",
|
||||
"stage": {
|
||||
"kind": "view",
|
||||
"view": "new-batch-path"
|
||||
},
|
||||
"focus": {
|
||||
"kind": "selection",
|
||||
"lanes": [],
|
||||
"nodes": [
|
||||
"get-suppressed-emails",
|
||||
"postmark"
|
||||
],
|
||||
"edges": [],
|
||||
"messages": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "old-path-goes-dark",
|
||||
"heading": "processBroadcast and sendSingleEmail removed",
|
||||
"body": "sendBroadcastBulk does their job for whole batches.",
|
||||
"stage": {
|
||||
"kind": "view",
|
||||
"view": "overview"
|
||||
},
|
||||
"focus": {
|
||||
"kind": "selection",
|
||||
"lanes": [],
|
||||
"nodes": [
|
||||
"process-broadcast",
|
||||
"send-single-email"
|
||||
],
|
||||
"edges": [],
|
||||
"messages": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sequence-start-to-finish",
|
||||
"heading": "The send sequence gained 6 new steps",
|
||||
"body": "The queue write is the only step that was there before, and it now stamps the batch size.",
|
||||
"stage": {
|
||||
"kind": "flow",
|
||||
"flow": "send-pipeline"
|
||||
},
|
||||
"focus": {
|
||||
"kind": "all"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"lanes": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"messages": [
|
||||
"batch-post",
|
||||
"batch-results"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"focus": {
|
||||
"kind": "all"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {
|
||||
"direction": "right",
|
||||
"laneOrder": [
|
||||
"web",
|
||||
"functions",
|
||||
"external"
|
||||
],
|
||||
"rank": {
|
||||
"queue-route": 0,
|
||||
"send-broadcast-bulk": 1,
|
||||
"postmark": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
When to load: writing or debugging a graph.json document — full field-by-field format, enums, limits.
|
||||
|
||||
# Authoring a graph document
|
||||
|
||||
This 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.
|
||||
|
||||
The 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.
|
||||
|
||||
Every schema here is **strict**: an unknown key is a rejection, not a warning. A field with a default may be left out.
|
||||
|
||||
## The document
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "0.1.1",
|
||||
"kind": "graph",
|
||||
"title": "Batch broadcast sending through Postmark",
|
||||
"summary": "One paragraph answering: what does this change do?",
|
||||
"lenses": ["architecture", "data-flow"],
|
||||
"provenance": { "repo": { "owner": "…", "name": "…" }, "base": { "sha": "…" }, "head": { "sha": "…" } },
|
||||
"lanes": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"flows": [],
|
||||
"stats": {},
|
||||
"views": []
|
||||
}
|
||||
```
|
||||
|
||||
`lenses` declares what the document carries enough detail to draw: `architecture`, `data-flow`, or both. A document carrying flows must declare `data-flow`.
|
||||
|
||||
`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.
|
||||
|
||||
## Ids
|
||||
|
||||
`^[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.
|
||||
|
||||
## Deltas
|
||||
|
||||
Every node, edge, flow and flow step declares one: `added`, `modified`, `removed`, `unchanged`.
|
||||
|
||||
`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.
|
||||
|
||||
## Lanes
|
||||
|
||||
1 to 16. Every node belongs to exactly one.
|
||||
|
||||
```json
|
||||
{ "id": "functions", "label": "Cloud Functions", "subtitle": "Node 20", "order": 1 }
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
## Nodes
|
||||
|
||||
1 to 256.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "send-broadcast-bulk",
|
||||
"label": "sendBroadcastBulk",
|
||||
"kind": "function",
|
||||
"delta": "added",
|
||||
"lane": "functions",
|
||||
"group": "broadcast-lib",
|
||||
"subtitle": "(broadcastId) => Promise<void>",
|
||||
"summary": "Claims the broadcast, builds one bulk payload and posts it.",
|
||||
"files": [{ "path": "functions/src/broadcast/sendBroadcastBulk.ts", "startLine": 1, "endLine": 142 }],
|
||||
"badges": ["retry"]
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
`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.
|
||||
|
||||
## Edges
|
||||
|
||||
Up to 512.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "bulk-to-postmark",
|
||||
"from": "send-broadcast-bulk",
|
||||
"to": "postmark",
|
||||
"kind": "http",
|
||||
"delta": "added",
|
||||
"label": "POST /email/bulk",
|
||||
"emphasis": "hero",
|
||||
"animated": true
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
## Flows
|
||||
|
||||
Up to 16, for the data-flow lens.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "send-pipeline",
|
||||
"title": "Sending a broadcast",
|
||||
"delta": "modified",
|
||||
"participants": [{ "node": "queue-route" }, { "node": "send-broadcast-bulk" }, { "node": "postmark" }],
|
||||
"messages": [
|
||||
{ "id": "enqueue", "from": "queue-route", "to": "send-broadcast-bulk", "label": "enqueue job", "kind": "async", "delta": "modified" },
|
||||
{ "id": "send", "from": "send-broadcast-bulk", "to": "postmark", "label": "POST /email/bulk", "kind": "sync", "delta": "added", "repeat": 4 },
|
||||
{ "id": "accepted", "from": "postmark", "to": "send-broadcast-bulk", "label": "200 Accepted", "kind": "return", "delta": "added" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- 2 to 12 participants, ordered by array position; each names a node id.
|
||||
- 1 to 64 messages. **Step order is array order**: there is no step number field, so a document cannot disagree with its own animation.
|
||||
- `kind` is `sync`, `async`, `return` or `self`. `self` requires `from === to`, and no other kind may have them equal.
|
||||
- Both endpoints must be participants of that flow, not merely nodes of the document.
|
||||
- `repeat` says a step happens more than once per run, e.g. 4 batched requests.
|
||||
|
||||
## Stats
|
||||
|
||||
```json
|
||||
{ "filesChanged": 27, "additions": 1979, "deletions": 1370, "chips": [{ "label": "Postmark calls", "value": "500x fewer", "tone": "hero" }] }
|
||||
```
|
||||
|
||||
Up 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.
|
||||
|
||||
## Views
|
||||
|
||||
The 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.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "the-new-path",
|
||||
"title": "The new batch path",
|
||||
"lens": "architecture",
|
||||
"summary": "What replaced the per-recipient loop.",
|
||||
"defaultOpen": false,
|
||||
"scope": { "kind": "selection", "nodes": ["send-broadcast-bulk", "postmark"] },
|
||||
"children": []
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
### Choosing architecture views
|
||||
|
||||
Treat the architecture tree as a set of decisions, not a quota:
|
||||
|
||||
1. 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.
|
||||
2. 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.
|
||||
3. 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.
|
||||
4. Stop at components unless someone explicitly asks for code-level detail.
|
||||
|
||||
One 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.
|
||||
|
||||
Set `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.
|
||||
|
||||
This compact fragment shows the shape. The selected ids refer to elements declared elsewhere in the document:
|
||||
|
||||
```json
|
||||
{
|
||||
"views": [
|
||||
{
|
||||
"id": "checkout-context",
|
||||
"title": "Checkout in its environment",
|
||||
"lens": "architecture",
|
||||
"defaultOpen": true,
|
||||
"scope": {
|
||||
"kind": "selection",
|
||||
"nodes": ["shopper", "commerce-platform", "payment-provider", "fulfilment-system"],
|
||||
"edges": ["shopper-to-commerce", "commerce-to-payment", "commerce-to-fulfilment"]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "checkout-containers",
|
||||
"title": "Checkout containers",
|
||||
"lens": "architecture",
|
||||
"scope": {
|
||||
"kind": "selection",
|
||||
"nodes": ["storefront", "checkout-api", "orders-db", "payment-provider"],
|
||||
"edges": ["storefront-to-checkout", "checkout-to-orders", "checkout-to-payment"]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "checkout-components",
|
||||
"title": "Checkout API components",
|
||||
"lens": "architecture",
|
||||
"scope": {
|
||||
"kind": "selection",
|
||||
"nodes": ["checkout-route", "order-service", "payment-client"],
|
||||
"edges": ["route-to-orders", "orders-to-payment-client"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "place-order-flow",
|
||||
"title": "Placing an order",
|
||||
"lens": "data-flow",
|
||||
"scope": { "kind": "selection", "flows": ["place-order"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Walkthrough
|
||||
|
||||
Optional in the format, but write one 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. A canvas or a share page plays it.
|
||||
|
||||
```json
|
||||
{
|
||||
"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"] }
|
||||
},
|
||||
{
|
||||
"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" },
|
||||
"focus": { "kind": "all" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Every step is one change, never a description of the diagram: the heading names the thing and what happened to it, built from change words such as added, removed, replaced, now, moved and split, and the body is one line on what that means for behaviour, with the numbers when they matter. The headline change is step one. Write it all for a smart twelve-year-old, in short common words and active voice. The skill page has the rule in full, with examples of a step written well and the same step written badly.
|
||||
|
||||
Each step has:
|
||||
|
||||
- `heading`: the thing and what happened to it, up to 48 characters, in sentence case. For example "Postmark now gets 500 emails per call".
|
||||
- `body`: one line under the heading, up to 140 characters, on what the change means for behaviour. For example "One call per batch instead of one call per person". Required: a heading with no body reads as unfinished.
|
||||
- `stage`: which diagram to show. A document can have several diagrams: its views (the drill-down diagrams) and its flows (the sequence diagrams). `{ "kind": "view", "view": "overview" }` shows the view called `overview`. `{ "kind": "flow", "flow": "send-pipeline" }` shows the flow called `send-pipeline`. Leave `stage` out and the step uses the diagram the reader is already on.
|
||||
- `focus`: what to zoom in on inside that diagram. `{ "kind": "all" }`, 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. A selection must name at least one thing.
|
||||
|
||||
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.
|
||||
- A stored map never carries a walkthrough. A map describes the system; a walkthrough tells the story of one change.
|
||||
|
||||
## Layout
|
||||
|
||||
```json
|
||||
{ "direction": "right", "laneOrder": ["api", "functions", "external"], "rank": { "send-broadcast-bulk": 2 } }
|
||||
```
|
||||
|
||||
Hints, 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.
|
||||
|
||||
## File references
|
||||
|
||||
```json
|
||||
{ "path": "functions/src/broadcast/sendBroadcastBulk.ts", "startLine": 1, "endLine": 142, "revision": "head" }
|
||||
```
|
||||
|
||||
Repository-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.
|
||||
|
||||
## Length limits
|
||||
|
||||
Labels 120 characters, summaries 2000, chip values 32. They are display fields: a label that needs 120 characters is a label the diagram cannot draw.
|
||||
|
||||
## Then validate
|
||||
|
||||
```bash
|
||||
npx @coldtea/pr-lens-cli@latest validate .pr-lens/graph.json
|
||||
```
|
||||
|
||||
Every problem is reported at once, with a path into the document. Fix them all and run it again until it is clean.
|
||||
@@ -44,6 +44,7 @@ hermes skills uninstall <skill-name>
|
||||
|-------|-------------|
|
||||
| [**evm**](/docs/user-guide/skills/optional/blockchain/blockchain-evm) | Read-only EVM client: wallets, tokens, gas across 8 chains. |
|
||||
| [**hyperliquid**](/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid) | Hyperliquid market data, account history, trade review. |
|
||||
| [**pr-lens**](/docs/user-guide/skills/optional/software-development/software-development-pr-lens) | Draw code changes as animated architecture/data-flow SVGs. |
|
||||
| [**solana**](/docs/user-guide/skills/optional/blockchain/blockchain-solana) | Query Solana wallets, tokens, txs, and NFTs in USD. |
|
||||
|
||||
## communication
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: "Pr Lens — Draw code changes as animated architecture/data-flow SVGs"
|
||||
sidebar_label: "Pr Lens"
|
||||
description: "Draw code changes as animated architecture/data-flow SVGs"
|
||||
---
|
||||
|
||||
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
|
||||
|
||||
# Pr Lens
|
||||
|
||||
Draw code changes as animated architecture/data-flow SVGs.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/software-development/pr-lens` |
|
||||
| Path | `optional-skills/software-development/pr-lens` |
|
||||
| Version | `1.0.0` |
|
||||
| Author | Coldtea AI (adapted by Nous Research) |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos |
|
||||
| Tags | `diagrams`, `pull-requests`, `code-review`, `svg` |
|
||||
|
||||
## Reference: full SKILL.md
|
||||
|
||||
:::info
|
||||
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
|
||||
:::
|
||||
|
||||
# PR Lens Skill
|
||||
|
||||
PR Lens draws code as visually rich animated diagrams: diffs, architecture, data flows. You describe the diff or codebase as one JSON document (lanes, nodes, edges, ordered flows) and the CLI renders it as animated SVGs. There is no findings lens — PR Lens is a comprehension layer, not a review bot. There is no field for a bug, risk, or security note, and a document that invents one is rejected.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Asked to diagram, visualise, or explain a code change or a system.
|
||||
- A pull request should carry an architecture or data-flow diagram.
|
||||
- Keywords: PR Lens, diagram, architecture, data flow, visualise, pull request.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js with `npx` (the CLI runs via `npx @coldtea/pr-lens-cli@latest`; no install step).
|
||||
- `gh` (GitHub CLI) — optional, only for attaching diagrams to PRs.
|
||||
- Optional canvas publishing calls the third-party service prlens.dev (see step 4b).
|
||||
|
||||
## How to Run
|
||||
|
||||
Run all commands with the terminal tool from the repository root.
|
||||
|
||||
1. **Read the diff.** When representing 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 diff, read the code to be visualised.
|
||||
|
||||
2. **Write the document** to `.pr-lens/graph.json`, following `references/graph-document.md`. `references/example.graph.json` is a 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 writing your first document — quicker than reading the reference.
|
||||
|
||||
3. **Validate, and fix.**
|
||||
|
||||
```bash
|
||||
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.**
|
||||
|
||||
```bash
|
||||
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 `.pr-lens/`, which the CLI adds to the repository's .gitignore. Do not commit any of it — these files are rebuilt from the diff on demand. Each SVG is named after its view, theme and content hash; `manifest.json` lists them by lens and view.
|
||||
|
||||
4b. **Canvas push — OPTIONAL, opt-in.** Only when the user explicitly asks for a shareable link. This publishes `.pr-lens/drawn.graph.json` to the third-party service prlens.dev:
|
||||
|
||||
```bash
|
||||
npx @coldtea/pr-lens-cli@latest canvas push
|
||||
```
|
||||
|
||||
It prints three links. Give the user the **view link** (`https://prlens.dev/c/{id}`): the full-screen diagram, every view on one page, no login. The **edit link** (ending in `#w=…`) lets its holder overwrite the canvas — it is a secret: leave it out of the reply unless asked, never paste it anywhere public. The embed link serves the top view as an SVG for a README. Pushing the same file again updates the same canvas, so "rename that node" is: edit, validate, render, push — the link stays the same. If the push fails, say so and tell the user where the local SVGs are and which is the top view.
|
||||
|
||||
5. **Attach to a PR, when there is one.** Upstream documents `gh pr create/edit/comment --attach <path>`, but `--attach` arrived in GitHub CLI 2.99 — check `gh --version` first (e.g. gh 2.97 does NOT have it). With gh ≥ 2.99: write the body with a Markdown image `` (an HTML `<img>` is left as written and the file appended at the bottom instead; alt text is the one-line caption a reader without images gets), then repeat `--attach <path>` per referenced diagram:
|
||||
|
||||
```bash
|
||||
gh pr create --title "…" --body-file .pr-lens/body.md --attach .pr-lens/overview-light-<hash>.svg
|
||||
```
|
||||
|
||||
Without `--attach`, use a commit-free path:
|
||||
- Upload the SVGs to a gist: `gh gist create .pr-lens/<view>.svg`, then reference the raw gist URL in the PR body/comment, or
|
||||
- Publish via the canvas link (step 4b, with user consent) and link the view URL, or
|
||||
- Note the local `.pr-lens/` path in the PR body so reviewers can rebuild.
|
||||
|
||||
Once published somewhere durable, let the CLI compose the comment markdown:
|
||||
|
||||
```bash
|
||||
npx @coldtea/pr-lens-cli@latest comment \
|
||||
--graph .pr-lens/drawn.graph.json \
|
||||
--manifest .pr-lens/manifest.json \
|
||||
--asset-base-url <where you published the SVGs>
|
||||
```
|
||||
|
||||
`--graph` takes `drawn.graph.json`, not the document you wrote — the CLI refuses a document its manifest does not describe. Leave out `--asset-base-url` and the markdown points at local paths no reader can fetch. The markdown goes to stdout; posting it is your business.
|
||||
|
||||
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. Two diagrams usually beat four.
|
||||
|
||||
6. **Optional automation:** `npx @coldtea/pr-lens-cli@latest analyze --base <ref>` does steps 1–2 by asking a provider (Gemini, OpenAI, or any `/chat/completions` endpoint) with a key of your own. That is the only path here that needs one; normally you author the document yourself.
|
||||
|
||||
## What makes a document worth reading
|
||||
|
||||
- **Include what did not change.** 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.
|
||||
- Architecture views are a C4-inspired decision tree: system context → container → component, each child materially narrower. Skip empty or repetitive levels; keep data-flow views as separate roots; set `defaultOpen: true` on the highest useful architecture view.
|
||||
- **Walkthroughs** (2–12 steps, aim 3–7): write one for anything non-trivial. Each step = one change (added/removed/moved), headline change first, overview last. Headings ≤48 chars built from change words; bodies ≤140 chars on behaviour, required. Write for a smart twelve-year-old; no "leverages"/"orchestrates". Keep consecutive steps on the same stage. The walkthrough field needs CLI ≥ 0.4.0 (contract 0.1.1).
|
||||
- **Fixing a wrong map:** never edit the generated document — write corrections into `.github/pr-lens.yml` (see `references/config.md`), then validate it: `npx @coldtea/pr-lens-cli@latest validate .github/pr-lens.yml`. Prefer path globs over `id:` matches.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `npx @coldtea/pr-lens-cli@latest validate .pr-lens/graph.json` | validate the document (also validates `.github/pr-lens.yml`) |
|
||||
| `npx @coldtea/pr-lens-cli@latest render .pr-lens/graph.json --theme light` | render SVGs + manifest into `.pr-lens/` |
|
||||
| `npx @coldtea/pr-lens-cli@latest canvas push` | OPTIONAL: publish to prlens.dev (opt-in only) |
|
||||
| `npx @coldtea/pr-lens-cli@latest comment --graph … --manifest … --asset-base-url …` | compose PR comment markdown to stdout |
|
||||
| `npx @coldtea/pr-lens-cli@latest analyze --base <ref>` | auto-author document via an LLM provider (needs API key) |
|
||||
|
||||
Validator failure codes:
|
||||
|
||||
| Code | What you did |
|
||||
| --- | --- |
|
||||
| `BROKEN_REFERENCE` | an edge, flow step, view or 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 |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Six rules are parser-only, not in the JSON Schema (referential integrity, inverted line ranges, disagreeing `self` endpoints, identical patch commits, too many views for a manifest, flow-step focus on the wrong stage) — always run `validate`, structured output alone is not enough.
|
||||
- Do not commit anything in `.pr-lens/`; it is regenerated and gitignored by the CLI.
|
||||
- The `--attach` gh flag needs gh ≥ 2.99; older gh silently lacks it — check before writing a body around it.
|
||||
- The canvas edit link (`#w=…`) is a write credential — never share it unprompted or paste it publicly.
|
||||
- A stored map never carries a walkthrough; a walkthrough tells the story of one change.
|
||||
- `pr-lens render` reports corrections in `.github/pr-lens.yml` that matched nothing — that is drift worth fixing, not an error.
|
||||
|
||||
## Verification
|
||||
|
||||
Smoke test (live-verified 2026-09-12 with `@coldtea/pr-lens-cli` via npx, node on Linux):
|
||||
|
||||
```bash
|
||||
cp references/example.graph.json /tmp/prlens-smoke/ && cd /tmp/prlens-smoke
|
||||
npx -y @coldtea/pr-lens-cli@latest validate example.graph.json
|
||||
# ✓ example.graph.json — graph document · 3 lanes, 10 nodes, 13 edges, 1 flow · 6 walkthrough steps
|
||||
npx -y @coldtea/pr-lens-cli@latest render example.graph.json --theme light
|
||||
# ✓ .pr-lens/manifest.json — 4 SVGs across 4 diagrams
|
||||
```
|
||||
|
||||
Expect exit 0 on both and four `*-light-<hash>.svg` files plus `manifest.json` and `drawn.graph.json` in `.pr-lens/`.
|
||||
|
||||
---
|
||||
|
||||
Adapted from [coldteadotai/pr-lens](https://github.com/coldteadotai/pr-lens) (packages/agent-skill, pinned 0993b4d), MIT License, Copyright (c) 2026 Coldtea AI. See LICENSE.txt.
|
||||
@@ -612,6 +612,7 @@ const sidebars: SidebarsConfig = {
|
||||
'user-guide/skills/optional/software-development/software-development-ast-grep',
|
||||
'user-guide/skills/optional/software-development/software-development-code-wiki',
|
||||
'user-guide/skills/optional/software-development/software-development-grill-me',
|
||||
'user-guide/skills/optional/software-development/software-development-pr-lens',
|
||||
'user-guide/skills/optional/software-development/software-development-rest-graphql-debug',
|
||||
'user-guide/skills/optional/software-development/software-development-subagent-driven-development',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user