@1agh/maude 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +34 -1
  2. package/cli/bin/maude.mjs +3 -0
  3. package/cli/commands/cache.mjs +181 -0
  4. package/cli/commands/cache.test.mjs +166 -0
  5. package/cli/commands/design-link.test.mjs +32 -2
  6. package/cli/commands/design.mjs +95 -4
  7. package/cli/commands/design.test.mjs +56 -0
  8. package/cli/commands/help.mjs +34 -0
  9. package/cli/commands/hub.mjs +255 -30
  10. package/cli/commands/hub.test.mjs +126 -2
  11. package/cli/commands/init.mjs +3 -0
  12. package/cli/commands/preflight.mjs +11 -0
  13. package/cli/commands/preflight.test.mjs +46 -0
  14. package/cli/commands/scenario-report.mjs +45 -0
  15. package/cli/lib/cache.mjs +407 -0
  16. package/cli/lib/cache.test.mjs +303 -0
  17. package/cli/lib/design-link.mjs +74 -10
  18. package/cli/lib/flow-design-integration.test.mjs +165 -0
  19. package/cli/lib/gitignore-block.mjs +90 -0
  20. package/cli/lib/gitignore-block.test.mjs +123 -0
  21. package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
  22. package/cli/lib/preflight.mjs +41 -10
  23. package/package.json +8 -8
  24. package/plugins/design/dependencies.json +30 -2
  25. package/plugins/design/dev-server/ai-banner.tsx +2 -2
  26. package/plugins/design/dev-server/annotations-context-toolbar.tsx +763 -123
  27. package/plugins/design/dev-server/annotations-layer.tsx +1624 -208
  28. package/plugins/design/dev-server/api.ts +123 -4
  29. package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
  30. package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
  31. package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
  32. package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
  33. package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
  34. package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
  35. package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
  36. package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
  37. package/plugins/design/dev-server/bin/preflight.sh +21 -10
  38. package/plugins/design/dev-server/bin/prep.sh +211 -0
  39. package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
  40. package/plugins/design/dev-server/bin/screenshot.sh +18 -1
  41. package/plugins/design/dev-server/bin/smoke.sh +208 -23
  42. package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
  43. package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
  44. package/plugins/design/dev-server/canvas-cursors.ts +173 -0
  45. package/plugins/design/dev-server/canvas-icons.tsx +299 -0
  46. package/plugins/design/dev-server/canvas-lib.tsx +25 -21
  47. package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
  48. package/plugins/design/dev-server/canvas-shell.tsx +404 -66
  49. package/plugins/design/dev-server/client/app.jsx +1231 -416
  50. package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
  51. package/plugins/design/dev-server/collab/index.ts +87 -9
  52. package/plugins/design/dev-server/collab/persistence.ts +34 -3
  53. package/plugins/design/dev-server/collab/registry.ts +80 -2
  54. package/plugins/design/dev-server/collab/room.ts +21 -8
  55. package/plugins/design/dev-server/context-menu.tsx +167 -23
  56. package/plugins/design/dev-server/context.ts +24 -0
  57. package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
  58. package/plugins/design/dev-server/dist/client.bundle.js +364 -29
  59. package/plugins/design/dev-server/dist/comment-mount.js +78 -33
  60. package/plugins/design/dev-server/dist/styles.css +16 -0
  61. package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
  62. package/plugins/design/dev-server/export-dialog.tsx +208 -18
  63. package/plugins/design/dev-server/exporters/html.ts +4 -1
  64. package/plugins/design/dev-server/exporters/index.ts +4 -1
  65. package/plugins/design/dev-server/exporters/pdf.ts +7 -1
  66. package/plugins/design/dev-server/exporters/png.ts +21 -2
  67. package/plugins/design/dev-server/exporters/pptx.ts +330 -201
  68. package/plugins/design/dev-server/exporters/scope.ts +107 -11
  69. package/plugins/design/dev-server/exporters/svg.ts +4 -1
  70. package/plugins/design/dev-server/fs-watch.ts +1 -0
  71. package/plugins/design/dev-server/http.ts +299 -28
  72. package/plugins/design/dev-server/input-router.tsx +35 -5
  73. package/plugins/design/dev-server/participants-chrome.tsx +10 -10
  74. package/plugins/design/dev-server/server.ts +123 -1
  75. package/plugins/design/dev-server/sync/agent.ts +95 -0
  76. package/plugins/design/dev-server/sync/codec.ts +155 -0
  77. package/plugins/design/dev-server/sync/connection-state.ts +203 -0
  78. package/plugins/design/dev-server/sync/index.ts +479 -35
  79. package/plugins/design/dev-server/sync/materialize.ts +62 -0
  80. package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
  81. package/plugins/design/dev-server/sync/origins.ts +57 -0
  82. package/plugins/design/dev-server/sync/projection.ts +368 -0
  83. package/plugins/design/dev-server/sync/status.ts +115 -0
  84. package/plugins/design/dev-server/sync/untrusted.ts +153 -0
  85. package/plugins/design/dev-server/test/_helpers.ts +6 -2
  86. package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
  87. package/plugins/design/dev-server/test/annotations-layer.test.ts +311 -0
  88. package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +519 -0
  89. package/plugins/design/dev-server/test/canvas-cursors.test.ts +162 -0
  90. package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
  91. package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
  92. package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
  93. package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
  94. package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
  95. package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
  96. package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
  97. package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
  98. package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
  99. package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
  100. package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
  101. package/plugins/design/dev-server/test/input-router.test.ts +32 -4
  102. package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +132 -0
  103. package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
  104. package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
  105. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
  106. package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
  107. package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
  108. package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
  109. package/plugins/design/dev-server/test/sync-runtime.test.ts +531 -4
  110. package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
  111. package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
  112. package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
  113. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +39 -13
  114. package/plugins/design/dev-server/tool-palette.tsx +155 -24
  115. package/plugins/design/dev-server/undo-hud.tsx +4 -4
  116. package/plugins/design/dev-server/undo-stack.ts +20 -4
  117. package/plugins/design/dev-server/use-annotation-resize.tsx +219 -61
  118. package/plugins/design/dev-server/use-tool-mode.tsx +68 -19
  119. package/plugins/design/dev-server/ws.ts +40 -1
  120. package/plugins/design/templates/_shell.html +85 -10
  121. package/plugins/design/templates/canvas.tsx.template +13 -0
  122. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +14 -7
  123. package/plugins/flow/.claude-plugin/config.schema.json +5 -0
  124. package/plugins/flow/templates/ai-skeleton/README.md +1 -1
  125. package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
  126. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
  127. package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env bash
2
+ # prep.sh — one-shot pre-flight context for the design slash commands.
3
+ #
4
+ # Collapses the 4–8 sequential bash invocations that /design:new, /design:edit
5
+ # and /design:setup-ds each ran in their step-0/step-1 (bootstrap-check + config
6
+ # jq reads + _active.json read + _preflight read + _server.json PID probe + slug
7
+ # compute) into a SINGLE call that emits one structured blob. Same shape family
8
+ # as bootstrap-check.sh / server-up.sh.
9
+ #
10
+ # Reads (all optional — missing files degrade gracefully, never error):
11
+ # .design/config.json → NAME, DESIGN_ROOT, ROOT_CLASS, THEME, TOKENS_REL,
12
+ # NEW_CANVAS_DIR, NEW_COMPONENT_DIR, TEAM_ACCENT,
13
+ # DEFAULT_DS, KNOWN_DS, ACCENT_STRATEGY, COLOR_SPACE
14
+ # <designRoot>/_active.json → ACTIVE_CANVAS, SELECTED_ELEMENT, SELECTED_FILE,
15
+ # SEL_VALID, OPEN_TABS, ACTIVE_SLUG
16
+ # <designRoot>/_preflight.json → DEPS_OK, DEPS_MISSING (Phase A cache)
17
+ # <designRoot>/_server.json → SERVER_UP, SERVER_PORT (PID liveness + /_health)
18
+ #
19
+ # Usage:
20
+ # prep.sh # JSON on stdout (default)
21
+ # prep.sh --json # JSON on stdout
22
+ # prep.sh --shell-export # `export FOO=bar` lines, eval-friendly
23
+ # prep.sh --shape <new|edit|setup-ds> # emit only the subset that command needs
24
+ # prep.sh --root <repo> # override repo root (else git root → $CLAUDE_PROJECT_DIR → pwd)
25
+ #
26
+ # Resolution (DDR-045): resolve the repo root from args/env/git — never from
27
+ # `dirname $0`, which maps to /$bunfs/root inside a bun --compile binary.
28
+ #
29
+ # Exit: 0 always (this is a context gatherer; callers branch on the fields).
30
+
31
+ MODE="json"
32
+ SHAPE="all"
33
+ REPO=""
34
+ while [ $# -gt 0 ]; do
35
+ case "$1" in
36
+ --json) MODE="json"; shift ;;
37
+ --shell-export) MODE="shell"; shift ;;
38
+ --shape) SHAPE="$2"; shift 2 ;;
39
+ --root) REPO="$2"; shift 2 ;;
40
+ --help|-h)
41
+ sed -n '2,33p' "$0" | sed 's/^# \?//'
42
+ exit 0
43
+ ;;
44
+ *)
45
+ echo "prep.sh: unknown arg '$1' (try --help)" >&2
46
+ exit 2
47
+ ;;
48
+ esac
49
+ done
50
+
51
+ case "$SHAPE" in
52
+ all|new|edit|setup-ds) ;;
53
+ *) echo "prep.sh: --shape must be one of new|edit|setup-ds (got '$SHAPE')" >&2; exit 2 ;;
54
+ esac
55
+
56
+ # ---- repo root ----------------------------------------------------------------
57
+ if [ -z "$REPO" ]; then
58
+ REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
59
+ fi
60
+
61
+ HAVE_JQ=0
62
+ command -v jq >/dev/null 2>&1 && HAVE_JQ=1
63
+
64
+ # ---- config -------------------------------------------------------------------
65
+ CFG="$REPO/.design/config.json"
66
+ CONFIG_PRESENT="false"; [ -f "$CFG" ] && CONFIG_PRESENT="true"
67
+
68
+ NAME="Project"
69
+ DESIGN_ROOT=".design"
70
+ ROOT_CLASS="app"
71
+ THEME="dark"
72
+ TOKENS_REL="system/colors_and_type.css"
73
+ NEW_CANVAS_DIR="ui/project"
74
+ NEW_COMPONENT_DIR="ui/project/components"
75
+ TEAM_ACCENT=""
76
+ DEFAULT_DS="project"
77
+ KNOWN_DS=""
78
+ ACCENT_STRATEGY="single"
79
+ COLOR_SPACE="oklch"
80
+
81
+ if [ "$CONFIG_PRESENT" = "true" ] && [ "$HAVE_JQ" = "1" ]; then
82
+ NAME=$(jq -r '.name // "Project"' "$CFG")
83
+ DESIGN_ROOT=$(jq -r '.designRoot // ".design"' "$CFG")
84
+ ROOT_CLASS=$(jq -r '.rootClass // "app"' "$CFG")
85
+ THEME=$(jq -r '.themeDefault // "dark"' "$CFG")
86
+ TOKENS_REL=$(jq -r '.tokensCssRel // "system/colors_and_type.css"' "$CFG")
87
+ NEW_CANVAS_DIR=$(jq -r '.newCanvasDir // "ui/project"' "$CFG")
88
+ NEW_COMPONENT_DIR=$(jq -r '.newComponentDir // "ui/project/components"' "$CFG")
89
+ TEAM_ACCENT=$(jq -r '.teamAccentDefault // empty' "$CFG")
90
+ DEFAULT_DS=$(jq -r '.defaultDesignSystem // "project"' "$CFG")
91
+ KNOWN_DS=$(jq -r '.designSystems[]?.name // empty' "$CFG" 2>/dev/null | tr '\n' ' ' | sed 's/ $//')
92
+ ACCENT_STRATEGY=$(jq -r '.accentStrategy // "single"' "$CFG")
93
+ COLOR_SPACE=$(jq -r '.colorSpace // "oklch"' "$CFG")
94
+ fi
95
+
96
+ DR_ABS="$REPO/$DESIGN_ROOT"
97
+
98
+ # ---- active canvas / selection ------------------------------------------------
99
+ ACTIVE_FILE="$DR_ABS/_active.json"
100
+ ACTIVE_CANVAS=""
101
+ SELECTED_ELEMENT=""
102
+ SELECTED_FILE=""
103
+ SEL_VALID="0"
104
+ OPEN_TABS="0"
105
+ ACTIVE_SLUG=""
106
+ if [ -f "$ACTIVE_FILE" ] && [ "$HAVE_JQ" = "1" ]; then
107
+ ACTIVE_CANVAS=$(jq -r '.active // empty' "$ACTIVE_FILE" 2>/dev/null)
108
+ [ "$ACTIVE_CANVAS" = "null" ] && ACTIVE_CANVAS=""
109
+ SELECTED_ELEMENT=$(jq -rc '.selected // empty' "$ACTIVE_FILE" 2>/dev/null)
110
+ SELECTED_FILE=$(jq -r '.selected.file // empty' "$ACTIVE_FILE" 2>/dev/null)
111
+ OPEN_TABS=$(jq -r '(.open_tabs // []) | length' "$ACTIVE_FILE" 2>/dev/null || echo 0)
112
+ if [ -n "$SELECTED_ELEMENT" ] && [ -n "$ACTIVE_CANVAS" ] && [ "$SELECTED_FILE" = "$ACTIVE_CANVAS" ]; then
113
+ SEL_VALID="1"
114
+ fi
115
+ if [ -n "$ACTIVE_CANVAS" ]; then
116
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
117
+ ACTIVE_SLUG=$(bash "$SCRIPT_DIR/slug.sh" "${ACTIVE_CANVAS#"$DESIGN_ROOT"/}" 2>/dev/null)
118
+ fi
119
+ fi
120
+
121
+ # ---- deps preflight cache -----------------------------------------------------
122
+ PREFLIGHT_FILE="$DR_ABS/_preflight.json"
123
+ DEPS_OK="unknown"
124
+ DEPS_MISSING=""
125
+ if [ -f "$PREFLIGHT_FILE" ] && [ "$HAVE_JQ" = "1" ]; then
126
+ DEPS_OK=$(jq -r 'if .ok == true then "true" elif .ok == false then "false" else "unknown" end' "$PREFLIGHT_FILE" 2>/dev/null || echo unknown)
127
+ DEPS_MISSING=$(jq -r '(.missing // []) | join(",")' "$PREFLIGHT_FILE" 2>/dev/null || echo "")
128
+ fi
129
+
130
+ # ---- server liveness ----------------------------------------------------------
131
+ SERVER_FILE="$DR_ABS/_server.json"
132
+ SERVER_UP="false"
133
+ SERVER_PORT=""
134
+ if [ -f "$SERVER_FILE" ] && [ "$HAVE_JQ" = "1" ]; then
135
+ SERVER_PORT=$(jq -r '.port // empty' "$SERVER_FILE" 2>/dev/null)
136
+ SERVER_PID=$(jq -r '.pid // empty' "$SERVER_FILE" 2>/dev/null)
137
+ if [ -n "$SERVER_PID" ] && [ -n "$SERVER_PORT" ] \
138
+ && kill -0 "$SERVER_PID" 2>/dev/null \
139
+ && curl -fs "http://localhost:$SERVER_PORT/_health" >/dev/null 2>&1; then
140
+ SERVER_UP="true"
141
+ fi
142
+ fi
143
+
144
+ # ---- emit ---------------------------------------------------------------------
145
+ # Shape membership: `new`/`setup-ds` omit the active-canvas/selection block
146
+ # (those commands don't act on an open canvas); `edit`/`all` emit everything.
147
+ if [ "$MODE" = "shell" ]; then
148
+ printf 'export REPO_ROOT=%q\n' "$REPO"
149
+ printf 'export CONFIG_PRESENT=%s\n' "$CONFIG_PRESENT"
150
+ printf 'export NAME=%q\n' "$NAME"
151
+ printf 'export DESIGN_ROOT=%q\n' "$DESIGN_ROOT"
152
+ printf 'export ROOT_CLASS=%q\n' "$ROOT_CLASS"
153
+ printf 'export THEME=%q\n' "$THEME"
154
+ printf 'export TOKENS_REL=%q\n' "$TOKENS_REL"
155
+ printf 'export NEW_CANVAS_DIR=%q\n' "$NEW_CANVAS_DIR"
156
+ printf 'export NEW_COMPONENT_DIR=%q\n' "$NEW_COMPONENT_DIR"
157
+ printf 'export TEAM_ACCENT=%q\n' "$TEAM_ACCENT"
158
+ printf 'export DEFAULT_DS=%q\n' "$DEFAULT_DS"
159
+ printf 'export KNOWN_DS=%q\n' "$KNOWN_DS"
160
+ printf 'export ACCENT_STRATEGY=%q\n' "$ACCENT_STRATEGY"
161
+ printf 'export COLOR_SPACE=%q\n' "$COLOR_SPACE"
162
+ printf 'export DEPS_OK=%s\n' "$DEPS_OK"
163
+ printf 'export DEPS_MISSING=%q\n' "$DEPS_MISSING"
164
+ printf 'export SERVER_UP=%s\n' "$SERVER_UP"
165
+ printf 'export SERVER_PORT=%q\n' "$SERVER_PORT"
166
+ if [ "$SHAPE" = "edit" ] || [ "$SHAPE" = "all" ]; then
167
+ printf 'export ACTIVE_CANVAS=%q\n' "$ACTIVE_CANVAS"
168
+ printf 'export SELECTED_FILE=%q\n' "$SELECTED_FILE"
169
+ printf 'export SEL_VALID=%s\n' "$SEL_VALID"
170
+ printf 'export OPEN_TABS=%s\n' "$OPEN_TABS"
171
+ printf 'export ACTIVE_SLUG=%q\n' "$ACTIVE_SLUG"
172
+ fi
173
+ exit 0
174
+ fi
175
+
176
+ # JSON mode — emit via jq when available for correct escaping; fall back to printf.
177
+ if [ "$HAVE_JQ" = "1" ]; then
178
+ jq -n \
179
+ --arg repo "$REPO" --arg cfg "$CONFIG_PRESENT" --arg name "$NAME" \
180
+ --arg dr "$DESIGN_ROOT" --arg rc "$ROOT_CLASS" --arg th "$THEME" \
181
+ --arg tok "$TOKENS_REL" --arg ncd "$NEW_CANVAS_DIR" --arg ncm "$NEW_COMPONENT_DIR" \
182
+ --arg acc "$TEAM_ACCENT" --arg dds "$DEFAULT_DS" --arg kds "$KNOWN_DS" \
183
+ --arg astrat "$ACCENT_STRATEGY" --arg cspace "$COLOR_SPACE" \
184
+ --arg depsok "$DEPS_OK" --arg depsmiss "$DEPS_MISSING" \
185
+ --arg sup "$SERVER_UP" --arg sport "$SERVER_PORT" \
186
+ --arg act "$ACTIVE_CANVAS" --arg selfile "$SELECTED_FILE" \
187
+ --arg selv "$SEL_VALID" --arg tabs "$OPEN_TABS" --arg slug "$ACTIVE_SLUG" \
188
+ --argjson sel "${SELECTED_ELEMENT:-null}" \
189
+ '{
190
+ repo_root: $repo,
191
+ config_present: ($cfg == "true"),
192
+ config: {
193
+ name: $name, design_root: $dr, root_class: $rc, theme: $th,
194
+ tokens_rel: $tok, new_canvas_dir: $ncd, new_component_dir: $ncm,
195
+ team_accent: $acc, default_ds: $dds,
196
+ known_ds: ($kds | if . == "" then [] else split(" ") end),
197
+ accent_strategy: $astrat, color_space: $cspace
198
+ },
199
+ active: {
200
+ canvas: $act, selected: $sel, selected_file: $selfile,
201
+ sel_valid: ($selv == "1"), open_tabs: ($tabs | tonumber? // 0), slug: $slug
202
+ },
203
+ deps: { ok: $depsok, missing: ($depsmiss | if . == "" then [] else split(",") end) },
204
+ server: { up: ($sup == "true"), port: ($sport | if . == "" then null else (tonumber? // .) end) }
205
+ }'
206
+ else
207
+ printf '{"repo_root":"%s","config_present":%s,"jq":false,"note":"jq unavailable — install jq for full prep.sh output"}\n' \
208
+ "$REPO" "$CONFIG_PRESENT"
209
+ fi
210
+
211
+ exit 0
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ // scenario-report.mjs — deterministic cross-platform scenario report generator.
3
+ //
4
+ // Phase C / DDR-061. Resolves the long-standing "report generator" TODO in
5
+ // plugins/flow/skills/scenario/SKILL.md. Walks a scenario run directory and
6
+ // emits the mechanical sections of report.md (TL;DR table, counter-delta table,
7
+ // per-step pivot, collapsed path-listing). The LLM authors ONLY the prose
8
+ // sections ("What surprised us", "Recommended follow-ups") — they're left as
9
+ // marked placeholders for it to fill in.
10
+ //
11
+ // Invoked from flow markdown via the on-PATH `maude` binary (DDR-062):
12
+ // maude scenario-report <run-dir> [--out <path>]
13
+ // or directly:
14
+ // node scenario-report.mjs <run-dir> [--out <path>]
15
+ //
16
+ // Input layout (produced by the scenario runners — see scenario SKILL.md):
17
+ // <run-dir>/
18
+ // ├── <platform>/result.txt # "pass" | "fail: <reason>" | "skipped: <reason>"
19
+ // ├── <platform>/step-<N>-<desc>.png # per-step screenshots
20
+ // └── <platform>/counters.json # OPTIONAL { "mastered": "+3", "remaining": "-3" }
21
+ //
22
+ // Output: <run-dir>/report.md (or --out), zero external dependencies (node:fs only).
23
+
24
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
25
+ import { basename, join } from 'node:path';
26
+
27
+ const PLATFORM_ORDER = ['web-desktop', 'web-mobile', 'ios-phone', 'ios-tablet', 'android-phone'];
28
+
29
+ const toolingFor = (platform) => (platform.startsWith('web-') ? 'agent-browser' : 'agent-device');
30
+
31
+ // Parse "pass" / "fail: reason" / "skipped: reason" → { result, reason }.
32
+ function parseResult(text) {
33
+ const t = (text || '').trim();
34
+ if (!t) return { result: 'UNKNOWN', reason: 'no result.txt' };
35
+ const lower = t.toLowerCase();
36
+ if (lower.startsWith('pass')) return { result: 'PASS', reason: '' };
37
+ if (lower.startsWith('fail')) return { result: 'FAIL', reason: t.replace(/^fail[:\s]*/i, '') };
38
+ if (lower.startsWith('skip'))
39
+ return { result: 'SKIPPED', reason: t.replace(/^skip(ped)?[:\s]*/i, '') };
40
+ return { result: 'UNKNOWN', reason: t.slice(0, 80) };
41
+ }
42
+
43
+ const SYM = { PASS: '✅', FAIL: '❌', SKIPPED: '⏭️', UNKNOWN: '❓' };
44
+
45
+ // Extract the numeric step index from "step-7-card-1-back.png" → 7.
46
+ function stepIndex(file) {
47
+ const m = /^step-(\d+)/.exec(file);
48
+ return m ? Number(m[1]) : Number.POSITIVE_INFINITY;
49
+ }
50
+
51
+ function readPlatform(runDir, platform) {
52
+ const dir = join(runDir, platform);
53
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
54
+ const resultPath = join(dir, 'result.txt');
55
+ const { result, reason } = parseResult(
56
+ existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : ''
57
+ );
58
+ const steps = readdirSync(dir)
59
+ .filter((f) => /^step-\d+.*\.png$/.test(f))
60
+ .sort((a, b) => stepIndex(a) - stepIndex(b) || a.localeCompare(b));
61
+ let counters = null;
62
+ const countersPath = join(dir, 'counters.json');
63
+ if (existsSync(countersPath)) {
64
+ try {
65
+ counters = JSON.parse(readFileSync(countersPath, 'utf8'));
66
+ } catch {
67
+ /* malformed counters — treated as absent */
68
+ }
69
+ }
70
+ return { platform, result, reason, steps, counters };
71
+ }
72
+
73
+ export function generateReport(runDir) {
74
+ if (!existsSync(runDir) || !statSync(runDir).isDirectory()) {
75
+ throw new Error(`scenario-report: run dir not found: ${runDir}`);
76
+ }
77
+ // Discover platforms: known order first, then any extra subdirs present.
78
+ const present = readdirSync(runDir).filter((n) => {
79
+ const p = join(runDir, n);
80
+ return statSync(p).isDirectory() && existsSync(join(p, 'result.txt'));
81
+ });
82
+ const ordered = [
83
+ ...PLATFORM_ORDER.filter((p) => present.includes(p)),
84
+ ...present.filter((p) => !PLATFORM_ORDER.includes(p)),
85
+ ];
86
+ const platforms = ordered.map((p) => readPlatform(runDir, p)).filter(Boolean);
87
+
88
+ const scenarioName =
89
+ basename(join(runDir, '..', '..')) === '.' ? 'scenario' : basename(join(runDir, '..'));
90
+ const out = [];
91
+ out.push(`# Scenario report — ${scenarioName}`);
92
+ out.push('');
93
+ out.push(`_generated ${new Date().toISOString()} · run: \`${runDir}\`_`);
94
+ out.push('');
95
+
96
+ // ── TL;DR ──────────────────────────────────────────────────────────────
97
+ out.push('## TL;DR');
98
+ out.push('');
99
+ out.push('| Platform | Result | Steps reached | Tooling |');
100
+ out.push('| --- | --- | --- | --- |');
101
+ for (const p of platforms) {
102
+ const detail = p.reason ? ` — ${p.reason}` : '';
103
+ out.push(
104
+ `| ${p.platform} | ${SYM[p.result]} ${p.result}${detail} | ${p.steps.length} | ${toolingFor(p.platform)} |`
105
+ );
106
+ }
107
+ out.push('');
108
+
109
+ // ── Counter-delta ──────────────────────────────────────────────────────
110
+ const counterKeys = [
111
+ ...new Set(platforms.flatMap((p) => (p.counters ? Object.keys(p.counters) : []))),
112
+ ];
113
+ out.push('## Counter delta (cross-platform parity)');
114
+ out.push('');
115
+ if (counterKeys.length === 0) {
116
+ out.push(
117
+ '_No `counters.json` recorded for any platform. Add `<platform>/counters.json` ' +
118
+ '(e.g. `{"mastered":"+3","remaining":"-3"}`) in the runners to enable the parity check._'
119
+ );
120
+ } else {
121
+ out.push(`| Platform | ${counterKeys.map((k) => `\`${k}\` Δ`).join(' | ')} |`);
122
+ out.push(`| --- | ${counterKeys.map(() => '---').join(' | ')} |`);
123
+ for (const p of platforms) {
124
+ const cells = counterKeys.map((k) =>
125
+ p.counters && p.counters[k] != null ? p.counters[k] : '—'
126
+ );
127
+ out.push(`| ${p.platform} | ${cells.join(' | ')} |`);
128
+ }
129
+ out.push('');
130
+ // Parity verdict: identical non-skipped rows = verified.
131
+ const nonSkipped = platforms.filter((p) => p.result !== 'SKIPPED' && p.counters);
132
+ const sigs = new Set(nonSkipped.map((p) => counterKeys.map((k) => p.counters[k]).join('|')));
133
+ out.push(
134
+ sigs.size <= 1
135
+ ? '**Parity: ✅ identical counter deltas across all non-skipped platforms.**'
136
+ : '**Parity: ❌ counter deltas diverge across platforms — investigate (a divergence needs a DDR or a fix).**'
137
+ );
138
+ }
139
+ out.push('');
140
+
141
+ // ── Per-step pivot ─────────────────────────────────────────────────────
142
+ const allStepIdx = [
143
+ ...new Set(platforms.flatMap((p) => p.steps.map(stepIndex)).filter(Number.isFinite)),
144
+ ].sort((a, b) => a - b);
145
+ out.push('## Per-step screenshots (pivot)');
146
+ out.push('');
147
+ if (allStepIdx.length === 0) {
148
+ out.push('_No `step-*.png` screenshots found in any platform directory._');
149
+ } else {
150
+ out.push(`| Platform | ${allStepIdx.map((n) => `Step ${n}`).join(' | ')} |`);
151
+ out.push(`| --- | ${allStepIdx.map(() => '---').join(' | ')} |`);
152
+ for (const p of platforms) {
153
+ const cells = allStepIdx.map((n) => {
154
+ const file = p.steps.find((f) => stepIndex(f) === n);
155
+ return file ? `![](${p.platform}/${file})` : '';
156
+ });
157
+ out.push(`| ${p.platform} | ${cells.join(' | ')} |`);
158
+ }
159
+ }
160
+ out.push('');
161
+
162
+ // ── LLM prose placeholders ─────────────────────────────────────────────
163
+ out.push('## What surprised us');
164
+ out.push('');
165
+ out.push(
166
+ '<!-- LLM-AUTHORED: replace this line with non-obvious findings — UX divergence, broken expectations, unexpected counter math, mid-run state changes. Delete this comment. -->'
167
+ );
168
+ out.push('');
169
+ out.push('## Recommended follow-ups');
170
+ out.push('');
171
+ out.push(
172
+ '<!-- LLM-AUTHORED: replace this line with a prioritized list of codebase changes that would make this scenario more reliable (missing testIDs, fragile selectors, parity gaps). Delete this comment. -->'
173
+ );
174
+ out.push('');
175
+
176
+ // ── Collapsed path listing ─────────────────────────────────────────────
177
+ out.push('<details>');
178
+ out.push('<summary>All screenshot paths (per platform)</summary>');
179
+ out.push('');
180
+ for (const p of platforms) {
181
+ out.push(`**${p.platform}** (${p.result})`);
182
+ for (const f of p.steps) out.push(`- \`${p.platform}/${f}\``);
183
+ out.push('');
184
+ }
185
+ out.push('</details>');
186
+ out.push('');
187
+
188
+ return out.join('\n');
189
+ }
190
+
191
+ // ── CLI entrypoint ───────────────────────────────────────────────────────
192
+ function main(argv) {
193
+ const args = argv.slice(2);
194
+ const runDir = args.find((a) => !a.startsWith('--'));
195
+ if (!runDir) {
196
+ process.stderr.write('usage: scenario-report <run-dir> [--out <path>]\n');
197
+ process.exit(2);
198
+ }
199
+ const outIdx = args.indexOf('--out');
200
+ const outPath = outIdx >= 0 ? args[outIdx + 1] : join(runDir, 'report.md');
201
+ const md = generateReport(runDir);
202
+ writeFileSync(outPath, md, 'utf8');
203
+ process.stdout.write(`${outPath}\n`);
204
+ }
205
+
206
+ // Run as a script (not when imported). `import.meta.main` is Bun; the argv[1]
207
+ // check covers Node.
208
+ const invokedDirectly = import.meta.main || process.argv[1]?.endsWith('scenario-report.mjs');
209
+ if (invokedDirectly) main(process.argv);
@@ -64,6 +64,19 @@ else
64
64
  [ -z "$OUT" ] && { echo "screenshot.sh: --out required for single-shot modes" >&2; exit 2; }
65
65
  fi
66
66
 
67
+ # agent-browser ignores RELATIVE screenshot paths (it writes to its own
68
+ # ~/.agent-browser/tmp instead), which strands the PNG and trips the
69
+ # missing-file guard below. Canonicalize OUT / OUT_DIR to absolute up front so
70
+ # captures land where the caller asked.
71
+ if [ -n "$OUT" ]; then
72
+ mkdir -p "$(dirname "$OUT")" 2>/dev/null || true
73
+ OUT="$(cd "$(dirname "$OUT")" 2>/dev/null && pwd)/$(basename "$OUT")"
74
+ fi
75
+ if [ -n "$OUT_DIR" ]; then
76
+ mkdir -p "$OUT_DIR" 2>/dev/null || true
77
+ OUT_DIR="$(cd "$OUT_DIR" 2>/dev/null && pwd)"
78
+ fi
79
+
67
80
  # TSX specimens cannot be opened via file:// — the browser would see raw JSX.
68
81
  # They must go through the dev-server route (http://localhost:PORT/<rel>),
69
82
  # which transpiles via _canvas-shell.html?canvas=<rel>. Phase 19 / DDR-044.
@@ -97,7 +110,11 @@ if [ -z "$URL" ]; then
97
110
 
98
111
  # URL-encode spaces (rough); leave other chars alone.
99
112
  ACTIVE_ENC=$(printf '%s' "$ACTIVE" | sed 's/ /%20/g')
100
- URL="http://localhost:${PORT}/${ACTIVE_ENC}"
113
+ # Canvases mount through the canvas shell. The bare `/<rel>` route 404s when
114
+ # the canvas-origin sandbox is on (default since phase-9.1); only
115
+ # `/_canvas-shell.html?canvas=<rel>` renders the canvas (valid in both
116
+ # split-on and legacy same-origin modes).
117
+ URL="http://localhost:${PORT}/_canvas-shell.html?canvas=${ACTIVE_ENC}"
101
118
  fi
102
119
 
103
120
  # ---------- selector mapping ----------