@coldtea/pr-lens-agent-skill 0.1.0 → 0.1.2

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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Ohans Emmanuel
3
+ Copyright (c) 2026 Coldtea AI
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @coldtea/pr-lens-agent-skill
2
2
 
3
- The PR Lens skill for coding agents. It teaches an agent to draw the change it just made: author a graph document from the diff, validate it against the contract, render it, attach it to the pull request and to fix a repository's map by writing corrections rather than editing generated output.
3
+ The PR Lens skill for coding agents. It teaches an agent to draw the change it just made: author a graph document from the diff, validate it against the contract, render it, attach it to the pull request, and to fix a repository's map by writing corrections rather than editing generated output.
4
4
 
5
- MIT © Ohans Emmanuel.
5
+ MIT © Coldtea AI.
6
6
 
7
7
  ## Install it
8
8
 
@@ -10,21 +10,21 @@ MIT © Ohans Emmanuel.
10
10
  npm install --save-dev @coldtea/pr-lens-agent-skill
11
11
  ```
12
12
 
13
- **Claude Code** copy it where skills live, per project or per user:
13
+ **Claude Code**: copy it where skills live, per project or per user:
14
14
 
15
15
  ```bash
16
16
  mkdir -p .claude/skills/pr-lens
17
17
  cp -R node_modules/@coldtea/pr-lens-agent-skill/{SKILL.md,references} .claude/skills/pr-lens/
18
18
  ```
19
19
 
20
- **Cursor** the same file works as a rule:
20
+ **Cursor**: the same file works as a rule:
21
21
 
22
22
  ```bash
23
23
  mkdir -p .cursor/rules
24
24
  cp node_modules/@coldtea/pr-lens-agent-skill/SKILL.md .cursor/rules/pr-lens.mdc
25
25
  ```
26
26
 
27
- **Anything else** point your agent's instructions file at `SKILL.md`. It is plain markdown with YAML frontmatter, and it assumes nothing beyond a shell and `npx`.
27
+ **Anything else**: point your agent's instructions file at `SKILL.md`. It is plain markdown with YAML frontmatter, and it assumes nothing beyond a shell and `npx`.
28
28
 
29
29
  ## What is in it
30
30
 
@@ -34,7 +34,7 @@ cp node_modules/@coldtea/pr-lens-agent-skill/SKILL.md .cursor/rules/pr-lens.mdc
34
34
  | `references/graph-document.md` | the document, field by field, and what the validator will catch |
35
35
  | `references/config.md` | `.github/pr-lens.yml` corrections, with recipes |
36
36
 
37
- The agent is usually the model. Rather than spending a provider key to describe a diff it already understands, it writes the document itself and lets `pr-lens validate` hold it to the contract every failure is a path into the document, so the loop closes without a human in it.
37
+ The agent is usually the model. Rather than spending a provider key to describe a diff it already understands, it writes the document itself and lets `pr-lens validate` hold it to the contract. Every failure is a path into the document, so the loop closes without a human in it.
38
38
 
39
39
  ## Why this exists
40
40
 
@@ -42,4 +42,4 @@ A coding agent that opens a pull request is asking a person to review code the p
42
42
 
43
43
  ---
44
44
 
45
- Part of [PR Lens](https://prlens.dev) review what actually matters.
45
+ Part of [PR Lens](https://prlens.dev). Review what actually matters.
package/SKILL.md CHANGED
@@ -1,36 +1,45 @@
1
1
  ---
2
2
  name: pr-lens
3
- description: Draw a code change as an architecture or data-flow diagram with PR Lens author a graph document from a diff, validate it, render it to SVG, and attach it to a pull request; or correct a repository's map in .github/pr-lens.yml. Use when asked to diagram, visualise or explain the shape of a change, when attaching a diagram to a pull request you opened, or when an existing PR Lens diagram names things wrongly.
3
+ description: Draw a code change as an architecture or data-flow diagram with PR Lens. Author a graph document from a diff, validate it, render it to SVG, and attach it to a pull request; or correct a repository's map in .github/pr-lens.yml. Use when asked to diagram, visualise or explain the shape of a change, when attaching a diagram to a pull request you opened, or when an existing PR Lens diagram names things wrongly.
4
4
  ---
5
5
 
6
6
  # PR Lens
7
7
 
8
- PR Lens turns a diff into one JSON document lanes, nodes, edges, ordered flows and renders it as an animated SVG that reads inside a GitHub pull request comment. The document is the whole contract: if it validates, it renders.
8
+ PR Lens turns a diff into one JSON document (lanes, nodes, edges, ordered flows) and renders it as an animated SVG that reads inside a GitHub pull request comment. The document is the whole contract: if it validates, it renders.
9
9
 
10
- You are usually the model. Rather than calling out to a provider, read the diff and write the document yourself, then let the tooling check it.
10
+ You are usually the model. Rather than calling out to a provider, read the diff and write the document yourself, then let the tooling check it. Nothing on this path needs a model key or names a model: whichever one you are running is the one doing the reasoning.
11
11
 
12
12
  ## The loop
13
13
 
14
14
  1. **Read the diff.** `git diff --find-renames <base>...<head>`. The base is the merge base, not the tip of the base branch.
15
- 2. **Write the document** to `pr-lens/graph.json`, following `references/graph-document.md`. The authoritative shape is the JSON Schema that ships with the contract:
16
- `node_modules/@coldtea/pr-lens-schema/json-schema/graph-doc.schema.json`
15
+ 2. **Write the document** to `.pr-lens/graph.json`, following `references/graph-document.md`. That page is the whole shape: every field, every enum, every limit, and the four rules a JSON Schema cannot express. `references/example.graph.json` is a document that validates 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.
17
16
  3. **Validate, and fix what it names.**
18
17
  ```bash
19
- npx @coldtea/pr-lens-cli validate pr-lens/graph.json
18
+ npx @coldtea/pr-lens-cli validate .pr-lens/graph.json
20
19
  ```
21
20
  Each failure is a path into your document, a reason and a code. Fix every one and run it again. Do not render an invalid document; do not "work around" a failure by deleting the element it names.
22
- 4. **Render and attach.**
21
+ 4. **Render.**
23
22
  ```bash
24
- npx @coldtea/pr-lens-cli render pr-lens/graph.json --out pr-lens/
23
+ npx @coldtea/pr-lens-cli render .pr-lens/graph.json
25
24
  ```
26
- Commit the SVGs somewhere durable an orphan branch, or a release asset and reference them from the pull request body with a `<picture>` pair so the diagram reads in both GitHub themes. `pr-lens comment --graph --manifest …` composes that markdown for you.
25
+ 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 a preview, rebuilt from the diff whenever anyone wants them again; the comment on the pull request is the thing you are making.
26
+ 5. **Attach.** Put the SVGs somewhere durable — an orphan branch, a release asset — then compose the comment from the document that was drawn:
27
+ ```bash
28
+ npx @coldtea/pr-lens-cli comment \
29
+ --graph .pr-lens/drawn.graph.json \
30
+ --manifest .pr-lens/manifest.json \
31
+ --asset-base-url https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<dir>
32
+ ```
33
+ The markdown goes to stdout, with each diagram as a `<picture>` pair so it reads in both GitHub themes. Posting it is your business.
34
+
35
+ Both flags matter. `--graph` takes `drawn.graph.json`, not the document you wrote: 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.
27
36
 
28
- If the repository has a model key configured and you would rather not author the document yourself, `npx @coldtea/pr-lens-cli analyze --base <ref>` does steps 1 and 2 with the provider of your choice.
37
+ If you would rather not author the document yourself, `npx @coldtea/pr-lens-cli 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.
29
38
 
30
39
  ## What makes a document worth reading
31
40
 
32
41
  - **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"`.
33
- - **Lanes are the reader's mental model** a runtime, a tier, a boundary not the folder tree.
42
+ - **Lanes are the reader's mental model** (a runtime, a tier, a boundary), not the folder tree.
34
43
  - **One hero edge**, two at the outside: the connection the change is really about.
35
44
  - **Add a flow only when there is a sequence** worth animating. One good flow beats three thin ones.
36
45
  - **Attach file refs**: they become the permalinks a reviewer clicks.
@@ -43,7 +52,7 @@ Read `references/graph-document.md` before writing. The four failures that accou
43
52
  | Code | What you did |
44
53
  | --- | --- |
45
54
  | `BROKEN_REFERENCE` | an edge, a flow step or a view names an id you never declared |
46
- | `INVALID_DOCUMENT` | an invented field the schemas are strict, unknown keys are rejected |
55
+ | `INVALID_DOCUMENT` | an invented field; the schemas are strict, unknown keys are rejected |
47
56
  | `DUPLICATE_ID` | two nodes, edges or views sharing an id |
48
57
  | `UNSUPPORTED_SCHEMA_VERSION` | `schemaVersion` is not the contract version installed |
49
58
 
@@ -51,7 +60,7 @@ Four rules cannot be expressed in JSON Schema and are checked only by the parser
51
60
 
52
61
  ## Fixing a map instead of writing one
53
62
 
54
- When someone says the diagram is wrong a node is misnamed, a folder should not be on it, something sits in the wrong lane do not edit the generated document. It is regenerated on every run. Write the correction into `.github/pr-lens.yml`, which is an overlay applied over fresh inference every time:
63
+ When someone says the diagram is wrong (a node is misnamed, a folder should not be on it, something sits in the wrong lane), do not edit the generated document. It is regenerated on every run. Write the correction into `.github/pr-lens.yml`, which is an overlay applied over fresh inference every time:
55
64
 
56
65
  ```yaml
57
66
  schemaVersion: 0.1.0
@@ -68,16 +77,18 @@ map:
68
77
 
69
78
  `references/config.md` has the full format and the recipes. Validate it the same way: `npx @coldtea/pr-lens-cli validate .github/pr-lens.yml`.
70
79
 
71
- A `match` beginning with `id:` addresses one node exactly; anything else is a path glob matched against a node's file paths prefer the glob, because it keeps holding when the next run names the node differently. A lane pin may name a lane the document never declared: the band is created, and takes the id for its label, so give it one a reader would want to see.
80
+ A `match` beginning with `id:` addresses one node exactly; anything else is a path glob matched against a node's file paths. Prefer the glob, because it keeps holding when the next run names the node differently. A lane pin may name a lane the document never declared: the band is created, and takes the id for its label, so give it one a reader would want to see.
72
81
 
73
- `pr-lens render` says so when a correction matched nothing, which is how a config that has drifted the file it named moved or was deleted becomes visible instead of quietly doing nothing.
82
+ `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.
74
83
 
75
- ## Reference documents
84
+ ## What ships with this skill
76
85
 
77
- The contract ships worked examples. `postmark-refactor.graph.json` is the realistic one three lanes, all four delta states, a hero edge, a seven-step flow, a nested drill-down tree — and `minimal.graph.json` is the smallest document that validates:
86
+ Everything you need is beside this page. Nothing here asks you to install a package first.
78
87
 
79
- ```bash
80
- ls node_modules/@coldtea/pr-lens-schema/examples/
81
- ```
88
+ | | |
89
+ | --- | --- |
90
+ | `references/graph-document.md` | the document, field by field: enums, limits, and where documents actually go wrong |
91
+ | `references/config.md` | `.github/pr-lens.yml`, the correction overlay, in full |
92
+ | `references/example.graph.json` | one complete document that validates, to read and to copy the shape of |
82
93
 
83
- Read the realistic one before authoring your first document. It is faster than reading the schema.
94
+ The same document ships as `postmark-refactor.graph.json` in `@coldtea/pr-lens-schema`, and the JSON Schema the validator enforces is published at `https://unpkg.com/@coldtea/pr-lens-schema/json-schema/graph-doc.schema.json`. Neither is something you need to fetch to write a document.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@coldtea/pr-lens-agent-skill",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "The PR Lens skill for coding agents: author a graph document from a diff, validate it, render it, and correct a repository's map.",
5
5
  "license": "MIT",
6
- "author": "Ohans Emmanuel",
6
+ "author": "Coldtea AI",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/coldteadotai/pr-lens.git",
@@ -30,13 +30,15 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20.19.0",
33
+ "tsx": "4.23.12",
33
34
  "typescript": "7.0.2",
34
35
  "vitest": "4.1.11",
35
36
  "yaml": "^2.8.1",
36
- "@coldtea/pr-lens-renderer": "^0.1.0",
37
- "@coldtea/pr-lens-schema": "^0.1.0"
37
+ "@coldtea/pr-lens-renderer": "^0.1.2",
38
+ "@coldtea/pr-lens-schema": "^0.1.2"
38
39
  },
39
40
  "scripts": {
41
+ "skill:sync": "tsx scripts/sync.ts",
40
42
  "test": "vitest run",
41
43
  "typecheck": "tsc -p tsconfig.json --noEmit"
42
44
  }
@@ -1,4 +1,4 @@
1
- # Correcting the map `.github/pr-lens.yml`
1
+ # Correcting the map: `.github/pr-lens.yml`
2
2
 
3
3
  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.
4
4
 
@@ -21,15 +21,15 @@ map:
21
21
  group: broadcast-lib
22
22
  ```
23
23
 
24
- Every field except `schemaVersion` is optional, and the file itself is optional. The JSON Schema for editor autocomplete ships with the contract:
24
+ Every field except `schemaVersion` is optional, and the file itself is optional. For editor autocomplete, point at the published JSON Schema — no install needed:
25
25
 
26
26
  ```jsonc
27
- { "$ref": "node_modules/@coldtea/pr-lens-schema/json-schema/config.schema.json" }
27
+ { "$ref": "https://unpkg.com/@coldtea/pr-lens-schema/json-schema/config.schema.json" }
28
28
  ```
29
29
 
30
30
  ## Selectors
31
31
 
32
- A `match` beginning with `id:` addresses exactly one node `id:build-bulk-payload`. Anything else is a repository-relative path glob matched against the node's file paths.
32
+ 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.
33
33
 
34
34
  **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).
35
35
 
@@ -42,7 +42,7 @@ A `match` beginning with `id:` addresses exactly one node — `id:build-bulk-pay
42
42
  | `lane` | moves matching nodes into a lane, **creating it** when the document declares no such id |
43
43
  | `group` | clusters matching nodes under a sub-group inside their lane |
44
44
 
45
- 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.
45
+ 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.
46
46
 
47
47
  ## Recipes
48
48
 
@@ -87,4 +87,4 @@ lenses: [architecture]
87
87
  npx @coldtea/pr-lens-cli validate .github/pr-lens.yml
88
88
  ```
89
89
 
90
- `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.
90
+ `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,580 @@
1
+ {
2
+ "schemaVersion": "0.1.0",
3
+ "kind": "graph",
4
+ "generatedAt": "2026-08-19T18:24:00.000Z",
5
+ "title": "Batch broadcast sending through Postmark",
6
+ "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.",
7
+ "lenses": [
8
+ "architecture",
9
+ "data-flow"
10
+ ],
11
+ "provenance": {
12
+ "repo": {
13
+ "owner": "ohansemmanuel",
14
+ "name": "bestregards",
15
+ "host": "github.com"
16
+ },
17
+ "base": {
18
+ "sha": "3f5c1ab9d24e7f08c6b1a5d3e9074c2b8a6f1d40",
19
+ "ref": "main"
20
+ },
21
+ "head": {
22
+ "sha": "b71e0d4c8a92f5361de7c0b4a8f2593d6c1e8a77",
23
+ "ref": "batch-broadcast-send"
24
+ },
25
+ "pullRequest": {
26
+ "number": 128,
27
+ "title": "Send broadcasts in batches of 500",
28
+ "url": "https://github.com/ohansemmanuel/bestregards/pull/128"
29
+ },
30
+ "generator": {
31
+ "name": "pr-lens-examples",
32
+ "version": "0.1.0"
33
+ }
34
+ },
35
+ "lanes": [
36
+ {
37
+ "id": "web",
38
+ "label": "Next.js",
39
+ "subtitle": "Vercel",
40
+ "order": 0
41
+ },
42
+ {
43
+ "id": "functions",
44
+ "label": "Cloud Functions",
45
+ "subtitle": "Firebase",
46
+ "order": 1
47
+ },
48
+ {
49
+ "id": "external",
50
+ "label": "External",
51
+ "subtitle": "Postmark",
52
+ "order": 2
53
+ }
54
+ ],
55
+ "nodes": [
56
+ {
57
+ "id": "broadcast-composer",
58
+ "label": "Broadcast composer",
59
+ "kind": "ui",
60
+ "delta": "unchanged",
61
+ "lane": "web",
62
+ "subtitle": "app/broadcasts/new",
63
+ "summary": "Where an author writes a broadcast and hits send. Untouched by this change.",
64
+ "files": [
65
+ {
66
+ "path": "app/broadcasts/new/page.tsx"
67
+ }
68
+ ],
69
+ "badges": []
70
+ },
71
+ {
72
+ "id": "queue-route",
73
+ "label": "POST /api/broadcasts/queue",
74
+ "kind": "route",
75
+ "delta": "modified",
76
+ "lane": "web",
77
+ "summary": "Writes the queue document. Now stamps the recipient count and batch size the sender will use instead of leaving batching to the worker.",
78
+ "files": [
79
+ {
80
+ "path": "app/api/broadcasts/queue/route.ts",
81
+ "startLine": 24,
82
+ "endLine": 96
83
+ }
84
+ ],
85
+ "badges": [
86
+ "+38 / -12"
87
+ ]
88
+ },
89
+ {
90
+ "id": "broadcast-queue",
91
+ "label": "broadcastQueue",
92
+ "kind": "datastore",
93
+ "delta": "modified",
94
+ "lane": "functions",
95
+ "subtitle": "Firestore collection",
96
+ "summary": "Queue documents gained batchSize and suppressedCount fields, and results are now written back per batch rather than per recipient.",
97
+ "files": [
98
+ {
99
+ "path": "functions/src/broadcast/schema.ts",
100
+ "startLine": 12,
101
+ "endLine": 48
102
+ }
103
+ ],
104
+ "badges": []
105
+ },
106
+ {
107
+ "id": "send-broadcast-bulk",
108
+ "label": "sendBroadcastBulk",
109
+ "kind": "function",
110
+ "delta": "added",
111
+ "lane": "functions",
112
+ "subtitle": "onWrite trigger",
113
+ "summary": "New trigger handler. Fetches suppressions once, builds batched payloads, and posts them to Postmark in chunks of 500.",
114
+ "files": [
115
+ {
116
+ "path": "functions/src/broadcast/sendBroadcastBulk.ts",
117
+ "startLine": 1,
118
+ "endLine": 142
119
+ }
120
+ ],
121
+ "badges": [
122
+ "new"
123
+ ]
124
+ },
125
+ {
126
+ "id": "build-bulk-payload",
127
+ "label": "buildBulkPayload",
128
+ "kind": "function",
129
+ "delta": "added",
130
+ "lane": "functions",
131
+ "summary": "Turns a broadcast and its recipient slice into a Postmark batch request body.",
132
+ "files": [
133
+ {
134
+ "path": "packages/broadcast-lib/src/buildBulkPayload.ts",
135
+ "startLine": 1,
136
+ "endLine": 74
137
+ }
138
+ ],
139
+ "badges": []
140
+ },
141
+ {
142
+ "id": "get-suppressed-emails",
143
+ "label": "getSuppressedEmails",
144
+ "kind": "function",
145
+ "delta": "added",
146
+ "lane": "functions",
147
+ "summary": "Pulls the Postmark suppression dump once per broadcast so suppressed addresses are filtered before any batch is sent.",
148
+ "files": [
149
+ {
150
+ "path": "packages/broadcast-lib/src/getSuppressedEmails.ts",
151
+ "startLine": 1,
152
+ "endLine": 58
153
+ }
154
+ ],
155
+ "badges": []
156
+ },
157
+ {
158
+ "id": "broadcast-lib",
159
+ "label": "broadcast-lib",
160
+ "kind": "package",
161
+ "delta": "added",
162
+ "lane": "functions",
163
+ "subtitle": "packages/broadcast-lib",
164
+ "summary": "New shared package so the queue route and the sender agree on payload shape and batch size.",
165
+ "files": [
166
+ {
167
+ "path": "packages/broadcast-lib/src/index.ts"
168
+ }
169
+ ],
170
+ "badges": [
171
+ "new package"
172
+ ]
173
+ },
174
+ {
175
+ "id": "process-broadcast",
176
+ "label": "processBroadcast",
177
+ "kind": "function",
178
+ "delta": "removed",
179
+ "lane": "functions",
180
+ "subtitle": "onWrite trigger",
181
+ "summary": "The per-recipient loop this change replaces.",
182
+ "files": [
183
+ {
184
+ "path": "functions/src/broadcast/processBroadcast.ts",
185
+ "startLine": 1,
186
+ "endLine": 118,
187
+ "revision": "base"
188
+ }
189
+ ],
190
+ "badges": []
191
+ },
192
+ {
193
+ "id": "send-single-email",
194
+ "label": "sendSingleEmail",
195
+ "kind": "function",
196
+ "delta": "removed",
197
+ "lane": "functions",
198
+ "summary": "One Postmark request per recipient. Gone with the loop that called it.",
199
+ "files": [
200
+ {
201
+ "path": "functions/src/broadcast/sendSingleEmail.ts",
202
+ "startLine": 1,
203
+ "endLine": 46,
204
+ "revision": "base"
205
+ }
206
+ ],
207
+ "badges": []
208
+ },
209
+ {
210
+ "id": "postmark",
211
+ "label": "Postmark",
212
+ "kind": "external",
213
+ "delta": "modified",
214
+ "lane": "external",
215
+ "subtitle": "Email API",
216
+ "summary": "Same provider, different endpoints: the batch endpoint and the suppression dump replace repeated single sends.",
217
+ "files": [],
218
+ "badges": []
219
+ }
220
+ ],
221
+ "edges": [
222
+ {
223
+ "id": "composer-to-queue",
224
+ "from": "broadcast-composer",
225
+ "to": "queue-route",
226
+ "kind": "http",
227
+ "delta": "unchanged",
228
+ "label": "send broadcast",
229
+ "emphasis": "normal",
230
+ "animated": false,
231
+ "files": []
232
+ },
233
+ {
234
+ "id": "queue-to-firestore",
235
+ "from": "queue-route",
236
+ "to": "broadcast-queue",
237
+ "kind": "data",
238
+ "delta": "modified",
239
+ "label": "enqueue job",
240
+ "emphasis": "normal",
241
+ "animated": false,
242
+ "files": []
243
+ },
244
+ {
245
+ "id": "queue-to-lib",
246
+ "from": "queue-route",
247
+ "to": "broadcast-lib",
248
+ "kind": "dependency",
249
+ "delta": "added",
250
+ "label": "batch size",
251
+ "emphasis": "normal",
252
+ "animated": false,
253
+ "files": []
254
+ },
255
+ {
256
+ "id": "firestore-to-bulk",
257
+ "from": "broadcast-queue",
258
+ "to": "send-broadcast-bulk",
259
+ "kind": "event",
260
+ "delta": "added",
261
+ "label": "onWrite",
262
+ "emphasis": "normal",
263
+ "animated": false,
264
+ "files": []
265
+ },
266
+ {
267
+ "id": "firestore-to-process",
268
+ "from": "broadcast-queue",
269
+ "to": "process-broadcast",
270
+ "kind": "event",
271
+ "delta": "removed",
272
+ "label": "onWrite",
273
+ "emphasis": "normal",
274
+ "animated": false,
275
+ "files": []
276
+ },
277
+ {
278
+ "id": "process-to-single",
279
+ "from": "process-broadcast",
280
+ "to": "send-single-email",
281
+ "kind": "call",
282
+ "delta": "removed",
283
+ "label": "per recipient",
284
+ "emphasis": "normal",
285
+ "animated": false,
286
+ "files": []
287
+ },
288
+ {
289
+ "id": "single-to-postmark",
290
+ "from": "send-single-email",
291
+ "to": "postmark",
292
+ "kind": "http",
293
+ "delta": "removed",
294
+ "label": "POST /email · 1 msg/call",
295
+ "emphasis": "normal",
296
+ "animated": false,
297
+ "files": []
298
+ },
299
+ {
300
+ "id": "bulk-to-payload",
301
+ "from": "send-broadcast-bulk",
302
+ "to": "build-bulk-payload",
303
+ "kind": "call",
304
+ "delta": "added",
305
+ "emphasis": "normal",
306
+ "animated": false,
307
+ "files": []
308
+ },
309
+ {
310
+ "id": "bulk-to-suppressions",
311
+ "from": "send-broadcast-bulk",
312
+ "to": "get-suppressed-emails",
313
+ "kind": "call",
314
+ "delta": "added",
315
+ "emphasis": "normal",
316
+ "animated": false,
317
+ "files": []
318
+ },
319
+ {
320
+ "id": "bulk-to-lib",
321
+ "from": "send-broadcast-bulk",
322
+ "to": "broadcast-lib",
323
+ "kind": "dependency",
324
+ "delta": "added",
325
+ "emphasis": "normal",
326
+ "animated": false,
327
+ "files": []
328
+ },
329
+ {
330
+ "id": "suppressions-to-postmark",
331
+ "from": "get-suppressed-emails",
332
+ "to": "postmark",
333
+ "kind": "http",
334
+ "delta": "added",
335
+ "label": "GET suppression dump",
336
+ "emphasis": "normal",
337
+ "animated": true,
338
+ "files": []
339
+ },
340
+ {
341
+ "id": "bulk-to-postmark",
342
+ "from": "send-broadcast-bulk",
343
+ "to": "postmark",
344
+ "kind": "http",
345
+ "delta": "added",
346
+ "label": "500 msgs/call",
347
+ "emphasis": "hero",
348
+ "animated": true,
349
+ "summary": "The change in one edge: a broadcast to 10,000 recipients drops from 10,000 requests to 20.",
350
+ "files": []
351
+ },
352
+ {
353
+ "id": "bulk-to-firestore",
354
+ "from": "send-broadcast-bulk",
355
+ "to": "broadcast-queue",
356
+ "kind": "data",
357
+ "delta": "added",
358
+ "label": "write results",
359
+ "emphasis": "normal",
360
+ "animated": false,
361
+ "files": []
362
+ }
363
+ ],
364
+ "flows": [
365
+ {
366
+ "id": "send-pipeline",
367
+ "title": "Sending a broadcast",
368
+ "summary": "The path a queued broadcast takes now, from enqueue to per-message results.",
369
+ "delta": "modified",
370
+ "participants": [
371
+ {
372
+ "node": "queue-route",
373
+ "label": "queue route"
374
+ },
375
+ {
376
+ "node": "broadcast-queue",
377
+ "label": "Firestore"
378
+ },
379
+ {
380
+ "node": "send-broadcast-bulk",
381
+ "label": "sendBroadcastBulk"
382
+ },
383
+ {
384
+ "node": "postmark",
385
+ "label": "Postmark"
386
+ }
387
+ ],
388
+ "messages": [
389
+ {
390
+ "id": "enqueue",
391
+ "from": "queue-route",
392
+ "to": "broadcast-queue",
393
+ "label": "enqueue broadcast job",
394
+ "kind": "async",
395
+ "delta": "modified",
396
+ "animated": true,
397
+ "files": []
398
+ },
399
+ {
400
+ "id": "trigger",
401
+ "from": "broadcast-queue",
402
+ "to": "send-broadcast-bulk",
403
+ "label": "onWrite trigger",
404
+ "kind": "async",
405
+ "delta": "added",
406
+ "animated": true,
407
+ "files": []
408
+ },
409
+ {
410
+ "id": "suppressions-request",
411
+ "from": "send-broadcast-bulk",
412
+ "to": "postmark",
413
+ "label": "GET suppression dump",
414
+ "kind": "sync",
415
+ "delta": "added",
416
+ "animated": true,
417
+ "files": []
418
+ },
419
+ {
420
+ "id": "suppressions-response",
421
+ "from": "postmark",
422
+ "to": "send-broadcast-bulk",
423
+ "label": "suppressed addresses",
424
+ "kind": "return",
425
+ "delta": "added",
426
+ "animated": true,
427
+ "note": "Fetched once per broadcast, not once per recipient.",
428
+ "files": []
429
+ },
430
+ {
431
+ "id": "batch-post",
432
+ "from": "send-broadcast-bulk",
433
+ "to": "postmark",
434
+ "label": "POST /email/batch · 500 msgs",
435
+ "kind": "sync",
436
+ "delta": "added",
437
+ "animated": true,
438
+ "repeat": 4,
439
+ "note": "One request per 500 recipients; four for this 2,000-recipient broadcast.",
440
+ "files": []
441
+ },
442
+ {
443
+ "id": "batch-results",
444
+ "from": "postmark",
445
+ "to": "send-broadcast-bulk",
446
+ "label": "per-message results",
447
+ "kind": "return",
448
+ "delta": "added",
449
+ "animated": true,
450
+ "files": []
451
+ },
452
+ {
453
+ "id": "write-results",
454
+ "from": "send-broadcast-bulk",
455
+ "to": "broadcast-queue",
456
+ "label": "write results",
457
+ "kind": "async",
458
+ "delta": "added",
459
+ "animated": true,
460
+ "files": []
461
+ }
462
+ ]
463
+ }
464
+ ],
465
+ "stats": {
466
+ "filesChanged": 14,
467
+ "additions": 486,
468
+ "deletions": 212,
469
+ "chips": [
470
+ {
471
+ "label": "Postmark calls",
472
+ "value": "500× fewer",
473
+ "tone": "hero"
474
+ },
475
+ {
476
+ "label": "New",
477
+ "value": "4 units",
478
+ "tone": "added"
479
+ },
480
+ {
481
+ "label": "Retired",
482
+ "value": "2 units",
483
+ "tone": "removed"
484
+ }
485
+ ]
486
+ },
487
+ "views": [
488
+ {
489
+ "id": "overview",
490
+ "title": "Architecture — blast radius",
491
+ "lens": "architecture",
492
+ "summary": "Everything this change touches, across all three lanes.",
493
+ "scope": {
494
+ "kind": "all"
495
+ },
496
+ "defaultOpen": true,
497
+ "children": [
498
+ {
499
+ "id": "new-batch-path",
500
+ "title": "The new batch path",
501
+ "lens": "architecture",
502
+ "summary": "What replaced the per-recipient loop.",
503
+ "scope": {
504
+ "kind": "selection",
505
+ "lanes": [],
506
+ "nodes": [
507
+ "send-broadcast-bulk",
508
+ "build-bulk-payload",
509
+ "get-suppressed-emails",
510
+ "broadcast-lib",
511
+ "postmark"
512
+ ],
513
+ "edges": [
514
+ "bulk-to-payload",
515
+ "bulk-to-suppressions",
516
+ "bulk-to-lib",
517
+ "suppressions-to-postmark",
518
+ "bulk-to-postmark",
519
+ "bulk-to-firestore"
520
+ ],
521
+ "flows": []
522
+ },
523
+ "defaultOpen": false,
524
+ "children": []
525
+ },
526
+ {
527
+ "id": "retired-path",
528
+ "title": "What was retired",
529
+ "lens": "architecture",
530
+ "summary": "The single-send path, kept visible so a reviewer can confirm nothing else called it.",
531
+ "scope": {
532
+ "kind": "selection",
533
+ "lanes": [],
534
+ "nodes": [
535
+ "process-broadcast",
536
+ "send-single-email"
537
+ ],
538
+ "edges": [
539
+ "firestore-to-process",
540
+ "process-to-single",
541
+ "single-to-postmark"
542
+ ],
543
+ "flows": []
544
+ },
545
+ "defaultOpen": false,
546
+ "children": []
547
+ }
548
+ ]
549
+ },
550
+ {
551
+ "id": "send-pipeline-view",
552
+ "title": "Data flow — sending a broadcast",
553
+ "lens": "data-flow",
554
+ "scope": {
555
+ "kind": "selection",
556
+ "lanes": [],
557
+ "nodes": [],
558
+ "edges": [],
559
+ "flows": [
560
+ "send-pipeline"
561
+ ]
562
+ },
563
+ "defaultOpen": false,
564
+ "children": []
565
+ }
566
+ ],
567
+ "layout": {
568
+ "direction": "right",
569
+ "laneOrder": [
570
+ "web",
571
+ "functions",
572
+ "external"
573
+ ],
574
+ "rank": {
575
+ "queue-route": 0,
576
+ "send-broadcast-bulk": 1,
577
+ "postmark": 2
578
+ }
579
+ }
580
+ }
@@ -1,8 +1,10 @@
1
1
  # Authoring a graph document
2
2
 
3
- The authoritative shape is `node_modules/@coldtea/pr-lens-schema/json-schema/graph-doc.schema.json`. This page is what that schema cannot tell you: which parts matter, and where documents actually go wrong.
3
+ 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.
4
4
 
5
- Every schema here is **strict** an unknown key is a rejection, not a warning. A field with a default may be left out.
5
+ 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.
6
+
7
+ Every schema here is **strict**: an unknown key is a rejection, not a warning. A field with a default may be left out.
6
8
 
7
9
  ## The document
8
10
 
@@ -25,7 +27,7 @@ Every schema here is **strict** — an unknown key is a rejection, not a warning
25
27
 
26
28
  `lenses` declares what the document carries enough detail to draw: `architecture`, `data-flow`, or both. A document carrying flows must declare `data-flow`.
27
29
 
28
- `provenance` is where the document came from: the repository, the base and head commit shas (lowercase hex, 740 characters), optionally the pull request and the generator. When you produce a document through the CLI these are filled in from the repository do not invent them.
30
+ `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.
29
31
 
30
32
  ## Ids
31
33
 
@@ -45,7 +47,7 @@ Every node, edge, flow and flow step declares one: `added`, `modified`, `removed
45
47
  { "id": "functions", "label": "Cloud Functions", "subtitle": "Node 20", "order": 1 }
46
48
  ```
47
49
 
48
- `order` (064) 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.
50
+ `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.
49
51
 
50
52
  ## Nodes
51
53
 
@@ -68,7 +70,7 @@ Every node, edge, flow and flow step declares one: `added`, `modified`, `removed
68
70
 
69
71
  `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.
70
72
 
71
- `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.
73
+ `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.
72
74
 
73
75
  ## Edges
74
76
 
@@ -87,7 +89,7 @@ Up to 512.
87
89
  }
88
90
  ```
89
91
 
90
- `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.
92
+ `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.
91
93
 
92
94
  ## Flows
93
95
 
@@ -108,7 +110,7 @@ Up to 16, for the data-flow lens.
108
110
  ```
109
111
 
110
112
  - 2 to 12 participants, ordered by array position; each names a node id.
111
- - 1 to 64 messages. **Step order is array order** there is no step number field, so a document cannot disagree with its own animation.
113
+ - 1 to 64 messages. **Step order is array order**: there is no step number field, so a document cannot disagree with its own animation.
112
114
  - `kind` is `sync`, `async`, `return` or `self`. `self` requires `from === to`, and no other kind may have them equal.
113
115
  - Both endpoints must be participants of that flow, not merely nodes of the document.
114
116
  - `repeat` says a step happens more than once per run, e.g. 4 batched requests.
@@ -145,7 +147,7 @@ The drill-down tree in the comment: up to 32 at the root, nesting up to 32 child
145
147
  { "direction": "right", "laneOrder": ["api", "functions", "external"], "rank": { "send-broadcast-bulk": 2 } }
146
148
  ```
147
149
 
148
- 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.
150
+ 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.
149
151
 
150
152
  ## File references
151
153
 
@@ -157,12 +159,12 @@ Repository-relative POSIX paths: no leading `/`, no drive letter, no backslash,
157
159
 
158
160
  ## Length limits
159
161
 
160
- 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.
162
+ 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.
161
163
 
162
164
  ## Then validate
163
165
 
164
166
  ```bash
165
- npx @coldtea/pr-lens-cli validate pr-lens/graph.json
167
+ npx @coldtea/pr-lens-cli validate .pr-lens/graph.json
166
168
  ```
167
169
 
168
170
  Every problem is reported at once, with a path into the document. Fix them all and run it again until it is clean.