Skip to content

Fix #298: derive the pack list from git, not from a live-tree walk - #300

Merged
pseudoseed merged 11 commits into
mainfrom
builder/bugfix-298
Aug 31, 2026
Merged

Fix #298: derive the pack list from git, not from a live-tree walk#300
pseudoseed merged 11 commits into
mainfrom
builder/bugfix-298

Conversation

@pseudoseed

@pseudoseed pseudoseed commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Summary

The extension-retirement pack test derived what the root package ships by packing the live
working tree
, which other suites churn scratch directories under. npm pack stat'd a path that
had just been removed and the test failed — five consecutive full-suite runs, five failures,
blocking porch approve for every project in the workspace. The list now comes from the
repository (git ls-files), packed in an isolated fixture.

Fixes #298

Root Cause

packedFiles() ran npm pack --dry-run --json --ignore-scripts with cwd: workspaceRoot. The
root package.json declares files: ["*", "!apps/streamdeck", "!apps/vscode"], so npm-packlist
walked the entire tree.

Other suites in the same vitest run create and delete scratch directories under that tree —
19 distinct .test-* directories, every one resolved from process.cwd() (= packages/codev)
and created and removed per test or per test case. packages/codev/src/agent-farm/__tests__/pir-832-migration.test.ts:18
is the one that produced the reported error, but it is one of 19, which is why the failure is
reliable rather than occasional. When the walk lstat'd a path that had vanished between readdir
and lstat, npm exited non-zero, execFileSync threw:

npm error code ENOENT
npm error syscall lstat
npm error path .../packages/codev/.test-pir-832/state.db

Deterministic rather than flaky: the walk took ~62s with dist/ present, and those directories
churned many times inside that window.

What the live walk was actually answering

Comparing the live pack list against git ls-files:

  • 43 files packed that git does not track.builder-*, files under .claude/hooks/, and
    each package's node_modules/.bin/ shims. Working-tree litter, and exactly the class of thing
    that disappears mid-walk.
  • 55 tracked files not packed, every one explained by a real packaging rule:
    apps/web/.npmignore (node_modules, src, *.config.*, tsconfig*) and npm's
    always-excluded names (.gitignore, .npmignore, pnpm-lock.yaml).

The first group is the bug. The second is why the packaging rules were not reimplemented.

Fix

packedFiles() now:

  1. Takes the tracked paths from git ls-files -z at the workspace root.
  2. Materialises them under mkdtemp in the OS temp directory, where no other test can reach them.
    Real content only for package.json, .npmignore and .gitignore — the only files
    npm-packlist reads rather than stats — and an empty placeholder for every other path.
  3. Runs npm pack --dry-run --json --ignore-scripts in that fixture, then removes it.

The file set comes from git; the packaging rules stay npm's. Neither is reimplemented in the test,
so there is no second copy of the rules to drift.

Why the placeholder skeleton is sound

Its pack list is identical to the real tree's, minus the untracked litter: 3575 entries against
3575, zero divergence in either direction.
All four original assertions hold on it.

Those numbers were measured on an unbuilt tree. The fixture holds only tracked files, so it
cannot answer anything about build output
dist/, dashboard-dist/, v2-dist/ and
client-dist/ are gitignored, and a files array outranks .gitignore in npm-packlist, so a
live walk of a built tree carries them: 4,702 entries against the fixture's 3,575, 950 of the
difference being build output. That is deliberate, and it is stated in the test's doc comment.
All four assertions turn on tracked paths — the apps/web and apps/v2 manifests are tracked,
and 216 tracked apps/vscode/ files would appear the moment the !apps/vscode negation stopped
excluding them. A question about the built tarball needs a built tree, which is what
bugfix-214-publish-scrub.test.ts asks, per package.

The skeleton-versus-real-content identity is now an assertion rather than a one-off measurement. A
second test packs the same git-derived set with its real contents and requires an identical list,
so a future ignore mechanism that reads a file the skeleton blanks turns the suite red instead of
silently changing the answer.

It deliberately does not compare against the live tree, and the test comment says so: packing the
live tree is the walk that failed 5 of 5, so asserting against it would reintroduce #298 inside
the test written to close it — and the live-versus-git difference is the 43 litter files, i.e. the
bug rather than drift to guard.

Test Plan

  • Regression test added — derives the pack list from the repository, not from whatever is on disk creates packages/codev/.test-bugfix-298/state.db, of exactly the shape the 19
    churners create, and asserts it never reaches the list. Nothing in the repo gitignores
    .test-* and the root files is ["*", …], so the old walk packed it.
  • Fidelity guard added — packs the placeholder skeleton identically to the tracked tree with real contents.
  • Build passes — porch check bugfix-298: build ✓ 15.2s.
  • All tests pass — porch check bugfix-298: tests ✓ 213.9s.

Revert verification

Reverting packedFiles() to the live-tree walk:

  • The regression test fails:
    expected [ '.af-cron/ci-health.yaml', …(3620) ] to not include 'packages/codev/.test-bugfix-298/state.db'.
  • Under 4 parallel churners creating and deleting 5 scratch directories per iteration (~455
    iterations each), the pack test fails with the issue's exact error:
    npm error code ENOENT / npm error syscall lstat / lstat '.../packages/codev/.test-churn-1-4'.
    Bare npm pack under the same churn: 6 of 6 runs ENOENT.
  • With the fix restored and identical churn running, 3 consecutive runs: 7 of 7 pass.

Cost

The file goes from ~62s for one test to 10.4s for all seven.

Adjacent, not fixed

CMAP

Reviewer Verdict Disposition
Claude (Agent SDK) APPROVE Three minor observations; two applied (framed git failure, restored timeout rationale). The third — a residual ENOENT window in the full-content copy — was withdrawn by the reviewer after verifying no suite mutates a tracked file.
opencode (xai/grok-4.6) APPROVE No issues.
Codex (gpt-5.6-sol) REQUEST_CHANGES → addressed Both points applied: the codev/reviews/ artifact is removed (BUGFIX ships no artifact files; the issue and this PR body are the record), and this body now carries the Summary / Root Cause / Fix / Test Plan structure the protocol specifies.

Gemini was not run: this workspace's .codev/config.json sets
porch.consultation.models to ["claude", "opencode"], and Codex was added as the third lane.

pseudoseed and others added 7 commits August 31, 2026 13:25
`packedFiles()` ran `npm pack --dry-run --json` with `cwd: workspaceRoot`, which
walks the entire live working tree. Other suites in the same vitest run create
and delete scratch directories under that root — pir-832-migration.test.ts does
it once per test case — and when the walk lstat'd a path that had just been
removed, npm exited non-zero and the test failed. Five consecutive full-suite
runs, five failures, because the walk took ~62s with `dist/` present.

The list now comes from `git ls-files`, materialised as a placeholder skeleton
under `mkdtemp` that nothing else can touch, with real content only for the
files npm-packlist reads rather than stats: `package.json`, `.npmignore`,
`.gitignore`. The packaging rules stay npm's; nothing is reimplemented here.

The skeleton's pack list is identical to the real tree's minus untracked
working-tree litter — 3575 entries against 3575, zero divergence — and a second
test now asserts that rather than leaving it as a one-off measurement.

Regression test: a scratch file of exactly the shape other suites churn must
never reach the list. Reverting the fix fails it, and fails the pack test with
the issue's ENOENT under concurrent churn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex (REQUEST_CHANGES):
- Remove codev/reviews/bugfix-298-*.md. The BUGFIX protocol is explicit —
  "No spec, no plan, no artifact files: the issue is the spec." Its substance
  moves into the PR body.
- The PR body now carries the Summary / Root Cause / Fix / Test Plan structure
  the protocol specifies.

Claude (APPROVE, minor):
- `trackedPaths()` threw a raw execFileSync error where the npm call beside it
  framed its own. Now framed, naming the step and why git is needed.
- Restore the timeout rationale the #215/#216 comment carried, rewritten for the
  new cost model: the budget grows with the repo rather than with what happens
  to be on disk.

Also correct the root cause in the thread and PR body: pir-832-migration.test.ts
is one of 19 distinct .test-* scratch directories churning under packages/codev,
not the only one. That is why the failure was reliable rather than occasional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
Owner Author

Integration review, one reviewer (claude). Verdict APPROVE, nothing blocking. Full output in .consult-runs/integration-298.md.

Two small things worth taking before merge

packedFiles() now answers a narrower question than its name. The fixture holds only tracked files, so it cannot see build output. For the four assertions in play that is equivalent, since excluding apps/vscode and apps/streamdeck from root files excludes their dist/ too. The next person adding an assertion about a built artifact will get a silently wrong answer. One sentence in the doc comment closes it.

The 30s timeout lost its rationale. The old 60s carried a paragraph tying the budget to a measurement, and this deletes it without replacing it. This file's own convention is strong on that: spec-146:208 spells out "MEASURED, not guessed" with three timings. One line.

Confirmed rather than assumed

The fidelity test is the right shape. It does not require RULE_FILES to be provably complete; it detects the moment it stops being, which is the one risk the placeholder trick introduces, and a future npm change turns the suite red instead of quietly changing the answer.

The revert verification is real: regression test red on revert, the exact ENOENT reproduced under churn, 7/7 green with the fix.

This is now the only test deriving the root pack list. bugfix-214-publish-scrub, spec-146-phase-9-porch-engine, v2-packaging.e2e and dashboard-terminals all pack packages/codev against an explicit allowlist and are unaffected.

copyFileSync on gitlinks and symlinks: no .gitmodules exists, both failure modes are a loud throw rather than a wrong list, and dereferencing does not change npm's inclusion decision. No change needed.

Not this PR

.test-* is not gitignored, and roughly a dozen suites write .test-<id> scratch directories into the live tree. That convention is the root-cause class behind both #298 and #263. Filing it separately.

#297, the existsSync(apps/streamdeck) read in the same file, stays scoped out correctly.

Merge on green. Four projects are waiting behind this.

pseudoseed and others added 4 commits August 31, 2026 13:54
Two pre-merge asks from the architect.

The fixture holds only tracked files, so it cannot answer anything about build
output. Measured rather than asserted: a live walk of a built tree returns 4,702
entries against the fixture's 3,575, 950 of the difference being build output —
`files` outranks `.gitignore` in npm-packlist, so the gitignored `dist/`,
`dashboard-dist/`, `v2-dist/` and `client-dist/` ship in a live walk and never in
the fixture. The comment records that this is deliberate, that all four
assertions turn on tracked paths, and that the built-tarball question is asked by
bugfix-214-publish-scrub.test.ts instead.

The 30s budget now carries its measurement: 2.0s, split 0.3s to materialise 3,846
paths and 1.7s for npm's startup and walk. The old 60s existed because its cost
tracked whatever was on disk; this one tracks the tracked-file count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pseudoseed
pseudoseed merged commit d0c5f1e into main Aug 31, 2026
9 checks passed
pseudoseed added a commit that referenced this pull request Aug 31, 2026
pseudoseed added a commit that referenced this pull request Aug 31, 2026
chore(porch): bugfix-298 state commits stranded after #300 merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The npm pack test walks the live repo while other tests mutate it, failing deterministically and blocking every gate

1 participant