@coldtea/pr-lens-schema 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -6
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/examples/index.d.ts +10 -0
- package/dist/examples/index.d.ts.map +1 -1
- package/examples/broadcast-baseline.graph.json +1 -1
- package/examples/broadcast-baseline.patch.json +1 -1
- package/examples/minimal.graph.json +1 -1
- package/examples/postmark-refactor.graph.json +106 -1
- package/examples/postmark-refactor.render-manifest.json +1 -1
- package/examples/pr-lens.config.json +7 -2
- package/json-schema/config.schema.json +20 -1
- package/json-schema/graph-doc.schema.json +221 -2
- package/json-schema/patch-doc.schema.json +2 -2
- package/json-schema/render-manifest.schema.json +2 -2
- package/package.json +1 -1
- package/src/apply.ts +16 -0
- package/src/config.ts +12 -0
- package/src/examples/postmark-refactor.ts +51 -1
- package/src/graph.ts +99 -1
- package/src/index.ts +8 -0
- package/src/integrity.ts +85 -0
- package/src/primitives.ts +21 -0
- package/src/version.ts +1 -1
- package/src/walkthrough.ts +146 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ const result = safeParseGraphDoc(json); // { ok: true, value } | { ok: false
|
|
|
19
19
|
|
|
20
20
|
| Document | Purpose | Parser |
|
|
21
21
|
| --- | --- | --- |
|
|
22
|
-
| `GraphDoc` | Lanes, nodes, edges, flows, stats
|
|
22
|
+
| `GraphDoc` | Lanes, nodes, edges, flows, stats, the drill-down tree and an optional walkthrough for one pull request, or for a stored baseline map | `parseGraphDoc` |
|
|
23
23
|
| `PatchDoc` | Ordered operations that move a stored graph forward as pull requests merge | `parsePatchDoc` |
|
|
24
24
|
| `Config` | What a repository commits as `.github/pr-lens.yml` | `parseConfig` |
|
|
25
25
|
| `RenderManifest` | The SVGs a render produced, and where they live | `parseRenderManifest` |
|
|
@@ -42,6 +42,48 @@ The cap is the worst case, every theme rendered, not what a particular render wo
|
|
|
42
42
|
|
|
43
43
|
`architecture` shows blast radius against the existing system. `data-flow` animates an ordered pipeline. There is deliberately no security lens and no findings field: PR Lens is the comprehension layer, and a document that carries findings is rejected rather than quietly stripped. The `Lens` enum is additive: a future contract version may add lenses, so treat one you do not recognise as a view to skip, not as a failure.
|
|
44
44
|
|
|
45
|
+
## Walkthroughs
|
|
46
|
+
|
|
47
|
+
A document may carry a `walkthrough`.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
walkthrough: {
|
|
51
|
+
steps: [
|
|
52
|
+
{
|
|
53
|
+
id: "four-batch-calls",
|
|
54
|
+
heading: "Postmark now gets 500 emails per call",
|
|
55
|
+
body: "One call per batch, and Postmark answers with a result for each message.",
|
|
56
|
+
stage: { kind: "flow", flow: "send-pipeline" },
|
|
57
|
+
focus: { kind: "selection", messages: ["batch-post", "batch-results"] },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "blast-radius",
|
|
61
|
+
heading: "4 parts added, 2 removed, across 3 lanes",
|
|
62
|
+
body: "A 2,000-person broadcast used to make 2,000 calls to Postmark. It now makes 4.",
|
|
63
|
+
stage: { kind: "view", view: "overview" },
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
Each step has:
|
|
72
|
+
|
|
73
|
+
- `heading`: the thing and what happened to it, up to 48 characters, in sentence case. For example "Postmark now gets 500 emails per call".
|
|
74
|
+
- `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". A heading with no body reads as unfinished, so the parser requires one.
|
|
75
|
+
- `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.
|
|
76
|
+
- `focus`: what to zoom in on inside that diagram. `{ "kind": "all" }` 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.
|
|
77
|
+
|
|
78
|
+
The validator checks:
|
|
79
|
+
|
|
80
|
+
- 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.
|
|
81
|
+
- `messages` needs a stage that shows a flow. Leave it out when the stage is an architecture view.
|
|
82
|
+
- Step ids are unique within the walkthrough. Two steps minimum, twelve maximum.
|
|
83
|
+
- A stored map never carries a walkthrough. A map describes the system; a walkthrough tells the story of one change.
|
|
84
|
+
|
|
85
|
+
When a patch or a correction removes something from the document, the walkthrough follows: a step loses the names that are gone, a step with nothing left to point at or whose diagram is gone is dropped, and if fewer than two steps remain the walkthrough is dropped. `pruneWalkthrough` does this and is exported for anything else that removes parts of a document.
|
|
86
|
+
|
|
45
87
|
## Deltas
|
|
46
88
|
|
|
47
89
|
Every node, edge and flow step declares how it relates to the base commit: `added`, `modified`, `removed` or `unchanged`. `unchanged` elements are the context a reviewer needs to judge blast radius, so they belong in the document rather than being filtered out.
|
|
@@ -52,7 +94,7 @@ Parsing runs four things in one pass, and reports every problem it finds rather
|
|
|
52
94
|
|
|
53
95
|
1. **Structure**: types, lengths, enums, no unknown keys, and file paths that can actually become a diff permalink (repository-relative, POSIX, no `..` segment).
|
|
54
96
|
2. **Contract version**: below `1.0.0` an exact `major.minor` match; from `1.0.0` on, the same major and a minor no newer than this package's.
|
|
55
|
-
3. **Referential integrity**: every node sits in a declared lane, every edge joins declared nodes, every flow step runs between declared participants, every drill-down view and layout hint names elements that exist, and a document carrying flows declares the `data-flow` lens.
|
|
97
|
+
3. **Referential integrity**: every node sits in a declared lane, every edge joins declared nodes, every flow step runs between declared participants, every drill-down view and layout hint names elements that exist, every walkthrough step stages a diagram the document has and focuses elements it has, and a document carrying flows declares the `data-flow` lens.
|
|
56
98
|
4. **That a render could describe the document**: see below.
|
|
57
99
|
|
|
58
100
|
A document nested deeper than the stack can walk is reported as a document too deep to read, not thrown: `safeParse*` returns a result whatever it is handed.
|
|
@@ -76,6 +118,7 @@ const result = applyPatchDoc(parseGraphDoc(baseline), parsePatchDoc(patch));
|
|
|
76
118
|
- Operations apply in array order, and the first conflict stops the batch, because a later operation was written against the state an earlier one was supposed to produce.
|
|
77
119
|
- `add_*` refuses an id that is taken; `update_*` and `remove_*` refuse an id that is absent; `update_*` writes only the fields it names.
|
|
78
120
|
- Removing a node takes its edges and flow steps with it, and drops a flow left with fewer than two participants or no steps. Removed ids are pruned out of the drill-down tree and the layout hints, and a view whose selection loses its last element is dropped rather than widened.
|
|
121
|
+
- A walkthrough is pruned once, against the document the operations produced rather than alongside each one. A step names a diagram as well as elements, so it can name a view the tree prune has just dropped, and an update that rewrites a flow's steps takes focus members away without removing anything the other prunes would notice. A step whose stage is gone, or whose focus loses its last element, goes with it, and a tour left with fewer than two steps goes whole. Focused flow steps are measured against the flow the step's own stage draws, never against the document at large.
|
|
79
122
|
- Removing a lane that still holds nodes is a conflict, not a cascade: move its nodes first.
|
|
80
123
|
- `remove_*` deletes an element from the stored graph. That is a different statement from `delta: "removed"`, which says an element still exists but is being deleted by the change under review.
|
|
81
124
|
- A successful result is always a document that would pass `parseGraphDoc`: the candidate is validated as a whole before it is returned.
|
|
@@ -91,6 +134,9 @@ A stored map describes a system rather than a change, so everything in it is `un
|
|
|
91
134
|
schemaVersion: 0.1.0
|
|
92
135
|
lenses: [architecture, data-flow]
|
|
93
136
|
branding: true
|
|
137
|
+
github:
|
|
138
|
+
comment:
|
|
139
|
+
collapsed: false
|
|
94
140
|
map:
|
|
95
141
|
rename:
|
|
96
142
|
- match: functions/src/broadcast/sendBroadcastBulk.ts
|
|
@@ -107,6 +153,12 @@ map:
|
|
|
107
153
|
|
|
108
154
|
A `match` beginning with `id:` addresses one node exactly; anything else is a path glob matched against a node's file paths, so a correction survives the model renaming the node between runs. Corrections are an overlay applied over fresh inference on every run. Inference never writes back into this file.
|
|
109
155
|
|
|
156
|
+
The hosted App reads `github` settings from the PR's head commit. Other options apply to the CLI.
|
|
157
|
+
|
|
158
|
+
| Setting | Default | Effect |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `github.comment.collapsed` | `false` | Start diagrams and details closed. Drawing still runs automatically. |
|
|
161
|
+
|
|
110
162
|
## JSON Schema
|
|
111
163
|
|
|
112
164
|
`json-schema/*.json` (draft 2020-12) is generated from the zod schemas and published with the package, for producers that do not run TypeScript and for editors validating `.github/pr-lens.yml`:
|
|
@@ -117,15 +169,16 @@ A `match` beginning with `id:` addresses one node exactly; anything else is a pa
|
|
|
117
169
|
|
|
118
170
|
They describe **what an author may write**: a field with a default is one you may leave out. Rules are carried across wherever JSON Schema can state them: the supported contract versions, the repository-relative path rule, `endLine` requiring `startLine`, an asset needing a `url` or a `path`, a `selection` view having to select something.
|
|
119
171
|
|
|
120
|
-
Exactly
|
|
172
|
+
Exactly six rules cannot be stated in JSON Schema and stay the parser's job, each of them a comparison the shape alone cannot make:
|
|
121
173
|
|
|
122
174
|
1. referential integrity between elements,
|
|
123
175
|
2. a line range that ends before it starts,
|
|
124
176
|
3. the agreement between a self message's endpoints,
|
|
125
177
|
4. a patch whose two commits are the same,
|
|
126
|
-
5. more views than a render manifest could describe
|
|
178
|
+
5. more views than a render manifest could describe,
|
|
179
|
+
6. a walkthrough step focusing flow steps the diagram on its stage does not draw.
|
|
127
180
|
|
|
128
|
-
The tests run a table of documents through both representations and assert the same verdict, accept and reject alike, including a case per divergence above, so they stay deliberate and cannot quietly grow a
|
|
181
|
+
The tests run a table of documents through both representations and assert the same verdict, accept and reject alike, including a case per divergence above, so they stay deliberate and cannot quietly grow a seventh.
|
|
129
182
|
|
|
130
183
|
## Goldens
|
|
131
184
|
|
|
@@ -137,7 +190,7 @@ import { postmarkRefactorGraph } from "@coldtea/pr-lens-schema/examples";
|
|
|
137
190
|
|
|
138
191
|
They tell one story: a real refactor that moved broadcast sending from one Postmark request per recipient to batches of 500:
|
|
139
192
|
|
|
140
|
-
- **`postmark-refactor.graph.json`** is the canonical document: the pull request itself, across three lanes, exercising all four delta states, a hero edge, a seven-step data flow with returns and a repeated batch step,
|
|
193
|
+
- **`postmark-refactor.graph.json`** is the canonical document: the pull request itself, across three lanes, exercising all four delta states, a hero edge, a seven-step data flow with returns and a repeated batch step, a nested drill-down tree, and a six-step walkthrough that stages both a view and a flow. Downstream renderer goldens are measured against it.
|
|
141
194
|
- **`broadcast-baseline.graph.json`** is the stored map of that subsystem as `main` stood before the change, and **`broadcast-baseline.patch.json`** carries it to the merged state, the transition `applyPatchDoc` performs.
|
|
142
195
|
- **`postmark-refactor.render-manifest.json`** is what rendering the pull-request document produces, **`pr-lens.config.json`** a repository's corrections, and **`minimal.graph.json`** the smallest document that validates.
|
|
143
196
|
|
package/dist/config.d.ts
CHANGED
|
@@ -52,6 +52,11 @@ export declare const Config: z.ZodObject<{
|
|
|
52
52
|
}, z.core.$strict>>>;
|
|
53
53
|
}, z.core.$strict>>;
|
|
54
54
|
branding: z.ZodDefault<z.ZodBoolean>;
|
|
55
|
+
github: z.ZodPrefault<z.ZodObject<{
|
|
56
|
+
comment: z.ZodPrefault<z.ZodObject<{
|
|
57
|
+
collapsed: z.ZodDefault<z.ZodBoolean>;
|
|
58
|
+
}, z.core.$strict>>;
|
|
59
|
+
}, z.core.$strict>>;
|
|
55
60
|
}, z.core.$strict>;
|
|
56
61
|
export type Config = z.infer<typeof Config>;
|
|
57
62
|
export type ConfigInput = z.input<typeof Config>;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,aAIsE,CAAC;AAC5F,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;kBAuBwC,CAAC;AACpE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,kFAAkF;AAClF,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,aAIsE,CAAC;AAC5F,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;kBAuBwC,CAAC;AACpE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,kFAAkF;AAClF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA6BiC,CAAC;AACrD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAC5C,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -56,6 +56,18 @@ export const Config = z
|
|
|
56
56
|
.boolean()
|
|
57
57
|
.default(true)
|
|
58
58
|
.describe("Show the 'Rendered by PR Lens' footer on comments."),
|
|
59
|
+
github: z
|
|
60
|
+
.strictObject({
|
|
61
|
+
comment: z
|
|
62
|
+
.strictObject({
|
|
63
|
+
collapsed: z
|
|
64
|
+
.boolean()
|
|
65
|
+
.default(false)
|
|
66
|
+
.describe("Start the hosted App's diagrams and details in a closed disclosure. Drawing still runs automatically."),
|
|
67
|
+
})
|
|
68
|
+
.prefault({}),
|
|
69
|
+
})
|
|
70
|
+
.prefault({}),
|
|
59
71
|
})
|
|
60
72
|
.describe("Repository configuration for PR Lens.");
|
|
61
73
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC;KACtB,MAAM,EAAE;KACR,GAAG,CAAC,CAAC,CAAC;KACN,GAAG,CAAC,GAAG,CAAC;KACR,QAAQ,CAAC,8EAA8E,CAAC,CAAC;AAG5F;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC5B,YAAY,CAAC;IACZ,MAAM,EAAE,CAAC;SACN,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;SACrD,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,6BAA6B,CAAC;IAC1C,OAAO,EAAE,CAAC;SACP,KAAK,CAAC,QAAQ,CAAC;SACf,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,mEAAmE,CAAC;IAChF,IAAI,EAAE,CAAC;SACJ,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;SACpD,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,kCAAkC,CAAC;IAC/C,KAAK,EAAE,CAAC;SACL,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;SACrD,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,6DAA6D,CAAC;CAC3E,CAAC;KACD,QAAQ,CAAC,sDAAsD,CAAC,CAAC;AAGpE,kFAAkF;AAClF,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC;KACpB,YAAY,CAAC;IACZ,aAAa,EAAE,kBAAkB,CAAC,QAAQ,CACxC,gGAAgG,CACjG;IACD,MAAM,EAAE,CAAC;SACN,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;SACtC,QAAQ,CAAC,uCAAuC,CAAC;IACpD,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC7E,QAAQ,EAAE,CAAC;SACR,OAAO,EAAE;SACT,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CAAC,oDAAoD,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC;KACtB,MAAM,EAAE;KACR,GAAG,CAAC,CAAC,CAAC;KACN,GAAG,CAAC,GAAG,CAAC;KACR,QAAQ,CAAC,8EAA8E,CAAC,CAAC;AAG5F;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC5B,YAAY,CAAC;IACZ,MAAM,EAAE,CAAC;SACN,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;SACrD,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,6BAA6B,CAAC;IAC1C,OAAO,EAAE,CAAC;SACP,KAAK,CAAC,QAAQ,CAAC;SACf,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,mEAAmE,CAAC;IAChF,IAAI,EAAE,CAAC;SACJ,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;SACpD,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,kCAAkC,CAAC;IAC/C,KAAK,EAAE,CAAC;SACL,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;SACrD,GAAG,CAAC,GAAG,CAAC;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,6DAA6D,CAAC;CAC3E,CAAC;KACD,QAAQ,CAAC,sDAAsD,CAAC,CAAC;AAGpE,kFAAkF;AAClF,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC;KACpB,YAAY,CAAC;IACZ,aAAa,EAAE,kBAAkB,CAAC,QAAQ,CACxC,gGAAgG,CACjG;IACD,MAAM,EAAE,CAAC;SACN,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;SACtC,QAAQ,CAAC,uCAAuC,CAAC;IACpD,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC7E,QAAQ,EAAE,CAAC;SACR,OAAO,EAAE;SACT,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CAAC,oDAAoD,CAAC;IACjE,MAAM,EAAE,CAAC;SACN,YAAY,CAAC;QACZ,OAAO,EAAE,CAAC;aACP,YAAY,CAAC;YACZ,SAAS,EAAE,CAAC;iBACT,OAAO,EAAE;iBACT,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,uGAAuG,CAAC;SACrH,CAAC;aACD,QAAQ,CAAC,EAAE,CAAC;KAChB,CAAC;SACD,QAAQ,CAAC,EAAE,CAAC;CAChB,CAAC;KACD,QAAQ,CAAC,uCAAuC,CAAC,CAAC"}
|
package/dist/examples/index.d.ts
CHANGED
|
@@ -473,6 +473,11 @@ export declare const exampleConfig: {
|
|
|
473
473
|
}[];
|
|
474
474
|
};
|
|
475
475
|
branding: boolean;
|
|
476
|
+
github: {
|
|
477
|
+
comment: {
|
|
478
|
+
collapsed: boolean;
|
|
479
|
+
};
|
|
480
|
+
};
|
|
476
481
|
};
|
|
477
482
|
export declare const minimalGraph: {
|
|
478
483
|
schemaVersion: string;
|
|
@@ -1070,6 +1075,11 @@ export declare const goldenDocuments: {
|
|
|
1070
1075
|
}[];
|
|
1071
1076
|
};
|
|
1072
1077
|
branding: boolean;
|
|
1078
|
+
github: {
|
|
1079
|
+
comment: {
|
|
1080
|
+
collapsed: boolean;
|
|
1081
|
+
};
|
|
1082
|
+
};
|
|
1073
1083
|
};
|
|
1074
1084
|
readonly "minimal.graph.json": {
|
|
1075
1085
|
schemaVersion: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/examples/index.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA4C,CAAC;AAC/E,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAqD,CAAC;AAC3F,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA6C,CAAC;AACjF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA6C,CAAC;AACjF,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/examples/index.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA4C,CAAC;AAC/E,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAqD,CAAC;AAC3F,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA6C,CAAC;AACjF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA6C,CAAC;AACjF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;CAAkC,CAAC;AAC7D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAmC,CAAC;AAE7D,gFAAgF;AAChF,eAAO,MAAM,eAAe;aAC1B,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAC9B,wCAAwC;;;;;;;;;;;;;;;;;;;;;;;;;;;;aACxC,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAC/B,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAC/B,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;aACrB,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CACZ,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion": "0.1.
|
|
2
|
+
"schemaVersion": "0.1.1",
|
|
3
3
|
"kind": "graph",
|
|
4
4
|
"generatedAt": "2026-08-19T18:24:00.000Z",
|
|
5
5
|
"title": "Batch broadcast sending through Postmark",
|
|
@@ -564,6 +564,111 @@
|
|
|
564
564
|
"children": []
|
|
565
565
|
}
|
|
566
566
|
],
|
|
567
|
+
"walkthrough": {
|
|
568
|
+
"steps": [
|
|
569
|
+
{
|
|
570
|
+
"id": "batches-of-500",
|
|
571
|
+
"heading": "sendBroadcastBulk and buildBulkPayload added",
|
|
572
|
+
"body": "Nothing loops over recipients any more. The sender works on a whole batch at a time.",
|
|
573
|
+
"stage": {
|
|
574
|
+
"kind": "view",
|
|
575
|
+
"view": "overview"
|
|
576
|
+
},
|
|
577
|
+
"focus": {
|
|
578
|
+
"kind": "selection",
|
|
579
|
+
"lanes": [],
|
|
580
|
+
"nodes": [
|
|
581
|
+
"send-broadcast-bulk",
|
|
582
|
+
"build-bulk-payload",
|
|
583
|
+
"postmark"
|
|
584
|
+
],
|
|
585
|
+
"edges": [],
|
|
586
|
+
"messages": []
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
"id": "suppression-first",
|
|
591
|
+
"heading": "getSuppressedEmails added before the send",
|
|
592
|
+
"body": "It pulls the blocked addresses once, before any batch is built.",
|
|
593
|
+
"stage": {
|
|
594
|
+
"kind": "view",
|
|
595
|
+
"view": "new-batch-path"
|
|
596
|
+
},
|
|
597
|
+
"focus": {
|
|
598
|
+
"kind": "selection",
|
|
599
|
+
"lanes": [],
|
|
600
|
+
"nodes": [
|
|
601
|
+
"get-suppressed-emails",
|
|
602
|
+
"postmark"
|
|
603
|
+
],
|
|
604
|
+
"edges": [],
|
|
605
|
+
"messages": []
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
"id": "old-path-goes-dark",
|
|
610
|
+
"heading": "processBroadcast and sendSingleEmail removed",
|
|
611
|
+
"body": "sendBroadcastBulk does their job for whole batches.",
|
|
612
|
+
"stage": {
|
|
613
|
+
"kind": "view",
|
|
614
|
+
"view": "overview"
|
|
615
|
+
},
|
|
616
|
+
"focus": {
|
|
617
|
+
"kind": "selection",
|
|
618
|
+
"lanes": [],
|
|
619
|
+
"nodes": [
|
|
620
|
+
"process-broadcast",
|
|
621
|
+
"send-single-email"
|
|
622
|
+
],
|
|
623
|
+
"edges": [],
|
|
624
|
+
"messages": []
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
"id": "sequence-start-to-finish",
|
|
629
|
+
"heading": "The send sequence gained 6 new steps",
|
|
630
|
+
"body": "The queue write is the only step that was there before, and it now stamps the batch size.",
|
|
631
|
+
"stage": {
|
|
632
|
+
"kind": "flow",
|
|
633
|
+
"flow": "send-pipeline"
|
|
634
|
+
},
|
|
635
|
+
"focus": {
|
|
636
|
+
"kind": "all"
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
"id": "four-batch-calls",
|
|
641
|
+
"heading": "Postmark now gets 500 emails per call",
|
|
642
|
+
"body": "One call per batch, and Postmark answers with a result for each message.",
|
|
643
|
+
"stage": {
|
|
644
|
+
"kind": "flow",
|
|
645
|
+
"flow": "send-pipeline"
|
|
646
|
+
},
|
|
647
|
+
"focus": {
|
|
648
|
+
"kind": "selection",
|
|
649
|
+
"lanes": [],
|
|
650
|
+
"nodes": [],
|
|
651
|
+
"edges": [],
|
|
652
|
+
"messages": [
|
|
653
|
+
"batch-post",
|
|
654
|
+
"batch-results"
|
|
655
|
+
]
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
"id": "blast-radius",
|
|
660
|
+
"heading": "4 parts added, 2 removed, across 3 lanes",
|
|
661
|
+
"body": "A 2,000-person broadcast used to make 2,000 calls to Postmark. It now makes 4.",
|
|
662
|
+
"stage": {
|
|
663
|
+
"kind": "view",
|
|
664
|
+
"view": "overview"
|
|
665
|
+
},
|
|
666
|
+
"focus": {
|
|
667
|
+
"kind": "all"
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
]
|
|
671
|
+
},
|
|
567
672
|
"layout": {
|
|
568
673
|
"direction": "right",
|
|
569
674
|
"laneOrder": [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion": "0.1.
|
|
2
|
+
"schemaVersion": "0.1.1",
|
|
3
3
|
"lenses": [
|
|
4
4
|
"architecture",
|
|
5
5
|
"data-flow"
|
|
@@ -28,5 +28,10 @@
|
|
|
28
28
|
}
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
|
-
"branding": true
|
|
31
|
+
"branding": true,
|
|
32
|
+
"github": {
|
|
33
|
+
"comment": {
|
|
34
|
+
"collapsed": false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
32
37
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/coldteadotai/pr-lens/main/packages/schema/json-schema/config.schema.json",
|
|
4
4
|
"title": "PR Lens repository config",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"schemaVersion": {
|
|
@@ -143,6 +143,25 @@
|
|
|
143
143
|
"default": true,
|
|
144
144
|
"description": "Show the 'Rendered by PR Lens' footer on comments.",
|
|
145
145
|
"type": "boolean"
|
|
146
|
+
},
|
|
147
|
+
"github": {
|
|
148
|
+
"default": {},
|
|
149
|
+
"type": "object",
|
|
150
|
+
"properties": {
|
|
151
|
+
"comment": {
|
|
152
|
+
"default": {},
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {
|
|
155
|
+
"collapsed": {
|
|
156
|
+
"default": false,
|
|
157
|
+
"description": "Start the hosted App's diagrams and details in a closed disclosure. Drawing still runs automatically.",
|
|
158
|
+
"type": "boolean"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"additionalProperties": false
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"additionalProperties": false
|
|
146
165
|
}
|
|
147
166
|
},
|
|
148
167
|
"required": [
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/coldteadotai/pr-lens/main/packages/schema/json-schema/graph-doc.schema.json",
|
|
4
4
|
"title": "PR Lens graph document",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"schemaVersion": {
|
|
9
9
|
"type": "string",
|
|
10
10
|
"pattern": "^0\\.1\\.\\d+$",
|
|
11
|
-
"description": "Contract version the document targets. Current: 0.1.
|
|
11
|
+
"description": "Contract version the document targets. Current: 0.1.1."
|
|
12
12
|
},
|
|
13
13
|
"kind": {
|
|
14
14
|
"type": "string",
|
|
@@ -795,6 +795,225 @@
|
|
|
795
795
|
"$ref": "#/$defs/View"
|
|
796
796
|
}
|
|
797
797
|
},
|
|
798
|
+
"walkthrough": {
|
|
799
|
+
"type": "object",
|
|
800
|
+
"properties": {
|
|
801
|
+
"steps": {
|
|
802
|
+
"minItems": 2,
|
|
803
|
+
"maxItems": 12,
|
|
804
|
+
"type": "array",
|
|
805
|
+
"items": {
|
|
806
|
+
"type": "object",
|
|
807
|
+
"properties": {
|
|
808
|
+
"id": {
|
|
809
|
+
"type": "string",
|
|
810
|
+
"minLength": 1,
|
|
811
|
+
"maxLength": 128,
|
|
812
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$",
|
|
813
|
+
"description": "Stable identifier, unique within its collection in a document."
|
|
814
|
+
},
|
|
815
|
+
"heading": {
|
|
816
|
+
"type": "string",
|
|
817
|
+
"minLength": 1,
|
|
818
|
+
"maxLength": 48,
|
|
819
|
+
"description": "A step heading. Short enough to read at a glance."
|
|
820
|
+
},
|
|
821
|
+
"body": {
|
|
822
|
+
"type": "string",
|
|
823
|
+
"minLength": 1,
|
|
824
|
+
"maxLength": 140,
|
|
825
|
+
"description": "A single line under a step heading."
|
|
826
|
+
},
|
|
827
|
+
"stage": {
|
|
828
|
+
"oneOf": [
|
|
829
|
+
{
|
|
830
|
+
"type": "object",
|
|
831
|
+
"properties": {
|
|
832
|
+
"kind": {
|
|
833
|
+
"type": "string",
|
|
834
|
+
"const": "view"
|
|
835
|
+
},
|
|
836
|
+
"view": {
|
|
837
|
+
"type": "string",
|
|
838
|
+
"minLength": 1,
|
|
839
|
+
"maxLength": 128,
|
|
840
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$",
|
|
841
|
+
"description": "Id of the drill-down view to draw."
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
"required": [
|
|
845
|
+
"kind",
|
|
846
|
+
"view"
|
|
847
|
+
],
|
|
848
|
+
"additionalProperties": false
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
"type": "object",
|
|
852
|
+
"properties": {
|
|
853
|
+
"kind": {
|
|
854
|
+
"type": "string",
|
|
855
|
+
"const": "flow"
|
|
856
|
+
},
|
|
857
|
+
"flow": {
|
|
858
|
+
"type": "string",
|
|
859
|
+
"minLength": 1,
|
|
860
|
+
"maxLength": 128,
|
|
861
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$",
|
|
862
|
+
"description": "Id of the flow to draw."
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
"required": [
|
|
866
|
+
"kind",
|
|
867
|
+
"flow"
|
|
868
|
+
],
|
|
869
|
+
"additionalProperties": false
|
|
870
|
+
}
|
|
871
|
+
],
|
|
872
|
+
"description": "Which diagram a walkthrough step plays over."
|
|
873
|
+
},
|
|
874
|
+
"focus": {
|
|
875
|
+
"default": {
|
|
876
|
+
"kind": "all"
|
|
877
|
+
},
|
|
878
|
+
"oneOf": [
|
|
879
|
+
{
|
|
880
|
+
"type": "object",
|
|
881
|
+
"properties": {
|
|
882
|
+
"kind": {
|
|
883
|
+
"type": "string",
|
|
884
|
+
"const": "all"
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
"required": [
|
|
888
|
+
"kind"
|
|
889
|
+
],
|
|
890
|
+
"additionalProperties": false
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
"type": "object",
|
|
894
|
+
"properties": {
|
|
895
|
+
"kind": {
|
|
896
|
+
"type": "string",
|
|
897
|
+
"const": "selection"
|
|
898
|
+
},
|
|
899
|
+
"lanes": {
|
|
900
|
+
"default": [],
|
|
901
|
+
"maxItems": 64,
|
|
902
|
+
"type": "array",
|
|
903
|
+
"items": {
|
|
904
|
+
"type": "string",
|
|
905
|
+
"minLength": 1,
|
|
906
|
+
"maxLength": 128,
|
|
907
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$",
|
|
908
|
+
"description": "Stable identifier, unique within its collection in a document."
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
"nodes": {
|
|
912
|
+
"default": [],
|
|
913
|
+
"maxItems": 256,
|
|
914
|
+
"type": "array",
|
|
915
|
+
"items": {
|
|
916
|
+
"type": "string",
|
|
917
|
+
"minLength": 1,
|
|
918
|
+
"maxLength": 128,
|
|
919
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$",
|
|
920
|
+
"description": "Stable identifier, unique within its collection in a document."
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
"edges": {
|
|
924
|
+
"default": [],
|
|
925
|
+
"maxItems": 512,
|
|
926
|
+
"type": "array",
|
|
927
|
+
"items": {
|
|
928
|
+
"type": "string",
|
|
929
|
+
"minLength": 1,
|
|
930
|
+
"maxLength": 128,
|
|
931
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$",
|
|
932
|
+
"description": "Stable identifier, unique within its collection in a document."
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
"messages": {
|
|
936
|
+
"default": [],
|
|
937
|
+
"description": "Steps of the flow on the stage.",
|
|
938
|
+
"maxItems": 64,
|
|
939
|
+
"type": "array",
|
|
940
|
+
"items": {
|
|
941
|
+
"type": "string",
|
|
942
|
+
"minLength": 1,
|
|
943
|
+
"maxLength": 128,
|
|
944
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$",
|
|
945
|
+
"description": "Stable identifier, unique within its collection in a document."
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
"required": [
|
|
950
|
+
"kind"
|
|
951
|
+
],
|
|
952
|
+
"additionalProperties": false,
|
|
953
|
+
"anyOf": [
|
|
954
|
+
{
|
|
955
|
+
"properties": {
|
|
956
|
+
"lanes": {
|
|
957
|
+
"minItems": 1
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
"required": [
|
|
961
|
+
"lanes"
|
|
962
|
+
]
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
"properties": {
|
|
966
|
+
"nodes": {
|
|
967
|
+
"minItems": 1
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
"required": [
|
|
971
|
+
"nodes"
|
|
972
|
+
]
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
"properties": {
|
|
976
|
+
"edges": {
|
|
977
|
+
"minItems": 1
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
"required": [
|
|
981
|
+
"edges"
|
|
982
|
+
]
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
"properties": {
|
|
986
|
+
"messages": {
|
|
987
|
+
"minItems": 1
|
|
988
|
+
}
|
|
989
|
+
},
|
|
990
|
+
"required": [
|
|
991
|
+
"messages"
|
|
992
|
+
]
|
|
993
|
+
}
|
|
994
|
+
]
|
|
995
|
+
}
|
|
996
|
+
],
|
|
997
|
+
"description": "What stays lit while a walkthrough step plays."
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
"required": [
|
|
1001
|
+
"id",
|
|
1002
|
+
"heading",
|
|
1003
|
+
"body"
|
|
1004
|
+
],
|
|
1005
|
+
"additionalProperties": false,
|
|
1006
|
+
"description": "One stop on the walkthrough: a heading, a line under it, and the part of one diagram it is about."
|
|
1007
|
+
},
|
|
1008
|
+
"description": "Ordered by array position."
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
"required": [
|
|
1012
|
+
"steps"
|
|
1013
|
+
],
|
|
1014
|
+
"additionalProperties": false,
|
|
1015
|
+
"description": "An ordered tour of this document's diagrams."
|
|
1016
|
+
},
|
|
798
1017
|
"layout": {
|
|
799
1018
|
"type": "object",
|
|
800
1019
|
"properties": {
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/coldteadotai/pr-lens/main/packages/schema/json-schema/patch-doc.schema.json",
|
|
4
4
|
"title": "PR Lens patch document",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"schemaVersion": {
|
|
9
9
|
"type": "string",
|
|
10
10
|
"pattern": "^0\\.1\\.\\d+$",
|
|
11
|
-
"description": "Contract version the document targets. Current: 0.1.
|
|
11
|
+
"description": "Contract version the document targets. Current: 0.1.1."
|
|
12
12
|
},
|
|
13
13
|
"kind": {
|
|
14
14
|
"type": "string",
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/coldteadotai/pr-lens/main/packages/schema/json-schema/render-manifest.schema.json",
|
|
4
4
|
"title": "PR Lens render manifest",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"schemaVersion": {
|
|
9
9
|
"type": "string",
|
|
10
10
|
"pattern": "^0\\.1\\.\\d+$",
|
|
11
|
-
"description": "Contract version the document targets. Current: 0.1.
|
|
11
|
+
"description": "Contract version the document targets. Current: 0.1.1."
|
|
12
12
|
},
|
|
13
13
|
"kind": {
|
|
14
14
|
"type": "string",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coldtea/pr-lens-schema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The PR Lens contract: versioned schemas for architecture / data-flow graph documents, baseline patches, config and render manifests.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Coldtea AI",
|
package/src/apply.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Flow, GraphDoc, GraphEdge, Lane, View } from "./graph.js";
|
|
|
4
4
|
import { graphSnapshotIssues } from "./integrity.js";
|
|
5
5
|
import { targetDescribesATransition, type PatchDoc, type PatchOp } from "./patch.js";
|
|
6
6
|
import { assertNever } from "./utils.js";
|
|
7
|
+
import { pruneWalkthrough } from "./walkthrough.js";
|
|
7
8
|
|
|
8
9
|
type Collections = {
|
|
9
10
|
lanes: Lane[];
|
|
@@ -298,6 +299,20 @@ export const applyPatch = (graph: GraphDoc, ops: readonly PatchOp[]): Parsed<Gra
|
|
|
298
299
|
}
|
|
299
300
|
}
|
|
300
301
|
|
|
302
|
+
/**
|
|
303
|
+
* The tour is pruned once, against the document the operations produced,
|
|
304
|
+
* rather than alongside each removal. A step names a diagram as well as
|
|
305
|
+
* elements, and an operation that rewrites a flow's steps takes members
|
|
306
|
+
* away without removing anything the other prunes would notice.
|
|
307
|
+
*/
|
|
308
|
+
const walkthrough = pruneWalkthrough(graph.walkthrough, {
|
|
309
|
+
lanes: working.lanes,
|
|
310
|
+
nodes: working.nodes,
|
|
311
|
+
edges: working.edges,
|
|
312
|
+
flows: working.flows,
|
|
313
|
+
views,
|
|
314
|
+
});
|
|
315
|
+
|
|
301
316
|
return safeParseGraphDoc({
|
|
302
317
|
...graph,
|
|
303
318
|
lanes: working.lanes,
|
|
@@ -306,6 +321,7 @@ export const applyPatch = (graph: GraphDoc, ops: readonly PatchOp[]): Parsed<Gra
|
|
|
306
321
|
flows: working.flows,
|
|
307
322
|
stats: working.stats,
|
|
308
323
|
views,
|
|
324
|
+
walkthrough,
|
|
309
325
|
layout,
|
|
310
326
|
});
|
|
311
327
|
};
|
package/src/config.ts
CHANGED
|
@@ -63,6 +63,18 @@ export const Config = z
|
|
|
63
63
|
.boolean()
|
|
64
64
|
.default(true)
|
|
65
65
|
.describe("Show the 'Rendered by PR Lens' footer on comments."),
|
|
66
|
+
github: z
|
|
67
|
+
.strictObject({
|
|
68
|
+
comment: z
|
|
69
|
+
.strictObject({
|
|
70
|
+
collapsed: z
|
|
71
|
+
.boolean()
|
|
72
|
+
.default(false)
|
|
73
|
+
.describe("Start the hosted App's diagrams and details in a closed disclosure. Drawing still runs automatically."),
|
|
74
|
+
})
|
|
75
|
+
.prefault({}),
|
|
76
|
+
})
|
|
77
|
+
.prefault({}),
|
|
66
78
|
})
|
|
67
79
|
.describe("Repository configuration for PR Lens.");
|
|
68
80
|
export type Config = z.infer<typeof Config>;
|
|
@@ -8,7 +8,8 @@ import { SCHEMA_VERSION } from "../version.js";
|
|
|
8
8
|
* refactor: broadcast sending moved from one Postmark request per recipient
|
|
9
9
|
* to batched requests of 500, behind a shared library. Every downstream
|
|
10
10
|
* renderer golden is measured against this document, so it exercises all
|
|
11
|
-
* four delta states, a hero edge,
|
|
11
|
+
* four delta states, a hero edge, a full data-flow sequence, and a
|
|
12
|
+
* walkthrough that stages both a drill-down view and a flow.
|
|
12
13
|
*/
|
|
13
14
|
export const postmarkRefactorGraphInput: GraphDocInput = {
|
|
14
15
|
schemaVersion: SCHEMA_VERSION,
|
|
@@ -393,6 +394,55 @@ export const postmarkRefactorGraphInput: GraphDocInput = {
|
|
|
393
394
|
scope: { kind: "selection", flows: ["send-pipeline"] },
|
|
394
395
|
},
|
|
395
396
|
],
|
|
397
|
+
walkthrough: {
|
|
398
|
+
steps: [
|
|
399
|
+
{
|
|
400
|
+
id: "batches-of-500",
|
|
401
|
+
heading: "sendBroadcastBulk and buildBulkPayload added",
|
|
402
|
+
body: "Nothing loops over recipients any more. The sender works on a whole batch at a time.",
|
|
403
|
+
stage: { kind: "view", view: "overview" },
|
|
404
|
+
focus: {
|
|
405
|
+
kind: "selection",
|
|
406
|
+
nodes: ["send-broadcast-bulk", "build-bulk-payload", "postmark"],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: "suppression-first",
|
|
411
|
+
heading: "getSuppressedEmails added before the send",
|
|
412
|
+
body: "It pulls the blocked addresses once, before any batch is built.",
|
|
413
|
+
stage: { kind: "view", view: "new-batch-path" },
|
|
414
|
+
focus: { kind: "selection", nodes: ["get-suppressed-emails", "postmark"] },
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "old-path-goes-dark",
|
|
418
|
+
heading: "processBroadcast and sendSingleEmail removed",
|
|
419
|
+
body: "sendBroadcastBulk does their job for whole batches.",
|
|
420
|
+
stage: { kind: "view", view: "overview" },
|
|
421
|
+
focus: { kind: "selection", nodes: ["process-broadcast", "send-single-email"] },
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "sequence-start-to-finish",
|
|
425
|
+
heading: "The send sequence gained 6 new steps",
|
|
426
|
+
body: "The queue write is the only step that was there before, and it now stamps the batch size.",
|
|
427
|
+
stage: { kind: "flow", flow: "send-pipeline" },
|
|
428
|
+
focus: { kind: "all" },
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "four-batch-calls",
|
|
432
|
+
heading: "Postmark now gets 500 emails per call",
|
|
433
|
+
body: "One call per batch, and Postmark answers with a result for each message.",
|
|
434
|
+
stage: { kind: "flow", flow: "send-pipeline" },
|
|
435
|
+
focus: { kind: "selection", messages: ["batch-post", "batch-results"] },
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: "blast-radius",
|
|
439
|
+
heading: "4 parts added, 2 removed, across 3 lanes",
|
|
440
|
+
body: "A 2,000-person broadcast used to make 2,000 calls to Postmark. It now makes 4.",
|
|
441
|
+
stage: { kind: "view", view: "overview" },
|
|
442
|
+
focus: { kind: "all" },
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
396
446
|
layout: {
|
|
397
447
|
direction: "right",
|
|
398
448
|
laneOrder: ["web", "functions", "external"],
|
package/src/graph.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Beat,
|
|
4
|
+
Delta,
|
|
5
|
+
FileRef,
|
|
6
|
+
Id,
|
|
7
|
+
Label,
|
|
8
|
+
Lens,
|
|
9
|
+
Line,
|
|
10
|
+
SchemaVersionField,
|
|
11
|
+
Sha,
|
|
12
|
+
Summary,
|
|
13
|
+
} from "./primitives.js";
|
|
3
14
|
|
|
4
15
|
/**
|
|
5
16
|
* Coarse on purpose: this drives the card icon and shape, never analysis.
|
|
@@ -255,6 +266,92 @@ export const View: z.ZodType<View, ViewInput> = z.lazy(() =>
|
|
|
255
266
|
.describe("A drill-down section; children nest as further <details> blocks."),
|
|
256
267
|
);
|
|
257
268
|
|
|
269
|
+
/**
|
|
270
|
+
* The picture a step plays over: one of the document's drill-down views, or
|
|
271
|
+
* one of its flows drawn on its own.
|
|
272
|
+
*
|
|
273
|
+
* Leaving it out means the picture the reader is already looking at. Which
|
|
274
|
+
* one that is belongs to the surface showing the document, not to the
|
|
275
|
+
* contract, so the contract says nothing about it.
|
|
276
|
+
*/
|
|
277
|
+
export const StepStage = z
|
|
278
|
+
.discriminatedUnion("kind", [
|
|
279
|
+
z.strictObject({
|
|
280
|
+
kind: z.literal("view"),
|
|
281
|
+
view: Id.describe("Id of the drill-down view to draw."),
|
|
282
|
+
}),
|
|
283
|
+
z.strictObject({
|
|
284
|
+
kind: z.literal("flow"),
|
|
285
|
+
flow: Id.describe("Id of the flow to draw."),
|
|
286
|
+
}),
|
|
287
|
+
])
|
|
288
|
+
.describe("Which diagram a walkthrough step plays over.");
|
|
289
|
+
export type StepStage = z.infer<typeof StepStage>;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* What a step points at inside its stage. The same two states a view scope
|
|
293
|
+
* has, and for the same reason: a step that loses the last element it named
|
|
294
|
+
* must never quietly become a step about everything.
|
|
295
|
+
*
|
|
296
|
+
* `messages` names steps of a flow, so it only means anything when the stage
|
|
297
|
+
* draws that flow. That pairing is the parser's to check.
|
|
298
|
+
*/
|
|
299
|
+
export const StepFocus = z
|
|
300
|
+
.discriminatedUnion("kind", [
|
|
301
|
+
z.strictObject({ kind: z.literal("all") }),
|
|
302
|
+
z
|
|
303
|
+
.strictObject({
|
|
304
|
+
kind: z.literal("selection"),
|
|
305
|
+
lanes: z.array(Id).max(64).default([]),
|
|
306
|
+
nodes: z.array(Id).max(256).default([]),
|
|
307
|
+
edges: z.array(Id).max(512).default([]),
|
|
308
|
+
messages: z.array(Id).max(64).default([]).describe("Steps of the flow on the stage."),
|
|
309
|
+
})
|
|
310
|
+
.meta({
|
|
311
|
+
anyOf: [
|
|
312
|
+
{ properties: { lanes: { minItems: 1 } }, required: ["lanes"] },
|
|
313
|
+
{ properties: { nodes: { minItems: 1 } }, required: ["nodes"] },
|
|
314
|
+
{ properties: { edges: { minItems: 1 } }, required: ["edges"] },
|
|
315
|
+
{ properties: { messages: { minItems: 1 } }, required: ["messages"] },
|
|
316
|
+
],
|
|
317
|
+
})
|
|
318
|
+
.refine(
|
|
319
|
+
(focus) =>
|
|
320
|
+
focus.lanes.length + focus.nodes.length + focus.edges.length + focus.messages.length > 0,
|
|
321
|
+
{ message: "a selection must name at least one element" },
|
|
322
|
+
),
|
|
323
|
+
])
|
|
324
|
+
.describe("What stays lit while a walkthrough step plays.");
|
|
325
|
+
export type StepFocus = z.infer<typeof StepFocus>;
|
|
326
|
+
|
|
327
|
+
export const WalkthroughStep = z
|
|
328
|
+
.strictObject({
|
|
329
|
+
id: Id,
|
|
330
|
+
heading: Beat,
|
|
331
|
+
body: Line,
|
|
332
|
+
stage: StepStage.optional(),
|
|
333
|
+
focus: StepFocus.default({ kind: "all" }),
|
|
334
|
+
})
|
|
335
|
+
.describe(
|
|
336
|
+
"One stop on the walkthrough: a heading, a line under it, and the part of one diagram it is about.",
|
|
337
|
+
);
|
|
338
|
+
export type WalkthroughStep = z.infer<typeof WalkthroughStep>;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* An ordered tour of the document's diagrams, authored alongside them.
|
|
342
|
+
*
|
|
343
|
+
* Two steps is the floor because one step is a caption, and twelve is the
|
|
344
|
+
* ceiling because a reader scrolling a rail loses the thread long before
|
|
345
|
+
* that. A document that carries one has already decided what a reader should
|
|
346
|
+
* see first; a surface playing it only reads.
|
|
347
|
+
*/
|
|
348
|
+
export const Walkthrough = z
|
|
349
|
+
.strictObject({
|
|
350
|
+
steps: z.array(WalkthroughStep).min(2).max(12).describe("Ordered by array position."),
|
|
351
|
+
})
|
|
352
|
+
.describe("An ordered tour of this document's diagrams.");
|
|
353
|
+
export type Walkthrough = z.infer<typeof Walkthrough>;
|
|
354
|
+
|
|
258
355
|
/**
|
|
259
356
|
* Hints, not instructions: the renderer owns final placement so that layout
|
|
260
357
|
* stays deterministic for a given document. A hint is a floor rather than an
|
|
@@ -324,6 +421,7 @@ export const GraphDoc = z
|
|
|
324
421
|
flows: z.array(Flow).max(16).default([]),
|
|
325
422
|
stats: Stats.optional(),
|
|
326
423
|
views: z.array(View).max(32).default([]),
|
|
424
|
+
walkthrough: Walkthrough.optional(),
|
|
327
425
|
layout: LayoutHints.optional(),
|
|
328
426
|
})
|
|
329
427
|
.describe("A PR Lens graph document.");
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { SCHEMA_VERSION, type SchemaVersion } from "./version.js";
|
|
2
2
|
|
|
3
3
|
export {
|
|
4
|
+
Beat,
|
|
4
5
|
Delta,
|
|
5
6
|
DELTAS,
|
|
6
7
|
FileRef,
|
|
@@ -9,6 +10,7 @@ export {
|
|
|
9
10
|
Label,
|
|
10
11
|
Lens,
|
|
11
12
|
LENSES,
|
|
13
|
+
Line,
|
|
12
14
|
MAX_RENDER_ASSETS,
|
|
13
15
|
MAX_VIEWS,
|
|
14
16
|
Theme,
|
|
@@ -34,8 +36,12 @@ export {
|
|
|
34
36
|
Provenance,
|
|
35
37
|
StatChip,
|
|
36
38
|
Stats,
|
|
39
|
+
StepFocus,
|
|
40
|
+
StepStage,
|
|
37
41
|
View,
|
|
38
42
|
ViewScope,
|
|
43
|
+
Walkthrough,
|
|
44
|
+
WalkthroughStep,
|
|
39
45
|
type GraphDocInput,
|
|
40
46
|
type ViewInput,
|
|
41
47
|
} from "./graph.js";
|
|
@@ -79,4 +85,6 @@ export {
|
|
|
79
85
|
|
|
80
86
|
export { applyPatch, applyPatchDoc } from "./apply.js";
|
|
81
87
|
|
|
88
|
+
export { pruneWalkthrough, type WalkthroughSubject } from "./walkthrough.js";
|
|
89
|
+
|
|
82
90
|
export { assertNever } from "./utils.js";
|
package/src/integrity.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { SchemaIssue } from "./errors.js";
|
|
|
2
2
|
import type { GraphDoc, View } from "./graph.js";
|
|
3
3
|
import { FullSha, MAX_VIEWS, THEMES, type Delta } from "./primitives.js";
|
|
4
4
|
import { assertNever } from "./utils.js";
|
|
5
|
+
import { indexViews, stagedMessages } from "./walkthrough.js";
|
|
5
6
|
|
|
6
7
|
const duplicates = (ids: readonly string[]): string[] => {
|
|
7
8
|
const seen = new Set<string>();
|
|
@@ -135,6 +136,82 @@ export const graphIntegrityIssues = (doc: GraphDoc): SchemaIssue[] => {
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
if (doc.walkthrough) {
|
|
140
|
+
const viewsById = indexViews(doc.views);
|
|
141
|
+
|
|
142
|
+
for (const id of duplicates(doc.walkthrough.steps.map((step) => step.id)))
|
|
143
|
+
duplicate("walkthrough.steps", `duplicate step id '${id}'`);
|
|
144
|
+
|
|
145
|
+
doc.walkthrough.steps.forEach((step, index) => {
|
|
146
|
+
const at = `walkthrough.steps[${index}]`;
|
|
147
|
+
|
|
148
|
+
if (step.stage !== undefined) {
|
|
149
|
+
switch (step.stage.kind) {
|
|
150
|
+
case "view":
|
|
151
|
+
if (!viewsById.has(step.stage.view))
|
|
152
|
+
broken(`${at}.stage.view`, `step '${step.id}' stages unknown view '${step.stage.view}'`);
|
|
153
|
+
break;
|
|
154
|
+
case "flow":
|
|
155
|
+
if (!flowIds.has(step.stage.flow))
|
|
156
|
+
broken(`${at}.stage.flow`, `step '${step.id}' stages unknown flow '${step.stage.flow}'`);
|
|
157
|
+
break;
|
|
158
|
+
default:
|
|
159
|
+
assertNever(step.stage, "Unhandled step stage");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
switch (step.focus.kind) {
|
|
164
|
+
case "all":
|
|
165
|
+
break;
|
|
166
|
+
case "selection": {
|
|
167
|
+
const focus = step.focus;
|
|
168
|
+
const focused: [keyof Omit<typeof focus, "kind" | "messages">, string, ReadonlySet<string>][] = [
|
|
169
|
+
["lanes", "lane", laneIds],
|
|
170
|
+
["nodes", "node", nodeIds],
|
|
171
|
+
["edges", "edge", edgeIds],
|
|
172
|
+
];
|
|
173
|
+
for (const [collection, singular, known] of focused) {
|
|
174
|
+
focus[collection].forEach((id, memberIndex) => {
|
|
175
|
+
if (!known.has(id))
|
|
176
|
+
broken(
|
|
177
|
+
`${at}.focus.${collection}[${memberIndex}]`,
|
|
178
|
+
`step '${step.id}' focuses unknown ${singular} '${id}'`,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const onStage = stagedMessages(step.stage, doc.flows, viewsById);
|
|
184
|
+
switch (onStage.kind) {
|
|
185
|
+
case "messages":
|
|
186
|
+
focus.messages.forEach((id, memberIndex) => {
|
|
187
|
+
if (!onStage.ids.has(id))
|
|
188
|
+
broken(
|
|
189
|
+
`${at}.focus.messages[${memberIndex}]`,
|
|
190
|
+
`step '${step.id}' focuses '${id}', which no flow on its stage carries`,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
break;
|
|
194
|
+
case "no-stage":
|
|
195
|
+
if (focus.messages.length > 0)
|
|
196
|
+
issues.push({
|
|
197
|
+
code: "INVALID_DOCUMENT",
|
|
198
|
+
path: `${at}.focus.messages`,
|
|
199
|
+
message: `step '${step.id}' focuses flow steps but names no stage to draw them on`,
|
|
200
|
+
});
|
|
201
|
+
break;
|
|
202
|
+
case "unknown-stage":
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
assertNever(onStage, "Unhandled staged messages");
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
default:
|
|
210
|
+
assertNever(step.focus, "Unhandled step focus");
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
138
215
|
if (doc.layout) {
|
|
139
216
|
doc.layout.laneOrder.forEach((id, index) => {
|
|
140
217
|
if (!laneIds.has(id)) broken(`layout.laneOrder[${index}]`, `unknown lane '${id}'`);
|
|
@@ -167,6 +244,14 @@ export const graphSnapshotIssues = (doc: GraphDoc): SchemaIssue[] => {
|
|
|
167
244
|
message: "a stored map needs an id, so a patch can say which map it targets",
|
|
168
245
|
});
|
|
169
246
|
|
|
247
|
+
if (doc.walkthrough !== undefined)
|
|
248
|
+
issues.push({
|
|
249
|
+
code: "NOT_A_SNAPSHOT",
|
|
250
|
+
path: "walkthrough",
|
|
251
|
+
message:
|
|
252
|
+
"a stored map carries a walkthrough, but a walkthrough narrates a change and a map describes a system",
|
|
253
|
+
});
|
|
254
|
+
|
|
170
255
|
for (const side of ["base", "head"] as const) {
|
|
171
256
|
if (!FullSha.safeParse(doc.provenance[side].sha).success)
|
|
172
257
|
issues.push({
|
package/src/primitives.ts
CHANGED
|
@@ -38,6 +38,27 @@ export const Summary = z
|
|
|
38
38
|
.max(2000)
|
|
39
39
|
.describe("One or two sentences of plain prose. No markdown headings.");
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A walkthrough step's heading. The cap is part of the contract rather than
|
|
43
|
+
* advice: the rail shows one line per step, so a heading long enough to wrap
|
|
44
|
+
* turns the tour into a wall of text, and no producer can pad its way past it.
|
|
45
|
+
*/
|
|
46
|
+
export const Beat = z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.max(48)
|
|
50
|
+
.describe("A step heading. Short enough to read at a glance.");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The one line of body under a step's heading. Required: a heading with
|
|
54
|
+
* nothing under it reads as a step someone started and never finished.
|
|
55
|
+
*/
|
|
56
|
+
export const Line = z
|
|
57
|
+
.string()
|
|
58
|
+
.min(1)
|
|
59
|
+
.max(140)
|
|
60
|
+
.describe("A single line under a step heading.");
|
|
61
|
+
|
|
41
62
|
export const Sha = z
|
|
42
63
|
.string()
|
|
43
64
|
.regex(/^[0-9a-f]{7,40}$/, "must be a lowercase hex git object name")
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* with semver semantics: patch/minor releases only ever add optional fields
|
|
7
7
|
* or widen an enum, a major release may remove or retype a field.
|
|
8
8
|
*/
|
|
9
|
-
export const SCHEMA_VERSION = "0.1.
|
|
9
|
+
export const SCHEMA_VERSION = "0.1.1" as const;
|
|
10
10
|
|
|
11
11
|
export type SchemaVersion = typeof SCHEMA_VERSION;
|
|
12
12
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Flow, GraphEdge, GraphNode, Lane, StepStage, View, Walkthrough } from "./graph.js";
|
|
2
|
+
import { assertNever } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
const NOTHING: ReadonlySet<string> = new Set();
|
|
5
|
+
|
|
6
|
+
export const indexViews = (views: readonly View[]): Map<string, View> =>
|
|
7
|
+
new Map(views.flatMap((view) => [[view.id, view] as const, ...indexViews(view.children)]));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A flow step is only ever identified within its own flow, so the stage rather
|
|
11
|
+
* than the document decides which one a focus meant. Two flows may each carry
|
|
12
|
+
* a step called `retry`, and neither document is wrong for it.
|
|
13
|
+
*
|
|
14
|
+
* `unknown-stage` is its own answer rather than an empty set, so a stage that
|
|
15
|
+
* names a view or flow the document lacks is reported once, as the broken
|
|
16
|
+
* reference it is, instead of again for every step underneath it.
|
|
17
|
+
*/
|
|
18
|
+
export type StagedMessages =
|
|
19
|
+
| { kind: "messages"; ids: ReadonlySet<string> }
|
|
20
|
+
| { kind: "no-stage" }
|
|
21
|
+
| { kind: "unknown-stage" };
|
|
22
|
+
|
|
23
|
+
const messageIdsOf = (flows: readonly Flow[]): Set<string> =>
|
|
24
|
+
new Set(flows.flatMap((flow) => flow.messages.map((message) => message.id)));
|
|
25
|
+
|
|
26
|
+
export const stagedMessages = (
|
|
27
|
+
stage: StepStage | undefined,
|
|
28
|
+
flows: readonly Flow[],
|
|
29
|
+
views: ReadonlyMap<string, View>,
|
|
30
|
+
): StagedMessages => {
|
|
31
|
+
if (stage === undefined) return { kind: "no-stage" };
|
|
32
|
+
|
|
33
|
+
switch (stage.kind) {
|
|
34
|
+
case "flow": {
|
|
35
|
+
const flow = flows.find(({ id }) => id === stage.flow);
|
|
36
|
+
return flow === undefined
|
|
37
|
+
? { kind: "unknown-stage" }
|
|
38
|
+
: { kind: "messages", ids: messageIdsOf([flow]) };
|
|
39
|
+
}
|
|
40
|
+
case "view": {
|
|
41
|
+
const view = views.get(stage.view);
|
|
42
|
+
if (view === undefined) return { kind: "unknown-stage" };
|
|
43
|
+
|
|
44
|
+
switch (view.scope.kind) {
|
|
45
|
+
case "all":
|
|
46
|
+
return { kind: "messages", ids: messageIdsOf(flows) };
|
|
47
|
+
case "selection": {
|
|
48
|
+
const scoped = view.scope.flows;
|
|
49
|
+
return {
|
|
50
|
+
kind: "messages",
|
|
51
|
+
ids: messageIdsOf(flows.filter((flow) => scoped.includes(flow.id))),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
return assertNever(view.scope, "Unhandled view scope");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
return assertNever(stage, "Unhandled step stage");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The document that survived, rather than the ids that went: a step names a
|
|
65
|
+
* diagram as well as elements, and a flow step means nothing outside the flow
|
|
66
|
+
* that carries it.
|
|
67
|
+
*/
|
|
68
|
+
export type WalkthroughSubject = {
|
|
69
|
+
lanes: readonly Lane[];
|
|
70
|
+
nodes: readonly GraphNode[];
|
|
71
|
+
edges: readonly GraphEdge[];
|
|
72
|
+
flows: readonly Flow[];
|
|
73
|
+
views: readonly View[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const stageSurvives = (
|
|
77
|
+
stage: StepStage,
|
|
78
|
+
flows: ReadonlySet<string>,
|
|
79
|
+
views: ReadonlyMap<string, View>,
|
|
80
|
+
): boolean => {
|
|
81
|
+
switch (stage.kind) {
|
|
82
|
+
case "view":
|
|
83
|
+
return views.has(stage.view);
|
|
84
|
+
case "flow":
|
|
85
|
+
return flows.has(stage.flow);
|
|
86
|
+
default:
|
|
87
|
+
return assertNever(stage, "Unhandled step stage");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const focusable = (staged: StagedMessages): ReadonlySet<string> => {
|
|
92
|
+
switch (staged.kind) {
|
|
93
|
+
case "messages":
|
|
94
|
+
return staged.ids;
|
|
95
|
+
case "no-stage":
|
|
96
|
+
case "unknown-stage":
|
|
97
|
+
return NOTHING;
|
|
98
|
+
default:
|
|
99
|
+
return assertNever(staged, "Unhandled staged messages");
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A step that loses the last element it focused is dropped rather than left
|
|
105
|
+
* to widen into a step about everything. A tour of one step is a caption, so
|
|
106
|
+
* a walkthrough cut below two steps goes whole.
|
|
107
|
+
*/
|
|
108
|
+
export const pruneWalkthrough = (
|
|
109
|
+
walkthrough: Walkthrough | undefined,
|
|
110
|
+
subject: WalkthroughSubject,
|
|
111
|
+
): Walkthrough | undefined => {
|
|
112
|
+
if (walkthrough === undefined) return undefined;
|
|
113
|
+
|
|
114
|
+
const lanes = new Set(subject.lanes.map((lane) => lane.id));
|
|
115
|
+
const nodes = new Set(subject.nodes.map((node) => node.id));
|
|
116
|
+
const edges = new Set(subject.edges.map((edge) => edge.id));
|
|
117
|
+
const flows = new Set(subject.flows.map((flow) => flow.id));
|
|
118
|
+
const views = indexViews(subject.views);
|
|
119
|
+
|
|
120
|
+
const steps = walkthrough.steps.flatMap((step) => {
|
|
121
|
+
if (step.stage !== undefined && !stageSurvives(step.stage, flows, views)) return [];
|
|
122
|
+
|
|
123
|
+
switch (step.focus.kind) {
|
|
124
|
+
case "all":
|
|
125
|
+
return [step];
|
|
126
|
+
case "selection": {
|
|
127
|
+
const onStage = focusable(stagedMessages(step.stage, subject.flows, views));
|
|
128
|
+
const focus = {
|
|
129
|
+
kind: "selection",
|
|
130
|
+
lanes: step.focus.lanes.filter((id) => lanes.has(id)),
|
|
131
|
+
nodes: step.focus.nodes.filter((id) => nodes.has(id)),
|
|
132
|
+
edges: step.focus.edges.filter((id) => edges.has(id)),
|
|
133
|
+
messages: step.focus.messages.filter((id) => onStage.has(id)),
|
|
134
|
+
} as const;
|
|
135
|
+
|
|
136
|
+
const focused =
|
|
137
|
+
focus.lanes.length + focus.nodes.length + focus.edges.length + focus.messages.length;
|
|
138
|
+
return focused === 0 ? [] : [{ ...step, focus }];
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
return assertNever(step.focus, "Unhandled step focus");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return steps.length < 2 ? undefined : { ...walkthrough, steps };
|
|
146
|
+
};
|