Release 0.7.1 — the tier 0.7.0 announced, actually reachable - #486
Open
MongLong0214 wants to merge 19 commits into
Open
Release 0.7.1 — the tier 0.7.0 announced, actually reachable#486MongLong0214 wants to merge 19 commits into
MongLong0214 wants to merge 19 commits into
Conversation
0.7.0's headline change made `[directive]` reachable and it reached nobody. `commander` declares `--trusted-author` with a default of `[]`, so the flag arrives as an empty array rather than undefined when absent, `?? ` passes it through, and the fallback to the author `init` records is dead code. Every record on every install still graded `[claim]` -- the condition #415 was opened about, reintroduced one layer up by the fix for it. Resolution is on length now. An explicit flag is always non-empty and an absent one is always empty, whichever shape commander hands over, so the test matches what the caller meant rather than what the framework happened to supply. The tests that passed drove `buildInjection` with options assembled by hand and never crossed the command line, which is the only path the hook uses. The four new cases spawn the built CLI. They fail against the 0.7.0 build, which is the only property that makes them worth having. That file's header already carried the warning, one layer down: a unit test of `gradeRecord` would have passed throughout the period the original bug existed. The same sentence applied to `buildInjection` and I did not hear it. `package-lock.json` declared 0.1.0 while both manifests read 0.7.0, stale since the first release, and moves with them now. The CHANGELOG carries two corrections to 0.7.0's own review evidence: the promotion PR said 132 commits where the range holds 137, and seven install checks in RELEASE-GATE section 4 where it lists six. Both were mine. Limit: 0.7.0 stays published with its notes amended to name the defect at the top; retracting a tag people may already have installed trades a known-bad version for an unknown one Ruled-out: deleting the v0.7.0 tag and re-cutting it | rewriting a published ref breaks every clone that already fetched it, and the release notes can carry the truth without that Ruled-out: reading the flag with a nullish check and documenting the commander default | the documentation would sit in this file while the defect sits in every install, which is the arrangement that produced this Warn: any option declared with a non-nullish default has the same shape, and nothing checks for it -- this fix repairs one site, not the class Blast: system Undo: easy Certainty: firm Verified: the released 0.7.0 artefact renders `[claim]` for `inject --path` and `[directive]` only with an explicit flag, the patched build renders `[directive]` for the same plain invocation, and the four CLI-spawning cases pass alongside the six that already existed Provenance: authored Record-Id: r-rel071
The first fix repaired `inject` and its own Warn line said it repaired one site rather than the class. Sweeping the class found the second site immediately: `query.ts` resolved trusted authors with the same nullish check against the same commander default, so `commitlore context` reported `claim` for a record the hook rendered `directive`. The comment at that line already said the two routes must answer alike, or the grade means one thing on the hook and another on the terminal. That sentence was written before the fallback existed on either route, stayed true while neither had it, and became false the moment one did. A comment asserting a property is not the property, and nothing was checking. Two cases now compare the routes directly, configured and unconfigured, and they run the built CLI rather than the functions behind it -- the same reason the first four exist. Limit: the sweep covered `?? ` against an options field in src/commands, which is the shape that produced both defects; an option whose default is consumed some other way would not have shown up Ruled-out: threading a cwd parameter into queryOptions to match inject | this route resolves the repository it was invoked in and has no cwd of its own, so a parameter would exist only to look symmetrical with a function that needs one Ruled-out: deleting the comment now that a test enforces it | it explains why the routes must agree, which the test asserts but does not say Warn: both routes now read git config on every invocation, including the hook on the hot path of every edit -- one local config read, but it is new work in a place that is measured in milliseconds Blast: system Undo: easy Certainty: firm Verified: against the same repository the released artefact was reproduced in, `context` and `inject --path` both render directive with a configured author and both render claim without one; twelve cases pass in the trusted-author suite Provenance: authored Record-Id: r-queryroute
Release 0.7.1: the tier 0.7.0 announced was never reachable
CommitLore — record lintTrailers: clean — 19 commits in Active constraints for the paths this PR touchesLimits (188)
Ruled out (435)
Truncated: 416 lines omitted — the comment hit GitHub's 65000 character limit. Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR. |
The defect history of this command is a history of values held and not recorded. #49 had the redirected hook target in hand and printed it into prose. #128 and #149 failed a working hook by probing a synthesized path, which a row naming what was actually executed would have exposed on first read. #382 printed the stale pin inside its own ok line. #63 had the fetch exit code. #335 and #458 were verdicts built on counting a different convention's vocabulary. Every row now carries an evidence map beside its sentence: what was executed, what it returned, and the counts a conclusion rests on. A non-ok row must carry at least one key, so a claim with nothing behind it does not typecheck as a complete row. Process output is kept as a bounded excerpt rather than collapsed to present-or-empty, because hook-runtime and inject-runtime diagnose from stderr first lines -- the reference implementation sanitises there and this one reads there. Paths render home-relative so a pasted bug report does not carry someone's directory layout. The text report is byte-identical. The pinned snapshot is what says so. Limit: evidence answers what was observed, not whether the observation was the right one to make -- #335's wrong count would now be visible in a field rather than prevented Ruled-out: collapsing stdout and stderr to present or empty, as the reference does | it sanitises against paths and tokens it cannot inspect, while these checks diagnose from the first line and would lose the diagnosis Ruled-out: leaving evidence optional on non-ok rows | a conclusion without its observation is the shape every defect above shares, and optional means absent in the row nobody looked at Warn: the 200-character cap is a judgement with no measurement behind it, and a diagnostic whose useful part sits past it will be silently cut with only the paired truncated flag to say so Blast: local Undo: easy Certainty: firm Verified: thirteen of thirteen rows carry evidence with zero empty maps, no value contains an absolute home path, the excerpt cap and its paired truncated flag hold in both directions, and the text and check-order snapshots are unchanged across the change Provenance: authored Record-Id: r-evidence465
Make a doctor row carry the observation its verdict rests on
Section 8.2 promised zero writes. Writing section 11's read-only test strictly is what found the promise false: a plain run touches `index.db-shm`, and `--fix` creates that and `-wal`. Those are SQLite's bookkeeping for a WAL database, created by opening one at all, for readers as much as writers, and they hold no committed data. Two ways to close the gap. Making the run literally write-free means opening the index outside WAL for doctor, which trades a documentation problem for the concurrency one #420 was about. So the document moves instead, and states the invariant that carries the meaning: `index.db` is byte-identical after a plain run, including under failure, and nothing else in the repository changes but those two sidecars. The acceptance row moves with it. A table that still said "zero writes" while the test asserted something narrower would leave the next reader to discover which one to believe. Limit: the exception is named for SQLite specifically, so a future dependency that touches the filesystem on open would need its own line rather than being covered by this one Ruled-out: opening the index outside WAL so the promise becomes literally true | it reintroduces the concurrency failure #420 fixed, to make a sentence accurate Ruled-out: leaving §8.2 and scoping only the test | the document is what a reader checks the code against, and a test quietly narrower than the promise it implements is the arrangement this milestone exists to remove Warn: `--fix` creates the `-wal` file that a plain run does not, so the two paths differ in what they leave behind even though neither writes data Blast: local Undo: easy Certainty: firm Verified: the three read-only invariant cases pass against the amended wording, asserting `index.db` equality and a filtered inventory rather than an unqualified zero Provenance: authored Record-Id: r-prdwrites
Say what a read-only run actually does
The generated block declared M4 while the paragraph above it stated an M5 result. That gap was filed rather than closed during the release, because rewriting the declaration between a CI failure and a tag is the careless publication the declaration exists to prevent. There is no tag pending now. Repointing it found a second thing. The block reported 1,240 runs where the registered analysis reports 1,160, because the generator counted rows on disk and M5 has seven shards of which three are re-runs. A study that re-runs a shard has more rows than measurements, and a line reporting the file count invites a reader to take the larger number for the study's size. The fix is in the generator rather than in the number. It now counts distinct task-arm-seed cells the way bench/m5-analysis.ts does, and says both figures with the difference named, so the published line and the registered analysis cannot disagree without one of them changing. The status note carries what the M4 note carried: why three shards are re-runs, where the supersession rule lives, the excluded error row, the unequal truncation and which direction it pushes, and that every record rendered `[claim]` so this is not the `[directive]` path 0.7.1 made reachable. M1, M1-b, M2 and M4 stay unpooled for the reason the M4 note already gave. Limit: the seven shards are declared individually, so a shard added later is invisible to this block until someone lists it -- which is the property the declaration was built for and the cost that comes with it Ruled-out: globbing bench/results for m5-*.jsonl | the file next to them is a withdrawn design log, and a glob is how the wrong dataset gets published without anyone deciding to Ruled-out: reporting only the 1,160 and dropping the row count | the rows are on disk and a reader who counts them would find the block understating; naming both and the reason is what makes either checkable Warn: the generator's supersession key is task, condition and seed, which is M5's cell shape; a study whose cell is keyed differently would silently collapse rows that are not duplicates Blast: local Undo: easy Certainty: firm Verified: 1,240 rows across the seven declared shards resolve to 1,160 distinct cells with 80 superseded, matching the registered analysis exactly; the gate passes with no --write and all four READMEs regenerate identically Provenance: authored Record-Id: r-m5sources
Publish the study the README's claim actually rests on
One dead hook runtime produced four independent-looking findings, and a reader had no way to tell the cause from the consequences. A dependent whose failure only restates its dependency's now carries `blockedBy` naming the root, so a fix plan can name causes once instead of repeating a symptom per row. The rule the whole milestone rests on is that annotation is not suppression. A blocked row keeps its status, its detail and its own evidence, and still appears in the report. A suppressed row is how a diagnostic lies by not looking, which is the defect #458 was, and a collapse that hid rows would have reintroduced it while claiming to reduce noise. The constructor refuses to name an `ok` blocker rather than trusting callers to avoid it. A collapse that points at a healthy check is worse than no collapse: it tells a reader to fix something that is not broken, and nothing downstream can detect the mistake. Independence is preserved deliberately. A stale stub whose runtime is also dead keeps its own finding unannotated, because that failure survives fixing the runtime and a fix plan that dropped it would leave the user with a repaired dependency and an unrepaired stub. Limit: only two edges are declared -- inject-version on inject-runtime, and the §2.2 checks on the capture chain -- so a dependency nobody wrote down still surfaces as an independent finding Ruled-out: declaring commit-msg-hook on hook-runtime to complete the graph | that edge runs backwards against registry order, and a declared edge the emission order cannot satisfy is a lie in the structure the fix plan walks Ruled-out: omitting a blocked row from the report and keeping it only in the JSON | the text report is what a user reads, and a row missing from it is indistinguishable from a check that was never run Warn: `blockedBy` resolves chains to the root, so a two-link chain reports the far cause and the intermediate failure is visible only as its own row -- correct for a fix plan, and surprising if read as a parent pointer Blast: local Undo: easy Certainty: firm Verified: thirteen rows in and thirteen out with collapse active, no row omitted and no `blockedBy: null` in the serialized report; a blocked dependent keeps its detail and evidence, a chain resolves to its root, an independent failure stays unannotated, and naming an ok blocker throws Provenance: authored Record-Id: r-collapse466
Say which failure is the cause and which are its echoes
The gate asked whether the plugin entry point resolved, and it passed for two releases while resolving to the wrong thing. Found running it against a fresh v0.7.0 clone on a machine carrying a 0.6.0 install: the clone answered 0.7.0 and the entry point answered 0.6.0, both exiting zero. `commitlore-run.sh` tries `commitlore` on PATH before CLAUDE_PLUGIN_ROOT, and that order is deliberate -- the installer's wrapper execs node itself, so it works where this script would otherwise have to find node, and on the hook hot path a missing node means no context at all. The order stays. What changes is that the gate narrows PATH and compares the version, so it asks whether the right thing resolved rather than whether something did. Two cases pin both halves. The entry point reaches the plugin when nothing shadows it, and a commitlore on PATH wins when one does. The second is not a bug being enshrined: it is the documented order, asserted so that changing it becomes a decision someone makes rather than a side effect of an unrelated edit. This is the smallest of the three options #483 recorded, and the only one that does not touch the hot path. Making the mismatch loud at runtime would mean running --version twice on every edit; preferring CLAUDE_PLUGIN_ROOT would reopen ADR-0011's relationship with the installer, which is a decision rather than a patch. Limit: the gate now catches the mismatch at release time, and doctor's inject-version check catches it for a user, but nothing tells a user who has never run doctor Ruled-out: preferring CLAUDE_PLUGIN_ROOT over PATH | it reopens how ADR-0011's ship-dist-in-the-repo interacts with the installer's wrapper, and the node-resolution advantage that motivated the current order would need re-examining rather than reversing Ruled-out: comparing versions inside the run script | it sits on the hot path of every edit and would spend a second process launch there to report a condition doctor already reports Warn: the narrowed PATH in the gate includes the interpreter's directory, so a machine whose node lives beside a commitlore wrapper still shadows the plugin and the check will pass for the wrong reason Blast: local Undo: easy Certainty: firm Verified: with PATH narrowed the entry point answers the repository's own version, and with a stub commitlore ahead of it on PATH it answers the stub -- both asserted, so the documented precedence cannot change silently Provenance: authored Record-Id: r-plugres483
Ask the release gate which commitlore the plugin actually ran
The release gate compared the tag, `package.json` and `commitlore --version`, and never opened `.claude-plugin/plugin.json` or `package-lock.json`. ADR-0026 makes the plugin the canonical install path, so the one manifest a plugin user's install resolves was the one the gate did not look at. That is not hypothetical. `package-lock.json` declared 0.1.0 from the first release through 0.7.0 while both manifests moved, and this gate passed every time, because it does not look there. A human found it by reading the file. The two lock fields are checked separately because they went stale separately: the root `version` and `packages[""].version` are independent, and a report naming only one sends an operator to fix half of it. A missing manifest is a failure rather than a skip. The defect being repaired is a gate that stayed quiet about a file it did not read, and a gate that stays quiet when the file is absent is the same gate. Limit: the gate now covers every versioned manifest that exists today, found by searching for `"version"` across the tree -- a manifest added later is not covered until someone adds it here, which is the same standing every declaration in this repository has Ruled-out: globbing for JSON with a top-level `version` key | node_modules and generated fixtures carry that shape, and a gate whose scope moves with the working tree fails differently on a clean clone than on a developer's machine Ruled-out: warning on a missing manifest and continuing | the tag is immutable once fetched, so a release that publishes past an unreadable manifest cannot be corrected afterwards Warn: the check runs against the built `dist` in the workspace, so it qualifies the artefact the workflow just built rather than one downloaded from the release -- the fresh-clone checks are what cover that direction Blast: system Undo: easy Certainty: firm Verified: five of the six new cases fail against the previous script and all six pass against this one; `check-release-version.mjs v0.7.1` reports all five sources agreeing at 0.7.1 in a tree where they do, and reports every disagreement rather than the first when three are seeded Provenance: authored Record-Id: r-vergate492
`publish` depended on `version-consistency` and nothing else, so pushing a tag created the GitHub Release and the six checks that decide whether it deserved to exist ran afterwards, if the operator remembered. That is how 0.7.0 was published with its headline feature broken: the checks were a written procedure, and a procedure is not a dependency. Section 4 is now the `install-gate` job and `publish` needs it. All six rows are automated and every one of them is blocking; none was dropped or made advisory, because a gate claiming six checks while running four is worse than one honestly running five. Two rows needed real fixtures rather than approximations. The PATH-less commit runs under `env -i` so node is genuinely absent, which is what makes a rejection evidence that the stub used its recorded interpreter instead of an ambient one. The stale-hook row installs a healthy stub first and then replaces only its body, so the fixture is a stale stub rather than a missing one. That second fixture also produces a hook-runtime fail of its own, and doctor exits 1 on it. The row being qualified is `commit-msg-hook`, whose required verdict is `warn`, so the check reads that row out of the JSON rather than the process exit -- reading the exit here would have qualified the wrong thing. The clone is of the pushed tag from the remote and runs the shipped bundle with no build and no install, because building in this job would qualify an artefact the tag does not carry. Limit: the job runs on ubuntu-latest only, so it qualifies the POSIX install; the Windows path is still covered by `install-ps1` in CI against the branch rather than against the tag Ruled-out: reusing the workflow's checkout instead of cloning | the row asks whether a fresh clone works, and a checkout is not the thing users get Ruled-out: making the stale-hook row non-blocking because its fixture also fails another check | the confounding finding is separable by reading the row, and a row excused for being awkward is the fail-open being repaired Warn: `publish` now waits on a job that clones over the network, so a transient clone failure withholds a release for a tag that is already pushed -- the tag is the durable thing and re-running the job is the recovery Blast: system Undo: easy Certainty: firm Verified: the six rows run end to end against a fresh clone and exit 0; the workflow loads as YAML with publish declaring both dependencies; and the gate is not vacuous -- breaking the doctor row exits 1, and comparing the plugin version against a wrong expectation exits 1 Provenance: authored Record-Id: r-failclosed493
The section-4 job made the installation a prerequisite, and a review found the gap that left: the workflow accepts every `v*` tag. Nothing asked whether the tagged commit is on the release branch, and nothing asked whether CI passed at that exact commit. Tagging any dev commit published it, and tagging a commit whose CI failed published that. Two scripts rather than two `if:` expressions, because a condition in YAML cannot be tested and these are the conditions that decide whether a wrong artefact reaches users. The CI gate asks whether the required checks passed, not whether any failure is visible. Everything that is not a success refuses: failure, cancelled, timed out, skipped, queued, still running, and a required check absent from the response entirely. An empty result set is a refusal rather than a quiet pass -- that inversion is the defect this repository has now published twice, and it is the reason the required list is declared in the script instead of inferred from whatever happens to be present at the commit. The ancestry gate refuses when it cannot see enough history to answer, so a shallow checkout produces a refusal rather than an accident. The job checks out with full depth for the same reason. `publish` now waits on all four, and has no `if:`. An omitted dependency is itself a bypass: nothing can withhold a release for a job it was never asked to wait for, so the test asserts the complete set rather than the absence of an escape hatch alone. Limit: the gates qualify the commit and its CI, not the tag's authorship or timing -- anyone who can push a `v*` tag to a qualified commit on main can still publish, and expiring or re-pointing a tag after these jobs pass is outside what any of them observe Ruled-out: expressing either check as an `if:` on publish | a condition in YAML has no test, and a release gate whose logic cannot fail in a suite is a claim rather than a control Ruled-out: treating a missing required check as nothing to report | that is the empty-set inversion above, and it is exactly how a release with no CI at all would have published Warn: the required-check list is a literal, so a job renamed in the CI workflow becomes a check this gate waits for and never finds -- it fails closed, which is right, and the failure will read as a CI problem rather than a rename Blast: system Undo: easy Certainty: firm Verified: fifteen cases pass, and the two scripts were driven directly -- a tag on main accepts, a tag on a side branch refuses, a shallow clone refuses rather than passing, six successes accept, and failure, cancelled, timed out, skipped, in-progress, an absent required check, an empty payload and successes recorded for a different commit each refuse Provenance: authored Record-Id: r-pubprereq
Check the manifest the canonical install path actually resolves
Qualify the release before it exists rather than after
Refuse a tag that is not on main or whose CI did not pass
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.
Promotion of
devintomainfor v0.7.1. Patch on the released 0.7.0.Why this exists
0.7.0 shipped with its headline change non-functional. The review verdict on that promotion was FAIL and named the defect; the promotion had already been merged and tagged before the verdict arrived. That sequencing failure is recorded in #484 and in the CHANGELOG, not only here.
The defect, and the one the sweep found
commanderdeclares--trusted-authorwith a default of[], sooptions.trustedAuthor ?? configuredTrustedAuthors(cwd)never fell through. Every record on every install still graded[claim]— the condition #415 was opened about, reintroduced one layer up by its own fix.The first commit's
Warn:said it repaired one site rather than the class. Sweeping the class foundquery.tsimmediately:commitlore contextreportedclaimfor a record the hook rendereddirective, while the comment at that line already asserted the two routes must answer alike. A comment asserting a property is not the property.Verified against the released artefact
Six new cases spawn the built CLI. They fail against the 0.7.0 build, which is the only property that makes them worth having — the tests that passed drove
buildInjectionwith hand-assembled options and never crossed the command line, which is the only path the hook uses.Also
package-lock.jsondeclared0.1.0while both manifests read0.7.0. Stale since the first release.RELEASE-GATE.md§4 lists six. Both were mine.Evidence at this exact head
Sequencing for the tag, corrected
The verdict's second blocker was that the tag publishes before the post-tag fresh-clone gates, making publication fail-open. This time the release notes are not attached and the release is not announced until
RELEASE-GATE.md§4 has run against a clone of the tag. The tag itself still auto-creates a release — that is the workflow, not a choice — so the gate runs immediately after the push and before any notes go on.0.7.0 is not retracted
Its notes carry the defect at the top with a reproduction and a pointer here. Deleting a published tag breaks every clone that already fetched it.
Stated costs
Both routes now read git config on every invocation, including the hook on the hot path of every edit. One local config read, but new work in a place measured in milliseconds — for #472's budget to bound.
The sweep covered
??against an options field insrc/commands, the shape that produced both defects. An option whose default is consumed some other way would not have shown up.