fix(devx): the docs-audit walker prunes the skill-examples build residue, and the emitter always cleans it up - #15649
Conversation
…due, and the emitter always cleans it up (#15446, #15457) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…amples-build-residue
📓 Docs Drift CheckNothing 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. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
ACCEPT — PM seat Reviewed against the three-dot diff at What the diff does, checked line by line
Measured by this seat on the PR head (throwaway worktree, PM checkout's The Fences held: only the two files the card named; Flipping ready + enabling auto-merge (squash). Generated by Claude Code |
Fixes #15446
Fixes #15457
Two gates, each correct in isolation, where running one first reds the other.
check:skill-examplesemits its throwaway compile trees underpackages/spec/;scanRouteSurface()inscripts/docs-audit/affected-docs.mjswalks the filesystem, so those emitted files enter the walk and five of them declare apath:beside an HTTP method — admitted as kind-contractroute sources, which reds that file's own live pin. CI is green today only becausecheck:docs-audit-scopehappens to run atlint.yml:2023andcheck:skill-examplesat:5292.Both halves land here, as triage ruled: the walker makes its population a stated property of the walk regardless of what else ran, and the emitter stops leaving the trees behind in the first place.
All figures below were measured on this branch at
ec0f7dc93.1. The reproduction, on this tree, before either fix
The card's order, run on a clean worktree off
origin/main6b8c67778with only the spec closure built:That is #15457's finding reproduced exactly: the residue is left by the refusal path, not by a completed run. The two trailing
rmSyncsweeps onmainsit only on the two paths that RETURN (the green verdict and the diagnostics verdict); the prerequisite refusal exits before either, and it is the ordinary outcome on a workspace that is not fully built.Then, in the card's order:
Control, both directions:
rm -rfthe two trees and the same command isEXIT=0,✓ affected-docs self-test: 568 cases pass.2. Each fix alone
Walker alone — measured with only
scripts/docs-audit/affected-docs.mjschanged and the 226 + 10 residue files re-planted on disk:The gate is now green with the residue present, which is the property the card asked for: correct regardless of which other gates have run.
Emitter alone — the same prerequisite refusal that left 226 + 10 files above:
Nothing is left for the walker to walk, and section 1's control already measured that the scope gate is green whenever the trees are absent.
The stale sweep, the half that repairs an already dirty tree —
writeBuildDirwipes only the dir of a surface a run REACHES, so a run that refuses at surface 1 leaves surface 2's stale tree standing indefinitely, and the exit-path guarantee cannot clean what this run never materialised:Together, the card's two commands in the card's order, on the merged head:
3. What changed
scripts/docs-audit/affected-docs.mjs— the walk prunes build residue as a CLASSwalkSourceFilesprunednode_modules,distand.turboby name. It now prunesnode_modules,distand any dot-directory, applied to directories only.The class rather than the roster, because a roster fixes today and leaves the next emitted tree to be found the same way — and
.gitignore:66-70already lists three trees, not the two the card names (packages/spec/.examples-build/,packages/spec/.examples-build-src/,packages/client-react/.examples-build/). It is safe because it is measured, not assumed:No tracked file under
packages/**lives in a dot-directory at all. That premise is not left to age in a comment: a new live pin in--self-testre-measures it on every run and reds the day a tracked source file appears under one, so the remedy then becomes a deliberate choice rather than a silent drop.Fixtures, both directions, in the existing hermetic fake-tree battery (floor raised 67 → 80):
CALL_SITE_FILE_RE, so the registrar list is pinned too;.turbokept as a fixture — the named arm the class replaced, so the rewrite cannot quietly lose what the roster covered;packages/spec/src/api/IS still admitted;startsWith('.')specifically:packages/foo/src/api.v2/real-route.ts, a directory whose name merely CONTAINS a dot, still reaches both populations.The contract-level assertion stays where it already is — the live pin the card quotes — because kind (b) admission reads real file CONTENT off the real filesystem and a fake tree cannot reach it. Section 1 is that pin measured red, and section 2 is it measured green.
packages/spec/scripts/check-skill-examples.ts— every exit path removes what it materialisedThe compile loop now runs inside
withBuildDirCleanup, which tracks each build dir beforewriteBuildDircreates it and removes it afterwards.try/finallyalone cannot carry this, and that is why it is a helper rather than four inline lines:process.exit()does not runfinallyblocks, andfail(),refuse()and the per-surface staleness guard all leave that way — which is precisely the path that left 226 + 10 files. So the guarantee is held in two places: thefinallyfor the returning and throwing paths, and aprocess.on('exit')listener (plus explicit SIGINT/SIGTERM handlers, since a default-handled signal terminates without emitting'exit') for the paths that exit outright.--keepstill keeps.cleanStaleBuildDirs()runs before the loop and announces on stderr what it removed, because "an earlier run of this gate died" is a fact the reader wants; a silent tidy-up is how this coupling stayed invisible.4. Ablations — every leg rebuilt, every mutation confirmed on disk, restore proven by blob hash
Each ablation is trap-guarded with absolute paths, mutates by anchored replacement with a
grep -ccount in BOTH directions, restores withgit checkout HEAD -- ABSOLUTE_PATH, and proves the restore by comparinggit hash-objectagainst the HEAD blob hash plus an emptygit diff HEAD. No build step is involved: both files are run directly from source bynode/tsx, so there is nodistfor a stale artifact to hide in.e.name.startsWith('.')→e.name === '.turbo'startsWith('.')count 1 → 0; injected.turbocount = 1.turborow and both positive controls stayed greenremoveBuildDirs(tracked, keep)from thefinally.examples-build-srcinstallBuildDirExitCleanup()callbaa50df4…/ce7df47c…matched HEAD;git diff HEADempty582 cases pass/ self-testEXIT=05. Verification
Exit codes captured before any pipe; verdict lines quoted from the gates themselves. All at
ec0f7dc93.pnpm --filter @objectstack/spec run check:skill-examples✅ 257 prose examples type-check across 3 surface(s); no.examples-build*leftpnpm check:docs-audit-scope(in that order)✓ affected-docs self-test: 582 cases pass.node scripts/docs-audit/affected-docs.mjs --self-test✓ affected-docs self-test: 582 cases pass.(was 568)tsx scripts/check-skill-examples.ts --self-test✅ self-test: …full verdict line, extended with the residue clausepnpm --filter @objectstack/spec typecheckcheck:test-typecheck: OK— andtsc -p tsconfig.scripts.json --listFilesconfirmspackages/spec/scripts/check-skill-examples.tsIS in that program (1 hit), so this is not a phantom greenpnpm lint(whole repo,eslint . --no-inline-config)pnpm check:nul-bytesgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over both edited files: 0 hits (grep exit 1)pnpm check:pm-dispatch-gates1445 cases passnode scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack⊘ NOT MEASURED, named rather than counted as green:
pnpm check:dual-build-cjs-loads—exit 3,PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/.It needs a fullpnpm build; 32 packages are unbuilt in this worktree. Its own--self-testpassed (93 cases) in the same run.check-cross-package-test-inputs --union-into "$RUNNER_TEMP/…", the threecheck-shard-attestationinvocations, the twocheck-test-completenessinvocations) — no value outside a CI run.silentfor every card in the tree; 10 of them keep their roster under a directory one of these paths is in, so their silence is evidence in neither direction. Read, not treated as cleared.No changeset:
packages/spec'sfilesarray isdist,json-schema,liveness,prompts,llms.txt,README.md,src/**/*.zod.ts,CHANGELOG.md,api-surface,spec-changes.json—scripts/is not published, and neither is the repo-rootscripts/.skip-changesetapplied;pnpm check:published-filesexit 0.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code