@beehexa/hexasync-template-frontend-flow 2608.20.18 → 2608.20.31

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/dist/render.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `renderMermaid` / `renderMarkdown` for a frontend workflow — Story 4.3, `PACKAGE-CONTRACT.md` §5.
3
+ *
4
+ * Thin, for the same reason as its worker twin: the emission rules live in `hexasync-template-model`, which is the one
5
+ * package both may import (AD-21). This file says what a FRONTEND node looks like — and the differences from the worker
6
+ * side are real, not incidental.
7
+ */
8
+ import { renderFlowMermaid, composeNodeLabel, renderFlowMarkdown, } from '@beehexa/hexasync-template-model';
9
+ /**
10
+ * A node's label.
11
+ *
12
+ * ⚠️ A step reads as its **name** where it has one, falling back to its key — the CLI baseline draws
13
+ * `SHOW_CREATE_PROFILE_ERROR["Couldn't Start Profile Creation"]`, and dropping `name` was Story 4.2's L1 finding. The
14
+ * worker side has no equivalent: a worker step's identity in that corpus IS its key.
15
+ */
16
+ function labelOf(node) {
17
+ // COMPOSED, not a fallback chain (2026-08-20). This was
18
+ // `node.label ?? node.name ?? node.stepKey ?? node.address`, so a step with a name drew its name and
19
+ // dropped the key and the type — both already on the node. A reader beside the YAML could not tell which
20
+ // `key:` a box was, and `next:`/`outputs:` reference steps BY KEY. One composer for both runtimes, in
21
+ // `template-model`, so the two diagrams cannot start labelling differently.
22
+ return composeNodeLabel(node);
23
+ }
24
+ function groupOf(one) {
25
+ return {
26
+ key: one.address,
27
+ title: one.title,
28
+ nodes: one.nodes.map((node) => ({
29
+ address: node.address,
30
+ label: labelOf(node),
31
+ decides: node.decides,
32
+ // START and END are stadiums, as both baselines draw them; an unresolved target is a plain box there, matching
33
+ // the reference, which ids it with the same shape as a real step.
34
+ terminal: node.junction === 'start' || node.junction === 'end',
35
+ })),
36
+ };
37
+ }
38
+ /** ONE workflow, which is what a diagram shows — a document with five of them is five diagrams, not one. */
39
+ export function renderMermaid(one, opts = {}) {
40
+ // A single workflow is a flat chart by default: its own title is the chart's, and a lone subgraph adds a box around
41
+ // everything for nothing.
42
+ return renderFlowMermaid([groupOf(one)], one.edges, {
43
+ multiStage: false,
44
+ ...opts,
45
+ });
46
+ }
47
+ /** Markdown for one workflow — `(flow, opts)`, the same shape as the worker twin (review MEDIUM-1). */
48
+ export function renderMarkdown(one, opts = {}) {
49
+ return renderFlowMarkdown(opts.title ?? one.title, [groupOf(one)], one.edges, {
50
+ multiStage: false,
51
+ ...opts,
52
+ });
53
+ }
54
+ /**
55
+ * Every workflow a document declares, each its own titled section.
56
+ *
57
+ * A connector definition carries up to five, and `epic-4-ground-truth.md` records that no shipped surface draws any of
58
+ * them — so a document-level entry point is what makes the set usable rather than a per-workflow call a consumer has
59
+ * to discover.
60
+ */
61
+ export function renderDocumentMarkdown(flow, opts = {}) {
62
+ /**
63
+ * A document with NO workflows says so (review MEDIUM-3).
64
+ *
65
+ * It returned `''`, so a caller could not tell "this connector declares no workflows" from "the renderer returned
66
+ * nothing" — the same failure the empty-diagram placeholder exists to prevent, one level up.
67
+ */
68
+ if (flow.workflows.length === 0) {
69
+ return `### ${opts.title ?? 'Workflows'}\n\nThis document declares no frontend workflows.\n`;
70
+ }
71
+ return flow.workflows.map((one) => renderMarkdown(one, opts)).join('\n');
72
+ }
73
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,GAGnB,MAAM,kCAAkC,CAAC;AAG1C;;;;;;GAMG;AACH,SAAS,OAAO,CAAC,IAA2C;IAC1D,wDAAwD;IACxD,qGAAqG;IACrG,yGAAyG;IACzG,sGAAsG;IACtG,4EAA4E;IAC5E,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,OAAO,CAAC,GAAyB;IACxC,OAAO;QACL,GAAG,EAAE,GAAG,CAAC,OAAO;QAChB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC9B,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC;YACpB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,+GAA+G;YAC/G,kEAAkE;YAClE,QAAQ,EAAE,IAAI,CAAC,QAAQ,KAAK,OAAO,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK;SAC/D,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED,4GAA4G;AAC5G,MAAM,UAAU,aAAa,CAC3B,GAAyB,EACzB,IAAI,GAAkB,EAAE;IAExB,oHAAoH;IACpH,0BAA0B;IAC1B,OAAO,iBAAiB,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,EAAE;QAClD,UAAU,EAAE,KAAK;QACjB,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,cAAc,CAC5B,GAAyB,EACzB,IAAI,GAAgD,EAAE;IAEtD,OAAO,kBAAkB,CACvB,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,EACvB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EACd,GAAG,CAAC,KAAK,EACT;QACE,UAAU,EAAE,KAAK;QACjB,GAAG,IAAI;KACR,CACF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,IAAkB,EAClB,IAAI,GAAgD,EAAE;IAEtD;;;;;OAKG;IACH,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,OAAO,IAAI,CAAC,KAAK,IAAI,WAAW,qDAAqD,CAAC;IAC/F,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Where a frontend step goes next — Story 4.2 AC-2 and AC-3.
3
+ *
4
+ * Ported from `dashboard-v2/src/pages/__dev/lib/connectorWorkflowMermaid.ts`, which is a complete and working
5
+ * implementation; the port's job is to keep its semantics and drop its positional ids.
6
+ *
7
+ * ### AC-2: both spellings — on a `next` OBJECT only, which is where the reference aliases
8
+ *
9
+ * ⛔ **CORRECTED by the Story 4.2 review.** The first version applied the alias to an `IF` step's `metadata` as well,
10
+ * and that was an **invention**: dashboard reads `step.metadata.then` / `.else` *literally* (`addStepEdges`), and its
11
+ * `branchValue` is used only inside `addNextCondition` — i.e. only for a nested branch object under `next`. The CLI
12
+ * aliases the same single place. So `{ type: 'IF', metadata: { success: 'A', failed: 'B' } }` drew YES/NO edges here
13
+ * and **nothing at all** in either reference.
14
+ *
15
+ * That is precisely the failure the package split exists to prevent, committed by the package that exists to prevent
16
+ * it: accepting a document shape the runtime rejects. Measured: `success`/`failed` occur in **8 real `next` objects
17
+ * and 0 real `metadata` blocks**, so the alias was tested only on the surface where it does not belong, while the one
18
+ * place it genuinely applies had no test.
19
+ *
20
+ * Where it DOES apply, `hasOwnProperty` before falling back is load-bearing: `then: null` is a
21
+ * **declared-but-empty** arm (the frontend's way of saying "this branch ends") and is distinct from an absent `then`
22
+ * that should defer to `success`. Truthiness collapses the two and silently takes the wrong arm — which is what
23
+ * AC-2's *"does not silently lose half its edges"* is about.
24
+ *
25
+ * ### AC-3: `onCancelled` is not a route out of the happy path
26
+ *
27
+ * It is the cancel route for a pause-for-input step (`FORM`, `*_CHOICE_FORM`) and is emitted for ANY step that
28
+ * declares one, alongside whatever `next` says — the reference's comment records why: otherwise the cancel branch and
29
+ * its downstream navigation steps *"aren't left orphaned in the diagram"*.
30
+ *
31
+ * ### Expressions are NOT resolved — and the reason first given for that was false
32
+ *
33
+ * ⚠️ The original note claimed *"the frontend runtime has no such form"*. It has: **23 real `next` values** are Liquid
34
+ * templates, across `installStandaloneCallbackSteps` in LightspeedXSeries, SapoOmniV3PublicApp, ShopifyPublicApp,
35
+ * ShoplinePublicApp, TikTokShopGatewayV3 and Zalo — e.g.
36
+ * `{% assign codeSize = ... %}{% if codeSize > 0 %}CHECK_PROFILE_STATUS{% else %}GET_SETUP_INSTRUCTIONS{% endif %}`.
37
+ *
38
+ * Both the reference and this model render each as ONE unresolved node labelled with the whole template, so ~23
39
+ * genuinely reachable routes are undrawn on both sides. That is faithful, and it is a real gap.
40
+ *
41
+ * It is not closed here because the worker machinery does not transfer: the worker resolves `"QUOTED"` keys in a
42
+ * Scriban expression, whereas these name steps **bare** inside Liquid tags. Resolving them needs its own reader and
43
+ * its own decision about what to draw when a branch is undecidable statically. Deferred, with the premise corrected
44
+ * so nobody re-derives the false one.
45
+ */
46
+ /** A frontend step as routing needs to see it. */
47
+ export interface FrontendStep {
48
+ readonly key: string;
49
+ /** A step key, a `{ then|success, else|failed }` branch, or `null`/absent for the end of the workflow. */
50
+ readonly next?: unknown;
51
+ /** `IF`, `SWITCH`, `FORM`, … Compared case-insensitively. */
52
+ readonly type?: string;
53
+ /** Where an `IF`/`SWITCH` declares its branches — `metadata`, not `data`. */
54
+ readonly metadata?: unknown;
55
+ /** The cancel route. */
56
+ readonly onCancelled?: unknown;
57
+ /**
58
+ * ⛔ `errorHandlers` is NOT here, and that absence is a decision — see the note below on dead properties.
59
+ *
60
+ * 3 real steps declare one (Zalo, TikTokShopGatewayV3, LightspeedXSeries) and NO implementation reads it:
61
+ * not the dashboard engine, not `core.api`, not the CLI, not the shipped schemas. Drawing its arms would
62
+ * invent routes the runtime never takes, which is what NFR-1 forbids and what this module's own note
63
+ * records having got wrong once before.
64
+ */
65
+ /**
66
+ * An ENTRY point. `true` or the STRING `'true'` — the reference's `isTruthyFlag` accepts both, and 3 real
67
+ * declarations use the string form.
68
+ */
69
+ readonly root?: unknown;
70
+ }
71
+ /** What a synthesised node is. */
72
+ export type JunctionKind = 'if' | 'unknown' | 'end' | 'start'
73
+ /**
74
+ * A `next:` that is a TEMPLATE rather than a step key — drawn as a diamond (2026-08-20).
75
+ *
76
+ * Its own kind rather than reusing `'if'`, because the two are different facts about the document: an `'if'`
77
+ * junction comes from an authored `then`/`else` branch, and this comes from a template whose branches the
78
+ * reader can see but a static view cannot resolve. Both DECIDE, so both draw as a diamond — see
79
+ * `flow.ts`'s `decides`. The kind is what lets a consumer tell them apart without parsing the label.
80
+ */
81
+ | 'expression';
82
+ /** A node the document does not declare. `path` is relative to the owning step. */
83
+ export interface Junction {
84
+ readonly path: readonly string[];
85
+ readonly kind: JunctionKind;
86
+ readonly label: string;
87
+ readonly owner: string;
88
+ }
89
+ export type EdgeEnd = {
90
+ readonly kind: 'step';
91
+ readonly key: string;
92
+ } | {
93
+ readonly kind: 'junction';
94
+ readonly owner: string;
95
+ readonly path: readonly string[];
96
+ };
97
+ export interface GraphEdge {
98
+ readonly from: EdgeEnd;
99
+ readonly to: EdgeEnd;
100
+ readonly label?: string;
101
+ }
102
+ export interface FrontendRouteGraph {
103
+ readonly junctions: readonly Junction[];
104
+ readonly edges: readonly GraphEdge[];
105
+ }
106
+ /**
107
+ * A branch arm, by either spelling.
108
+ *
109
+ * `hasOwnProperty` on the primary before falling back — see the module note. Returns a sentinel-free result: the
110
+ * caller cannot distinguish "absent" from "declared null" from the value alone, so both are returned as-is and the
111
+ * caller treats `null` as an end.
112
+ */
113
+ export declare function branchArm(branch: Record<string, unknown>, primary: 'then' | 'else', alternate: 'success' | 'failed'): unknown;
114
+ export declare function frontendRouteGraph(steps: readonly FrontendStep[],
115
+ /** Drawn inside the START terminal. The workflow's title, where the caller has one. */
116
+ startLabel?: string): FrontendRouteGraph;
117
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,0GAA0G;IAC1G,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,wBAAwB;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;OAOG;IACH;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,kCAAkC;AAClC,MAAM,MAAM,YAAY,GACpB,IAAI,GACJ,SAAS,GACT,KAAK,GACL,OAAO;AACT;;;;;;;GAOG;GACD,YAAY,CAAC;AAEjB,mFAAmF;AACnF,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,OAAO,GACf;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/C;IACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEN,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;CACtC;AAuCD;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,SAAS,EAAE,SAAS,GAAG,QAAQ,GAC9B,OAAO,CAIT;AAYD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,YAAY,EAAE;AAC9B,uFAAuF;AACvF,UAAU,SAAU,GACnB,kBAAkB,CAkSpB"}
package/dist/routes.js ADDED
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Where a frontend step goes next — Story 4.2 AC-2 and AC-3.
3
+ *
4
+ * Ported from `dashboard-v2/src/pages/__dev/lib/connectorWorkflowMermaid.ts`, which is a complete and working
5
+ * implementation; the port's job is to keep its semantics and drop its positional ids.
6
+ *
7
+ * ### AC-2: both spellings — on a `next` OBJECT only, which is where the reference aliases
8
+ *
9
+ * ⛔ **CORRECTED by the Story 4.2 review.** The first version applied the alias to an `IF` step's `metadata` as well,
10
+ * and that was an **invention**: dashboard reads `step.metadata.then` / `.else` *literally* (`addStepEdges`), and its
11
+ * `branchValue` is used only inside `addNextCondition` — i.e. only for a nested branch object under `next`. The CLI
12
+ * aliases the same single place. So `{ type: 'IF', metadata: { success: 'A', failed: 'B' } }` drew YES/NO edges here
13
+ * and **nothing at all** in either reference.
14
+ *
15
+ * That is precisely the failure the package split exists to prevent, committed by the package that exists to prevent
16
+ * it: accepting a document shape the runtime rejects. Measured: `success`/`failed` occur in **8 real `next` objects
17
+ * and 0 real `metadata` blocks**, so the alias was tested only on the surface where it does not belong, while the one
18
+ * place it genuinely applies had no test.
19
+ *
20
+ * Where it DOES apply, `hasOwnProperty` before falling back is load-bearing: `then: null` is a
21
+ * **declared-but-empty** arm (the frontend's way of saying "this branch ends") and is distinct from an absent `then`
22
+ * that should defer to `success`. Truthiness collapses the two and silently takes the wrong arm — which is what
23
+ * AC-2's *"does not silently lose half its edges"* is about.
24
+ *
25
+ * ### AC-3: `onCancelled` is not a route out of the happy path
26
+ *
27
+ * It is the cancel route for a pause-for-input step (`FORM`, `*_CHOICE_FORM`) and is emitted for ANY step that
28
+ * declares one, alongside whatever `next` says — the reference's comment records why: otherwise the cancel branch and
29
+ * its downstream navigation steps *"aren't left orphaned in the diagram"*.
30
+ *
31
+ * ### Expressions are NOT resolved — and the reason first given for that was false
32
+ *
33
+ * ⚠️ The original note claimed *"the frontend runtime has no such form"*. It has: **23 real `next` values** are Liquid
34
+ * templates, across `installStandaloneCallbackSteps` in LightspeedXSeries, SapoOmniV3PublicApp, ShopifyPublicApp,
35
+ * ShoplinePublicApp, TikTokShopGatewayV3 and Zalo — e.g.
36
+ * `{% assign codeSize = ... %}{% if codeSize > 0 %}CHECK_PROFILE_STATUS{% else %}GET_SETUP_INSTRUCTIONS{% endif %}`.
37
+ *
38
+ * Both the reference and this model render each as ONE unresolved node labelled with the whole template, so ~23
39
+ * genuinely reachable routes are undrawn on both sides. That is faithful, and it is a real gap.
40
+ *
41
+ * It is not closed here because the worker machinery does not transfer: the worker resolves `"QUOTED"` keys in a
42
+ * Scriban expression, whereas these name steps **bare** inside Liquid tags. Resolving them needs its own reader and
43
+ * its own decision about what to draw when a branch is undecidable statically. Deferred, with the premise corrected
44
+ * so nobody re-derives the false one.
45
+ */
46
+ import { looksLikeTemplate, templateTargets } from './liquid.js';
47
+ const stepEnd = (key) => ({ kind: 'step', key });
48
+ /**
49
+ * A short, stable digest of an unresolved target — used as its ADDRESS segment instead of the target text.
50
+ *
51
+ * ⛔ Story 4.4 review. The segment used to be the raw target, and `renderedNodeId` sanitises everything outside
52
+ * `[A-Za-z0-9_]` to `_` — so two Liquid templates differing only in punctuation collapsed onto one mermaid id.
53
+ * Reproduced: `{% if x == 1 %}TARGET{% endif %}` and `{% if x != 1 %}TARGET{% endif %}` produce ONE node, which takes
54
+ * the second label and inherits both incoming edges as a false join, while `addressForRenderedId` then declines so the
55
+ * click resolves to nothing. It also produced 221-character ids carrying whole templates.
56
+ *
57
+ * A digest, not a counter: it is derived from the CONTENT, so it is stable under any insertion or reorder — which is
58
+ * what AD-33 actually requires — and two routes naming the same target still converge on one node, which is the
59
+ * behaviour the references have. The reader loses nothing: the junction's LABEL still carries the target verbatim.
60
+ *
61
+ * FNV-1a, because `node:crypto` is a host module this core package may not touch (AD-3).
62
+ */
63
+ function targetDigest(target) {
64
+ let hash = 0x811c9dc5;
65
+ for (let index = 0; index < target.length; index += 1) {
66
+ hash ^= target.charCodeAt(index);
67
+ hash = Math.imul(hash, 0x01000193) >>> 0;
68
+ }
69
+ return hash.toString(16).padStart(8, '0');
70
+ }
71
+ /**
72
+ * The owner of a junction that belongs to the WORKFLOW rather than to one step — an unresolved target, which every
73
+ * route naming it shares. Not a valid step key, so it cannot collide with one.
74
+ */
75
+ const WORKFLOW_OWNED = '';
76
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
77
+ /**
78
+ * A branch arm, by either spelling.
79
+ *
80
+ * `hasOwnProperty` on the primary before falling back — see the module note. Returns a sentinel-free result: the
81
+ * caller cannot distinguish "absent" from "declared null" from the value alone, so both are returned as-is and the
82
+ * caller treats `null` as an end.
83
+ */
84
+ export function branchArm(branch, primary, alternate) {
85
+ return Object.prototype.hasOwnProperty.call(branch, primary)
86
+ ? branch[primary]
87
+ : branch[alternate];
88
+ }
89
+ /**
90
+ * Every junction and edge for ONE workflow's steps.
91
+ *
92
+ * Iterative, never recursive — a branch arm can itself be a branch, without bound, and a document is untrusted input.
93
+ */
94
+ /** `true` or the string `'true'` — the reference's `isTruthyFlag`, which 3 real declarations depend on. */
95
+ function isTruthyFlag(value) {
96
+ return value === true || String(value).trim().toLowerCase() === 'true';
97
+ }
98
+ export function frontendRouteGraph(steps,
99
+ /** Drawn inside the START terminal. The workflow's title, where the caller has one. */
100
+ startLabel = 'START') {
101
+ const known = new Set(steps.map((s) => String(s.key)));
102
+ const junctions = [];
103
+ const edges = [];
104
+ const seen = new Set();
105
+ const junction = (owner, path, kind, label) => {
106
+ /**
107
+ * `JSON.stringify`, not a space join (Story 4.2 review, MEDIUM-1).
108
+ *
109
+ * A SWITCH case label may contain spaces, and `['metadata','cases','q','if','then','unknown']` and
110
+ * `['metadata','cases','q if then','unknown']` joined to the SAME string — so one junction was never created and
111
+ * an edge resolved to the other one. The `q` diamond's YES arm pointed at the wrong box. 0 corpus labels contain a
112
+ * space, which is why this is a structural guard rather than a live fix.
113
+ */
114
+ const id = JSON.stringify([owner, ...path]);
115
+ if (!seen.has(id)) {
116
+ seen.add(id);
117
+ junctions.push({ owner, path, kind, label });
118
+ }
119
+ return { kind: 'junction', owner, path };
120
+ };
121
+ /**
122
+ * The ENTRY points, and an arrow into each — Story 4.2 review, HIGH-1 / H4.
123
+ *
124
+ * ⛔ The first version dropped `root` entirely: no field, no node, no edge. Measured: **84** `root: true` in the
125
+ * corpus, **3** of them written as the STRING `'true'`, and **two workflows declare TWO roots** —
126
+ * `GoogleSheets.yaml` and `Hubspot.yaml` `authorizationCallbackSteps` each mark a second, genuine direct-entry step
127
+ * for the OAuth callback.
128
+ *
129
+ * Not deferrable to the renderer: with no `root` bit in the model a renderer can only guess "the first step", which
130
+ * is wrong for those two and for the four workflows whose root is not first. The fact is unrecoverable downstream.
131
+ *
132
+ * Fallback to the first step, as the reference does — a workflow with no `root` still has an entry.
133
+ */
134
+ const roots = steps.filter((step) => isTruthyFlag(step.root));
135
+ const entries = roots.length > 0 ? roots : steps.slice(0, 1);
136
+ if (entries.length > 0) {
137
+ const start = junction(WORKFLOW_OWNED, ['start'], 'start', startLabel);
138
+ for (const entry of entries) {
139
+ edges.push({ from: start, to: stepEnd(String(entry.key)) });
140
+ }
141
+ }
142
+ for (const raw of steps) {
143
+ const owner = String(raw.key);
144
+ const type = String(raw.type ?? '').toUpperCase();
145
+ const metadata = isRecord(raw.metadata) ? raw.metadata : undefined;
146
+ /**
147
+ * Branch objects already entered on THIS step's walk — the cycle guard.
148
+ *
149
+ * ⛔ Without it a cyclic branch does not loop, it **kills the process**: exit 134, `FATAL ERROR: heap out of
150
+ * memory`, in seconds. The path grows two segments per iteration and the junction id is keyed by that path, so it
151
+ * never repeats and both lists grow without bound. Reachable from YAML with the parser the CLI already uses:
152
+ *
153
+ * ```yaml
154
+ * next: &loop
155
+ * if: "x"
156
+ * then: *loop
157
+ * ```
158
+ *
159
+ * The module note argued *"iterative, never recursive — a document is untrusted input"*, which was right and
160
+ * removed the only bound that existed: the reference recurses and throws a **catchable** `RangeError`. An
161
+ * uncatchable V8 fatal is strictly worse, and this package is bundled into the VS Code extension host.
162
+ *
163
+ * Identity, not depth: the same object reached twice is the cycle, however long the path. The sibling worker
164
+ * package had the identical defect and is fixed the same way.
165
+ */
166
+ const entered = new WeakSet();
167
+ /** Work list: where the arrow leaves, the route value, the path that reaches it, the label. */
168
+ const pending = [];
169
+ // ── AC-3: the cancel route, independent of the happy path and emitted first so it reads as a side exit ──
170
+ if (typeof raw.onCancelled === 'string' && raw.onCancelled.trim() !== '') {
171
+ pending.push({
172
+ from: stepEnd(owner),
173
+ value: raw.onCancelled.trim(),
174
+ path: ['onCancelled'],
175
+ label: 'Cancel',
176
+ });
177
+ }
178
+ if (type === 'IF' && metadata) {
179
+ /**
180
+ * `metadata.then` / `.else`, read LITERALLY — no alias. See the module note: aliasing here was an invention that
181
+ * accepted a shape both references reject. `metadata`, not `data`, is the frontend vocabulary; the alias is a
182
+ * property of a `next` OBJECT, and only there.
183
+ */
184
+ pending.push({
185
+ from: stepEnd(owner),
186
+ value: metadata.then,
187
+ path: ['metadata', 'then'],
188
+ label: 'YES',
189
+ });
190
+ pending.push({
191
+ from: stepEnd(owner),
192
+ value: metadata.else,
193
+ path: ['metadata', 'else'],
194
+ label: 'NO',
195
+ });
196
+ }
197
+ else if (type === 'SWITCH' && metadata) {
198
+ /**
199
+ * A frontend SWITCH reads `metadata.cases` — a RECORD of label → target — plus an optional `metadata.default`.
200
+ * The worker's SWITCH branches on its `data` KEYS instead. Two runtimes, two shapes.
201
+ */
202
+ if (isRecord(metadata.cases)) {
203
+ for (const [label, target] of Object.entries(metadata.cases)) {
204
+ pending.push({
205
+ from: stepEnd(owner),
206
+ value: target,
207
+ path: ['metadata', 'cases', label],
208
+ label,
209
+ });
210
+ }
211
+ }
212
+ if (typeof metadata.default === 'string') {
213
+ pending.push({
214
+ from: stepEnd(owner),
215
+ value: metadata.default,
216
+ path: ['metadata', 'default'],
217
+ label: 'default',
218
+ });
219
+ }
220
+ }
221
+ else if (typeof raw.next === 'string' || isRecord(raw.next)) {
222
+ pending.push({ from: stepEnd(owner), value: raw.next, path: ['next'] });
223
+ }
224
+ else {
225
+ /**
226
+ * No usable `next` ⇒ the workflow ENDS here, drawn as a terminal.
227
+ *
228
+ * The worker model has no equivalent: a worker step with no `next` simply has no outgoing edge. The frontend
229
+ * reference draws `END` explicitly, and it is worth keeping — an onboarding workflow that stops is a fact the
230
+ * reader wants stated, not inferred from an absence of arrows.
231
+ */
232
+ const end = junction(owner, ['next', 'end'], 'end', 'END');
233
+ edges.push({ from: stepEnd(owner), to: end });
234
+ }
235
+ while (pending.length > 0) {
236
+ const { from, value, path, label } = pending.shift();
237
+ const labelled = label !== undefined ? { label } : {};
238
+ if (typeof value === 'string' && value.trim() !== '') {
239
+ const target = value.trim();
240
+ if (known.has(target)) {
241
+ edges.push({ from, to: stepEnd(target), ...labelled });
242
+ continue;
243
+ }
244
+ /**
245
+ * A TEMPLATE names its targets, so it is a DIAMOND with one arrow per target (2026-08-20).
246
+ *
247
+ * Reported: *"this is actually kind of liquid syntax which has `{% … %}` inside it, this must works
248
+ * similar to how scriban syntax works. and the `next` MUST ALWAYS be a diamond if it is not a step
249
+ * key … currently the node is not connected to the next which is not correct."*
250
+ *
251
+ * Right on both counts, and the gap was already written down in this module's own note — 20 real
252
+ * `next` templates across six connectors drew as one unresolved BOX with no outgoing edges, so every
253
+ * route they name was undrawn. `liquid.ts` reads the targets; this draws them.
254
+ *
255
+ * Owned by the STEP, not the workflow, and NOT converged by target text — unlike the ghost below. A
256
+ * decision belongs to the step that makes it, so two steps carrying the same template get a diamond
257
+ * each. That is the worker's shape for the same situation (`routeGraph.ts`, the `expression` branch)
258
+ * and the reason this is `[...path, 'expression']` there too.
259
+ *
260
+ * The arrows are UNLABELLED, exactly as the worker draws them: which branch it takes is a run-time
261
+ * fact, and labelling one `YES` would assert an order the template does not promise.
262
+ *
263
+ * A template that resolves NOTHING still draws its diamond. It is a decision either way, and a
264
+ * diamond with no exits says "the flow branches here and I cannot tell where it goes" — which is
265
+ * true, and is what the reader asked for over a box that says nothing at all.
266
+ */
267
+ if (looksLikeTemplate(target)) {
268
+ const point = junction(owner, [...path, 'expression'], 'expression', target);
269
+ edges.push({ from, to: point, ...labelled });
270
+ for (const reachable of templateTargets(target)) {
271
+ if (known.has(reachable))
272
+ edges.push({ from: point, to: stepEnd(reachable) });
273
+ }
274
+ continue;
275
+ }
276
+ /**
277
+ * An unresolved target is a NODE, and routes naming the SAME target CONVERGE on it.
278
+ *
279
+ * ⚠️ The first version gave each route its own node, so two steps routing to `GONE` drew two boxes labelled
280
+ * `GONE` where both references draw one with two incoming arrows (the reference ids the ghost by target, not
281
+ * by route). Convergence restored — but keyed on the target TEXT rather than the reference's
282
+ * `mermaidId(target)`, which also fixes a reference bug: two different Liquid expressions differing only in
283
+ * punctuation collapse onto one node there, and a ghost target `A-B` hijacks a real step keyed `A_B`.
284
+ *
285
+ * Owned by the WORKFLOW rather than a step, since that is what "the same target" means.
286
+ */
287
+ edges.push({
288
+ from,
289
+ // Digest, not the raw target — see `targetDigest`. Convergence is preserved: one target, one node.
290
+ to: junction(WORKFLOW_OWNED, ['unknown', targetDigest(target)], 'unknown', target),
291
+ ...labelled,
292
+ });
293
+ continue;
294
+ }
295
+ if (isRecord(value)) {
296
+ if (entered.has(value)) {
297
+ const point = junction(owner, [...path, 'cycle'], 'unknown', 'cycle: this branch refers to itself');
298
+ edges.push({ from, to: point, ...labelled });
299
+ continue;
300
+ }
301
+ entered.add(value);
302
+ /**
303
+ * A nested condition, labelled with its CONDITION where the document gives one.
304
+ *
305
+ * ⚠️ The first version labelled every one of these the literal `"IF"`, on the stated grounds that *"the
306
+ * frontend branch object carries no `if` field to draw"*. False: **all 61** real `next` branch objects in the
307
+ * corpus carry an `if:` holding the Liquid condition. The reference does label them `"IF"`, so this is an
308
+ * *"accept the change"* against the baseline rather than a bug fix — and the change is worth making, because
309
+ * the worker twin already carries its condition text and a diagram that says `IF` three times tells a reader
310
+ * nothing about which branch is which.
311
+ */
312
+ const condition = value.if;
313
+ const label = typeof condition === 'string' && condition.trim() !== ''
314
+ ? condition.trim()
315
+ : 'IF';
316
+ const point = junction(owner, [...path, 'if'], 'if', label);
317
+ edges.push({ from, to: point, ...labelled });
318
+ pending.push({
319
+ from: point,
320
+ value: branchArm(value, 'then', 'success'),
321
+ path: [...path, 'if', 'then'],
322
+ label: 'YES',
323
+ });
324
+ pending.push({
325
+ from: point,
326
+ value: branchArm(value, 'else', 'failed'),
327
+ path: [...path, 'if', 'else'],
328
+ label: 'NO',
329
+ });
330
+ continue;
331
+ }
332
+ if (value === null) {
333
+ /**
334
+ * A DECLARED end — `then: null` says this arm stops.
335
+ *
336
+ * ONE end per owning step, not one per arm (review M3): `{ then: null, else: null }` drew two, where the
337
+ * reference collapses them to a single unlabelled terminal. The label is dropped for the same reason — a
338
+ * terminal reached by both arms cannot honestly carry either arm's label.
339
+ */
340
+ const end = junction(owner, ['next', 'end'], 'end', 'END');
341
+ edges.push({ from, to: end });
342
+ continue;
343
+ }
344
+ /**
345
+ * Anything else — `undefined`, a number, an array — yields nothing, matching the reference exactly.
346
+ *
347
+ * ⚠️ Recorded rather than improved: an arm the document wrote as `then: 42` disappears silently here. Changing
348
+ * it would change the shipped diagram, which is Story 4.4's golden-suite decision to make deliberately, not a
349
+ * port's to make in passing.
350
+ */
351
+ }
352
+ }
353
+ return { junctions, edges };
354
+ }
355
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The frontend workflows, and their titles — Story 4.2 AC-1.
3
+ *
4
+ * ### A separate package, deliberately (AD-21)
5
+ *
6
+ * A frontend step is never resolved with the worker vocabulary, and this is where that becomes structural rather than
7
+ * a convention. The two runtimes genuinely differ, in four ways measured off the reference implementation
8
+ * (`dashboard-v2/src/pages/__dev/lib/connectorWorkflowMermaid.ts`):
9
+ *
10
+ * | | worker | frontend |
11
+ * | --- | --- | --- |
12
+ * | branch arms | `then` / `else` | `then` **or** `success` / `else` **or** `failed` |
13
+ * | `IF` reads | `data` | `metadata` |
14
+ * | `SWITCH` reads | `data`'s KEYS | `metadata.cases` (a record) + `metadata.default` |
15
+ * | extra route | `onError` | `onCancelled`, labelled `Cancel` |
16
+ *
17
+ * Two more the worker side has no equivalent for: an explicit **`END`** terminal when a step declares no `next`, and
18
+ * no expression handling at all — a frontend `next` is a key, a branch object, or nothing.
19
+ *
20
+ * ⛔ **So the two packages must NOT share a branch reader.** The temptation to unify them is the thing to resist:
21
+ * teaching the worker model about `success`/`failed` would make it accept documents the worker runtime rejects, which
22
+ * is exactly the invention NFR-1 forbids.
23
+ */
24
+ /** The five workflows a CONNECTOR DEFINITION may declare, in the order the reference implementation lists them. */
25
+ export declare const CONNECTOR_WORKFLOW_KEYS: readonly ['appRegistrationSteps', 'authorizationSteps', 'authorizationCallbackSteps', 'installStandaloneCallbackSteps', 'setupSteps'];
26
+ /** The one a TEMPLATE declares. Separate because it lives on a different document. */
27
+ export declare const TEMPLATE_WORKFLOW_KEYS: readonly ['creationSteps'];
28
+ export type ConnectorWorkflowKey = (typeof CONNECTOR_WORKFLOW_KEYS)[number];
29
+ export type TemplateWorkflowKey = (typeof TEMPLATE_WORKFLOW_KEYS)[number];
30
+ export type FrontendWorkflowKey = ConnectorWorkflowKey | TemplateWorkflowKey;
31
+ export declare const FRONTEND_WORKFLOW_KEYS: readonly FrontendWorkflowKey[];
32
+ /**
33
+ * A human title per workflow — AC-1's *"drawn with its own title"*.
34
+ *
35
+ * ⚠️ **PRESERVED from the recorded baseline** (Story 4.2 review, M2). The first version renamed five of the six —
36
+ * `App Registration` → `App registration`, `Profile Setup` → `Setup`, `Profile Creation` → `Creation` and so on — on
37
+ * the reasoning that the word *Steps* is noise and sentence case reads better. Two things were wrong with that: the
38
+ * baseline README records these titles as rendered output, so changing them needs an adopt/preserve/accept verdict per
39
+ * `golden-flows/README.md`, and nothing was asserting them — **setting all six to the same string left all 27 tests
40
+ * green**, because only `authorizationSteps` (the one title that happened not to change) was compared exactly.
41
+ *
42
+ * PRESERVE is the verdict: these are the titles two shipped surfaces already draw, and the case argument is a
43
+ * preference, not a defect. Each is now asserted exactly.
44
+ */
45
+ export declare const FRONTEND_WORKFLOW_TITLES: Readonly<Record<FrontendWorkflowKey, string>>;
46
+ /** Where a workflow may be declared. Reported on every node so a consumer can tell the two documents apart. */
47
+ export type FrontendSurface = 'connector' | 'template';
48
+ export declare function surfaceOf(key: FrontendWorkflowKey): FrontendSurface;
49
+ /**
50
+ * The workflows a document actually declares, in the order above.
51
+ *
52
+ * Non-empty, for the same reason the worker package requires it: a declared-but-empty workflow is a fact about the
53
+ * document, not a diagram a reader can be sent to. (The worker side shipped the looser rule and the review measured
54
+ * 4,519 corpus occurrences of an arrow into nothing.)
55
+ */
56
+ export declare function workflowsPresent(document: Record<string, unknown>,
57
+ /**
58
+ * How many of an array's items are actually DRAWABLE. Injected so this module stays free of step semantics while
59
+ * still measuring the right thing.
60
+ */
61
+ drawableCount?: (items: readonly unknown[]) => number): readonly FrontendWorkflowKey[];
62
+ //# sourceMappingURL=workflows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflows.d.ts","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,mHAAmH;AACnH,eAAO,MAAM,uBAAuB,YAClC,sBAAsB,EACtB,oBAAoB,EACpB,4BAA4B,EAC5B,gCAAgC,EAChC,YAAY,CACJ,CAAC;AAEX,sFAAsF;AACtF,eAAO,MAAM,sBAAsB,YAAI,eAAe,CAAU,CAAC;AAEjE,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5E,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAC1E,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,GAAG,mBAAmB,CAAC;AAE7E,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAGhE,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAC7C,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAQpC,CAAC;AAEF,+GAA+G;AAC/G,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,UAAU,CAAC;AAEvD,wBAAgB,SAAS,CAAC,GAAG,EAAE,mBAAmB,GAAG,eAAe,CAInE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;AACjC;;;GAGG;AACH,aAAa,GAAE,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,KAAK,MAChC,GACb,SAAS,mBAAmB,EAAE,CAKhC"}