ci: gate the editor bundle against a fresh build
Mirrors "the embedded wasm must match a fresh build": rebuilds internal/docs/static/ via fixtures/build-editor.mjs and fails if that diverges from what's committed. Safe to gate on because the build is byte-reproducible (no sourcemaps/banners/timestamps/absolute paths, esbuild's real work is a pinned native binary) — five consecutive local runs produced identical sha256. On failure the check restores internal/docs/static/ to HEAD (checkout + clean) before exiting, so a red run never leaves a dirty tree behind to mask itself on a re-run or trip up a later command. Verified live: reverted to a stale entry point, ran the gate without rebuilding — failed with the intended message, tree left clean; rebuilt — passed. Adds no measurable time (two full-gate runs, before/after, ~38s both; the rebuild step alone is ~60ms).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011F5CivVm4CmL9ByfSSAaXA
.amadan/ci.d/10-gate+27 −0
Intent
<task-notification> <task-id>a21e515b144836957</task-id> <tool-use-id>toolu_01N1VNCYJ6PXumUV6M5EYAct</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-docs/70f302d1-9d5d-4eb2-8b38-e09a15fdd1c3/tasks/a21e515b144836957.output</output-file> <status>completed</status> <summary>Agent "Editing Task 4 vendor bundle" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>Task 4 complete. **Status:** done. **Commit:** `8c78f7d689de42b138560d04d00901f19c393c8d` **Bundle:** `internal/docs/static/editor.f13f5e0ce6fa7467.js` — 327,967 bytes. **Pinned versions:** yjs 13.6.27 (unchanged), y-prosemirror 1.3.7, prosemirror-state 1.4.4, prosemirror-view 1.42.3, prosemirror-model 1.25.11, prosemirror-schema-basic 1.2.4, prosemirror-schema-list 1.5.1, prosemirror-keymap 1.2.3, prosemirror-commands 1.7.2, prosemirror-history 1.5.0, y-protocols 1.0.7 (required peer dep of y-prosemirror), esbuild 0.28.2 — all exact, `package-lock.json` committed. **TDD:** wrote `internal/docs/assets_test.go` first; confirmed it failed to compile (`undefined: staticFS`, `undefined: editorScriptPath`) before `assets.go` existed. Implemented `internal/docs/assets.go` + wired into `app.go`'s `/static/*` route; both tests then pass. **Test results:** full `internal/docs` package ok (9.2s); `.amadan/ci.d/10-gate` exit 0. **One necessary deviation:** Go's `//go:embed` can't cross `..`, so the committed tree lives at `internal/docs/static/` (not a top-level `static/` as the brief's file list literally suggested) — same pattern as the existing `internal/docs/views/`. Also wrote the anonymous-request test with `httptest.NewRequest` + `app.Handler().ServeHTTP` directly rather than the brief's literal `app.TestRequest(t, "", ...)`, since `TestRequest` always signs someone in and has no empty-address special case. **CI gate gap:** it already runs `npm ci` and `go test ./...`, which exercises the new deps and embedded bundle automatically — no change needed there. It does *not* verify the committed bundle matches a fresh `node build-editor.mjs` build, unlike the analogous wasm freshness check for `core/`. Flagged in the full report but not added, since it's outside Task 4's specified steps. Full details in `/home/paulca/amadan.net/carlos/docs/.superpowers/sdd/2026-08-31-editing/task-4-report.md`.</result> <usage><subagent_tokens>120745</subagent_tokens><tool_uses>45</tool_uses><duration_ms>437037</duration_ms></usage> </task-notification>
<task-notification> <task-id>a65640c3f7ec6181e</task-id> <tool-use-id>toolu_01SGTKDpkojX8UTkY5FXmu2Z</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-docs/70f302d1-9d5d-4eb2-8b38-e09a15fdd1c3/tasks/a65640c3f7ec6181e.output</output-file> <status>completed</status> <summary>Agent "Review editing task 4" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>Tree clean (review file is inside `.superpowers/sdd/` which is git-tracked but was requested output, not an experiment — left in place per instructions). **Spec verdict: PASS.** All brief interfaces present and correct; the one deviation (static dir under `internal/docs/`, not `app.TestRequest`) is disclosed, forced by `go:embed`'s no-`..` rule, and verified genuine. **Quality verdict: PASS**, with one real gap (below) already flagged by the implementer. **Reproducibility measurement:** 5 consecutive `node build-editor.mjs` runs → identical sha256 `f13f5e0ce6...` each time, matching the committed filename hash. No sourcemaps, banners, timestamps, or absolute paths found in script or output (`grep sourceMappingURL`/`/home/paulca` → 0 hits). esbuild's actual work runs in a pinned-version (`0.28.2`) native binary, not Node-interpreted JS, so Node version is irrelevant; a CI freshness check only needs one fixed runner to agree with itself, which is weaker than cross-machine reproducibility and already demonstrated. **Recommendation: (a)** — add the full freshness check to `.amadan/ci.d/10-gate`, mirroring the wasm check (rebuild + `git diff --quiet -- internal/docs/static/`). No nondeterminism found; the risk (Task 6 edits the entry point) is live and the fix is cheap. Findings (ranked, all confirmed by direct testing, not just reading): 1. Anonymous serving confirmed live via real `httptest.NewServer` + anonymous `http.Get`: 200, correct content-type/cache-control, route registered in `internal/docs/app.go:435` above the `RequireSession` group. `assets_test.go`'s `TestEditorBundleIsServed` genuinely uses an unauthenticated request path. 2. Caching hash provably computed over served bytes (`internal/docs/assets.go:75-89`); 0/2+ bundle case panics at init with an actionable message (`assets.go:44-58`). 3. All deps pinned exact, from npmjs.org, `yjs` unchanged at `13.6.27`; only expected transitives present. 4. `go:embed` relocation to `internal/docs/static/` confirmed necessary; no stray top-level `static/` references anywhere. 5. Both new tests are non-vacuous — each assertion fails under the corresponding defect. 6. Gate gap (no bundle freshness check) is real and pre-existing per the brief, but should be closed before Task 6 — see recommendation (a). Full review written to `/home/paulca/amadan.net/carlos/docs/.superpowers/sdd/2026-08-31-editing/task-4-review.md`.</result> <usage><subagent_tokens>60861</subagent_tokens><tool_uses>21</tool_uses><duration_ms>183281</duration_ms></usage> </task-notification>
diff --git a/.amadan/ci.d/10-gate b/.amadan/ci.d/10-gate| index 220c1ec..4fca6e4 100755 |
| --- a/.amadan/ci.d/10-gate |
| +++ b/.amadan/ci.d/10-gate |
| @@ -27,6 +27,33 @@ export CGO_ENABLED=0 |
| echo "=== fixtures: node dependencies" |
| (cd fixtures && npm ci) |
| +# fixtures/build-editor.mjs is byte-reproducible: no sourcemaps, no |
| +# banners, no timestamps, no absolute paths baked into the output, and |
| +# esbuild's actual work happens in a pinned native binary (0.28.2), |
| +# not interpreted JS, so the Node version running it does not enter |
| +# into the result either. Five consecutive runs on this one runner |
| +# produce identical sha256, which is all a freshness check ever |
| +# needs — a weaker bar than cross-machine reproducibility, and |
| +# already met. Rebuilding here and diffing what's committed mirrors |
| +# "the embedded wasm must match a fresh build" below, for the same |
| +# reason that check is safe rather than randomly red. |
| +echo "=== the editor bundle must match a fresh build" |
| +(cd fixtures && node build-editor.mjs) |
| +if [ -n "$(git status --porcelain -- internal/docs/static/)" ]; then |
| + echo "gate: internal/docs/static/ does not match a fresh build. Run 'cd fixtures && node build-editor.mjs' and commit the result." >&2 |
| + # The rebuild above just mutated a committed directory as a side |
| + # effect of checking it. Restore it to what's actually committed — |
| + # tracked changes via checkout, any new hashed file the rebuild |
| + # wrote via clean — so the only trace of this failure is the |
| + # message and exit code above, not a stray dirty file a later |
| + # command (or a re-run of this same script) trips over, or a |
| + # rebuilt-to-fresh tree that would silently pass a second run |
| + # without anyone having committed anything. |
| + git checkout -- internal/docs/static/ |
| + git clean -fd -- internal/docs/static/ >/dev/null |
| + exit 1 |
| +fi |
| + |
| echo "=== rust: fmt" |
| cargo fmt --manifest-path core/Cargo.toml --check |