Skip to content

Run the result-schema gate, over a scope a new file lands inside by default - #394

Merged
MongLong0214 merged 1 commit into
devfrom
fix-issue-392
Aug 1, 2026
Merged

Run the result-schema gate, over a scope a new file lands inside by default#394
MongLong0214 merged 1 commit into
devfrom
fix-issue-392

Conversation

@MongLong0214

Copy link
Copy Markdown
Owner

Closes #392.

bench/verify.mjs had no npm script and no CI step, so the result schema drifted five fields behind the runner and a committed matrix failed the gate for two days with nobody looking. #390 fixed that drift; this fixes the reason it went unnoticed.

The gap, confirmed

$ git rev-parse --short HEAD          # dev
85ea006
$ grep -rn "verify.mjs" .github/workflows/ package.json
$ echo $?
1

Reproduced at 6d4e828^ (the commit before #390 fixed the schema):

$ node bench/verify.mjs bench/results/m5-off-design-20-tasks.jsonl
FAIL …/m5-off-design-20-tasks.jsonl:1 / must NOT have additional properties; …
FAIL …/m5-off-design-20-tasks.jsonl:2 / must NOT have additional properties; …
…
verify: 80 problem(s) across 80 rows

Survey of bench/results/

17 *.jsonl files, 802 rows, and two row families that share a directory and nothing else.

family files rows schema_version written by verify.mjs before this PR
run records (RunRecord) 9 632 absent on every row bench/runner.ts 3 pass, 6 fail
metric rows (BaseRow) 8 203 present on every row bench/deterministic.ts, bench/external/run.ts 8 fail

The 6 failing run-record files fail on one axis only: harness_commit and dist_digest, which 1073fa4 made required on 2026-07-27. The 8 metric-row files fail on run_id, task, cond and the rest — they are one row per measurement, not per (task, condition, seed) run, so result.schema.json was never meant to describe them.

Scope, and why

The gate is default-in: npm run bench:verify reads bench/results/*.jsonl, all of it. Nothing registers, nothing opts in, and a new results file is gated the moment it is committed. Two exemptions, both decided per row rather than per filename, both printed on every run. The rule for each is stated at the top of bench/verify.mjs, where the scope is defined.

1. A file whose every row carries schema_version holds metric rows, and this schema does not describe them. The field is the discriminator because both sides guarantee the answer: BaseRow declares readonly schema_version: 1, and result.schema.json is additionalProperties: false with no such property. Note the direction — the marker belongs to the other family, so the gate reads its absence as inclusion. Keying inclusion on its presence, as the issue suggested, would select exactly the files this schema cannot describe. A file carrying it on some rows and not others is failed, not classified — that is a corrupt file, and it closes the only accident by which a run-record file could leave the gate.

2. Rows recorded before 1073fa4 are not required to carry the two provenance fields. Those six matrices cannot be corrected — bench/results/ holds committed measurements, and ADR-0018 is why a superseded or invalidated matrix stays in the tree. They are not skipped and the schema is not weakened: the gate compiles a second validator from the committed schema with exactly those two required entries dropped. Every other constraint — every type, every pattern, the closed property set — still applies to all 380 rows. The cutoff is not a judgement call: the last pre-provenance row started at 2026-07-27T02:21:50.808Z, the first post-provenance row at 2026-07-27T07:21:31.025Z, and 1073fa4 is dated 2026-07-27T02:56:12Z, inside that gap. It cannot creep forward, because started_at comes from the clock.

Ruled out, in the commit trailers: a declared file list (opt-in under another name), validating only what passes today (a description, not a rule), a naming convention (every run-record file in the tree was named by hand through --out), a marker file (still a step to remember), widening the schema to cover both families (they share no required field, so the union rejects nothing), and making the provenance fields optional (weakens every future row to admit six old files).

The gate bites

A scratch file added to bench/results/, carrying one field the schema does not declare:

$ npm run bench:verify
…
skip …/token-ledger-20260801T122953Z.jsonl — 1 metric row(s) (schema_version present); result.schema.json describes run records
FAIL …/zz-scratch-gate-proof.jsonl:1 / must NOT have additional properties

verify: 1 problem(s) across 633 rows
exit=1

Removed again:

$ rm bench/results/zz-scratch-gate-proof.jsonl
$ git status --porcelain bench/results/     # clean
$ npm run bench:verify
…
verify: 632 rows in 9 file(s) valid against result.schema.json; 8 metric-row file(s) out of scope
exit=0

Eleven tests in test/bench-result-scope.test.ts hold the rules, including the one that matters most: the gate's output must account for every .jsonl on disk, so replacing the directory scan with a list, a prefix filter or a "passes today" set fails immediately.

Verification

npx vitest run — Test Files 83 passed, Tests 2092 passed | 1 skipped. npm run typecheck, npx tsc -p bench/tsconfig.json --noEmit, npm run build (dist/ byte-unchanged), node scripts/check-readme-numbers.mjs, bash spec/verify.sh (OK: 26 fixtures), npm run bench:verify — all exit 0. node dist/commitlore.mjs validate -c HEAD — shape ok, references ok.

No file under bench/results/ was modified, and neither README.md nor any translation was touched.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

CommitLore — record lint

Trailers: clean — 1 commit in origin/dev..67f4375faab613c9f2c42537bba06fa0fc773fe1
Active constraints: 47 limits · 129 ruled-out · 33 warnings — from 62 records over 5 changed paths

Active constraints for the paths this PR touches

Limits (47)

  • r-benchscope 67f4375 — nothing checks the shape of the eight metric-row files. This gate names them and steps over them, and bench/deterministic/types.ts is the only definition that family has -- there is no JSON schema for it, so drift on that side is still invisible
  • r-benchscope 67f4375 — the pre-provenance exemption reads started_at, which is data on the row rather than a fact about the file. A row that misreported it would be held to the shorter list of requirements; that is a deliberate falsification rather than the omission this fixes, and nothing here detects it
  • r-4e7c19 6d4e828turn_usage carries the provider's tokenizer; every figure in bench/TOKEN-LEDGER.md is in the product's own ceil(chars / 4) proxy. A future W4 must be reported beside the floor, never added into it, until both sides are on one tokenizer
  • r-4e7c19 6d4e828content_blocks says a turn called a tool, not which tool or on what — the tool_use block's name and input are discarded, so reproposed counts correct behaviour as failure — measure rejected work, not rejected mentions #141's rejected-path tool actions stay uncounted
  • r-3c9d52 dc9e769 — the sweep is two git log calls per path and the delivery phase runs git log --follow on every tracked path, so a full run over the four externals is hours rather than minutes on one machine
  • r-tokenledger 298e47c — both sides are byte-derived token proxies under CHARS_PER_TOKEN=4, so the ratio cancels a uniform tokenizer error and does not cancel a differential one between prose and diffs
  • r-rel060 e999b9d — the install one-liner in all four READMEs now points at a tag that does not exist until this is tagged, so the window between merging to main and pushing v0.6.0 is one where the documented install is broken
  • r-release051 19810d2 — the hook is written at install time, so no release repairs a repository that already has one; every release touching hook behaviour has to restate what does
  • r-convertreadme e12c816 — a README claim about the default workflow is only true if the shipped skill performs it, and the skill currently requires the user to name CommitLore first
  • r-release050 ad402c7 — the hook is written at install time, so a corrected release never reaches a repository that already has one; every release fixing hook behaviour has to say what repairs an existing install
  • r-winsupported1124 6333251 — repositories that installed the hook before Windows: the commit-msg hook hangs instead of returning, and #71's containment can never match there #321 keep the old stub and must re-run commitlore hooks install; a corrected release does not reach them, and this row's claim is about a working install
  • r-winderive1124 282693b — a diagnostic that retypes the code it describes goes stale silently, because nothing fails when the two drift apart -- it has to be read out of the artifact that ships
  • r-winbound1124 156deed — an unbounded hang is not evidence; it is a job that dies at the runner timeout having printed nothing, so every hook-invoking commit here has to carry its own bound
  • r-winassert1124 616005d — an assertion whose only oracle is an absent side effect cannot distinguish "refused" from "the mechanism never worked", so each one needs a positive control that fires before the attack
  • r-winshell1124 aa68a9a — a GitHub Actions bash step is invoked as bash -eo pipefail, so set -uo pipefail inside the step does not clear -e and any bare command that fails ends the step at that line
  • r-ps1scope282 72f23df — the hook-runtime probe spawns /bin/sh, so it cannot report on a Windows hook at all until T-1124 changes it
  • r-ps1shell282 97735d6 — a step's shell key takes no expression, so a per-host matrix cannot select the interpreter
  • r-t1120nodeinst 14deeb4 — git and node are hard prerequisites now, so a host without them installs nothing and says which one is missing
  • r-rel041notes 71efe1f — 0.4.1 makes the installer honest about a verification it cannot complete rather than fixing the kill, so an upgrading user may still see the unverified message instead of a version
  • r-rel040notes 5d57a72 — the 26.3-point density gap quoted in the notes is measured at this head and will drift with merge volume; it is illustrative of the denominator problem rather than a stable figure
  • r-m4basis 5e2d2cb — the guard question stays unanswered until the exposure instrument is verified and M4 is rerun on it
  • r-m4withdraw e5f9b73 — the guard question is now unanswered rather than answered null
  • r-instpath119 9e1fce7 — a user who ignores the printed line still gets "not found" on the next command
  • r-rel021a a79e350 — v0.2.0 remains on the remote with no release attached
  • r-expomerge1 d6ad014 — M4's existing rows have no exposure field and must read as unknown, not as not-exposed — backfilling by inference would erase the finding
  • r-exposure52 ba69411 — legacy JSONL artifacts predate model and guard-exposure fields | their absence remains unknown and is never inferred or backfilled
  • r-fix93pkg 9c4a396 — package.json remains a development artifact (build, typecheck, dependency floor) -- it is not read as a distribution manifest by anything in this repository
  • r-relinstall c6e1d04 — never tested against the real GitHub release infrastructure (no release exists yet — that is the owner's action) — verified against a locally built SEA binary, a hand-made SHA256SUMS, and a local HTTP server standing in for GitHub's release-asset redirects, which is everything this repository lets a change verify before a tag exists.
  • r-det058 695cdf6 — the suite must need no model, agent, network or uncommitted benchmark input
  • r-7a3e91 cf859e4 — better-sqlite3 stays external because it is native — the bundle degrades to --no-index without it, which only works because r-6f2a08 made that load lazy first
  • r-3b8f52 1f8b4be — the figure is one machine, one run — it establishes the order of magnitude, not a regression baseline anyone should tune against
  • r-3e8a41 2f0a8a0 — scoping is not implemented, so off-path records reach every arm
  • r-9c2f74 d653153 — the ablation arms cannot discriminate on these fixtures -- no-grade and no-lifecycle are byte-identical to the treatment in 9 of 10 tasks, because the seeds carry one reconstructed record and one task with a lifecycle trailer between them
  • r-9c2f74 d653153 — the harness assembles its own projection rather than calling the shipped injector, so what is measured is the harness's rendering of the records, not src/core/inject.ts (issue B-08 · Replace the benchmark harness injector with the actual src/core/inject.ts #36)
  • r-4a8e15 49e12c7 — git's grammar requires a subject before a trailer block, so a serialized block is not by itself a parseable message
  • r-0c5d38 aeb54a6 — the suite runs against the source tree, so no test in it can observe what packaging drops
  • r-3f7a29 49817dc — reconstruction reads text written before the protocol existed, so the evidence is thinner than a harvest and the discard rate is expected to be high
  • r-6f2e58 ea9ae6d — a library test and a binary test cover different failures, and the packaging layer between them is exactly where a working module becomes a broken install
  • r-5c8b31 60ddc39 — the agent CLI exposes no in-flight turn limit, so a per-task turn budget can only ever be observed with this driver
  • r-7e5f02 e5f5e00 — npm installs through an engine mismatch, so the ecosystem's own signal cannot be relied on to stop anything
  • r-3a9d68 6a3fc3b — a test runner reports what ran, and nothing in its summary distinguishes "did not run" from "does not exist"
  • r-0b7c44 d2b2ce3 — a command is only real once --help names it, because that is where users look before they read source
  • r-6c2b95 5da793c — the CLI holds no API key, so any real driver runs through the user's own agent session and cannot be exercised in this environment
  • r-8e2d51 ef93c0e — git is the parser, so a git version that folds or bounds trailers differently is a correctness problem for us, not a compatibility footnote
  • r-5a8c04 c46a577 — git owns the definition of a trailer block, so any behavior we cannot get from interpret-trailers is behavior we must not invent
  • r-c0f4e2 3d249cd — npm gitlore is held by an active same-domain CLI, so the owner's first-choice name was not available
  • r-b2e7f1 00d348d — Parsing must delegate to git interpret-trailers -- reimplementing the block rules would drift from the rest of the git ecosystem

Ruled out (129)

  • r-benchscope 67f4375 — A declared list of result files, in the gate or in a manifest | opt-in under another name, and a file left off it is silently ungated -- which is precisely the failure bench/verify.mjs is not run by anything, so the result schema drifted unnoticed #392 reports
  • r-benchscope 67f4375 — Validating only the three files that pass today | a description of the present rather than a rule, and it goes stale the first time a matrix is added or the schema moves
  • r-benchscope 67f4375 — A naming convention on results filenames | the runner's default output is an ISO stamp plus hex and the metric writers' is a family name plus an ISO stamp, but every run-record file in the tree was named by hand through --out (t702-m2, m5-off-design-20-tasks), so the convention is enforced nowhere and cannot carry a gate
  • r-benchscope 67f4375 — A marker file dropped beside each gated result | still a step somebody has to remember, and a forgotten step is the defect being fixed
  • r-benchscope 67f4375 — Widening result.schema.json to describe both row families | they share no required field, so the union would accept a run record with no task and a metric row with no metric, which is a schema that rejects nothing
  • r-benchscope 67f4375 — Making harness_commit and dist_digest optional in the schema | weakens the gate for every future row in order to admit six historical files, and Make per-turn token usage observable, and say what it still cannot price #390's drift was an additionalProperties failure that a laxer schema catches later rather than sooner
  • r-benchscope 67f4375 — Skipping the six pre-provenance files whole | cheaper to write, but it stops checking types, patterns and the closed property set on 380 rows in order to excuse two absent fields
  • r-4e7c19 6d4e828 — turning per-turn capture on by default | it changes the CLI invocation, and a harness flag that quietly rewrote how a run is observed would make old and new rows two different things under one name
  • r-4e7c19 6d4e828 — summing the usage on assistant events | they repeat once per content block and their output_tokens is a pre-generation snapshot; that reading is wrong by 397 of 403 on the one probe that checked it
  • r-4e7c19 6d4e828 — writing turn_usage on every row with nulls when uninstrumented | an always-null field is worse than no field, and absent already reads as "not instrumented" everywhere else on this row
  • r-4e7c19 6d4e828 — retaining the whole stream to parse at the end | partial messages emit an event per chunk, so a long run would hold tens of megabytes to produce a few hundred integers
  • r-3c9d52 dc9e769 — reimplementing the scorer for the external corpus | two scorers that drift by a line produce two numbers that look comparable and are not, which is the failure No-index query at 100k regressed from 7.7s to 90s — cause not yet identified #163 already recorded here
  • r-3c9d52 dc9e769 — replacing the per-path --follow chain with one forward pass over a rename graph | it is much faster and it is a different attachment predicate from the one DECISION-DELIVERY.md §3 registered, so the two corpora would stop being comparable
  • r-3c9d52 dc9e769 — turning the census notes option on by default | every measurement registered before it existed read commit messages only, and a denominator that silently grew would invalidate them without saying so
  • r-tokenledger 298e47c — Remeasuring decision_delivery inside this harness | it would price the read side on a corpus that already includes the commits adding this measurement, and docs/evidence.md would cite two corpora for one ratio
  • r-tokenledger 298e47c — Reporting a single write-side accounting | issue The break-even figure does not say which token accounting it uses, and both accountings are defensible #138 established that a break-even which does not say what it counted is underspecified, so the diff term is reported both charged and free
  • r-tokenledger 298e47c — Estimating the drafting turn from the 112 recorded runs | those rows carry a session total from --output-format json with no per-turn attribution, so any split across turns would be invented
  • r-rel060 e999b9d — 0.5.2 | three observable behaviour changes and one narrowed acceptance rule are not a patch, and an upgrader reading the number would not go looking
  • r-rel060 e999b9d — Publishing the delivery figure in the README | the measurement is one corpus and one repository, and the README already carries more numbers than a first-time reader can weigh; docs/evidence.md and bench/ hold it
  • r-release051 19810d2 — Waiting for 0.5.2 to carry index ingests any key: value line as a trailer; doctor reports 106 records where git has 0, and context serves commit subjects to the agent #335 | false decision context reaching an agent is the inversion of the product claim, and a stable release should not hold it
  • r-release051 19810d2 — 0.6.0 | no command is added and nothing breaks; calling it minor would hide a correctness fix behind a feature number
  • r-release051 19810d2 — Filtering Verified: because a report listed it as noise | it is vocabulary, and separating a release-note use from a record use needs a guess that can discard real records
  • r-convertreadme e12c816 — Adding "Keep coding normally" now | the commit skill still triggers on explicit requests and teaches harvest; shipping the sentence first repeats the doc-ahead-of-code failure this session fixed twice
  • r-convertreadme e12c816 — Writing testimonials to fill the social-proof section | one real field report exists, and fabricated or padded quotes would cost the claim honesty that is this project's strongest asset
  • r-convertreadme e12c816 — Moving the benchmark block and protocol detail out of the README now | the generated block is byte-compared by check-readme-numbers and the guard figures are asserted inside the limitations slice; the move is worth doing and is worth its own change
  • r-convertreadme e12c816 — Dropping "decision authority" everywhere | it is the right term in the architecture sections; it is only the wrong term for a first screen
  • r-release050 ad402c7 — 0.4.2 | a new command and a platform support claim are not a patch, and calling them one hides both from anyone reading version numbers to decide whether to upgrade
  • r-release050 ad402c7 — 1.0.0 | nothing here breaks an existing install, and the milestone that closed is Gate B rather than a stability commitment
  • r-release050 ad402c7 — Describing Windows as supported without the caveat | the word is the one most likely to be read as more than it is, and the release that earns it is the right place to bound it
  • r-winsupported1124 6333251 — Flipping the cell on the throwaway verification head | the ticket requires the attacks to pass in a required job in this ticket's own pull request, and a head that exists to be discarded is not that
  • r-winsupported1124 6333251 — Leaving the assertion pinned and editing only the document | the two contradict each other, so the suite would fail; a guard that blocks the change it was guarding for is a defect in the guard
  • r-winsupported1124 6333251 — Widening this ticket to the hook fix so it could be self-contained | the stop condition sent that to T-1127 for good reason, and this ticket's job was to measure and then say the word once measurement allowed it
  • r-winderive1124 282693b — Updating the two hardcoded lines to match the fixed stub | it would be correct today and wrong at the next change, for the same reason it is wrong now
  • r-winderive1124 282693b — Dropping the two diagnostics and relying on the commit alone | the commit says whether the hook worked, not which branch decided it, and that is the reading the next Windows question starts from
  • r-winbound1124 156deed — Fixing the walk in src/hooks/commit-msg.ts as part of this change | the stub is not this ticket's to own, and the ticket says to record a Windows failure as its own issue rather than absorb it
  • r-winbound1124 156deed — Instrumenting the stub to report its internal state | same ownership objection, and the chained-hook slot already reads the same environment without touching it
  • r-winbound1124 156deed — Raising the job timeout and letting the hang run to completion | it never completes, and the assertions behind it would never execute
  • r-winassert1124 616005d — Keeping the extensionless payload for attack 2 | the stub execs a recorded value only inside the .mjs|.js arm, so an extensionless target is refused by a branch that was never going to run it, and the step asserted its own control flow back to itself
  • r-winassert1124 616005d — Calling attack 2 evidence of containment once the payload is a .cmd | it guards a loosening, it does not exercise the install-root check; conflating the two is what would let a green sticker land on an untested branch
  • r-winassert1124 616005d — Keeping the chained-hook slot as the probe | it was never invoked, and a scratch repo with its own hook measures the platform without depending on the stub's argv0 handling
  • r-winassert1124 616005d — A trailing rm for the planted file | it is skipped by every early exit, and the install root is shared by every later step on the runner
  • r-winshell1124 aa68a9a — set +e at the top of each step | it also disables the abort for genuinely unexpected failures, where stopping is the right answer; the || form is scoped to the one command whose failure is a measurement
  • r-winshell1124 aa68a9a — Reading the outcome from the step's own exit code instead | 124 tells you a bound fired somewhere, not which commit, and it discards the probe output that explains why
  • r-ps1scope282 72f23df — Making the hook probe pass by installing a shim named commitlore with no extension | Windows will not execute it, and inventing a second artifact to satisfy a probe is the wrong direction; the probe is what needs to learn about Windows
  • r-ps1scope282 72f23df — Committing through the hook in this job to prove the hook works | the hook path on Windows is exactly what is unestablished, so the assertion would be testing T-1124's subject in T-1121's job
  • r-ps1scope282 72f23df — Dropping the doctor call so the log stays clean | its output is the evidence T-1124 starts from, and a clean log that hides it would cost the next ticket the finding
  • r-ps1shell282 97735d6 — Duplicating the job once per host | eighty lines twice, kept in step by hand, to assert the same things about two interpreters
  • r-ps1shell282 97735d6 — Running everything through cmd with the host as a variable | every assertion would become a quoted one-liner passed to -Command, which is the least readable form of the most important job here
  • r-ps1shell282 97735d6 — Testing only PowerShell 7 | 5.1 is what a Windows machine has before anyone installs anything, and a 7-only construct is a parse error for the whole file
  • r-instps1121 7bf6ced — Writing the user-scope PATH with SetEnvironmentVariable | two active records on install.sh reject an installer that changes the user's environment behind their back, and the Windows registry spelling of that act is not a different decision
  • r-instps1121 7bf6ced — An extensionless shim matching the shell wrapper's name | Windows will not execute it, and the git hook resolves through recorded config rather than PATH, so the name would buy nothing
  • r-instps1121 7bf6ced — Testing only under PowerShell 7 | 5.1 is the version a Windows machine already has, and a 7-only construct is a parse error for the whole file rather than a failure in one branch
  • r-instps1121 7bf6ced — Requiring a helper for JSON merges the way install.sh needs jq | PowerShell parses and writes JSON natively, so the cannot-merge branch has no counterpart and inventing one would be a copied limitation
  • r-instps1121 7bf6ced — Passing the version through $args so irm | iex could take one | iex gives a piped script no arguments, so the pinned form is documented as a scriptblock invocation and COMMITLORE_VERSION is accepted as well
  • r-nobin284 ab2f08f — Keeping the binary classification arm for the installer's wrapper | the wrapper is a shell script that execs node, so trusting it by name would re-admit any extensionless executable called commitlore while the bundle it runs is the thing with an interpreter to check
  • r-nobin284 ab2f08f — Relaxing the dogfood Evidence check to allow a missing path | it exists to catch a citation nobody can follow, and dropping the check to accommodate a deletion would retire the guarantee instead of correcting the question it asked
  • r-nobin284 ab2f08f — Leaving src/core/paths.ts alone because ADR-0026's inventory did not name it | it was the only remaining code that read files out of a compiled artifact, so leaving it would have left the invariant false while the test asserting it passed
  • r-nobin284 ab2f08f — Keeping /dist/commitlore in gitignore in case a binary returns | an ignore rule for an artifact nothing produces is a claim that something might, and the ADR says otherwise
  • r-t1120fatal 0412668 — Reading the remaining failure by inference from the generic advice line | two cycles already went that way, and the informative line was present in the log the whole time
  • r-t1120cijob 1871106 — Keeping the separate staged clone and adding more mount configuration | each addition would be configuration protecting a copy that exists for no reason once the checkout can be cloned directly
  • r-t1120cijob 1871106 — Dropping the container steps and relying on the vitest suite | the suite runs on a machine that has node and git; the point of a bare image is that the prerequisite is genuinely absent, which is the assertion the active record on this file asks for
  • r-t1120clonemsg 55ffb0a — Keeping the guessed message and fixing only the CI mount | the guess would still be printed for every other clone failure, and this one was found only because a container happened to hit it
  • r-t1120clonemsg 55ffb0a — Letting the clone's stderr through to the terminal unfiltered | it interleaves with the installer's own log lines and a piped installer should speak in one voice; the last lines quoted into one named error keep both
  • r-t1120nodeinst 14deeb4 — Hardcoding the release version in install.sh as the default | it would add a bump obligation to every release and a stale default between a tag and the bump; git ls-remote resolves the newest tag with no API token and no rate limit
  • r-t1120nodeinst 14deeb4 — Defaulting to a branch when no tag is given | installing a moving target is what pinning exists to prevent, and the release gate already forbids a dev-referencing one-liner
  • r-t1120nodeinst 14deeb4 — Writing the wrapper with cp over the destination | an in-place overwrite of a file that may be executing is the defect that forced a same-day patch release; the wrapper is written beside the target and renamed
  • r-t1120nodeinst 14deeb4 — Letting post-install verification decide the exit code | that was the other half of the same defect, where a killed --version became the installer's exit status; verification now retries once and reports, and an install that succeeded exits 0
  • r-t1120nodeinst 14deeb4 — Editing the shell profile when the wrapper directory is not on PATH | an active record on this file rejects it, and printing the line is what this script does about PATH
  • r-t1120nodeinst 14deeb4 — A test-only environment hook to force a verification failure | a source repository whose bundle exits non-zero is a real broken release and needs no seam in shipped code
  • r-t1120nodeinst 14deeb4 — Checking for git with command -v alone | a git that cannot execute is as useless here as a missing one, so the check runs git --version and catches both
  • r-rel041notes 71efe1f — folding this into the next feature release | the documented install is broken for upgraders now, and every day it stays tagged is a day the one-liner in the README fails for them
  • r-rel041notes 71efe1f — moving the v0.4.0 tag to the fixed commit | a tag users may already have fetched must not change under them; the fix gets its own version
  • r-rel040notes 5d57a72 — describing 0.4.0 as making agents safer | no study here measures agent behaviour, and the recovery protocol is registered and unrun; the release delivers a loop, not an outcome
  • r-rel040notes 5d57a72 — omitting guard's figures from the release notes because they are unflattering | the notes are read by people deciding whether to trust the tool, and a quality limit hidden at release time is discovered in use
  • r-m4basis 5e2d2cb — keeping the non-delivery wording as a hedged claim | it asserts something I never established, and a verdict that overstates its own evidence is the defect this repository exists to catch
  • r-m4withdraw e5f9b73 — retracting the dataset or calling M4 invalid | the data is valid and its provenance is clean; what it measured was not the treatment, and those are different words
  • r-instpath119 9e1fce7 — editing the user's shell profile from the installer | printing the line is honest, rewriting .bashrc silently is what makes people distrust curl-to-shell installers, and this project sells verifiability
  • r-rel021a a79e350 — retagging v0.2.0 | the tag is already on the remote and moving a published tag breaks anyone who fetched it
  • r-exposure52 ba69411 — compute a treatment effect with unknown guard exposure | an old row that never recorded whether the guard reached the run cannot distinguish no treatment from an ignored treatment
  • r-m4docs1 32d5bf1 — keeping the withdrawal notice and only landing the verdict document | bench/report.ts already draws this line -- a provenanced dataset that still shows a withdrawal is a hard failure in check-readme-numbers.mjs (checked here), not a style choice left open
  • r-rel0200a a074754 — bumping ci.yml's "v0.1.0 was published with zero attached assets" comments | those describe a historical fact about the actual v0.1.0 release, not a version this project declares; the check they document (releases/latest/download/SHA256SUMS returning 200) is written to start exercising the real path automatically the day any release ships assets, v0.2.0 included, with no workflow edit
  • r-rel0200a a074754 — touching docs/adr/ADR-0001-scope-v010.md, docs/tickets/release.md, bench/VERDICT-M1.md, HANDOFF.md, bench/ROUTE-GAP.md | planning and historical-record prose that names v0.1.0 as a past decision or measurement subject, not a live version carrier
  • r-rel0200a a074754 — changing test/mcp.test.ts's CommitLore-Version: 0.1.0 fixture trailer | that's protocol-version content inside a synthetic seed commit (what an old commit's trailer looked like), unrelated to and never asserted against package.json's version
  • r-instci99a 318b9a2 — pre-installing curl/jq/tar/git in the containers before running install.sh | would hide exactly the gap this work exists to find — Debian ships neither curl nor wget by default, and asserting that failure cleanly is the point of the bare debian:stable-slim step, not an obstacle to route around
  • r-instci99a 318b9a2 — publishing a -musl release target so Alpine could install successfully | a release.yml/build-matrix change, not an install.sh or CI-verification fix; out of scope here (DO NOT: no Docker in the release build matrix) and orthogonal to making the existing failure clear instead of a raw crash
  • r-instci99a 318b9a2 — heuristically detecting musl before download (checking for /lib/ld-musl-*.so.1) | executing the freshly extracted, already checksum-verified binary directly is a stronger, more general check — it catches any reason the binary cannot run on this machine, not only the one musl signature this repository happens to know the path for
  • r-instci99a 318b9a2 — a matrix job splitting debian/alpine | the two images need different assertions (debian: bare failure, then success once curl is added; alpine: one clean failure, nothing more to add) and share one staged release + HTTP server; one job with named, ordered steps keeps that shared setup and reads linearly in the log
  • r-fix93pkg 9c4a396 — repointing bin at dist/commitlore.mjs instead of removing it | that entry exists only to serve a package-manager install ADR-0011 already replaced with a git clone; repointing it keeps maintaining a path to an install mode that will never happen
  • r-fix93pkg 9c4a396 — also removing files now | the issue's own "After B-09 · Single static binary — remove the Node runtime dependency #39" note groups bin/files/dependencies as one cleanup once the static binary lands; files alone is inert without a registry rather than actively broken, so doing it now means doing it twice
  • r-relinstall c6e1d04 — guessing the current version to build the asset URL directly | would need either the GitHub API (rate-limited, needs no-auth headers handled correctly) or trusting a redirect's final Location header parsing. Downloading the fixed-URL SHA256SUMS first and reading the real asset name back out of it needs neither and is what the checksum step has to fetch anyway.
  • r-relinstall c6e1d04local for scoping — not POSIX per se, but supported by dash, bash, and every shell this script is realistically piped into (verified directly, see Verified) | not used in the end; the script has few enough variables that scoping was not needed, only noted here because it was considered.
  • r-det058 695cdf6 — add the 100k history suite to CI | CI budget and deterministic local measurement are separate decisions
  • r-5e91c7 f991389 — leaving pull_request as the only check for develop | a direct push to an integration branch is exactly what nobody reviews
  • r-2b58d4 4842356 — exempting datasets written before the fields existed | it is one line and it deletes the guarantee
  • r-7a3e91 cf859e4 — inlining spec/SPEC.md and the schema into the bundle | SPEC.md would need a codegen step that itself needs a drift guard, and the package-root walk removes the reason to want it
  • r-7a3e91 cf859e4 — replacing the tsc output with the bundle | test/cli.test.ts, test/hooks.test.ts and test/mcp.test.ts import dist internals by path
  • r-0d4b81 8005227 — a longer quickstart that demonstrates context, limits, ruled-out, warnings and stale | an agent calls those itself once the MCP server is registered, so listing them teaches the human a workflow that is not theirs
  • r-3b8f52 1f8b4be — enabling the large fixture for the whole default suite | generating 100k commits costs ~22s and the other tests in that file gain nothing from it, so it runs as its own step against its own file
  • r-7f31c9 750ab17 — reporting both datasets from one source list | readSources groups by condition and cannot separate repositories, so any second dataset with a commitlore-on arm silently corrupts the headline test
  • r-3e8a41 2f0a8a0 — off-path records that advocate the ruled-out option | with no scoping they reach all three arms, raising re-proposal everywhere and compressing the grading and lifecycle contrasts the set is built to isolate
  • r-9c2f74 d653153 — resume the pilot into the same file | a new process would load the edited code and create the mixing that had not happened
  • r-9c2f74 d653153 — run the ablation arms as they stand | three nulls from comparing identical inputs read as "these guarantees do not matter"
  • r-9c2f74 d653153 — keep the tasks that showed an effect and rewrite only the rest | the property is the criterion, not the direction of the result
  • r-4a8e15 49e12c7 — make the parser accept a bare block | it would disagree with git, and delegating to git is the one thing SPEC section 2 requires
  • r-0c5d38 aeb54a6 — smoke with --version only | it passes with the schema missing, which is the failure worth catching
  • r-0c5d38 aeb54a6 — assert a file list in package.json | it goes stale silently, and installing is the actual question
  • r-3f7a29 49817dc — repair a draft that fails verification | backfill's source material is weak enough that a repair loop would mostly be inventing
  • r-3f7a29 49817dc — write reconstructed records into commit messages | history rewriting is irreversible and reaches every existing clone
  • r-3f7a29 49817dc — post a fresh comment per push | it turns the signal into noise and the check gets muted
  • r-6f2e58 ea9ae6d — pin the adoption sha in the workflow | it is the same stale cutoff the dogfooding suite was written to avoid
  • r-6f2e58 ea9ae6d — spawn git per commit to read trailers | one pass with %(trailers:key=…) uses git's parser and does not scale with history
  • r-5c8b31 60ddc39 — keep turns and explain it in prose | the JSONL outlives the prose, and whoever reads the rows later will not have it
  • r-5c8b31 60ddc39 — drop the turn budget since it cannot be enforced | the overrun is still the signal that a run went off the rails
  • r-7e5f02 e5f5e00 — hold Node 20 and downgrade both dependencies | paying maintenance cost to keep supporting an unpatched runtime
  • r-7e5f02 e5f5e00 — floor at 24 | excludes Node 22 users today for no benefit, and 22 outlives v0.1.0
  • r-7e5f02 e5f5e00 — drop engines entirely | the failure moves from install time to runtime, where it is someone else's confusing bug
  • r-3a9d68 6a3fc3b — pin an expected test count | it goes stale on the next file added, and a stale count reads as passing
  • r-3a9d68 6a3fc3b — keep threads and skip the index tests in CI | that is the same hole with the sign flipped, made deliberate
  • r-6c2b95 5da793c — skip the dry-run driver | then nothing exercises the harness until a key exists, and the first real run debugs the harness instead of measuring anything
  • r-6c2b95 5da793c — emit dry-run rows without a marker | indistinguishable from measurements the moment they leave the terminal
  • r-8e2d51 ef93c0e — pin one git version in CI | it would hide exactly the divergence the matrix exists to surface
  • r-8e2d51 ef93c0e — allow shallow checkout and skip dogfooding | a skipped gate reports the same green as a passing one
  • r-5a8c04 c46a577 — regex trailer parsing | B3 prose false-positives are the failure mode this protocol exists to prevent
  • r-5a8c04 c46a577 — a custom GitError subclass | the convention here is new Error with structured context, and callers read GitResult
  • r-c0f4e2 3d249cd — GitLore published as git-lore | the binary and search results still collide with the existing gitlore tool
  • r-c0f4e2 3d249cd — keep Annals | the sound problem does not decay, and with code near zero this is the cheapest moment the project will ever have
  • r-c0f4e2 3d249cd — rename code and spec first, documents later | the drift window makes every artifact written in it wrong
  • r-b2e7f1 00d348d — line-matching Key: prefixes | prose containing a colon line parses as a record and feeds agents false context (verified B3)
  • r-b2e7f1 00d348d — content-hash Record-Id | must survive rebase and squash, which a hash of the commit does not

Warnings (33)

  • r-4e7c19 6d4e828 (claim) — this closes the attribution half of blocker B only. W4 is still unmeasured, W3 is still unrecoverable for this corpus, and no figure in TOKEN-LEDGER.md section 9 changes
  • r-4e7c19 6d4e828 (claim) — section 5 of TOKEN-LEDGER.md is pre-registration and was appended to, not edited — the floor has to stay visibly declared in advance
  • r-3c9d52 dc9e769 (claim)budgeted_log_coverage is not recall — it counts commits, not records, and only the calibration row at b3f5692 says how far apart the two are on a repository where both are known
  • r-instps1121 7bf6ced (claim) — nothing on this machine can execute this file -- there is no PowerShell here, so every claim about its behaviour comes from the Windows runner in this pull request rather than from a local run
  • r-nobin284 ab2f08f (claim) — an extensionless COMMITLORE_BIN is now ignored rather than honoured -- the hook falls through to the recorded install and to PATH, so a caller who pointed it at the wrapper still gets a working hook, by a different route
  • r-t1120nodeinst 14deeb4 (claim) — keep install.sh ASCII only -- a non-ASCII character in a string silently terminated /bin/sh here while sh -n accepted the file
  • r-release030 510d70e (claim) — Tagging and publishing remain separate release steps.
  • r-exposure52 ba69411 (claim) — guard exposure is instrumented in the benchmark hook adapter, not by changing guard scoring | M4 rows remain unexposed and metrics refuses their effect estimate
  • r-rel0200a a074754 (claim) — scripts/commitlore-bootstrap.sh is orphaned -- no hooks.json entry invokes it, and its npm-install strategy contradicts ADR-0011. It still carries a live version default, now bumped for consistency, but nothing exercises it. Worth a follow-up issue: either wire it up correctly or delete it.
  • r-instci99a 318b9a2 (claim) — the new binary-execution guard's die() message names musl/glibc specifically because that is the only way this repository's own release actually fails to execute today (glibc-only "-gnu" targets, verified against Alpine). If a -musl target is ever published, or the binary can fail to execute for an unrelated reason, this message will misdiagnose it — revisit the wording if that guard ever needs to distinguish causes.
  • r-det058 695cdf6 (claim) — guard precision is against the frozen archived labels, not a new hand-adjudication
  • r-7f24a9 96d960a (claim) — this file is still NOT validated as a GitHub workflow. actionlint is unavailable and the sandbox cannot reach the API. Conflating a YAML parse with workflow validation broke gitseed's CI earlier today; the first run is the proof
  • r-7f24a9 96d960a (claim) — the same task was told to update the factory skill's Gitflow section and its phase-gate.py, and could not — it was launched rooted at this repository and had no write access to ~/.claude/skills. The gate still checks for a branch named develop and fails both repositories for having dev. My spec error, not the delegate's; it is the next task
  • r-5e91c7 f991389 (claim) — these files are NOT validated as GitHub workflows. actionlint is unavailable and the sandbox cannot reach the API, so only the YAML parse and structural assertions ran. Conflating those two broke gitseed's CI four commits ago; the first GitHub run is the proof
  • r-2b58d4 4842356 (claim) — this leaves the README with no measured numbers at all until M3-b runs. That is the honest state and it is also a worse first impression. The alternative was publishing numbers produced by a binary nobody recorded
  • r-7a3e91 cf859e4 (claim) — hardcoding ../ counts back to the package root is what broke this — new code reads assets through installedPath(), never through import.meta.url
  • r-0d4b81 8005227 (claim)claude mcp add commitlore -- commitlore mcp is Claude Code's syntax — other MCP clients register a stdio server their own way
  • r-7f31c9 750ab17 (claim) — adding a file to README_SOURCES pools it into every aggregate in the block, including the significance test — check the arm names first
  • r-8b41e6 0bbad66 (claim) — the payload share and the fixture share are not interchangeable — the payload adds framing and drops what grading and lifecycle withhold, so a reader who swaps one for the other will be wrong by a small, plausible margin
  • r-3e8a41 2f0a8a0 (claim) — the ablation set is a different synthetic repository from bench/tasks, so its commitlore-on arm must never be pooled with the primary matrix's
  • r-9c2f74 d653153 (claim) — after the measurement, check that git status is clean and the recorded sha is still HEAD -- an edit mid-run breaks reproducibility silently, and that check is the only thing that catches it
  • r-4a8e15 49e12c7 (claim) — canonical output is a block, not a message -- prepend a subject before parsing it back, or the round-trip silently returns nothing
  • r-0c5d38 aeb54a6 (claim) — spec/ ships on purpose -- alternative implementations are told the fixtures are the contract, and this step is what keeps that true
  • r-3f7a29 49817dc (claim) — every backfilled record is Provenance: reconstructed, which the trust model always renders as a claim -- do not add a path that lets a draft override that field
  • r-5c8b31 60ddc39 (claim) — a driver that gains a real turn limit should stop emitting over-turns and start emitting an enforced label -- do not reuse over-turns for something the harness actually stopped
  • r-7e5f02 e5f5e00 (claim) — raising the floor again needs an ADR superseding ADR-0010 -- a package.json-only change will pass tests and lie to users
  • r-3a9d68 6a3fc3b (claim) — any native addon added later needs the fork pool -- moving back to threads will silently drop whatever loads it
  • r-6c2b95 5da793c (claim) — never cite a row with simulated:true -- README numbers come from bench/results logs and those rows are not results
  • r-8e2d51 ef93c0e (claim) — keep fetch-depth 0 on any job that runs npm test -- the dogfooding suite fails deliberately on a shallow clone, and "fixing" that by relaxing the assertion removes the gate
  • r-5a8c04 c46a577 (claim) — do not add --grep anywhere under src/ -- test/source-guards enforces its absence and will fail the build
  • r-c0f4e2 3d249cd (claim) — ADR-0008 and ADR-0009 keep the literal string Annals on purpose -- mechanical substitution there destroys the decision trail
  • r-c0f4e2 3d249cd (claim) — the residual grep for lore_query reports a false positive because commitlore_query contains it as a substring, so check the prefix
  • r-b2e7f1 00d348d (claim) — spec/fixtures and spec/contract-cases are the contract -- prose in SPEC.md explains them, it does not define them

git log --follow accepts exactly one pathspec, so renames are not followed for 5 paths; query one path at a time to follow its rename chain

withheld the content of 3 record(s) graded blocked: a Limit, Ruled-out, Verified trailers matching an injection pattern is reported, never quoted (SPEC §7)

Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR.

…efault

bench/verify.mjs had no npm script and no CI step: `grep -rn "verify.mjs"
.github/workflows/ package.json` returned nothing. So the schema drifted five
fields behind the runner and bench/results/m5-off-design-20-tasks.jsonl sat in
the tree failing the gate for two days. Reproduced at 6d4e828^, the commit
before #390 fixed the schema: 80 problem(s) across 80 rows, every one `must NOT
have additional properties`. #390 fixed that drift. This is about the reason
nobody saw it, and docs/RELEASE-GATE.md already names the shape -- "A gate
nobody can check is a slogan."

Running it needs an answer to which files, and bench/results/ is not one
population. Seventeen .jsonl files there hold two row families that share a
directory and nothing else. Nine are run records -- one row per (task,
condition, seed), written by bench/runner.ts, which is what result.schema.json
describes in its own words. Eight are metric rows written by
bench/deterministic.ts and bench/external/run.ts: one row per measurement, never
a run id, a task or a condition. Checking those against this schema is not a
drift check, it is a category error, and it fails eight of seventeen files
permanently.

So the scope is the directory, with two exemptions decided per row. Nothing opts
in: `npm run bench:verify` reads bench/results/*.jsonl and a new results file is
gated the moment it is committed. A declared list of filenames was the obvious
alternative and is the one thing that cannot work -- it is opt-in wearing a
different hat, and a file left off it is silently ungated, which is the failure
being fixed.

The family split keys on `schema_version`, and on its absence rather than its
presence. #392 suggested the field as the inclusion marker; it is the opposite.
BaseRow (bench/deterministic/types.ts) declares `readonly schema_version: 1`, so
every metric row carries it, and result.schema.json is
additionalProperties:false with no such property, so no valid run record can.
Keying inclusion on presence would have selected exactly the eight files this
schema cannot describe. A file carrying it on some rows and not others is failed
rather than classified: that is a corrupt file, and it is also the only accident
by which a run-record file could leave the gate.

Six run-record matrices predate 1073fa4, which made harness_commit and
dist_digest required. They cannot be corrected -- bench/results/ holds committed
measurements, and ADR-0018 is why a superseded or invalidated matrix stays in
the tree rather than being deleted -- but they are not skipped and the schema is
not weakened. The gate compiles a second validator from the committed schema
with exactly those two `required` entries dropped, and applies it only to rows
that identify themselves as old by their own started_at. Every other constraint
still holds for all 380 of them. The cutoff is not a judgement call: the last
row without provenance started at 2026-07-27T02:21:50.808Z, the first row with
it at 2026-07-27T07:21:31.025Z, and 1073fa4 is dated 2026-07-27T02:56:12Z,
inside that gap. It cannot creep forward, because started_at comes from the
clock when the runner writes the row.

Both exemptions are printed per file on every run, so the scope is checkable by
running the gate rather than by trusting the comment that defines it -- which is
the whole complaint this closes.

Record-Id: r-benchscope
Limit: nothing checks the shape of the eight metric-row files. This gate names them and steps over them, and bench/deterministic/types.ts is the only definition that family has -- there is no JSON schema for it, so drift on that side is still invisible
Limit: the pre-provenance exemption reads started_at, which is data on the row rather than a fact about the file. A row that misreported it would be held to the shorter list of requirements; that is a deliberate falsification rather than the omission this fixes, and nothing here detects it
Ruled-out: A declared list of result files, in the gate or in a manifest | opt-in under another name, and a file left off it is silently ungated -- which is precisely the failure #392 reports
Ruled-out: Validating only the three files that pass today | a description of the present rather than a rule, and it goes stale the first time a matrix is added or the schema moves
Ruled-out: A naming convention on results filenames | the runner's default output is an ISO stamp plus hex and the metric writers' is a family name plus an ISO stamp, but every run-record file in the tree was named by hand through --out (t702-m2, m5-off-design-20-tasks), so the convention is enforced nowhere and cannot carry a gate
Ruled-out: A marker file dropped beside each gated result | still a step somebody has to remember, and a forgotten step is the defect being fixed
Ruled-out: Widening result.schema.json to describe both row families | they share no required field, so the union would accept a run record with no task and a metric row with no metric, which is a schema that rejects nothing
Ruled-out: Making harness_commit and dist_digest optional in the schema | weakens the gate for every future row in order to admit six historical files, and #390's drift was an additionalProperties failure that a laxer schema catches later rather than sooner
Ruled-out: Skipping the six pre-provenance files whole | cheaper to write, but it stops checking types, patterns and the closed property set on 380 rows in order to excuse two absent fields
Certainty: firm
Blast: module
Undo: easy
Verified: the gap confirmed at 85ea006 -- grep for verify.mjs across .github/workflows/ and package.json exits 1; the drift reproduced at 6d4e828^ (e5d8eed), where verify.mjs reports 80 problem(s) across 80 rows of m5-off-design-20-tasks.jsonl, all additionalProperties; the survey run file by file before any change -- 17 .jsonl, 3 passing and 14 failing, the 14 splitting into 8 metric-row files and 6 run-record files missing only harness_commit and dist_digest; npm run bench:verify now exits 0 over 632 rows in 9 files, naming the 8 metric-row files as out of scope and 380 rows as pre-provenance; the gate bites -- a scratch bench/results/*.jsonl carrying one undeclared field made it exit 1 naming file and line, and deleting that file returned it to exit 0 with bench/results/ clean; the exemption is bounded -- the same row passes dated 2026-07-26 and fails dated 2026-08-01, and a pre-provenance row with an extra field still fails; npx vitest run reports Test Files 83 passed, Tests 2092 passed and 1 skipped, 11 of them new here; npm run typecheck, npx tsc -p bench/tsconfig.json --noEmit and npm run build clean with dist/ byte-unchanged; node scripts/check-readme-numbers.mjs exits 0 and bash spec/verify.sh reports OK on 26 fixtures; no file under bench/results/ was modified
Unverified: the CI step has not run on a GitHub runner from this commit -- only the command it invokes has been run locally, on macOS and one Node version; whether the metric family keeps declaring schema_version, which holds because BaseRow requires it today and is enforced nowhere outside bench/deterministic/types.ts; whether any pre-provenance row misstates its own started_at, since the cutoff was derived from the rows in the tree rather than from an independent record of when each matrix ran
@MongLong0214
MongLong0214 merged commit c99be34 into dev Aug 1, 2026
8 checks passed
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.

1 participant