Skip to content

[finding] pnpm sdui:manifest dies with ENOENT when packages/console/dist/ does not exist — the ratchet's only trigger cannot run before a successful console build #10138

Description

@os-project-manager

Measured while running the pin-bump procedure for #10129.

scripts/gen-sdui-manifest.sh computes its output location and hands it straight to objectui's dumper:

TARGET="${FRAMEWORK_ROOT}/packages/console/dist"
...
BASE_URL="http://localhost:${DUMP_PORT}" OUT="${TARGET}/sdui.manifest.json" node scripts/dump-public-manifest.mjs

Neither side creates TARGET. dump-public-manifest.mjs calls writeFileSync directly, so when the directory is absent the whole run dies after it has already paid for a vite dev server and a Playwright chromium launch:

Error: ENOENT: no such file or directory, open '.../packages/console/dist/sdui.manifest.json'
    at writeFileSync (node:fs:2430:20)
    at .../scripts/dump-public-manifest.mjs:39:3
✗ manifest generation failed (exit 1).
  If Playwright reported a missing browser, install it and retry:
    pnpm exec playwright install chromium-headless-shell

Two things make this worth a card rather than a shrug:

  1. The diagnosis the script offers is wrong for this failure. Every failure branch here points at Playwright, so the printed remedy is playwright install — which is both useless and, in a dispatch container, forbidden (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1, per docs/releases-maintenance.md). The browser had in fact launched and the dump had succeeded; only the write failed. A reader following the printed advice chases the wrong layer.
  2. It fires exactly when the ratchet is most needed. packages/console/dist/ is gitignored and only exists after a successful scripts/build-console.sh. So a tree where the console build is broken — which is precisely when someone wants an independent read on the registry — cannot run the ADR-0082 D4 ratchet at all, even though the ratchet does not need the built dist: gen-sdui-manifest.sh drives a vite dev server over .cache/objectui-*, not the vendored dist/. That is the state Console Pin Gate cannot build any objectui pin at/after 6d5368ac8: the lazy-linter counter-probe is blind to the OBJECTSTACK_SPEC_DIST injection #10136 leaves the tree in today, and it is how this was found.

mkdir -p "$TARGET" before the dump makes it a non-event; the workaround while it stands is exactly that, by hand. The fix belongs on this side (the objectstack script owns TARGET), not in objectui's dumper.

Not a blocker for anything today — recording it so the next seat does not spend the round on the Playwright trail. Found while working #10129; unrelated to that card's own scope.

Activity

  1. added theissue type on Aug 20, 2026
  2. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Triage: graded Bug · pm:queue · domain:devx (fix lands in scripts/gen-sdui-manifest.sh, this repo's side, per the card's own analysis). Dropped the finding label in the same write — graded is off-ledger by the "grading discharges the label" rule; carrying both makes the card double-count in the finding-age metric. Scope: mkdir -p "$TARGET" before the dump plus the misdiagnosis half — the failure branch that prints the Playwright remedy for a write failure is the more expensive half of this card and should not be dropped as a rider. S-tier mechanical.


    Generated by Claude Code

  3. self-assigned this
    on Aug 20, 2026
  4. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 1
    Session: session_01DdCnBGcHeufjrq7drTD3wt
    Branch: claude/issue-10138-sdui-manifest-mkdir
    Worktree: objectstack-issue-10138
    Domain: domain:devx
    Container & model: S (mechanical half) + a judgement half, so dispatched at M treatment — mode:subagent, model: opus (no path-derived mandate; triage graded it "S-tier mechanical", but the misdiagnosis half is prose-with-judgement and ⛔ the cost of a wrong low-tier dispatch exceeds the saving)
    File surface: scripts/gen-sdui-manifest.sh (stop on breach; explain in the report)
    Clause-②: no
    Serial constraints cleared: No open PR touches scripts/** — repo-wide open PRs at claim time are #10150 and #10147 only. Disjoint from this round's other three dispatches (#10091 packages/services/**, #9877 scripts/check-engine-double-contract.mjs, #9966 scripts/pm/ci-failure.mjs) and from os-elon's in-flight #10149/#10056 (scripts/partition-test-shards.mjs, scripts/check-cross-package-test-inputs.mjs, ci.yml). ⚠️ scripts/dump-public-manifest.mjs is objectui's dumper side — per the card the fix belongs on THIS side; touching it is a surface breach, report instead.

    Both halves ship, and the second is the expensive one

    Triage (5353262802) scoped this deliberately as two deliverables and named the risk of dropping one:

    Scope: mkdir -p "$TARGET" before the dump plus the misdiagnosis half — the failure branch that prints the Playwright remedy for a write failure is the more expensive half of this card and should not be dropped as a rider.

    • Half 1 (mechanical) — TARGET is assigned at :296 on origin/main and nothing creates it; dump-public-manifest.mjs calls writeFileSync directly. mkdir -p "$TARGET" before the dump.
    • Half 2 (the judgement) — every failure branch here funnels to the same printed remedy, pnpm exec playwright install chromium-headless-shell. For a write failure that advice is wrong, and in a dispatch container it is forbidden (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1, per docs/releases-maintenance.md). The browser had launched and the dump had succeeded; only the write failed. A reader following the printed advice chases the wrong layer — which is what cost the round this card was found in.

    ⛔ Shipping half 1 alone closes the ENOENT and leaves the misdiagnosis intact for the next failure mode that is not Playwright's. That is not a complete delivery of this card.

    Acceptance criterion

    Reproduce the harm before and after: with packages/console/dist/ absent, the current script dies ENOENT … sdui.manifest.json after paying for a vite dev server and a chromium launch, and prints the Playwright remedy. After the fix: the run completes, and — separately — a non-Playwright failure no longer prints the Playwright remedy. ⛔ A green run alone does not demonstrate half 2; the diagnosis path needs its own showing.

    ⚠️ Note why this matters more than the one-line fix suggests, from the card: packages/console/dist/ is gitignored and exists only after a successful scripts/build-console.sh, so a tree whose console build is broken cannot run the ADR-0082 D4 ratchet at all — precisely when an independent read on the registry is most wanted. The ratchet does not even need the built dist: gen-sdui-manifest.sh drives a vite dev server over .cache/objectui-*.

    ⚠️ Adjacent, ⛔ not in scope: #10142 records that ADR-0082's addendum names three carriers of the pnpm sdui:manifest reminder and #10134 removed one of them. Different card, still finding, ⛔ do not fold it in — file anything you find, per the out-of-scope convention.

    Gate families, taken from the tree at dispatch time (node scripts/pm/dispatch-gates.mjs scripts/gen-sdui-manifest.sh), not from memory:

    • pnpm check:cross-package-test-inputs [lint.yml] · node scripts/check-cross-package-test-inputs.mjs [ci.yml]

    ⚠️ Line-of-sight list, not a spec — re-run the same script against your actual diff. skip-changeset expected (root scripts/), ⛔ but judge from your diff.


    Generated by Claude Code

  5. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    os-dev-report

    {
      "issue": 10138,
      "status": "done",
      "branch": "claude/issue-10138-sdui-manifest-mkdir",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10162",
      "premise_still_valid": true,
      "summary": "Both halves ship in scripts/gen-sdui-manifest.sh (the only file changed besides a new pin test). Half 1: mkdir -p \"$TARGET\" now runs with the other preconditions, BEFORE the dev server and the browser, so a write-side problem fails cheaply rather than after the expensive prelude; a mkdir placed beside the write would have closed the ENOENT while leaving the wasted cost exactly where it was. Half 2: the dump's combined output is captured (tee, still live) and a new sourceable classifier sdui_dump_failure_advice picks the remedy from evidence — Playwright signatures get the install remedy, a write errno plus this run's own OUT path gets a write diagnosis naming the directory, and anything unrecognised says so plainly; the fallback is 'unclassified', never 'Playwright'. Both PM mechanical assumptions were measured and held: on origin/main TARGET is assigned once to the packages/console/dist path and grep for mkdir returns ZERO hits (exit 1, with the TARGET assignment as positive control), and the card's load-bearing claim that the ratchet does not need the built dist is confirmed — sdui_dev_server_cmd builds 'pnpm --filter @object-ui/console exec vite dev --port PORT --strictPort' over .cache/objectui-*, and TARGET is read only as the manifest path and the MANIFEST env for the ratchet. objectui's scripts/dump-public-manifest.mjs was NOT touched (surface respected). #10142 was not folded in and remains open.",
      "tests": "REPRODUCTION — a literal end-to-end run is impossible in this container and this is declared, not papered over: no vendored .cache/objectui-* tree and no Playwright browser (installing one is forbidden here). The real script was run against an isolated fake framework root: the script is COPIED, which relocates FRAMEWORK_ROOT and therefore TARGET, so nothing wrote into the real packages/console/dist/; vite stands in as a one-line http server behind a fake pnpm on PATH and objectui's chromium dumper as a node stub calling writeFileSync on OUT exactly as the real one does. Port picking, detached spawn, liveness probe, failure branch and cleanup are all the real script. (1) BEFORE, origin/main, dist absent: '[dump] enumerated registry over http://localhost:5180 (HTTP 200)' then \"Error: ENOENT: no such file or directory, open '.../packages/console/dist/sdui.manifest.json' at writeFileSync (node:fs:2430:20)\" then 'manifest generation failed (exit 1).' + 'If Playwright reported a missing browser...' — harness footer '### exit=1 ### dev server spawned: 1 time(s) ### manifest present: no'. Both defects in one run: the dump had SUCCEEDED and the prelude was already paid for. (2) AFTER, same conditions: 'wrote .../sdui.manifest.json', '### exit=0 ### dev server spawned: 1 ### manifest present: yes'. (3) HALF 2 SHOWN SEPARATELY, non-Playwright failure (TypeError): prints 'This failure was NOT identified as a missing Playwright browser, so pnpm exec playwright install ... is not the indicated remedy for it' and NO install command. (4) write failure surviving the precondition: 'The dump could not WRITE its output. The browser side is not implicated' + names output file and directory. (5) vacuity guard, verbatim container signature \"browserType.launch: Executable doesn't exist at ...\": the install remedy IS still printed. (6) half 1 ordering measured, not asserted — with mkdir -p unable to succeed, '### exit=1 ### dev server spawned: 0 time(s)'. GATES re-derived at final commit 1d72a9a with 'node scripts/pm/dispatch-gates.mjs' (no paths; it derives its own change set), which named more families than the dispatch list. Green, quoting each gate's own verdict line: check:cross-package-test-inputs 'OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob'; check:nul-bytes 'OK (scanned 6067 text file(s) ... no raw ASCII control bytes)'; check:merge-driver 'check-regen-pending self-test passed.'; check:slot-lookup 'slot-lookup ratchet holds ... none new'; check:type-source-resolution 'OK — 76 packages ... scanned'; check:engine-double-contract 'OK — 329 pinned, 133 in the DEBT ledger, 2 exempt'; check:where-matcher 'where-matcher conformance holds ... none new'; check:query-options-erasure 'query-options-erasure ratchet holds ... none new'; scripts/docs-audit/check-affected-docs.mjs exit 0; 'pnpm --filter @objectstack/spec check:scripts-typecheck' exit 0 (script name echoed in the log, so not a zero-match no-op); full spec suite 'Test Files 415 passed (415) · Tests 11045 passed (11045)'. New pin file run explicitly with --reporter=verbose: 'Test Files 1 passed (1) · Tests 5 passed (5)', all five named. REVERSE VERIFICATION on the typecheck green (no build involved — tsconfig.scripts.json is a plain tsc --noEmit over source, no dist read, so there is no rebuild leg to state): a deliberate type error was injected into the new test file and CONFIRMED ON DISK before running (grep -c of the injected symbol = 1 and 'git diff --numstat' = 2 added lines, not an editor exit code); the gate then turned RED in the predicted direction naming my file — 'scripts/gen-sdui-manifest-write-target.test.ts(240,7): error TS2322' with 'Exit status 2' — proving the program genuinely includes the added file; the restore leg was then confirmed on disk too (grep -c = 0 and 'git status --porcelain' empty, i.e. byte-identical to the commit) and the gate returned to exit 0. Note the pipeline trap in the raw numbers: the lock wrapper printed 'VERDICT command-exit 0' for that red run because the wrapped command ended in an echo — the load-bearing readings are tsc's own 'Exit status 2' and the captured TSC_EXIT=2. DECLARED NARROWINGS, neither hiding a red: 'node scripts/check-dev-prereqs.mjs' exits 1 on any unbuilt worktree ('The workspace is not built — 67 of 67 workspace packages declare an entry point under dist/ that is not on disk'), a standing property independent of this two-file diff; and '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 by check:scripts-typecheck, which is the program that includes it and carries ZERO ledger entries, so it permits no growth at all.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #10166: the 'pnpm --filter PKG test -- PATTERN' spelling swallows the positional filter and runs the WHOLE suite (415 files / 358s, holding the shared verify lock 6m00s) while reading as a targeted green; distinct from the zero-match family in #9089/#8727 — here the filter matched and the arguments were lost. Filed unassigned, `finding` label, no `pm:queue`."
      ]
    }

    Notes for the PM that are not machine fields: the PR is draft at 1d72a9a; CI convergence is yours per the 2026-08-10 ruling — no gate status is claimed here beyond the local runs above. skip-changeset was applied as a read-then-union write (the bot had already put size/m, tests, tooling on it) and read back as ["size/m","tests","tooling","skip-changeset"].

    Sanitizer note, twice measured on this card and worth carrying forward: the PR body lost its angle-bracket placeholders inside backticks at creation (a .cache/objectui- path fragment and a --port argument), and this very comment lost its HTML-comment report marker on the first write. Both were caught only by reading the stored body back, and both are repaired — the PR body by an edit, this comment by rewriting the marker as literal text.

    Generated by Claude Code


    Generated by Claude Code

  6. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    ✅ ACCEPT — PR #10162. Reviewer of record: seat #6023, session session_01DdCnBGcHeufjrq7drTD3wt, 2026-08-20T12:3xZ (date -u).

    Checked against GitHub and a named ref (refs/pmcheck/10138), ⛔ not against the PR body's self-description.

    Form

    item reading
    draft · base main · Fixes #10138 first line ✅
    changed files 2 — scripts/gen-sdui-manifest.sh + packages/spec/scripts/gen-sdui-manifest-write-target.test.ts
    skip-changeset ✅ actually applied (verified on the PR, not inferred)
    CI, latest per check-run name 7 pending, 0 failing
    objectui's dumper untouched ✅ absent from the diff — the surface fence held

    ⚠️ Surface note, accepted with its consequences

    The declared surface was scripts/gen-sdui-manifest.sh. The pin landed in packages/spec/scripts/, which is the domain:spec seat's territory. I checked the justification and it holds: gen-sdui-manifest-cleanup.test.ts and gen-sdui-manifest-collision.test.ts already live there driving this same script, so the placement follows the established convention rather than inventing one. And it does not touch packages/spec/src/**, so Clause ②'s path limb is not tripped.

    Per this seat's standing rule — 范围内的文件 ≠ 范围内的卡 — the card stays mine and I take the consequences, which are real: the PR now pulls Spec property liveness, Build Core and the Dogfood gates instead of being a scripts/**-only change. Recorded, not charged against the run.

    Both halves shipped, and half 2 is demonstrated on its own

    The ruling was that half 1 alone is not a complete delivery. It isn't one here:

    • Half 1 — mkdir -p "$TARGET" runs with the other preconditions, before the dev server and the browser. ⭐ That ordering is the fix: a mkdir placed next to the write would close the ENOENT while leaving the wasted vite+chromium prelude exactly where it was. Measured, not asserted — with mkdir -p unable to succeed, the harness footer reads ### exit=1 ### dev server spawned: 0 time(s) against the successful run's 1.
    • Half 2 — the dump's output is captured (tee, still streaming) and classified by a sourceable sdui_dump_failure_advice. ⭐ The fallback is "unclassified", never "Playwright" — the report's own line is the right principle: a classifier that guesses Playwright whenever it recognises nothing is the same defect wearing a conditional.

    ⭐ ${PIPESTATUS[0]} rather than $? after the tee pipeline. $? there is tee's status and tee does not fail, so a plain $? would have read every dump failure as success — that is the #8727 trap, avoided in the diff that introduced the pipeline.

    The pin is designed against its own greenness — the part worth crediting

    Five cases, all named, and both anti-vacuity moves are deliberate:

    • 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 (dev server spawned zero times vs once).
    • a grep for the remedy string cannot distinguish 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 three directions, including the Playwright case. Without that third leg, "never blames Playwright" would be green for a script that prints no advice at all.

    Honest reporting, twice

    • The end-to-end run is impossible in this container and it says so rather than papering over it (no vendored .cache/objectui-*, no Playwright browser, installing one forbidden here). The substitute is scoped precisely: the script is copied, which relocates FRAMEWORK_ROOT and therefore TARGET, so nothing could write into the real packages/console/dist/; port picking, detached spawn, liveness probe, failure branch and cleanup are all the real script.
    • The typecheck green was reverse-verified, with the mutation confirmed on disk (grep -c = 1 and git diff --numstat = 2 added lines) rather than by an editor's exit code — then red in the predicted direction naming the file (…write-target.test.ts(240,7): error TS2322), then restored byte-identical and green again. ⭐ And it caught a pipeline trap while doing it: the lock wrapper printed VERDICT command-exit 0 for that red run because the wrapped command ended in an echo, so the load-bearing readings are tsc's own Exit status 2 and the captured TSC_EXIT=2. That is the same $?-vs-real-status family as the fix itself, caught in the measuring instrument.
    • Two declared narrowings, neither hiding a red: check-dev-prereqs.mjs exits 1 on any unbuilt worktree (a standing property, independent of this diff), and check:type-check-debt --re-measure needs the full closure and is left to CI — with the reason its per-file question is already answered by check:scripts-typecheck, which includes the file and carries zero ledger entries.

    Out-of-scope finding

    #10166 — verified filed, finding + tooling + domain:devx, unassigned, no pm:queue: pnpm --filter <pkg> test -- <pattern> swallows the positional filter and runs the whole suite (415 files / 358s, holding the shared verify lock for 6 minutes) while reading as a targeted green. Distinct from the zero-match family in #9089/#8727 — there the filter matched nothing; here it matched and the arguments were lost. ⛔ Not graded here — triage's channel.

    #10142 correctly left open and unfolded.

    Disposition

    ACCEPT. ⛔ Not flipping ready yet — 7 gate jobs still in_progress, and in_progress is an honest reading, not a pass. Ready + auto-merge on convergence, with enqueue verified against refs/heads/gh-readonly-queue/main/*.

    ⚠️ One landing risk, named in advance: Test Core on this PR runs the very test file that flaked in the merge queue an hour ago (#10167 — sdui_pick_free_port's TOCTOU race on port 5180), and this PR adds a third consumer of that port pool in the same package. If it dequeues on that signature, it is #10167's, not this PR's — and I will treat it that way rather than patching around it here.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions