Skip to content

fix(devx): the docs-audit walker prunes the skill-examples build residue, and the emitter always cleans it up - #15649

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-15446-examples-build-residue
Sep 5, 2026
Merged

fix(devx): the docs-audit walker prunes the skill-examples build residue, and the emitter always cleans it up#15649
baozhoutao merged 2 commits into
mainfrom
claude/issue-15446-examples-build-residue

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15446
Fixes #15457

Two gates, each correct in isolation, where running one first reds the other. check:skill-examples emits its throwaway compile trees under packages/spec/; scanRouteSurface() in scripts/docs-audit/affected-docs.mjs walks the filesystem, so those emitted files enter the walk and five of them declare a path: beside an HTTP method — admitted as kind-contract route sources, which reds that file's own live pin. CI is green today only because check:docs-audit-scope happens to run at lint.yml:2023 and check:skill-examples at :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/main 6b8c67778 with only the spec closure built:

$ pnpm --filter @objectstack/spec run check:skill-examples
EXIT=1
   257 marked example(s) across 105 file(s), 3 surface(s):
[client SDK (@objectstack/client-react, @objectstack/client)]
❌ packages/client-react/dist holds no .d.ts declarations — the package is not built
$ ls packages/spec/.examples-build packages/spec/.examples-build-src
226 files + 10 files left behind

That is #15457's finding reproduced exactly: the residue is left by the refusal path, not by a completed run. The two trailing rmSync sweeps on main sit 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:

$ pnpm check:docs-audit-scope
EXIT=1
  ✗ self-test "every contract declaration admitted is a packages/spec API declaration":
    kind=contract → expected scanRouteSurface=true, got false
✗ affected-docs self-test failed (1 case(s)).

Control, both directions: rm -rf the two trees and the same command is EXIT=0, ✓ affected-docs self-test: 568 cases pass.

2. Each fix alone

Walker alone — measured with only scripts/docs-audit/affected-docs.mjs changed and the 226 + 10 residue files re-planted on disk:

$ ls packages/spec/.examples-build | wc -l   # 226   (+ 10 in .examples-build-src)
$ pnpm check:docs-audit-scope
EXIT=0
✓ affected-docs self-test: 582 cases pass.

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:

$ pnpm --filter @objectstack/spec run check:skill-examples
EXIT=1                                  # the refusal is unchanged, as it should be
$ ls -d packages/spec/.examples-build* packages/client-react/.examples-build
ls: cannot access ...: No such file or directory

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 — writeBuildDir wipes 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:

$ cp -r ...  # re-plant 226 + 10
$ pnpm --filter @objectstack/spec run check:skill-examples
⚠ removed 2 stale build dir(s) left behind by an earlier interrupted run:
    packages/spec/.examples-build
    packages/spec/.examples-build-src
$ ls -d packages/spec/.examples-build*      # gone

Together, the card's two commands in the card's order, on the merged head:

$ pnpm --filter @objectstack/spec run check:skill-examples   # EXIT=0
✅ 257 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
$ ls -d packages/spec/.examples-build* packages/client-react/.examples-build   # nothing
$ pnpm check:docs-audit-scope                                # EXIT=0
✓ affected-docs self-test: 582 cases pass.

3. What changed

scripts/docs-audit/affected-docs.mjs — the walk prunes build residue as a CLASS

walkSourceFiles pruned node_modules, dist and .turbo by name. It now prunes node_modules, dist and 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-70 already 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:

$ git ls-files 'packages/**' | grep -cE '(^|/)\.[^/]+/'
0

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-test re-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):

  • all three emitted trees excluded, one row each, so a regression names which one came back;
  • one residue fixture whose basename also matches CALL_SITE_FILE_RE, so the registrar list is pinned too;
  • .turbo kept as a fixture — the named arm the class replaced, so the rewrite cannot quietly lose what the roster covered;
  • the admitting direction: the same declaration under packages/spec/src/api/ IS still admitted;
  • the over-reach control for 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 materialised

The compile loop now runs inside withBuildDirCleanup, which tracks each build dir before writeBuildDir creates it and removes it afterwards.

⚠️ A try/finally alone cannot carry this, and that is why it is a helper rather than four inline lines: process.exit() does not run finally blocks, and fail(), 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: the finally for the returning and throwing paths, and a process.on('exit') listener (plus explicit SIGINT/SIGTERM handlers, since a default-handled signal terminates without emitting 'exit') for the paths that exit outright. --keep still 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 -c count in BOTH directions, restores with git checkout HEAD -- ABSOLUTE_PATH, and proves the restore by comparing git hash-object against the HEAD blob hash plus an empty git diff HEAD. No build step is involved: both files are run directly from source by node / tsx, so there is no dist for a stale artifact to hide in.

ablation on-disk proof result
walker: e.name.startsWith('.')e.name === '.turbo' startsWith('.') count 1 → 0; injected .turbo count = 1 7 cases red, naming all three trees and both equality assertions; .turbo row and both positive controls stayed green
emitter A: delete removeBuildDirs(tracked, keep) from the finally count 1 → 0; marker = 1 2 cases red — "a thrown compile error left .examples-build behind", and the same for .examples-build-src
emitter B: delete the installBuildDirExitCleanup() call count 1 → 0; marker = 1 1 case red — "no 'exit' listener is installed"
restore, all three blob baa50df4… / ce7df47c… matched HEAD; git diff HEAD empty 582 cases pass / self-test EXIT=0

5. Verification

Exit codes captured before any pipe; verdict lines quoted from the gates themselves. All at ec0f7dc93.

command exit verdict
pnpm --filter @objectstack/spec run check:skill-examples 0 ✅ 257 prose examples type-check across 3 surface(s); no .examples-build* left
pnpm check:docs-audit-scope (in that order) 0 ✓ affected-docs self-test: 582 cases pass.
node scripts/docs-audit/affected-docs.mjs --self-test 0 ✓ affected-docs self-test: 582 cases pass. (was 568)
tsx scripts/check-skill-examples.ts --self-test 0 ✅ self-test: … full verdict line, extended with the residue clause
pnpm --filter @objectstack/spec typecheck 0 check:test-typecheck: OK — and tsc -p tsconfig.scripts.json --listFiles confirms packages/spec/scripts/check-skill-examples.ts IS in that program (1 hit), so this is not a phantom green
pnpm lint (whole repo, eslint . --no-inline-config) 0 clean
pnpm check:nul-bytes 0 plus a direct grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over both edited files: 0 hits (grep exit 1)
pnpm check:pm-dispatch-gates 0 1445 cases pass
derived family, node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack 0 63 runnable commands; 62 exit 0

NOT MEASURED, named rather than counted as green:

  • pnpm check:dual-build-cjs-loadsexit 3, PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/. It needs a full pnpm build; 32 packages are unbuilt in this worktree. Its own --self-test passed (93 cases) in the same run.
  • The 6 families whose argv takes a value from the workflow (check-cross-package-test-inputs --union-into "$RUNNER_TEMP/…", the three check-shard-attestation invocations, the two check-test-completeness invocations) — no value outside a CI run.
  • The 35 artifact-roster families dispatch-gates scores silent for 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's files array is dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.jsonscripts/ is not published, and neither is the repo-root scripts/. skip-changeset applied; pnpm check:published-files exit 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

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.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1be26b0de27b2c45d208a20edffefbac1e4892c2packageMentionDocs.

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff at ec0f7dc93 (2 files, +363/−104), not the report.

What the diff does, checked line by line

  • scripts/docs-audit/affected-docs.mjs:2091walkSourceFiles now prunes node_modules, dist, and any leading-dot directory, applied to directories only. The over-reach control (packages/foo/src/api.v2/real-route.ts) and the admitting-direction control (packages/spec/src/api/declarative-endpoints.ts) are both in the fixture, so the rule cannot widen past the leading dot or drop real source silently. The premise (zero tracked files under a dot-directory in packages/**) is a live pin in --self-test (git ls-files packages filtered), not a comment. Battery floors raised 67→80 and 23→24.
  • packages/spec/scripts/check-skill-examples.ts — the compile loop runs inside withBuildDirCleanup; each build dir is tracked BEFORE writeBuildDir creates it (:3129 onward). The finally covers return/throw; the process.on('exit') listener plus SIGINT/SIGTERM handlers cover fail()/refuse()/the staleness process.exit(1). --keep is honoured on every path. cleanStaleBuildDirs() sweeps a previous run's trees before the loop and announces it on stderr. The two trailing rmSync sweeps are gone, replaced rather than duplicated.
  • No changeset: neither file is published (packages/spec files excludes scripts/); skip-changeset is on the PR.

Measured by this seat on the PR head (throwaway worktree, PM checkout's node_modules):

node scripts/docs-audit/affected-docs.mjs --self-test   EXIT=0
✓ affected-docs self-test: 582 cases pass.

The check-skill-examples self-test imports typescript, which this checkout lacks — NOT MEASURED here; CI is the arbiter (Lint & Repo Gates in progress at review time).

Fences held: only the two files the card named; .gitignore untouched; hot-file scan across the 35 other open PRs found no overlap; git merge-tree origin/main clean.

Flipping ready + enabling auto-merge (squash). Fixes #15446 / Fixes #15457 close on merge.


Generated by Claude Code

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

2 participants