@coldtea/pr-lens-schema 0.1.3 → 0.2.1
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 +50 -6
- package/dist/apply.d.ts.map +1 -1
- package/dist/apply.js +15 -0
- package/dist/apply.js.map +1 -1
- package/dist/examples/index.d.ts +138 -0
- package/dist/examples/index.d.ts.map +1 -1
- package/dist/examples/postmark-refactor.d.ts +2 -1
- package/dist/examples/postmark-refactor.d.ts.map +1 -1
- package/dist/examples/postmark-refactor.js +51 -1
- package/dist/examples/postmark-refactor.js.map +1 -1
- package/dist/graph.d.ts +111 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +74 -1
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +71 -0
- package/dist/integrity.js.map +1 -1
- package/dist/primitives.d.ts +11 -0
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +19 -0
- package/dist/primitives.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walkthrough.d.ts +39 -0
- package/dist/walkthrough.d.ts.map +1 -0
- package/dist/walkthrough.js +95 -0
- package/dist/walkthrough.js.map +1 -0
- 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 +1 -1
- package/json-schema/config.schema.json +1 -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 +9 -9
- package/src/apply.ts +16 -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
|
@@ -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
|
+
};
|