Predict the release commit from staged-blob truth - #17
Merged
Conversation
The dry-run tail promised a commit unconditionally while the executed path detects an empty cached diff and tags existing HEAD instead, so a re-cut preview named an action the release would not take. The manifest preview was worse than mislabelled: it compared canonical bytes against canonical bytes, so its formatting-drift label named an unreachable state while a noncanonical manifest at the same version really did stage a diff and commit. Both now decide the one question the executed path decides - would `git add` stage anything - through `git hash-object --path <file> --stdin` against `HEAD:<path>`, which applies the same clean filters and writes no object. The dry run consults `ensureCleanTree`, reports the refusal the real release raises, and predicts no landing on a dirty tree; the executed re-cut sentence moves behind a shared constant byte for byte, so preview and execution cannot drift. The mutation matrix that proved these behaviours load-bearing also exposed the reason the old reporting survived: the fake-adapter release contracts and the dirty-tree comparison were reachable only from a flag-gated scenario that nothing schedules. Both now run in the default suite, and the adapter's two fail-loud predicates are asserted against real git. The shim passes through only the three exact read-only argument vectors and refuses `-w`, index, and ref writes.
Round-1 review found the safety scenario could certify a false negative. The shim passes nested `node` argv through with the real PATH, because the scenario exists to run a real plugin gate, and an object write is the one mutation such a child could make that neither status nor refs reveal. Restricting the argv would make the harness brittle without closing the hole, so the before/after snapshot now counts objects: injecting one `hash-object -w` into that exact branch fails the scenario at the comparison, 2845 objects to 2846, while the real gate leaves the count byte-identical. The real-git fixtures inherited ambient Git configuration, so a developer's `commit.gpgSign` or `core.hooksPath` could fail or alter fixtures the test calls hermetic. Fixture spawns now null out global and system config and disable signing, hooks, and prompts, without ever mutating `process.env`; a hostile injected config failed the suite before this change and passes after it. The fixture whose purpose is a required-but-broken clean filter keeps its repository-local filter. The dirty-tree negative list stopped at add, commit, push, and tag, so moving the workflow or release forecast above the blocked return would still have passed. One list now covers every line the unblocked tail can print.
The dry-run truth work added real-git fixtures, and two of them let the developer's environment decide the result. `releaseTagExists` called `git ls-remote` directly, so the default suite path reached the network: `node scripts/ci.mjs` failed with "cannot reach origin to check whether plan-lifecycle--v0.6.0 is already released" whenever origin was unreachable. The consult is now the sixteenth closed-set operation, `tagPublished`, keeping the local-tag-then-origin order and the refusal text byte for byte, with the fixture stubbing it false. The same-version consult and the already-released refusal are now asserted instead of inherited. `git help config` documents that `GIT_CONFIG_KEY_<n>` pairs override every configuration file, a fixture's own `--local` settings included, so pointing `GIT_CONFIG_GLOBAL` at devNull was not isolation. The adapter spawns git with this process's environment and must keep doing so, because in production it has to predict what the operator's `git add` would stage; the boundary is therefore the test process, which now drops the pairs around the adapter contracts and restores every prior value. The shim matrix carries the same zero pair count.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
release.mjs --dry-runlied twice about a re-cut. The tail printed agit commitline unconditionally, while the executed path detects an empty cached diff and tags existing HEAD; and the manifest preview compared canonical bytes against canonical bytes, so itsnone — formatting drift!label named an unreachable state while a noncanonical manifest at the same version really did stage a diff and commit.Both sides now decide the question the executed path decides — would
git addstage anything — throughgit hash-object --path <file> --stdinagainstHEAD:<path>. That applies the same clean filtersgit addapplies and never passes-w, so no object is written. The dry run also consultsensureCleanTree, reports the refusal the real release raises, and suppresses the landing prediction on a dirty tree. The executed re-cut sentence moved behind a shared constant, byte for byte.Test-gap this exposed
The mutation matrix proving each behaviour load-bearing found two mutations the suite could not catch: the fake-adapter release contracts and the dirty-tree comparison were reachable only from
--dry-run-release-safety, a flag nothing schedules. Both now run in the default suite path, the adapter's two fail-loud git predicates are asserted against real git with hermetic fixtures, and the shim passes through only the three exact read-only argument vectors while refusing-w, index, and ref writes.Verification
node scripts/ci.mjsexits 0. All twenty-one acceptance rows pass, including the flag-gated safety scenario re-run from a clean clone of these bytes. Details in the record.Closes #16