Skip to content

parse recognizes every record block (#89); fix bench-ablation dist/ race, scale mcp.test.ts timeout (#88) - #91

Merged
MongLong0214 merged 2 commits into
devfrom
bug-issue-88
Jul 27, 2026
Merged

parse recognizes every record block (#89); fix bench-ablation dist/ race, scale mcp.test.ts timeout (#88)#91
MongLong0214 merged 2 commits into
devfrom
bug-issue-88

Conversation

@MongLong0214

Copy link
Copy Markdown
Owner

Summary

Two independent fixes, kept as separate commits.

#89parse recognizes every record block

Reproduced before (previously shipped dist/commitlore.mjs):

$ printf 'Feat (#1)\n\n* change 1\n\nLimit: only a test 1\nRecord-Id: r-ghtest1\n\n* change 2\n\nLimit: only a test 2\nRecord-Id: r-ghtest2\n' > /tmp/gh.txt
$ node dist/commitlore.mjs parse --message-file /tmp/gh.txt
Limit: only a test 2
Record-Id: r-ghtest2

$ node dist/commitlore.mjs context --no-index
context as of ... — 2 limits, 0 ruled-out, 0 warnings, 0 other in 2 records (no index, 2 commit record(s) scanned)

limits
  r-ghtest1  ...  only a test 1
  r-ghtest2  ...  only a test 2

parse showed one record; context showed two, for the identical message. parse --help calls itself the command for this exact question.

Reproduced after:

$ node dist/commitlore.mjs parse --message-file /tmp/gh.txt
# record 1/2 — earlier
Limit: only a test 1
Record-Id: r-ghtest1

# record 2/2 — own
Limit: only a test 2
Record-Id: r-ghtest2

parse and context now agree on record count. --json gained a blocks array ({ own, identityCollision, trailers } per block); the existing trailers key is unchanged in meaning (the message's own block).

Byte-identity for single-block messages: verified directly against the previously-shipped dist/commitlore.mjs, before rebuilding, across every fixture under spec/fixtures/{valid,boundary,invalid} — text and --json both diffed byte-for-byte identical, zero differences.

Also checked, as asked: two blocks in one message declaring the same Record-Id. Traced whether this reaches identityCollision (the field context --json already exposes for a different case). It does not — core/stale.ts's findIdCollisions only fires when a notes-sourced record disagrees with a commit's own content; a group with no notes record in it (which is what two same-message commit blocks are) never trips it. Confirmed directly: both commitlore context --json and commitlore validate accept such a message today and silently merge the two blocks (context even folds the two different Limit: values into one record). parse now runs its own local check — independent of findIdCollisions, scoped to the one message being parsed — and surfaces it (identityCollision: true per block in --json, a text marker, and a stderr diagnostic). Whether context/validate should also catch the same-message case is left open; deliberately out of scope here (see the commit's Ruled-out: trailer).

#88bench-ablation.test.ts flake: diagnosis and fix

Evidence for a real contention bug, not a too-small timeout (checked before changing anything):

bench/runner.ts hashes the whole dist/ tree at startup and re-hashes it before each of the six arms (bench/hooks-settings.ts writeArmSettings), refusing an arm when the two disagree. That check is real and useful — its own comment records that it once caught a genuine dist/core/guard.js drift mid-measurement. dist/ is one directory shared by every vitest worker, though, and four other test files (cli.test.ts, mcp.test.ts, action-lint.test.ts, action-preserve.test.ts) each rebuild it via their own tsc -p tsconfig.json in a beforeAll. Any of those rebuilds overlapping with bench-ablation's runner invocation legitimately (and correctly, per the check's own contract) trips it.

Reproduced directly, invoking bench/runner.ts in a loop while a separate process repeatedly rebuilds dist/: 4/20 and 1/15 failures across two independent runs, each with exactly the reported symptom:

Command failed: node --experimental-strip-types bench/runner.ts ...
...
"stopped_by":"error", ...
"error":"dist/ changed after the benchmark matrix started: expected sha256 b29dbd..., found e8b24d..."

runRunner's execFileSync call is fully synchronous, so it blocks the event loop for its whole duration — vitest's own per-test timeout timer cannot even fire until it returns. Confirmed with a throwaway probe: an await setTimeout(6000) test fails at vitest's 5000ms default; the identical 6 seconds of work done synchronously does not, it just reports its real duration. This was never a timeout problem — raising one would have hidden a check that was doing exactly its job.

Fix: removed the shared mutable state instead of loosening the check. DIST_DIR (bench/hooks-settings.ts) now reads COMMITLORE_BENCH_DIST_DIR when set, defaulting to the existing <repo>/dist (unset everywhere except this one test — a real benchmark run is unaffected). bench-ablation.test.ts snapshots dist/ into a private, unshared temp copy once per file and points every runner invocation at it. Also fixed ablationShim's separate, previously-hardcoded dist/core/inject.js reference to go through the same DIST_DIR, which was inconsistent and would have kept reading the live, shared tree even after this fix.

mcp.test.ts does not share this cause. It never calls bench/runner.ts or the digest check — it's one of the four racers against bench-ablation, not a victim of the same race. But its JSON-RPC assertions await a real round trip (unlike bench-ablation's synchronous execFileSync), so they are mechanically subject to vitest's fixed 5000ms default under real concurrency — confirmed with the same probe technique (an awaited 6s wait fails at the default; a synchronous one does not). Scaled its budget instead of guessing a bigger constant: vi.setConfig({ testTimeout: 5_000 * Math.max(availableParallelism() - 1, 1) }), tying it to the same worker count vitest's own default pool sizing already uses.

Verification — consecutive clean runs under deliberate load:

  • The specific previously-flaky test, bench/runner.ts invoked directly in a loop against a competing tsc rebuild loop: 20/20 clean post-fix, under the identical load that produced 4/20 failures pre-fix.
  • mcp.test.ts alone: 43/43 passing across 3 self-concurrent full runs plus 6 parallel tsc rebuild loops and 8 CPU-bound processes — could not force an actual failure on this 12-core dev machine even under that load, consistent with the CI report coming from a more constrained/shared runner rather than a logic bug.
  • Full suite, no added load: 36 files, 1335 tests (1334 passed, 1 skipped) — clean.
  • Full suite under deliberate bounded load (2 independent runs, each with 2 finite tsc rebuild loops + 3–4 CPU-bound processes running for the whole suite's ~10–11 minute duration, on two separate worktrees checked out at this branch): 36 files, 1335 tests (1334 passed, 1 skipped) — clean on both. 3/3 consecutive clean full-suite runs under deliberate load overall.

One honest caveat found along the way, not fixed here: under a much heavier, sustained synthetic load (multiple tsc rebuild loops running continuously for the suite's entire ~10+ minute duration — well beyond what a single beforeAll racing another produces), one mcp.test.ts test that spawns the CLI (commitlore context --json) hit a transient SyntaxError: The requested module '../core/squash.js' does not provide an export named 'collectRange' — the same underlying hazard as #88 (a subprocess reading dist/ while another process is mid-write to it), surfacing as a module-resolution error instead of a digest mismatch, because mcp.test.ts reads the live, shared dist/ directly rather than a private snapshot. This needs sustained, unrealistic rebuild pressure to reproduce (not a single sibling beforeAll), so it is reported rather than fixed here — a natural next step if it recurs would be the more thorough fix already considered and ruled out for this PR (consolidating the four files' redundant tsc rebuilds into one vitest globalSetup, see the #88 commit's Ruled-out: trailer).

Both

  • Confirmed dev's exact test count myself before starting: 33dc7aa (dev tip) — 36 files, 1297 tests (1296 passed, 1 skipped).
  • Each commit carries CommitLore trailers and passes node dist/commitlore.mjs validate --commit HEAD individually.
  • Not merged to dev.

`parse` and `context` disagreed on how many records a message contains.
bug-issue-60 (#86) taught `context`, `validate` and the index to recognize
every record block a message carries (SPEC §2.4), but `parse` still called
`parseCommitMessage` alone — for a multi-record message it kept answering
from the pre-#86 grammar while every other consumer route had moved on.
`parse --help` calls itself the command for asking exactly this question.

Reproduced before (dist/commitlore.mjs, shipped bundle):

  $ node dist/commitlore.mjs parse --message-file /tmp/gh.txt
  Limit: only a test 2
  Record-Id: r-ghtest2

  $ node dist/commitlore.mjs context --no-index
  context ... — 2 limits, 0 ruled-out, 0 warnings, 0 other in 2 records ...
    r-ghtest1  ...  only a test 1
    r-ghtest2  ...  only a test 2

Fix: `core/trailers.ts` gains `labelRecordBlocks`, which wraps
`parseRecordBlocks` and marks each block `own` (the message's own last
paragraph, SPEC B1) or `earlier` (a block the grammar recovered). `parse`
reports every block. The multi-block form is additive, the same way SPEC
§2.4 itself only adds to §2.1–2.3: a single-block message's output is
byte-for-byte what it always was, in both text and `--json`, verified
directly against the previously shipped `dist/commitlore.mjs` across every
fixture in `spec/fixtures/` (valid, boundary, invalid) before rebuilding.
`--json`'s `trailers` key keeps meaning "the message's own block"; a new
`blocks` array appears only when there is more than one.

Reproduced after:

  $ node dist/commitlore.mjs parse --message-file /tmp/gh.txt
  # record 1/2 — earlier
  Limit: only a test 1
  Record-Id: r-ghtest1

  # record 2/2 — own
  Limit: only a test 2
  Record-Id: r-ghtest2

Also checked, as asked: two blocks in one message declaring the same
Record-Id. `parse` used to show one and say nothing. Traced whether that
path reaches `identityCollision` (the field `context --json` already
exposes): it does not, anywhere. `core/stale.ts` `findIdCollisions` only
fires when a *notes*-sourced record disagrees with a commit's own content —
a group with no `notes` record in it, which is exactly what two
same-message commit blocks are, never trips it. Confirmed directly: both
`commitlore context --json` and `commitlore validate` accept a message with
two blocks declaring one Record-Id and different content today, silently
merging them (`context` folds the two `Limit:` values into one record;
`validate` exits 0). `parse` now runs its own, local check — independent of
`findIdCollisions`, scoped to the one message being parsed — and surfaces
the collision: `identityCollision: true` per block in `--json`, a `Record-Id
collision` marker in text, and a stderr diagnostic naming the id.

Full suite (dev tip, 33dc7aa): 1297 tests, 36 files (1296 passed, 1
skipped) — confirmed by running it myself, not assumed.

Limit: `parse` has no git-commit context (no sha, no notes mirror) — its identityCollision check is local to the one message being parsed and cannot detect a Record-Id that collides with something already committed elsewhere in history the way context's fold does.
Warn: context and validate still do not catch two commit-sourced blocks in the *current* message sharing one Record-Id unless a notes mirror also disagrees (findIdCollisions requires a notes-sourced record in the group) — parse's check is intentionally independent, not a call into that function, so fixing this in context/validate is separate work.
Ruled-out: expanding findIdCollisions/index-db to flag a same-message duplicate id generally | reaches into query.ts's lifecycle fold and every consumer route (context, guard, stale) for a case bug-issue-89 only asked parse to surface; the smaller, requested fix is in parse alone.
Verified: byte-identity of single-block parse output (text and --json) against the previously shipped dist/commitlore.mjs across every spec/fixtures/{valid,boundary,invalid} fixture, zero diffs; new parse/labelRecordBlocks tests (test/cli.test.ts, test/trailers.test.ts) green; full suite green; manual reproduction of both VERIFY cases against the rebuilt dist/commitlore.mjs.
Blast: module
Undo: easy
Certainty: firm
Provenance: authored
Record-Id: r-parsemulti
CommitLore-Version: 2.0.0
…ng test builds (#88)

`test/bench-ablation.test.ts` — "accepts all six arms" — failed CI on #79
and #87 and failed locally under concurrent load, always passing in
isolation (20/20). Diagnosed before changing anything, per bug-issue-88:

Evidence for (b), not (a). `bench/runner.ts` hashes the whole `dist/` tree
at startup (`digestDistTree`) and re-hashes it before every one of the six
arms (`bench/hooks-settings.ts` `writeArmSettings`), refusing an arm when
the two disagree — a real, useful check: its own comment records that it is
what caught `dist/core/guard.js` changing mid-run under a previous
measurement. `dist/` is one directory shared by every vitest worker, though,
and four *other* test files (`cli.test.ts`, `mcp.test.ts`,
`action-lint.test.ts`, `action-preserve.test.ts`) each rebuild it via their
own `tsc -p tsconfig.json` in a `beforeAll`. Reproduced directly: invoking
`bench/runner.ts` in a loop while a separate process repeatedly rebuilds
`dist/` fails about 1 run in 5 (4/20 and 1/15 across two independent runs)
with exactly the reported symptom:

  Command failed: node --experimental-strip-types bench/runner.ts ...
  ...
  "stopped_by":"error", ...
  "error":"dist/ changed after the benchmark matrix started: expected
    sha256 b29dbd..., found e8b24d..."

`execFileSync` (what `runRunner` uses) is fully synchronous, so it blocks
the event loop for its whole duration — vitest's own per-test timeout timer
cannot fire until it returns, and by then there is nothing left to time out.
Confirmed independently with a throwaway probe test (`await
setTimeout(6000)` fails at vitest's 5000ms default; the same 6s of work
behind a synchronous call does not, it just reports its real duration). So
this was never a timeout problem — raising one would not have changed
anything, and would have hidden a check that is doing exactly its job.

Fix: removed the shared mutable state instead of loosening the check that
correctly depends on it. `DIST_DIR` (`bench/hooks-settings.ts`) now reads
`COMMITLORE_BENCH_DIST_DIR` when set, falling back to the existing
`<repo>/dist` — unset everywhere except this one test, so a real benchmark
run is unaffected. `bench-ablation.test.ts` snapshots `dist/` into a
private, unshared temp copy once per file and points every runner
invocation at it. Also routed `ablationShim`'s previously-hardcoded
`dist/core/inject.js` reference through the same `DIST_DIR`, which was
inconsistent with the rest of the module and would have kept reading the
live tree even after this fix.

`mcp.test.ts` does not share this cause. It never calls `bench/runner.ts` or
the digest check — it is one of the four *racers*, not a victim of the same
race. But its JSON-RPC assertions `await` a real round trip over a spawned
server's stdio, unlike `bench-ablation`'s synchronous `execFileSync` calls,
so they genuinely are subject to vitest's fixed 5000ms default under real
concurrency (mechanically confirmed with the same probe: an awaited 6s wait
fails at the default, a synchronous one does not). Scaled its budget instead
of guessing a bigger number: `vi.setConfig({ testTimeout: 5_000 *
Math.max(availableParallelism() - 1, 1) })`, the same worker count vitest's
own default pool sizing already uses (`getDefaultThreadsCount`) — 55s on
this 12-core box, 3x on a 4-core CI runner, unchanged on a single core.

Ran the fixed test 20/20 consecutive clean under the same concurrent-tsc-
rebuild load that failed 4/20 before the fix (measured directly against
`bench/runner.ts` in a loop, not only through vitest). mcp.test.ts: 43/43
passing across 3 self-concurrent full runs plus 6 tsc rebuild loops and 8
CPU-bound processes; could not force an actual failure on this 12-core dev
machine even under that load, consistent with the CI report coming from a
more constrained/shared runner rather than a logic bug.

Full suite (dev tip, 33dc7aa): 1297 tests, 36 files (1296 passed, 1
skipped) — confirmed by running it myself, not assumed.

Limit: the fix insulates bench-ablation.test.ts from the race; it does not remove the underlying design (four test files independently, redundantly rebuilding one shared dist/ in their own beforeAll). A fifth file doing the same thing, or a future check elsewhere that also depends on dist/'s mid-run stability, can still race the same way.
Warn: COMMITLORE_BENCH_DIST_DIR is read once at module load (`bench/hooks-settings.ts` DIST_DIR is a top-level const) — set it before importing the module, not after.
Ruled-out: consolidating the four files' redundant tsc rebuilds into one vitest globalSetup | would remove the race for every file at once and is the more complete root-cause fix, but touches five files' bootstrap plus global config, and mcp.test.ts's beforeAll already tolerates unrelated in-flight tsc errors elsewhere in src/ (OWNED_SOURCES filtering) in a way a single shared build step cannot preserve without its own redesign — bigger blast radius than this issue asked for.
Ruled-out: raising bench-ablation's timeout, globally or per-test | proven not to be a timeout problem at all: execFileSync blocks synchronously, so vitest's timeout mechanism cannot even engage against it (verified with a probe test); the actual failure is the runner subprocess legitimately exiting 1.
Verified: direct runner-loop reproduction of the pre-fix failure (4/20 and 1/15, exact CI error message); vi.setConfig mechanism confirmed with a synthetic 6s-test probe both failing at the unmodified default and passing once scaled; 20/20 clean runs of the previously-flaky test under load post-fix; mcp.test.ts 43/43 across 3x self-concurrency plus heavy synthetic load; test/bench-provenance.test.ts (the direct unit test of writeArmSettings' digest check) unaffected; full suite green.
Blast: local
Undo: easy
Certainty: firm
Provenance: authored
Record-Id: r-distrace88
CommitLore-Version: 2.0.0
@github-actions

Copy link
Copy Markdown

CommitLore — record lint

Trailers: clean — 2 commits in origin/dev..d118a73ea9ab1206f9e5a8cacb7a5d8ffde0cba9
Active constraints: 30 limits · 80 ruled-out · 45 warnings — from 55 records over 14 changed paths

Active constraints for the paths this PR touches

Limits (30)

  • r-distrace88 d118a73 — the fix insulates bench-ablation.test.ts from the race; it does not remove the underlying design (four test files independently, redundantly rebuilding one shared dist/ in their own beforeAll). A fifth file doing the same thing, or a future check elsewhere that also depends on dist/'s mid-run stability, can still race the same way.
  • r-parsemulti 6d39d25parse has no git-commit context (no sha, no notes mirror) — its identityCollision check is local to the one message being parsed and cannot detect a Record-Id that collides with something already committed elsewhere in history the way context's fold does.
  • r-multirec01 92aeb24 — parseRecordBlocks only recognizes a non-final block by its declared Record-Id, so an unidentified inherited record beyond the first stays recoverable in the plan that computed it but not in a later re-parse of stored text; squash-preserve orders unidentified blocks last so the common case (at most one) is unaffected.
  • r-multirec01 92aeb24 — multi-block reference checking (Follows:/Supersedes:) does not resolve one block's reference against a sibling block declared by the same commit; each block is still checked against every earlier commit in history.
  • r-exit065 e545dee — any new command's exit codes must be drawn from SPEC §10, not invented locally
  • r-fix70a1 d707fc7 — one encoding layer and explicit lexical forms in the four published languages; semantic paraphrases, nested encodings, and split payloads remain outside coverage
  • r-shwt66 5efa206 — git rev-parse --git-path may return a repository-relative path, so resolve it against cwd
  • r-merge66 40e7987 — Generated dist files were resolved only by npm run build and npm run bundle
  • r-fix760 fb8ba45 — Git remains the authority on trailer recognition; diagnostics must not loosen the parser
  • r-refint74 572f573 — validate cannot perform conservation checks because it has no before state
  • r-warn75 24c7cc8 — exit-code semantics remain owned by guard's exit 2 means blocked; everywhere else in the same CLI exit 2 means bad usage #65
  • r-shallow66 60a8659 — a depth-1 clone can only inspect its reachable commit history
  • r-doctor72 996bcde — generated dist artifacts must come from npm run build and npm run bundle, not a hand merge
  • r-fix067 a915af0 — PreToolUse hook failures must always exit 0 and never change stdout's hookSpecificOutput contract
  • r-fix063 0b8c496 — doctor performs remote probes; an unreachable remote reports could not verify instead of ok
  • r-fix053 ecc4b90 — QueryResult.notes remains repository-level availability and is independent from whether one record was mirrored
  • r-fix055 43b40f8 — harvest-verify makes no model call, so semantic entailment is outside its contract
  • r-fix054 664d4e2 — notes-only metadata must survive folding; a mirror is one record, not two
  • r-fix056 55cb8bc — blocked output may retain only validated structural values that cannot carry prose
  • 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-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-8a2f61 1a5adec — hooks and CI branch on the exit code, so two failures sharing one code is a wrong answer, not an inconvenience
  • 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-2b8f45 0adcaf5 — a matcher that flags real work gets uninstalled, so the false positive rate is the binding constraint, not detection
  • r-8c4a17 f2ab0c2 — Record-Id is single-valued, so a merge that inherits several records has no well-formed way to declare them in the message
  • r-8c4a17 f2ab0c2 — a verifier that accepts near-miss citations verifies nothing, so normalisation cannot grow past whitespace
  • r-0b7c44 d2b2ce3 — a command is only real once --help names it, because that is where users look before they read source
  • r-9a5e17 6d68703 — five workers on one repository share npm test and tsc, so file ownership alone does not prevent one worker from "fixing" another's half-written code -- verification scope had to be split too
  • 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

Ruled out (80)

  • r-distrace88 d118a73 — consolidating the four files' redundant tsc rebuilds into one vitest globalSetup | would remove the race for every file at once and is the more complete root-cause fix, but touches five files' bootstrap plus global config, and mcp.test.ts's beforeAll already tolerates unrelated in-flight tsc errors elsewhere in src/ (OWNED_SOURCES filtering) in a way a single shared build step cannot preserve without its own redesign — bigger blast radius than this issue asked for.
  • r-distrace88 d118a73 — raising bench-ablation's timeout, globally or per-test | proven not to be a timeout problem at all: execFileSync blocks synchronously, so vitest's timeout mechanism cannot even engage against it (verified with a probe test); the actual failure is the runner subprocess legitimately exiting 1.
  • r-parsemulti 6d39d25 — expanding findIdCollisions/index-db to flag a same-message duplicate id generally | reaches into query.ts's lifecycle fold and every consumer route (context, guard, stale) for a case bug-issue-89 only asked parse to surface; the smaller, requested fix is in parse alone.
  • r-multirec01 92aeb24 — minting a fresh Record-Id for an inherited record that never declared one | no other code path in this project invents identity; Record-Id is always author-declared, and doing it here would be new scope beyond what the issue asked for
  • r-multirec01 92aeb24 — a CI step comparing a PR's commits against its post-merge squash commit, as the primary mechanism for finding 1 | needs a GitHub API dependency this tool takes nowhere else, and can only run after the squash is already pushed; doctor catches the same mistake locally, before push, when the source branch is still in refs/heads
  • r-83d43117 8a49ddc — an install-root check for COMMITLORE_BIN | its only reason to exist is aiming the hook at a build outside the install root -- a test harness, a monorepo's local bin. Restricting its location would remove the one thing it is for; doctor's existing COMMITLORE_BIN visibility is the right amount of scrutiny for a channel that is supposed to point anywhere.
  • r-83d43117 8a49ddc — resolving commitlore.bin with an external realpath/readlink -f binary | neither is guaranteed on every machine a hook runs on, the same reasoning 1fd0d53 gave for not shelling out to shasum. cd ... && pwd -P is a POSIX-specified builtin and covers the same physical-resolution need without a new dependency.
  • r-exit065 e545dee — leaving guard's inversion in place and only documenting it | guard and inject are the two commands wired into hooks and CI, so a script written against guard today is already trusting the number; a documented inversion is still an inversion, and it gets load-bearing the longer it sits. The surface is one boolean per command right now — cheaper to fix once than to explain forever.
  • r-fix70a1 d707fc7 — exhaustive per-language phrase enumeration | unbounded phrase lists cannot provide semantic coverage, so this fix documents a bounded lexical policy and independent corpus
  • r-shwt66 5efa206 — checking --git-dir/shallow | linked worktrees keep the shallow marker in the common Git directory
  • r-fix760 fb8ba45 — testing commits with more than one parent in dogfood scope | their platform-generated merge messages carry no authored decision, so requiring a record would require one nobody wrote
  • r-refint74 572f573 — allowing a note to extend a commit record under the same Record-Id | notes are remote-reachable, so divergent content would inherit a human-approved identity
  • r-warn75 24c7cc8 — accepting run-on or indented trailers | Git treats them as prose and accepting them would make records ambiguous
  • r-doctor72 996bcde — choosing either parent doctor list | each drops a runtime check required by the other incident
  • r-fix067 a915af0 — treat an outside-repository file_path as a legitimate no-record result | only an in-repository query can truthfully establish that no records apply; silence would hide a broken matcher
  • r-fix063h f0bb995 — leave hook runtime as a separate contradictory check | a fix-bearing installation check must not report ok or warn when the installed hook demonstrably fails
  • r-fix063 0b8c496 — add the explicit refspec only after confirming the remote ref exists | setup would depend on today’s remote state and require another doctor run after the first notes push
  • r-fix053 ecc4b90 — add a public mirrored boolean | the per-record sources array already models contributing channels, so a second representation would create drift
  • r-fix055 43b40f8 — infer whether a quote supports Verified | deterministic text matching can prove presence, not that a check ran
  • r-fix055 43b40f8 — downgrade harvested Verified to reconstructed | it preserves a citation-bearing assertion the verifier cannot substantiate
  • r-fix054 664d4e2 — write X-Inherited-From into commit messages too | transport metadata would lengthen every preserved user-facing message merely to restore symmetry
  • r-fix054 664d4e2 — stop squash-preserve from writing notes | it discards the mirror instead of fixing the query seam
  • r-fix056 55cb8bc — reuse the injection omission list for blocked withholding | it includes prose-bearing Evidence and Expires, so it is not a safety boundary
  • r-7b26f1 ec070ec — retrying the read inside the transaction | a write lock held across a subprocess is a worse failure than the one being fixed
  • r-1e58d3 1fd0d53 — removing the recorded-path branch entirely | a clone is on no PATH and in no node_modules (ADR-0011), and that branch is the only thing that finds the CLI there
  • r-1e58d3 1fd0d53 — hashing the recorded binary at install time | shasum is not guaranteed on every machine a hook runs on, and a check that silently no-ops is worse than the one being replaced
  • r-9c74b3 68340e4 — withholding only in --json | a shell agent reads stdout, and the text form is what it reads
  • r-8e15c2 70f6bcf — hashing only the files the entry point transitively imports | it is the same judgement call that produced this bug, made with more machinery
  • r-4b17f8 7efba5c — retrying the read inside the transaction | a transaction holding a write lock while it shells out to git is a lock held across a subprocess
  • r-7a48c3 b85d847 — a CONFLICTED lifecycle state for divergent declarations | see above — it would block work on records that have a correct answer
  • r-8d51a6 27f73b0 — filtering blocked matches out of the result | the caller needs to know something matched; withholding is a rendering decision, made once
  • r-8d51a6 27f73b0 — reusing exit 1 for "could not check" | 1 already means a broken invocation, and a hook that cannot tell a bad flag from an unreadable repository will treat both as noise
  • r-2f7d94 a7673d0 — an allow-list of free-text keys | it is the shape of the original bug, and a new key would be unguarded until someone remembered
  • r-2f7d94 a7673d0 — leaving the wording generic ("a trailer") | an operator needs to know which line to edit, and the key was available two frames up
  • r-4e29b7 66829bb — folding this into the existing notes field | they are independent axes and can co-occur; one enum would have to enumerate the product
  • r-4e29b7 66829bb — throwing on an unreadable repository | context runs from a hook on every edit, and an exception there is a broken editor rather than a refusal
  • r-1c47e9 0e9930b — dropping the check | the two installation failures it exists for are real and were both invisible to configuration reads
  • r-1c47e9 0e9930b — probing whichever file is newer | "which artifact is this installation" is a fact about the layout, not about timestamps
  • r-3d92a8 f85101a — keeping the searches first and fixing the shim | the shim belongs to npm, not to us, and the version-skew problem survives the fix
  • r-3d92a8 f85101a — a config-only hook check | it was written, it reported ok, and the hook failed on the next commit
  • r-7c05e2 218ea28 — fetching notes automatically when the ref is missing | a query is a read, and silently reaching the network on a read is a surprise that belongs to git fetch
  • r-7c05e2 218ea28 — leaving it to doctor | doctor is run by a person once, and the answer that misleads is the one an agent gets on every task
  • r-7c05e2 218ea28 — a diagnostic string alone | the field it qualifies is records: [], and prose is not something a consumer can branch on
  • r-9b31c7 e8d45fb — keeping the placeholder until author trust was configurable | the placeholder was the permissive direction, so waiting meant shipping the hole
  • r-9b31c7 e8d45fb — withholding blocked payloads from the CLI too | a person reading a terminal can disbelieve a sentence; a tool result is retrieved fact
  • r-9b31c7 e8d45fb — dropping blocked records from the MCP answer entirely | an agent that silently receives less than there is cannot notice, and cannot audit
  • r-4a17d9 68d4c92 — guard versus the control as the primary test | it would confound the route with whether records exist at all, and the interesting question is which route works better once they do
  • r-4a17d9 68d4c92 — a blocking guard arm | GUARD-CANNOT-BLOCK measured true and false positives in one score band; the only precision-safe threshold catches one in five
  • r-6c48b2 aaadedf — matching the whole file at edit time | the file contains everything the agent did not write, and GUARD-CANNOT-BLOCK measured prose surfaces producing false alarms specifically on compliant agents
  • r-6c48b2 aaadedf — blocking on a match | the score bands overlap, measured
  • r-6c48b2 aaadedf — running T-705 · guard route benchmark — measure the path SPEC §5 assigned to Ruled-out #37 without this pre-check | an arm that fires zero times measures nothing, and 120 runs is an expensive way to learn that
  • r-3d61a8 b9f34b3 — adding --ablate-* flags to the shipped inject command | it would create exactly the path from user input to an ablation that the design forbids, and the one that matters injects a prompt-injection payload
  • r-3d61a8 b9f34b3 — importing src/core/inject.ts from bench | src/ uses NodeNext .js specifiers, so --experimental-strip-types cannot load it. dist/ is committed (ADR-0011) and is always present
  • r-3d61a8 b9f34b3 — letting the shim grow into a second injector | that is what bench/context.ts became, and then the benchmark measured the copy
  • r-5b9e37 010782c — baking the resolved path into the hook stub | hooks status is a byte comparison against commitMsgStub(), so every hook installed from a different checkout would report outdated forever
  • r-5b9e37 010782c — an npx fallback | the existing comment is right — npx --no still queries the registry when the package is absent, putting a network call on every commit and breaking offline commits
  • r-2f9c40 07f47ca — wiring guard into the plugin as a blocking hook | true and false positives occupy the same score band on real agent output, so the only precision-safe threshold catches 1 of 5 and every useful threshold blocks four compliant edits in twenty-five
  • r-2f9c40 07f47ca — raising RECORD_ID_WEIGHT's threshold instead of gating the signal | the false alarms scored 1.0000, so no threshold below the maximum excludes them and the maximum excludes everything
  • r-2f9c40 07f47ca — semantic matching to separate the populations | ADR-0002 keeps the core LLM-free and zero-cost, and B-04 · Optional embedding-search tier #31 registers embeddings as opt-in — nothing measured here justifies moving that into the core
  • 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-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-8a2f61 1a5adec — leave commander's default and document it | the code is the interface a hook sees, and documentation does not reach it
  • r-8a2f61 1a5adec — give guard a distinct code for matches | 2 is the contract its ticket specified and callers already treat it as a warning
  • 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-2b8f45 0adcaf5 — raise the threshold until the false positives clear | it takes the weakest true positive with them
  • r-2b8f45 0adcaf5 — exclude the observed false positives by hand | it fixes the ten measured and nothing else
  • r-2b8f45 0adcaf5 — return an error from the MCP guard tool when nothing matches | an agent cannot tell that from "not ruled out"
  • r-8c4a17 f2ab0c2 — pick the first or last value for a single-valued key on conflict | ordering is not judgement, and the value feeds an approval gate
  • r-8c4a17 f2ab0c2 — fuzzy or token-similarity matching for evidence | a fabricated quote that shares most of its words is exactly the case this exists to catch
  • r-8c4a17 f2ab0c2 — mint a fresh Record-Id for the merge | it would claim authorship of decisions made on the branch
  • r-9a5e17 6d68703 — let each command edit src/cli.ts | guaranteed conflict, and the conflict surfaces only after every worker has finished
  • r-9a5e17 6d68703 — npx fallback in the hook stub | a network call on every commit, and offline commits start failing
  • 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

Warnings (45)

  • r-distrace88 d118a73 (claim) — COMMITLORE_BENCH_DIST_DIR is read once at module load (bench/hooks-settings.ts DIST_DIR is a top-level const) — set it before importing the module, not after.
  • r-parsemulti 6d39d25 (claim) — context and validate still do not catch two commit-sourced blocks in the current message sharing one Record-Id unless a notes mirror also disagrees (findIdCollisions requires a notes-sourced record in the group) — parse's check is intentionally independent, not a call into that function, so fixing this in context/validate is separate work.
  • r-multirec01 92aeb24 (claim) — index-db.ts SCHEMA_VERSION is now 2 for the added block column — bump it again, not the shape in place, the next time the trailers table changes.
  • r-83d43117 8a49ddc (claim) — the containment check resolves the recorded path's directory physically and separately rejects a symlink at the final component, but does not protect a relative commitlore.bin with no path separator (a bare filename), which resolves against the hook's cwd and could coincide with the install root in a repository that installs commitlore against itself. That residual case sits in the same accepted-risk category as the severity note above: an attacker who can write that config key already has an equivalent, direct route.
  • r-exit065 e545dee (claim) — guard's swap is a breaking change for anything scripted against the old exit 2 for "matched" — see CHANGELOG Unreleased/Breaking
  • r-fix70a1 d707fc7 (claim) — add malicious and benign fixtures together when extending scanner patterns; false positives can make the defence unusable
  • r-merge66 40e7987 (claim) — test/hooks.test.ts must keep both worktree git-path resolution and reference-integrity assertions
  • r-fix760 fb8ba45 (claim) — unknown-only final paragraphs are treated as prose only when the source is a multi-parent commit
  • r-refint74 572f573 (claim) — exact commit and note mirrors remain one logical record; only divergent note payloads collide
  • r-warn75 24c7cc8 (claim) — query several paths one at a time when rename history matters
  • r-shallow66 60a8659 (claim) — shallow history remains advisory; query and guard exit-code semantics are unchanged
  • r-doctor72 996bcde (claim) — keep commit-msg health bound to hook-runtime and keep inject-runtime as a separate known-good payload probe
  • r-fix067 a915af0 (claim) — the accepted tool set is the union of the plugin and settings matchers; aligning those matchers remains Shallow clone: answers from 1 commit of history without saying history is truncated #66
  • r-fix063 0b8c496 (claim) — the wildcard fetches every ref under refs/notes, including notes owned by other tools
  • r-fix055 43b40f8 (claim) — Verified remains valid protocol vocabulary for facts recorded from actual command or test execution; only harvest refuses it
  • r-7b26f1 ec070ec (claim) — the concurrency test is deterministic rather than sleep-based, so it proves the transaction boundary and not the absence of every race
  • r-1e58d3 1fd0d53 (claim)COMMITLORE_BIN still accepts any executable, deliberately — a harness must be able to aim the hook at a specific build. It is now reported rather than restricted
  • r-9c74b3 68340e4 (claim)context now prints [blocked] beside a record whose payload is gone, which is more visually alarming than the old silent leak. That is the intended direction — a withheld record should be conspicuous — but it changes what a clean repository's output looks like the first time someone commits a Warn: that trips a pattern by accident
  • r-1a63f5 2bb4993 (claim) — "CI is green" was said five times today against a red CI, including in the commit that introduced the rule saying to check CI before saying it. The rule is in docs/RELEASE-GATE.md §5 and it was not followed by its own author. This commit is not claiming CI is green; that claim comes after the run reports
  • r-8e15c2 70f6bcf (claim) — this is the second time in two commits that a fix for the M3 failure did not actually cover the M3 failure. The first was caught by testing the fix against the incident rather than against its own tests
  • r-4b17f8 7efba5c (claim)deleteNoteRows opens its own transaction inside the new outer one. better-sqlite3 nests these as savepoints; node:sqlite has neither, so ADR-0012's migration must flatten this rather than assume it works
  • r-7a48c3 b85d847 (claim) — these two changes were developed concurrently in one worktree and share a built dist/. Splitting them would leave one commit whose dist/ did not match its src/, so they land together and are described together
  • r-5c92e0 73b1285 (claim) — the delegate reported "943 passed" for a suite whose baseline is 1108. It ran while another task was writing to the same worktree and collected a partial set. The real count, verified here on a quiet tree, is 1109 across 31 files — but a delegated test count is now a claim to check, not a result to accept
  • r-8d51a6 27f73b0 (claim) — guard stays advisory. Nothing here makes it block, and GUARD-CANNOT-BLOCK still holds — the point is that it no longer lies about what it saw
  • r-2f7d94 a7673d0 (claim)Evidence: and Expires: are now scanned. Both usually hold paths and dates, so a false positive there withholds a legitimate record. No case is known; a legitimate record carrying a path, a URL and a date was checked and passes
  • r-4e29b7 66829bb (claim)historyAvailability spends two git invocations per query. Both are metadata reads, but this is a hot path and nothing measures it yet
  • r-1c47e9 0e9930b (claim) — this is the second defect in three days from assuming the development checkout is the deployment. The first was exec node in the run script
  • r-3d92a8 f85101a (claim)hook-runtime executes the hook on every doctor run. The probe message is valid so nothing is written, but it is no longer a read-only command
  • r-3d92a8 f85101a (claim) — the check pins PATH to /usr/bin:/bin, which assumes git is there. On a system where it is not, this reports a hook failure that is really a probe failure
  • r-7c05e2 218ea28 (claim)notesAvailability runs git rev-parse and up to two git config reads on every query. Config-only, no network, but it is not free on a hot path
  • r-9b31c7 e8d45fb (claim) — the default is now fail-closed on every route — with no --trusted-author, every Warn: grades claim. That is SPEC §7 and it is what inject already did, but a user who saw [directive] yesterday will see [claim] today
  • r-4a17d9 68d4c92 (claim) — both hooked arms pay a subprocess per edit. M2 put all three of its timeouts in its hooked arm, and a timeout truncates a run, which flatters whichever arm suffers it — stated before the numbers exist
  • r-6c48b2 aaadedf (claim) — recall here is against four re-proposals. It is a go/no-go signal for whether an arm has anything to measure, not an effect size
  • r-3d61a8 b9f34b3 (claim) — this changes what the harness measures, so M1-b's numbers do not carry forward. A run under this delivery path is a new measurement and needs its own registration
  • r-5b9e37 010782c (claim)commitlore.bin and commitlore.node are local config, so they do not survive a fresh clone of a repository whose hook was installed elsewhere — re-run hooks install there, which is what the failure message now says
  • r-2f9c40 07f47ca (claim) — --require-content changes precision, not recall; it removes a false-alarm class and catches nothing new
  • 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-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-8a2f61 1a5adec (claim) — exitOverride on the program alone silently misses subcommands -- the test matrix includes a bad flag on two different subcommands for exactly that reason
  • 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-2b8f45 0adcaf5 (claim) — the matcher is tuned against this repository's rejection corpus, where those common words come from -- re-measure the margin on a corpus with different vocabulary before trusting the threshold there
  • r-8c4a17 f2ab0c2 (claim) — squash-preserve writes nothing without --message-file or --target; the bare form is a plan, and scripting it means passing one
  • r-9a5e17 6d68703 (claim) — commands are advertised in --help only once they work -- test/cli.test.ts holds the landed and unlanded lists, and moving a name between them belongs in the commit that wires it
  • r-5a8c04 c46a577 (claim) — do not add --grep anywhere under src/ -- test/source-guards enforces its absence and will fail the build

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

withheld the content of 3 record(s) graded blocked: a 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.

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