Skip to content

fix(scripts): create the SDUI manifest output directory, and stop blaming Playwright for every dump failure - #10162

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10138-sdui-manifest-mkdir
Aug 20, 2026
Merged

fix(scripts): create the SDUI manifest output directory, and stop blaming Playwright for every dump failure#10162
os-zhuang merged 1 commit into
mainfrom
claude/issue-10138-sdui-manifest-mkdir

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Fixes#10138

scripts/gen-sdui-manifest.sh assigned TARGET="${FRAMEWORK_ROOT}/packages/console/dist" and nothing created it, while objectui's dumper calls writeFileSync straight out. That directory is gitignored and exists only after a successful scripts/build-console.sh, so on a tree whose console build is broken the run died — after it had already paid for a vite dev server and a chromium launch — and then printed the Playwright remedy for a failure in which the browser had already done its job.

Both halves of the card ship here. The second one is the expensive half: shipping only the mkdir closes this ENOENT and leaves the misdiagnosis in place for the next failure that is not Playwright's.

1. The output directory is a precondition, not a surprise at write time

mkdir -p "$TARGET" runs with the other preconditions — before the dev server and the browser — so a write-side problem fails cheaply instead of at the end of a long run. That ordering is the fix: a mkdir placed beside the write would close the ENOENT while leaving the wasted prelude exactly where it was.

It also removes a false coupling. The ADR-0082 D4 ratchet never needed the built dist: this script drives a vite dev server over .cache/objectui-*, and dist/ is only where the manifest lands. Verified on origin/mainsdui_dev_server_cmd builds pnpm --filter @object-ui/console exec vite dev --port PORT --strictPort, and the only other read of TARGET is the manifest path itself. So a broken console build made the ratchet unrunnable for want of one directory, precisely on the trees that most want an independent read on the registry.

2. The remedy is chosen from what failed

Every failure branch of the dump used to print pnpm exec playwright install chromium-headless-shell. For a write failure that advice is wrong, and in an agent dispatch container it cannot even be followed — PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 is set there (docs/releases-maintenance.md).

The dump's combined output is now captured (tee, so it still streams live) and classified by sdui_dump_failure_advice:

  • Playwright evidence → the install remedy, unchanged. The signatures are the ones this repo has actually measured, not invented: browserType.launch: Executable doesn't exist at ... is verbatim from docs/releases-maintenance.md's revision-mismatch section.
  • A write failure (a write errno and this run's own OUT path in the text) → names the output file and its directory, and says the browser side is not implicated.
  • Neither → says so plainly. The fallback is "unclassified", never "Playwright" — a classifier that guesses Playwright whenever it recognises nothing is the same defect wearing a conditional.

The status capture is ${PIPESTATUS[0]}, never $?: after a pipeline $? is tee's status, and tee does not fail, so a plain $? would read every failure of the dump as a success.

Reproduction — the harm, then each half separately

A literal end-to-end run is not possible in this container and this is stated rather than papered over: there is no vendored .cache/objectui-* tree (that needs pnpm objectui:build) and no Playwright browser (installing one is forbidden here). So the real script runs against an isolated fake framework root — the script is copied, which relocates FRAMEWORK_ROOT and therefore TARGET, so nothing writes into the real packages/console/dist/. vite stands in as a one-line http server behind a fake pnpm on PATH; objectui's chromium dumper stands in as a node stub that writes OUT with writeFileSync exactly as the real one does. Everything between them — port picking, the detached spawn, the liveness probe, the failure branch, the cleanup — is the real script.

Before (origin/main), packages/console/dist/ absent:

 dev server: port 5180, log /tmp/sdui-dump-dev.bUA25j.log
[dump] enumerated registry over http://localhost:5180 (HTTP 200)
Error: ENOENT: no such file or directory, open '.../packages/console/dist/sdui.manifest.json'
at writeFileSync (node:fs:2430:20)
✗ manifest generation failed (exit 1).
If Playwright reported a missing browser, install it and retry:
pnpm exec playwright install chromium-headless-shell
### exit=1 ### dev server spawned: 1 time(s) ### manifest present: no

Both defects in one run: the dump had succeeded (it enumerated the registry over HTTP 200), the dev server had already been paid for, and the advice points at Playwright anyway.

After, same conditions:

[dump] enumerated registry over http://localhost:5180 (HTTP 200)
[dump] wrote .../packages/console/dist/sdui.manifest.json
✓ wrote .../packages/console/dist/sdui.manifest.json
### exit=0 ### dev server spawned: 1 time(s) ### manifest present: yes

A green run does not demonstrate half 2, so the diagnosis path is shown on its own. A non-Playwright dump failure (TypeError, nothing to do with a browser):

TypeError: Cannot read properties of undefined (reading 'blocks')
✗ manifest generation failed (exit 1).
This failure was NOT identified as a missing Playwright browser, so
'pnpm exec playwright install ...' is not the indicated remedy for it.
Read the dump's own output above first.

A write failure that survives the new precondition (the directory removed mid-run) now names the right layer:

✗ manifest generation failed (exit 1).
The dump could not WRITE its output. The browser side is not implicated
and 'playwright install' would change nothing here.
output file: .../packages/console/dist/sdui.manifest.json
its directory: .../packages/console/dist

And the vacuity guard in the other direction — the remedy is still printed when Playwright really is what failed, fed the verbatim container signature:

browserType.launch: Executable doesn't exist at /opt/pw-browsers/chromium_headless_shell-1234/...
✗ manifest generation failed (exit 1).
Playwright could not start a browser — that IS this failure. Install the
matching one and retry:
pnpm exec playwright install chromium-headless-shell

Finally, half 1's ordering claim, measured rather than asserted — with mkdir -p unable to succeed, the run stops before anything expensive starts:

mkdir: cannot create directory '.../packages/console': Not a directory
✗ could not create the manifest output directory: .../packages/console/dist
### exit=1 ### dev server spawned: 0 time(s)

The pin

packages/spec/scripts/gen-sdui-manifest-write-target.test.ts, beside the existing cleanup and collision pins for this script and following their shape — executed assertions, not greps:

  • a grep for mkdir -p would pass against one written after the dump, which fixes nothing about the cost, so the test measures the order: the failing-precondition case asserts the dev server was spawned zero times, against the successful run's one.
  • a grep for the remedy string cannot tell a remedy printed always from one printed on evidence, so the real classifier is sourced out of the real script and fed real failure text in all three directions — including the Playwright case, without which "never blames Playwright" would be green for a script that prints no advice at all.

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs (no paths — it takes its own change set from the merge base), which named more than the dispatch list. Run at 1d72a9a:

gateverdict
pnpm check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob. (scripts/** already covers the new test)
pnpm check:nul-bytesOK (scanned 6067 text file(s) ... no raw ASCII control bytes)
pnpm check:merge-driver✓ check-regen-pending self-test passed.
pnpm check:slot-lookup✓ slot-lookup ratchet holds ... none new
pnpm check:type-source-resolutioncheck-type-source-resolution OK — 76 packages ... scanned
pnpm check:engine-double-contractOK — 329 pinned, 133 in the DEBT ledger, 2 exempt
pnpm check:where-matcher✓ where-matcher conformance holds ... none new
pnpm check:query-options-erasure✓ query-options-erasure ratchet holds ... none new
node scripts/docs-audit/check-affected-docs.mjsexit 0
pnpm --filter @objectstack/spec check:scripts-typecheckexit 0 — and this program is the one that reads the new file (tsconfig.scripts.json, include scripts/**/*, plain tsc with zero ledger entries)
pnpm --filter @objectstack/spec testTest Files 415 passed (415) · Tests 11045 passed (11045)

That typecheck green was reverse-verified rather than trusted: a deliberate type error in the new test file turned it red naming scripts/gen-sdui-manifest-write-target.test.ts(240,7): error TS2322, and restoring the file (byte-identical to the commit) returned it to green — so the program genuinely reads the added file.

Two declared narrowings, neither hiding a red:

  • node scripts/check-dev-prereqs.mjs exits 1 on this worktree with The workspace is not built — 67 of 67 workspace packages declare an entry point under dist/ that is not on disk. That is a standing property of any unbuilt worktree, independent of this two-file diff.
  • check:type-check-debt --re-measure needs the full workspace closure built and is left to CI. Its per-file question for the added test is answered directly by check:scripts-typecheck above, which is the program that includes it — and being ledger-free, it permits no growth at all.

No changeset: the diff is a root scripts/ file plus a test file, neither of which is published. skip-changeset applied.

Out of scope, deliberately

#10142 is not addressed here and remains open — ADR-0082's addendum naming three carriers of the pnpm sdui:manifest reminder is a separate card.

Generated by Claude Code

…laywright for every dump failure
`scripts/gen-sdui-manifest.sh` assigned TARGET=packages/console/dist and nothing
created it. That directory is gitignored and exists only after a successful
scripts/build-console.sh, so on a tree whose console build is broken the run died
`ENOENT ... sdui.manifest.json` -- after it had already paid for a vite dev server
and a chromium launch. The ADR-0082 D4 ratchet does not even need the built dist
(this script drives a vite DEV server over .cache/objectui-<sha>; dist/ is only
where the manifest lands), so the ratchet was unrunnable precisely on the trees
that most want an independent read on the registry.
Two changes:
1. `mkdir -p "$TARGET"` runs with the other preconditions, before the dev server
and the browser, so a write-side problem fails cheaply instead of at the end
of a long run.
2. The dump's failure advice is now chosen from what failed. Every failure branch
used to print `playwright install chromium-headless-shell`; in the measured
failure the browser had launched and the dump had succeeded, and only the
write failed, so that remedy sent the reader to the wrong layer -- and inside
an agent dispatch container it cannot be followed at all
(PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1, docs/releases-maintenance.md). The
Playwright remedy now needs Playwright evidence; the fallback is
"unclassified", never "Playwright".
Pinned by packages/spec/scripts/gen-sdui-manifest-write-target.test.ts, which
measures the ORDER (the failing precondition spawns zero dev servers) rather than
grepping for `mkdir`, and feeds real failure text to the real classifier.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gateteststooling

Projects

None yet

2 participants

@os-zhuang@claude