@caddis/cli 0.4.44 → 0.4.46
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/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +15 -3
- package/bundles/antigravity-plugin/scripts/claude-oss.sh +12 -1
- package/bundles/antigravity-plugin/skills/_registry.md +1 -0
- package/bundles/antigravity-plugin/skills/flow-studio/README.md +92 -0
- package/bundles/antigravity-plugin/skills/flow-studio/SKILL.md +102 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/README.md +19 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/agent-journey.json +103 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/approval-flow.json +134 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/ci-cd-pipeline.json +132 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/hostile.json +1783 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/incident-timeline.json +109 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/lineage.json +1778 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/order-desk.json +99 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/service-map.json +133 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/state-machine.json +106 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/ticket-lifecycle.json +107 -0
- package/bundles/antigravity-plugin/skills/flow-studio/model.schema.json +142 -0
- package/bundles/antigravity-plugin/skills/flow-studio/package.json +11 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.test.tsx +82 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.tsx +67 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/README.md +37 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/index.ts +3 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/FlowCanvas.tsx +58 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/Inspector.tsx +27 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/index.ts +6 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.css +18 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.test.tsx +161 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/useFlowGeometry.ts +22 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/useTrace.ts +29 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/useViewport.ts +75 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/states.tsx +13 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/design-language.md +169 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/implement-in-app.md +213 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/model-schema.md +254 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/notes-and-spec.md +74 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/patterns.md +415 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/ux-rules.md +91 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/verify.md +124 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/build-engine.mjs +95 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/build.mjs +54 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/check.mjs +680 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/default-notes.json +67 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/build-engine.test.mjs +54 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.mjs +18 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.test.mjs +15 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-selftest.test.mjs +135 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/cli.test.mjs +179 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.mjs +23 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/docs.test.mjs +128 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/embed.mjs +24 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/embed.test.mjs +29 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/library.test.mjs +26 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/react-proof.mjs +92 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/validate.mjs +42 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.test.ts +78 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.ts +132 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/chrome.ts +175 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/controls.ts +239 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/ctx.ts +134 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/diff.test.ts +31 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/diff.ts +34 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/engine.ts +193 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/inspector.ts +126 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/interact.ts +253 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/layout.test.ts +144 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/layout.ts +95 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/lib.ts +20 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.test.ts +57 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.ts +29 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/model.test.ts +167 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/model.ts +250 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/notes.ts +100 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/page.ts +21 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/playback.test.ts +104 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/playback.ts +106 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/playbackui.ts +193 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/render.ts +203 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/routing.test.ts +33 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/routing.ts +43 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/rules.test.ts +135 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/rules.ts +86 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/selection.test.ts +12 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/selection.ts +4 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.test.ts +73 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.ts +45 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/styles.css +190 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/trace.test.ts +40 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/trace.ts +40 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/viewport.test.ts +55 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/viewport.ts +45 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.html +2656 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.shell.html +23 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.css +190 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.d.mts +1 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.mjs +2491 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/shell-body.html +50 -0
- package/bundles/antigravity-plugin/skills/flow-studio/tsconfig.json +17 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/README.md +92 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/SKILL.md +102 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/README.md +19 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/agent-journey.json +103 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/approval-flow.json +134 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ci-cd-pipeline.json +132 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/hostile.json +1783 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/incident-timeline.json +109 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/lineage.json +1778 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/order-desk.json +99 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/service-map.json +133 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/state-machine.json +106 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ticket-lifecycle.json +107 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/model.schema.json +142 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/package.json +11 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.test.tsx +82 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.tsx +67 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/README.md +37 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/index.ts +3 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/FlowCanvas.tsx +58 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/Inspector.tsx +27 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/index.ts +6 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.css +18 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.test.tsx +161 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useFlowGeometry.ts +22 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useTrace.ts +29 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useViewport.ts +75 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/states.tsx +13 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/design-language.md +169 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/implement-in-app.md +213 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/model-schema.md +254 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/notes-and-spec.md +74 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/patterns.md +415 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/ux-rules.md +91 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/verify.md +124 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build-engine.mjs +95 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build.mjs +54 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/check.mjs +680 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/default-notes.json +67 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/build-engine.test.mjs +54 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.mjs +18 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.test.mjs +15 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-selftest.test.mjs +135 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/cli.test.mjs +179 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.mjs +23 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/docs.test.mjs +128 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/embed.mjs +24 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/embed.test.mjs +29 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/library.test.mjs +26 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/react-proof.mjs +92 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/validate.mjs +42 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.test.ts +78 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.ts +132 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/chrome.ts +175 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/controls.ts +239 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/ctx.ts +134 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.test.ts +31 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.ts +34 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/engine.ts +193 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/inspector.ts +126 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/interact.ts +253 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.test.ts +144 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.ts +95 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/lib.ts +20 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.test.ts +57 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.ts +29 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.test.ts +167 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.ts +250 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/notes.ts +100 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/page.ts +21 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.test.ts +104 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.ts +106 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/playbackui.ts +193 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/render.ts +203 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.test.ts +33 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.ts +43 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.test.ts +135 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.ts +86 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.test.ts +12 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.ts +4 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.test.ts +73 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.ts +45 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/styles.css +190 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.test.ts +40 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.ts +40 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/viewport.test.ts +55 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/viewport.ts +45 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.html +2656 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.shell.html +23 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.css +190 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.d.mts +1 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.mjs +2491 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/shell-body.html +50 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/tsconfig.json +17 -0
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"anchor": "#title",
|
|
4
|
+
"title": "Page frame",
|
|
5
|
+
"text": "Display the main scenario headline, subtitle, and timestamp. Render all text using textContent or esc() to prevent script injection."
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"anchor": "#layerstrip",
|
|
9
|
+
"title": "Completeness strip",
|
|
10
|
+
"text": "Show completeness per column with stacked horizontal bars for good, neutral, and gap counts. Render dotted tracks for empty columns."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"anchor": "#kpis",
|
|
14
|
+
"title": "Summary tiles",
|
|
15
|
+
"text": "Show total links, gap count, and upgrade opportunities. Make tiles interactive buttons that filter canvas focus when clicked."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"anchor": "#modeSwitch",
|
|
19
|
+
"title": "View switch",
|
|
20
|
+
"text": "Provide segmented buttons to switch perspectives. Update visible nodes and overridden link states from a single underlying data model."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"anchor": "#q",
|
|
24
|
+
"title": "Search and radius",
|
|
25
|
+
"text": "Filter nodes by name or identifier. Use the hops selector to reveal upstream and downstream connections within N degrees."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"anchor": "#viewport",
|
|
29
|
+
"title": "Canvas viewport",
|
|
30
|
+
"text": "Support smooth pan and zoom around cursor. Fit initial view to width so users see all phases immediately without manual zooming."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"anchor": "#tools",
|
|
34
|
+
"title": "Canvas controls",
|
|
35
|
+
"text": "Provide quick zoom in, zoom out, fit-to-screen, 100% scale, and pan hand controls. Bind standard keyboard shortcuts V, H, +, -, 0, and F."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"anchor": "#inspector",
|
|
39
|
+
"title": "Side inspector",
|
|
40
|
+
"text": "Slide out an inspector card on node selection. Display incoming links, outgoing links, metadata, and what-if simulation buttons."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"anchor": "#legend",
|
|
44
|
+
"title": "State legend",
|
|
45
|
+
"text": "Display color chips and state labels for good, neutral, warning, and critical states. Keep states distinguishable without color alone."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"anchor": "#ledger",
|
|
49
|
+
"title": "Change ledger",
|
|
50
|
+
"text": "List recent schema updates, audit entries, or unverified links beneath the canvas. Render tabular details cleanly."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"anchor": "#tbl",
|
|
54
|
+
"title": "Accessible table view",
|
|
55
|
+
"text": "Provide a collapsible HTML table containing all graph nodes, states, and relations. Ensures screen reader parity with canvas visuals."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"anchor": "#playbar",
|
|
59
|
+
"title": "Scenario playback",
|
|
60
|
+
"text": "Provide play, pause, step-by-step, reset, and scrubbing controls. Animate token flow across links during scenario execution."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"anchor": "#notesBtn",
|
|
64
|
+
"title": "Implementation notes",
|
|
65
|
+
"text": "Toggle developer guidance cards and anchored target markers. Map numeric badges directly to corresponding drawer documentation cards."
|
|
66
|
+
}
|
|
67
|
+
]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Keep malformed auto models intact so validation reports their actual errors. */
|
|
2
|
+
export function prepareModel(input, autoLayout) {
|
|
3
|
+
return input?.layout === "auto" && Array.isArray(input.nodes) && Array.isArray(input.links)
|
|
4
|
+
&& Array.isArray(input.columns) && Array.isArray(input.lanes) ? autoLayout(input) : input;
|
|
5
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { prepareModel } from "./auto-entry-guard.mjs";
|
|
4
|
+
|
|
5
|
+
test("auto layout runs only when its four input arrays exist", () => {
|
|
6
|
+
const model = { layout: "auto", nodes: [], links: [], columns: [], lanes: [] };
|
|
7
|
+
const laidOut = { ...model, applied: true };
|
|
8
|
+
const layout = input => { assert.equal(input, model); return laidOut; };
|
|
9
|
+
assert.equal(prepareModel(model, layout), laidOut);
|
|
10
|
+
for (const field of ["nodes", "links", "columns", "lanes"]) {
|
|
11
|
+
assert.equal(prepareModel({ ...model, [field]: null }, () => { throw Error("called"); })[field], null);
|
|
12
|
+
}
|
|
13
|
+
assert.equal(prepareModel({ ...model, layout: "free" }, () => { throw Error("called"); }).layout, "free");
|
|
14
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const validateScript = fileURLToPath(new URL("../validate.mjs", import.meta.url));
|
|
9
|
+
const buildScript = fileURLToPath(new URL("../build.mjs", import.meta.url));
|
|
10
|
+
|
|
11
|
+
test("validate and build lay out an auto model before checking it", () => {
|
|
12
|
+
const dir = mkdtempSync(join(fileURLToPath(new URL(".", import.meta.url)), ".auto-entry-"));
|
|
13
|
+
try {
|
|
14
|
+
const modelPath = join(dir, "model.json"), pagePath = join(dir, "page.html");
|
|
15
|
+
const model = {
|
|
16
|
+
version: 1, meta: { title: "Auto", help: "", asOf: "", source: "" }, layout: "auto", columns: [], lanes: [],
|
|
17
|
+
states: { ok: { word: "OK", tone: "ok", border: "solid", edge: "flow", motion: "flow", bucket: "good" } },
|
|
18
|
+
nodes: ["a", "b"].map(id => ({ id, kind: "card", state: "ok", title: id })),
|
|
19
|
+
links: [{ id: "ab", from: "a", to: "b", state: "ok" }],
|
|
20
|
+
};
|
|
21
|
+
writeFileSync(modelPath, JSON.stringify(model));
|
|
22
|
+
const run = (script, extra = []) => spawnSync(process.execPath, ["--experimental-strip-types", "--no-warnings", script, modelPath, ...extra], { cwd: dir, encoding: "utf8" });
|
|
23
|
+
const valid = run(validateScript);
|
|
24
|
+
assert.equal(valid.status, 0, valid.stdout + valid.stderr);
|
|
25
|
+
const built = run(buildScript, [pagePath]);
|
|
26
|
+
assert.equal(built.status, 0, built.stdout + built.stderr);
|
|
27
|
+
const page = readFileSync(pagePath, "utf8");
|
|
28
|
+
assert.match(page, /"layout":"auto"/);
|
|
29
|
+
assert.match(page, /"title":"Stage 2"/);
|
|
30
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
31
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { buildEngine, resolveEsbuild } from "../build-engine.mjs";
|
|
6
|
+
|
|
7
|
+
const normalise = t => t.replace(/\r\n/g, "\n");
|
|
8
|
+
test("esbuild resolution chooses the first candidate and respects order", () => {
|
|
9
|
+
const dir = mkdtempSync(new URL("./flow-studio-esbuild-", import.meta.url));
|
|
10
|
+
try {
|
|
11
|
+
const candidates = [join(dir, "first"), join(dir, "second")];
|
|
12
|
+
for (const [i, candidate] of candidates.entries()) {
|
|
13
|
+
const pkg = join(candidate, "node_modules", "esbuild");
|
|
14
|
+
mkdirSync(pkg, { recursive: true });
|
|
15
|
+
writeFileSync(join(pkg, "package.json"), JSON.stringify({ name: "esbuild", main: "index.js" }));
|
|
16
|
+
writeFileSync(join(pkg, "index.js"), `module.exports = { source: ${JSON.stringify(i)} };`);
|
|
17
|
+
}
|
|
18
|
+
assert.equal(resolveEsbuild(candidates).source, 0);
|
|
19
|
+
assert.equal(resolveEsbuild(candidates.toReversed()).source, 1);
|
|
20
|
+
} finally {
|
|
21
|
+
rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("empty candidate folders give an actionable esbuild error", () => {
|
|
26
|
+
const dir = mkdtempSync(new URL("./flow-studio-esbuild-", import.meta.url));
|
|
27
|
+
try {
|
|
28
|
+
const empty = [join(dir, "one"), join(dir, "two")];
|
|
29
|
+
empty.forEach(p => mkdirSync(p));
|
|
30
|
+
assert.throws(() => resolveEsbuild(empty), /FLOW_STUDIO_NODE_MODULES.*npm i -D esbuild/);
|
|
31
|
+
} finally {
|
|
32
|
+
rmSync(dir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the generated engine has no external URL and keeps only the model and notes slots", async () => {
|
|
37
|
+
const html = await buildEngine();
|
|
38
|
+
const urls = html.match(/https?:\/\/[^\s"'<>)\\]+/g) ?? [];
|
|
39
|
+
const external = urls.filter(u => u !== "http://www.w3.org/2000/svg");
|
|
40
|
+
assert.deepEqual(external, [], "external URL found");
|
|
41
|
+
assert.ok(!html.includes("/*__CSS__*/") && !html.includes("/*__ENGINE__*/"));
|
|
42
|
+
assert.equal(html.split("/*__MODEL__*/{}").length - 1, 1);
|
|
43
|
+
assert.equal(html.split("/*__NOTES__*/[]").length - 1, 1);
|
|
44
|
+
});
|
|
45
|
+
test("the engine bundle cannot end its own script tag", async () => {
|
|
46
|
+
const html = await buildEngine();
|
|
47
|
+
const scripts = html.match(/<\/script>/g) ?? [];
|
|
48
|
+
assert.equal(scripts.length, 3, "exactly the three script tags of the shell");
|
|
49
|
+
});
|
|
50
|
+
test("the committed engine matches the sources", async () => {
|
|
51
|
+
const fresh = await buildEngine();
|
|
52
|
+
const committed = normalise(readFileSync(new URL("../../templates/engine.html", import.meta.url), "utf8"));
|
|
53
|
+
assert.equal(committed, fresh, "run: node --experimental-strip-types scripts/build-engine.mjs");
|
|
54
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const allowed = new Set(['notes', 'playback', 'update', 'hostile']);
|
|
5
|
+
|
|
6
|
+
export function pageUrl(input) {
|
|
7
|
+
// try/catch, not URL.canParse: canParse needs Node 18.17+ and this skill runs on older tooling too.
|
|
8
|
+
let parsed = null;
|
|
9
|
+
try { parsed = new URL(input); } catch { /* not an absolute URL; treat the input as a local path */ }
|
|
10
|
+
if (parsed?.protocol === 'http:' || parsed?.protocol === 'https:' || parsed?.protocol === 'file:') return parsed.href;
|
|
11
|
+
return pathToFileURL(resolve(input)).href;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function skippedGroups(value = '') {
|
|
15
|
+
const groups = value ? value.split(',').map(s => s.trim()) : [];
|
|
16
|
+
for (const group of groups) if (!allowed.has(group)) throw new Error(`unknown skip group: ${group}`);
|
|
17
|
+
return new Set(groups);
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { pageUrl, skippedGroups } from './check-args.mjs';
|
|
4
|
+
|
|
5
|
+
test('pageUrl accepts served pages and local files', () => {
|
|
6
|
+
assert.equal(pageUrl('http://127.0.0.1:8123/path'), 'http://127.0.0.1:8123/path');
|
|
7
|
+
assert.equal(pageUrl('https://example.test/flow'), 'https://example.test/flow');
|
|
8
|
+
assert.equal(pageUrl('file:///tmp/page.html'), 'file:///tmp/page.html');
|
|
9
|
+
assert.match(pageUrl('page.html'), /^file:\/\//);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('skip parser accepts only known groups', () => {
|
|
13
|
+
assert.deepEqual([...skippedGroups('notes,playback,update,hostile')], ['notes', 'playback', 'update', 'hostile']);
|
|
14
|
+
assert.throws(() => skippedGroups('safety'), /unknown skip group/);
|
|
15
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Proves the checks can fail: each fixture is a page with one deliberate defect, and check.mjs must exit 1
|
|
2
|
+
// and name the matching check. Skipped when Playwright cannot be resolved from the current folder.
|
|
3
|
+
// Run it from a folder that has Playwright, for example:
|
|
4
|
+
// FLOW_STUDIO_CHANNEL=msedge node --test <skill>/scripts/lib/check-selftest.test.mjs
|
|
5
|
+
|
|
6
|
+
import { test, after } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { embedModel } from "./embed.mjs";
|
|
14
|
+
|
|
15
|
+
const root = resolve(fileURLToPath(import.meta.url), "../../..");
|
|
16
|
+
let hasPlaywright = true;
|
|
17
|
+
try { createRequire(join(process.cwd(), "x.js")).resolve("playwright"); } catch { hasPlaywright = false; }
|
|
18
|
+
const skip = hasPlaywright ? false : "playwright is not resolvable from the current folder";
|
|
19
|
+
|
|
20
|
+
const engine = readFileSync(join(root, "templates/engine.html"), "utf8");
|
|
21
|
+
const base = JSON.parse(readFileSync(join(root, "examples/lineage.json"), "utf8"));
|
|
22
|
+
const order = JSON.parse(readFileSync(join(root, "examples/order-desk.json"), "utf8"));
|
|
23
|
+
const dir = mkdtempSync(join(root, ".flow-selftest-"));
|
|
24
|
+
after(() => rmSync(dir, { recursive: true, force: true }));
|
|
25
|
+
|
|
26
|
+
function page(name, { model = base, inject = "" } = {}) {
|
|
27
|
+
const html = embedModel(engine, model, []).replace("</body>", `${inject}</body>`);
|
|
28
|
+
const p = join(dir, name + ".html");
|
|
29
|
+
writeFileSync(p, html);
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
function run(p) {
|
|
33
|
+
const args = [join(root, "scripts/check.mjs"), p, "--out", join(dir, "shots"), "--viewports", "1600x900", "--skip-perf"];
|
|
34
|
+
if (process.env.FLOW_STUDIO_CHANNEL) args.push("--channel", process.env.FLOW_STUDIO_CHANNEL);
|
|
35
|
+
return spawnSync(process.execPath, args, { encoding: "utf8", cwd: process.cwd(), timeout: 120000 });
|
|
36
|
+
}
|
|
37
|
+
const expectFail = (r, pattern) => {
|
|
38
|
+
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
39
|
+
assert.match(r.stdout, new RegExp("FAIL.*" + pattern, "i"), r.stdout);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
test("a clean page passes every check", { skip }, () => {
|
|
43
|
+
const r = run(page("clean"));
|
|
44
|
+
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("a clean page with a metric on a collapsed child passes every check", { skip }, () => {
|
|
48
|
+
const r = run(page("clean-order-desk", { model: order }));
|
|
49
|
+
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("two nodes in one place fail the intersection check", { skip }, () => {
|
|
53
|
+
const m = structuredClone(base);
|
|
54
|
+
m.nodes.find(n => n.id === "T_WH").row = m.nodes.find(n => n.id === "D_WH").row; // same row, other column: move it onto a neighbour
|
|
55
|
+
const a = m.nodes.find(n => n.id === "J_W47"), b = m.nodes.find(n => n.id === "J_BILL");
|
|
56
|
+
b.row = a.row; // same column, same row
|
|
57
|
+
expectFail(run(page("overlap", { model: m })), "zero node-box intersections");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a control with no name fails the accessible-name check", { skip }, () => {
|
|
61
|
+
const inject = `<script>window.addEventListener("load", () => setTimeout(() => { const b = document.querySelector("#tools button"); b.removeAttribute("aria-label"); b.removeAttribute("title"); }, 300));</script>`;
|
|
62
|
+
expectFail(run(page("noname", { inject })), "accessible name");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a remote image fails the no-network check", { skip }, () => {
|
|
66
|
+
expectFail(run(page("remote", { inject: `<img src="https://example.invalid/x.png" alt="">` })), "no network requests");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("faint text fails the contrast check", { skip }, () => {
|
|
70
|
+
expectFail(run(page("faint", { inject: `<style>.node .s, .node .name { color:#d8d8d8 !important; }</style>` })), "contrast");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a looping animation under reduced motion fails the reduced-motion check", { skip }, () => {
|
|
74
|
+
expectFail(run(page("motion", { inject: `<style>@media (prefers-reduced-motion: reduce) { .node { animation: rise 1s infinite !important; } }</style>` })), "reduced motion");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a canvas too wide to read fails the first-view check", { skip }, () => {
|
|
78
|
+
const m = structuredClone(base);
|
|
79
|
+
m.columns.forEach(c => { c.width = 700; });
|
|
80
|
+
expectFail(run(page("wide", { model: m })), "first view is readable");
|
|
81
|
+
});
|
|
82
|
+
test("a moved loop path fails the link-endpoint check", { skip }, () => {
|
|
83
|
+
const m = structuredClone(base);
|
|
84
|
+
const link = m.links[0];
|
|
85
|
+
link.from = "H_DB1";
|
|
86
|
+
link.to = "ORD";
|
|
87
|
+
link.kind = "loop-back";
|
|
88
|
+
const inject = `<script>window.addEventListener("load", () => setTimeout(() => { document.querySelector("path.edge.loop")?.setAttribute("d", "M0,0 H10"); }, 300));</script>`;
|
|
89
|
+
expectFail(run(page("moved-loop", { model: m, inject })), "every link end touches its node box");
|
|
90
|
+
});
|
|
91
|
+
test("a node moved onto a loop path fails the loop clearance check", { skip }, () => {
|
|
92
|
+
const m = structuredClone(base);
|
|
93
|
+
m.links.push({ id: "return-check", from: "H_DB1", to: "ORD", state: m.links[0].state, kind: "loop-back" });
|
|
94
|
+
const inject = `<script>window.addEventListener("load", () => setTimeout(() => {
|
|
95
|
+
const path = [...document.querySelectorAll("path.edge.loop")].find(p => !p.classList.contains("off"));
|
|
96
|
+
const node = [...document.querySelectorAll(".node:not(.off)")].find(n => n.id !== "n_" + path?.dataset.a && n.id !== "n_" + path?.dataset.b);
|
|
97
|
+
if (path && node) { const point = path.getPointAtLength(path.getTotalLength() / 2);
|
|
98
|
+
node.style.left = (point.x - 20) + "px"; node.style.top = (point.y - 20) + "px"; }
|
|
99
|
+
}, 300));</script>`;
|
|
100
|
+
expectFail(run(page("blocked-loop", { model: m, inject })), "no loop-back path passes through a visible node");
|
|
101
|
+
});
|
|
102
|
+
test("a gateway without its diamond fails the gateway check", { skip }, () => {
|
|
103
|
+
const m = structuredClone(base);
|
|
104
|
+
const gate = m.nodes.find(n => n.id === "H_DB1");
|
|
105
|
+
gate.kind = "gateway";
|
|
106
|
+
const inject = `<script>window.addEventListener("load", () => setTimeout(() => { document.querySelector(".node.gateway polygon")?.remove(); }, 300));</script>`;
|
|
107
|
+
expectFail(run(page("no-gateway-polygon", { model: m, inject })), "gateway.*polygon");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a hidden playback token fails the movement check", { skip }, () => {
|
|
111
|
+
expectFail(run(page("hidden-token", { model: order, inject: `<style>#token { display:none !important; }</style>` })), "Play moves the token");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("an update that does nothing fails the live update check", { skip }, () => {
|
|
115
|
+
const inject = `<script>window.addEventListener("load", () => setTimeout(() => { window.FlowStudio.update = () => ({ ok:true, issues:[] }); }, 300));</script>`;
|
|
116
|
+
expectFail(run(page("noop-update", { model: order, inject })), "update marks changed and added nodes");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a first scenario with a single step still passes every check", { skip }, () => {
|
|
120
|
+
const m = structuredClone(order);
|
|
121
|
+
m.scenarios[0].steps = m.scenarios[0].steps.slice(0, 1); // one step is legal (minItems 1)
|
|
122
|
+
const r = run(page("one-step", { model: m }));
|
|
123
|
+
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("an SLA breach is found without a scenario named escalated", { skip }, () => {
|
|
127
|
+
const m = structuredClone(order);
|
|
128
|
+
m.scenarios.find(s => s.id === "escalated").id = "surged";
|
|
129
|
+
const r = run(page("renamed-escalated", { model: m }));
|
|
130
|
+
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("hidden branch buttons fail the branch-button checks", { skip }, () => {
|
|
134
|
+
expectFail(run(page("hidden-choices", { model: order, inject: `<style>#pbChoices { display:none !important; }</style>` })), "keeps the branch buttons");
|
|
135
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { keepDefaultNotes } from "./demo-pack.mjs";
|
|
9
|
+
|
|
10
|
+
const root = resolve(fileURLToPath(import.meta.url), "../../..");
|
|
11
|
+
const run = (script, ...args) =>
|
|
12
|
+
spawnSync(process.execPath, ["--experimental-strip-types", "--no-warnings", join(root, "scripts", script), ...args], { encoding: "utf8" });
|
|
13
|
+
|
|
14
|
+
const good = {
|
|
15
|
+
version: 1,
|
|
16
|
+
meta: { title: "T", help: "h", asOf: "2026-01-01", source: "s" },
|
|
17
|
+
layout: "columns-lanes",
|
|
18
|
+
columns: [{ id: "c0", title: "A", width: 150 }, { id: "c1", title: "B", width: 150 }],
|
|
19
|
+
lanes: [{ id: "l1", title: "Lane" }],
|
|
20
|
+
states: { ok: { word: "OK", tone: "ok", border: "solid", edge: "flow", motion: "flow", bucket: "good" } },
|
|
21
|
+
nodes: [
|
|
22
|
+
{ id: "a", kind: "card", lane: "l1", col: 0, row: 0, state: "ok", title: "A" },
|
|
23
|
+
{ id: "b", kind: "card", lane: "l1", col: 1, row: 0, state: "ok", title: "B" },
|
|
24
|
+
],
|
|
25
|
+
links: [{ id: "a-b", from: "a", to: "b", state: "ok" }],
|
|
26
|
+
};
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), "flow-studio-"));
|
|
28
|
+
const write = (name, value) => { const p = join(dir, name); writeFileSync(p, typeof value === "string" ? value : JSON.stringify(value)); return p; };
|
|
29
|
+
|
|
30
|
+
test("validate: a good model exits 0 and prints OK", () => {
|
|
31
|
+
const r = run("validate.mjs", write("good.json", good));
|
|
32
|
+
assert.equal(r.status, 0, r.stderr);
|
|
33
|
+
assert.match(r.stdout, /OK/);
|
|
34
|
+
});
|
|
35
|
+
test("validate: a missing file exits 2", () => assert.equal(run("validate.mjs", join(dir, "nope.json")).status, 2));
|
|
36
|
+
test("validate: invalid JSON exits 2", () => assert.equal(run("validate.mjs", write("bad.json", "{not json")).status, 2));
|
|
37
|
+
test("validate: a link to a missing node exits 1 and names the link", () => {
|
|
38
|
+
const m = structuredClone(good);
|
|
39
|
+
m.links[0].to = "zzz";
|
|
40
|
+
const r = run("validate.mjs", write("broken.json", m));
|
|
41
|
+
assert.equal(r.status, 1);
|
|
42
|
+
assert.match(r.stdout, /links\[a-b\]/);
|
|
43
|
+
});
|
|
44
|
+
test("build: a note file cannot override the numbering and must be an array", () => {
|
|
45
|
+
const out = join(dir, "out-notes.html");
|
|
46
|
+
const p = write("notes-model.json", good);
|
|
47
|
+
const notes = write("notes.json", [{ id: 99, anchor: "#viewport", title: "One", text: "t" }, { anchor: "#tools", title: "Two", text: "t" }]);
|
|
48
|
+
// the build needs the generated engine; this test only needs it to exist
|
|
49
|
+
const r = run("build.mjs", p, out, "--notes", notes);
|
|
50
|
+
assert.equal(r.status, 0, r.stderr);
|
|
51
|
+
const html = readFileSync(out, "utf8");
|
|
52
|
+
const m = html.match(/id="flow-notes">(.*?)<\/script>/s);
|
|
53
|
+
assert.ok(m, "the embedded notes script tag is present");
|
|
54
|
+
const parsed = JSON.parse(m[1]);
|
|
55
|
+
assert.deepEqual(parsed.map(n => n.id), [1, 2]);
|
|
56
|
+
const bad = write("notes-bad.json", { not: "an array" });
|
|
57
|
+
assert.equal(run("build.mjs", p, join(dir, "out-bad.html"), "--notes", bad).status, 2);
|
|
58
|
+
});
|
|
59
|
+
test("build: refuses a model with errors and writes nothing", () => {
|
|
60
|
+
const m = structuredClone(good);
|
|
61
|
+
m.nodes[0].lane = "nope";
|
|
62
|
+
const out = join(dir, "out-refused.html");
|
|
63
|
+
const r = run("build.mjs", write("refused.json", m), out);
|
|
64
|
+
assert.equal(r.status, 1);
|
|
65
|
+
assert.equal(existsSync(out), false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("build --demo-pack: order and numbering (default first, model second, extra third)", () => {
|
|
69
|
+
const m = structuredClone(good);
|
|
70
|
+
m.notes = [{ anchor: "#viewport", title: "Model Note", text: "model" }];
|
|
71
|
+
const p = write("demo-pack-model.json", m);
|
|
72
|
+
const extra = write("demo-pack-extra.json", [{ anchor: "#kpis", title: "Extra Note", text: "extra" }]);
|
|
73
|
+
const out = join(dir, "out-demo-pack-order.html");
|
|
74
|
+
const r = run("build.mjs", p, out, "--demo-pack", "--notes", extra);
|
|
75
|
+
assert.equal(r.status, 0, r.stderr);
|
|
76
|
+
const html = readFileSync(out, "utf8");
|
|
77
|
+
const match = html.match(/id="flow-notes">(.*?)<\/script>/s);
|
|
78
|
+
assert.ok(match, "the embedded notes script tag is present");
|
|
79
|
+
const parsed = JSON.parse(match[1]);
|
|
80
|
+
|
|
81
|
+
// Numbering 1..n
|
|
82
|
+
assert.deepEqual(parsed.map(n => n.id), Array.from({ length: parsed.length }, (_, i) => i + 1));
|
|
83
|
+
|
|
84
|
+
// Default notes come first (e.g. Page frame)
|
|
85
|
+
assert.equal(parsed[0].title, "Page frame");
|
|
86
|
+
|
|
87
|
+
// Model notes come second
|
|
88
|
+
const modelIdx = parsed.findIndex(n => n.title === "Model Note");
|
|
89
|
+
assert.ok(modelIdx > 0, "model note should be present");
|
|
90
|
+
|
|
91
|
+
// Extra notes come third
|
|
92
|
+
const extraIdx = parsed.findIndex(n => n.title === "Extra Note");
|
|
93
|
+
assert.equal(extraIdx, parsed.length - 1);
|
|
94
|
+
assert.ok(modelIdx < extraIdx, "model notes come before extra notes");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("build --demo-pack: drops #playbar without scenarios and #modeSwitch with < 2 views", () => {
|
|
98
|
+
const m = structuredClone(good); // no scenarios, no views
|
|
99
|
+
const p = write("demo-pack-drop.json", m);
|
|
100
|
+
const out = join(dir, "out-demo-pack-drop.html");
|
|
101
|
+
const r = run("build.mjs", p, out, "--demo-pack");
|
|
102
|
+
assert.equal(r.status, 0, r.stderr);
|
|
103
|
+
const html = readFileSync(out, "utf8");
|
|
104
|
+
const match = html.match(/id="flow-notes">(.*?)<\/script>/s);
|
|
105
|
+
assert.ok(match, "the embedded notes script tag is present");
|
|
106
|
+
const parsed = JSON.parse(match[1]);
|
|
107
|
+
assert.equal(parsed.some(n => n.anchor === "#playbar"), false, "should drop playbar note");
|
|
108
|
+
assert.equal(parsed.some(n => n.anchor === "#modeSwitch"), false, "should drop modeSwitch note");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("build --demo-pack: keeps #playbar with scenarios and #modeSwitch with 2+ views", () => {
|
|
112
|
+
const m = structuredClone(good);
|
|
113
|
+
m.views = [
|
|
114
|
+
{ id: "v1", title: "View 1" },
|
|
115
|
+
{ id: "v2", title: "View 2" },
|
|
116
|
+
];
|
|
117
|
+
m.scenarios = [
|
|
118
|
+
{ id: "s1", label: "Scenario 1", steps: [{ link: "a-b" }] },
|
|
119
|
+
];
|
|
120
|
+
const p = write("demo-pack-keep.json", m);
|
|
121
|
+
const out = join(dir, "out-demo-pack-keep.html");
|
|
122
|
+
const r = run("build.mjs", p, out, "--demo-pack");
|
|
123
|
+
assert.equal(r.status, 0, r.stderr);
|
|
124
|
+
const html = readFileSync(out, "utf8");
|
|
125
|
+
const match = html.match(/id="flow-notes">(.*?)<\/script>/s);
|
|
126
|
+
assert.ok(match, "the embedded notes script tag is present");
|
|
127
|
+
const parsed = JSON.parse(match[1]);
|
|
128
|
+
assert.equal(parsed.some(n => n.anchor === "#playbar"), true, "should keep playbar note");
|
|
129
|
+
assert.equal(parsed.some(n => n.anchor === "#modeSwitch"), true, "should keep modeSwitch note");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("build: hostile anchors in model notes are preserved safely", () => {
|
|
133
|
+
const m = structuredClone(good);
|
|
134
|
+
m.notes = [
|
|
135
|
+
{ anchor: "<script>alert('xss')</script>", title: "Hostile Tag", text: "t" },
|
|
136
|
+
{ anchor: "[invalid:css(selector", title: "Invalid Selector", text: "t" },
|
|
137
|
+
{ anchor: "\"></script><script>alert(1)</script>", title: "Breakout", text: "t" },
|
|
138
|
+
];
|
|
139
|
+
const p = write("hostile-notes.json", m);
|
|
140
|
+
const out = join(dir, "out-hostile-notes.html");
|
|
141
|
+
const r = run("build.mjs", p, out);
|
|
142
|
+
assert.equal(r.status, 0, r.stderr);
|
|
143
|
+
const html = readFileSync(out, "utf8");
|
|
144
|
+
const match = html.match(/id="flow-notes">(.*?)<\/script>/s);
|
|
145
|
+
assert.ok(match, "notes script tag should be present and intact");
|
|
146
|
+
const parsed = JSON.parse(match[1]);
|
|
147
|
+
assert.equal(parsed.length, 3);
|
|
148
|
+
assert.equal(parsed[0].anchor, "<script>alert('xss')</script>");
|
|
149
|
+
assert.equal(parsed[1].anchor, "[invalid:css(selector");
|
|
150
|
+
assert.equal(parsed[2].anchor, "\"></script><script>alert(1)</script>");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("build --demo-pack: examples/lineage.json gets kept defaults first, then its own model notes", () => {
|
|
154
|
+
// Expectations come from the example and the shared filter, so the test does not break when
|
|
155
|
+
// the example changes. The filter's own logic is pinned by demo-pack.test.mjs.
|
|
156
|
+
const lineage = JSON.parse(readFileSync(join(root, "examples", "lineage.json"), "utf8"));
|
|
157
|
+
const defaults = JSON.parse(readFileSync(join(root, "scripts", "default-notes.json"), "utf8"));
|
|
158
|
+
const shell = readFileSync(join(root, "templates", "engine.html"), "utf8");
|
|
159
|
+
const kept = keepDefaultNotes(defaults, lineage, shell);
|
|
160
|
+
const expected = kept.length + (lineage.notes?.length ?? 0);
|
|
161
|
+
const outPack = join(dir, "out-lineage-pack.html"); // temp folder: the build must not leave repo artifacts
|
|
162
|
+
const r = run("build.mjs", join(root, "examples", "lineage.json"), outPack, "--demo-pack");
|
|
163
|
+
assert.equal(r.status, 0, r.stderr);
|
|
164
|
+
assert.equal(existsSync(outPack), true);
|
|
165
|
+
const html = readFileSync(outPack, "utf8");
|
|
166
|
+
const match = html.match(/id="flow-notes">(.*?)<\/script>/s);
|
|
167
|
+
assert.ok(match, "the embedded notes script tag is present");
|
|
168
|
+
const parsed = JSON.parse(match[1]);
|
|
169
|
+
assert.equal(parsed.length, expected, `expected ${expected} notes, got ${parsed.length}`);
|
|
170
|
+
for (let i = 0; i < kept.length; i++) {
|
|
171
|
+
assert.equal(parsed[i].anchor, kept[i].anchor, `note ${i + 1} should be the kept default ${kept[i].anchor}`);
|
|
172
|
+
}
|
|
173
|
+
if (lineage.notes?.length) {
|
|
174
|
+
assert.equal(parsed[kept.length].anchor, lineage.notes[0].anchor, "the first model note follows the kept defaults");
|
|
175
|
+
assert.equal(parsed[kept.length].title, lineage.notes[0].title, "the first model note follows the kept defaults");
|
|
176
|
+
}
|
|
177
|
+
assert.deepEqual(parsed.map(n => n.id), Array.from({ length: expected }, (_, i) => i + 1));
|
|
178
|
+
});
|
|
179
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Which default demo-pack notes survive for one model and one built shell. Shared by build.mjs
|
|
2
|
+
// and its tests. Only default notes are filtered here: notes from the model and from --notes are
|
|
3
|
+
// appended unfiltered (at runtime a note whose selector matches nothing simply gets no marker).
|
|
4
|
+
|
|
5
|
+
// An exact attribute match, not a pattern: an id with regex metacharacters must never match a
|
|
6
|
+
// different id, and a data-id="..." substring must not count as an id.
|
|
7
|
+
const hasId = (shell, id) => shell.includes(` id="${id}"`) || shell.includes(` id='${id}'`);
|
|
8
|
+
|
|
9
|
+
export function keepDefaultNotes(defaults, model, shell) {
|
|
10
|
+
return defaults.filter(n => {
|
|
11
|
+
if (n.anchor === "#playbar" && (!Array.isArray(model.scenarios) || model.scenarios.length === 0)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (n.anchor === "#modeSwitch" && (!Array.isArray(model.views) || model.views.length < 2)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (typeof n.anchor === "string" && n.anchor.startsWith("#")) {
|
|
18
|
+
const id = n.anchor.slice(1);
|
|
19
|
+
if (!hasId(shell, id)) return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { keepDefaultNotes } from "./demo-pack.mjs";
|
|
4
|
+
|
|
5
|
+
// The shell here is a fixture, not the real engine.html: the filter only reads id attributes.
|
|
6
|
+
const shell = '<div id="q"></div><span data-id="title"></span><i id="qxy"></i><section id="playbar"></section><nav id="modeSwitch"></nav>';
|
|
7
|
+
const note = anchor => ({ anchor, title: anchor, text: "t" });
|
|
8
|
+
|
|
9
|
+
test("demo-pack: keeps a default note whose id exists in the shell", () => {
|
|
10
|
+
assert.equal(keepDefaultNotes([note("#q")], {}, shell).length, 1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("demo-pack: drops a default note whose id is missing; a data-id substring is not an id", () => {
|
|
14
|
+
assert.deepEqual(keepDefaultNotes([note("#title")], {}, shell), []);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("demo-pack: does not treat regex metacharacters in an id as a pattern", () => {
|
|
18
|
+
// "qx+y" read as a regular expression would match the shell's id="qxy"; read as an id it must not.
|
|
19
|
+
assert.deepEqual(keepDefaultNotes([note("#qx+y")], {}, shell), []);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("demo-pack: drops #playbar without scenarios and keeps it with at least one", () => {
|
|
23
|
+
assert.deepEqual(keepDefaultNotes([note("#playbar")], {}, shell), []);
|
|
24
|
+
assert.equal(keepDefaultNotes([note("#playbar")], { scenarios: [{ id: "s1", steps: [{ link: "l1" }] }] }, shell).length, 1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("demo-pack: drops #modeSwitch with fewer than two views and keeps it with two", () => {
|
|
28
|
+
assert.deepEqual(keepDefaultNotes([note("#modeSwitch")], { views: [{ id: "v1", label: "V" }] }, shell), []);
|
|
29
|
+
assert.equal(keepDefaultNotes([note("#modeSwitch")], { views: [{ id: "v1", label: "V" }, { id: "v2", label: "W" }] }, shell).length, 1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("demo-pack: leaves anchors that are not ids (class and attribute selectors) unfiltered", () => {
|
|
33
|
+
assert.equal(keepDefaultNotes([note(".anything"), note("[data-x='y']")], {}, shell).length, 2);
|
|
34
|
+
});
|