Skip to content

Fix npm publish surface (#93), Shape verdict divergence (#90), same-message Record-Id collisions (#92) - #97

Merged
MongLong0214 merged 3 commits into
devfrom
bug-issue-93
Jul 28, 2026
Merged

Fix npm publish surface (#93), Shape verdict divergence (#90), same-message Record-Id collisions (#92)#97
MongLong0214 merged 3 commits into
devfrom
bug-issue-93

Conversation

@MongLong0214

Copy link
Copy Markdown
Owner

Summary

Three issues, addressed in three commits on top of the current dev (which already includes feat-issue-39's single-executable-binary work):

#93 — npm surface

npm publish would have succeeded (no "private": true), and bin pointed commitlore at dist/cli.js, the unbundled tsc output that crashes ERR_MODULE_NOT_FOUND: commander on a fresh clone with no node_modules.

  • Added "private": true.
  • Removed bin (not repointed — it served an install path ADR-0011 already replaced with a git clone). dist/cli.js itself stays; CI and scripts/commitlore-run.sh still run it directly when node_modules is already present.
  • Moved the 5 runtime dependencies into devDependencies, verified by rebuilding and running the bundle with node_modules deleted.
  • files is untouched — deliberately out of scope even though feat-issue-39 has since landed; folding it in would be scope creep past what The package is still publishable to npm, and bin points at the entry that crashes without node_modules #93's audit found.

Fresh-clone verification (after this PR, node_modules deleted):

$ node dist/commitlore.mjs --version
0.1.0
$ node -e "const p=require('./package.json'); console.log(p.private, p.bin, p.dependencies)"
true undefined undefined

#90 — Shape verdict depended on repository presence

Reproduced against gitseed before changing anything:

$ node dist/commitlore.mjs validate --commit $(git rev-parse HEAD)
shape ok · references ok

$ git log -1 --format=%B HEAD > /tmp/hm.txt
$ node dist/commitlore.mjs validate --message-file /tmp/hm.txt
shape failed · references ok
3: unknown-key F7 — got "F7", want "a key from SPEC §3 or X-<Name>"

What I found: the paths had diverged, not the check class. #76's merge-title exclusion (nonTrailerParagraph in validate.ts) gated on source.merge, computed from git log --format=%P parent-counting — repository information a --message-file/stdin caller never has. --commit/--range populated it; --message-file/stdin silently left it undefined. Shape itself does not need to know a commit has multiple parents — the actual signal wanted ("this paragraph is platform-generated, not an attempted record") is recoverable from the message's own first line, since that's exactly what git merge's templates and GitHub's PR-merge button write there.

Fix: looksLikeMergeTitle matches the first line against those templates (Merge pull request #N from …, Merge branch '…', Merge remote-tracking branch '…', Merge tag '…') — pure text, available identically in every input mode. readCommitSource no longer fetches %P at all.

After:

$ node dist/commitlore.mjs validate --commit $(git rev-parse HEAD)
shape ok · references ok

$ node dist/commitlore.mjs validate --message-file /tmp/hm.txt
shape ok · references ok
commitlore: commit:3: final paragraph does not look like a CommitLore trailer block; saw "F7: Category Pack and evidence-based categorization (#10)"

Both paths now agree, exit 0.

#92 — two blocks, one Record-Id, silently merged

core/stale.ts's findIdCollisions only fired when a notes-sourced record disagreed with a commit's own content — a group with no notes record in it, which is what two same-message commit blocks are, never reached it. parse already detects this locally (#89); context and validate disagreed with it about the same message.

Before (synthetic repro — a message with two blocks sharing Record-Id: r-dupdup):

$ node dist/commitlore.mjs context --json src/queue/squash.ts
"trust": "claim", "identityCollision": false, "trailers": [{"key":"Limit", ...}]   # only one block visible, no collision
$ node dist/commitlore.mjs validate --commit <sha>
shape ok · references ok    # exit 0

Fix:

  • findIdCollisions now also flags a Record-Id claimed by two commit-sourced records sharing a sha (declared by the same message) — a legitimate later-commit re-declaration (SPEC §5 lifecycle update) never shares a sha, so that case stays unflagged, and a clean note mirroring its own commit (which does share a sha) stays gated on payload drift exactly as before.
  • validate's checkReferences was pairing repositoryRecords (which already carries the message's own last block once) with a per-block candidate, so checking the message's last block paired it with a second copy of itself instead of ever placing two different blocks side by side. Rebuilt to pass the message's own blocks once each (plus any notes record for that commit, so #74's divergent-note case stays covered).
  • Also fixed, without which the above fix is invisible through commitlore context <path> (the shape a user actually runs): core/query.ts's collectRows deduplicated rows across aliases by sha+source+seq alone. seq restarts at 0 within every block (SPEC §2.4), so a commit with two blocks has a seq: 1 row in each — the second block's rows were silently read as "already seen" and dropped. Fixed by keying on block too, matching the trailers table's own unique index.

After:

$ node dist/commitlore.mjs context --json src/queue/squash.ts
"trust": "blocked", "identityCollision": true, "trailers": [{"key":"Record-Id","value":"r-dupdup"}]
$ node dist/commitlore.mjs validate --commit <sha>
shape ok · references failed    # exit 1, two duplicate-id violations, one per block

Test plan

  • tsc --noEmit clean
  • npm run build deterministic (git diff --exit-code -- dist/ after rebuild)
  • Full suite: 37 files, 1365 passed / 1 skipped (1366 total) — up 8 tests from dev's current tip (which already includes feat-issue-39, itself well past the 1334/1/36 baseline recorded at PR parse recognizes every record block (#89); fix bench-ablation dist/ race, scale mcp.test.ts timeout (#88) #91); 0 removed, 0 regressed
  • spec/verify.sh, scripts/check-engines.mjs, scripts/check-readme-numbers.mjs all pass
  • Fresh clone (no node_modules) runs --version, validate, doctor
  • node dist/commitlore.mjs validate --range "origin/dev..HEAD"shape ok · references ok, every commit carries CommitLore trailers
  • gitseed reproduction for Shape verdict depends on repository presence, contradicting SPEC §6.1's own class definition #90 confirmed fixed both directions
  • Rebased onto the current dev tip (which already merged feat-issue-39); resolved the expected package.json/CHANGELOG.md conflict by keeping both sides (kept feat-issue-39's private: true, build:binary script, postject devDependency, and this PR's bin removal + dependenciesdevDependencies move)

Not merged to dev — opening for review.

ADR-0011 decided distribution is git clone, not a registry, but
package.json did not enforce that: no `private: true` meant `npm publish`
would still succeed, and `bin` pointed `commitlore` at `dist/cli.js` --
the unbundled tsc output, which crashes ERR_MODULE_NOT_FOUND: commander
on a fresh clone with no node_modules, reproduced before changing
anything. `bin` served only an install path ADR-0011 already replaced,
so it is removed rather than repointed; `dist/cli.js` itself stays,
since CI and scripts/commitlore-run.sh both still run it directly when
node_modules already sits next to it, unrelated to what `bin` does.

The five `dependencies` moved to `devDependencies`: rebuilt and ran the
bundle with node_modules deleted (--version, validate) to confirm
esbuild inlines all five, so they were describing a runtime that does
not exist. `files` is untouched -- the issue's own "After #39" note
says the bin/files/dependencies cleanup is one job once the static
binary lands, not two.

Verified the rebuild is a no-op (git diff --exit-code -- dist/), so
this touches no shipped behavior, only what a registry sees.

Closes #93

Ruled-out: 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
Ruled-out: also removing files now | the issue's own "After #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
Limit: package.json remains a development artifact (build, typecheck, dependency floor) -- it is not read as a distribution manifest by anything in this repository
Blast: local
Undo: easy
Certainty: firm
Verified: dist/ rebuild is byte-identical to HEAD (git diff --exit-code -- dist/), confirming package.json has no effect on the shipped bundle
Verified: fresh copy with node_modules deleted runs `commitlore --version` and reports private:true, bin:undefined, dependencies:undefined
Verified: scripts/check-engines.mjs, spec/verify.sh, scripts/check-readme-numbers.mjs all pass unchanged
Evidence: package.json
Evidence: docs/adr/ADR-0011-plugin-first-distribution.md
Record-Id: r-fix93pkg
Provenance: authored
CommitLore-Version: 2.0.0
…ssage-file agree

Reproduced against gitseed before changing anything: the same merge
commit message read `shape ok` through `--commit` and `shape failed`
(unknown-key on the PR-title paragraph) through `--message-file` fed
the extracted message text. SPEC S6.1 defines Shape as needing "the
message alone" and running "anywhere, including stdin" -- the two
paths disagreeing on the same input is exactly what that clause exists
to rule out.

What I found: the paths had diverged, not the check class. Bug-issue-76's
merge-title exclusion (nonTrailerParagraph) gated on source.merge,
populated by readCommitSource from `git log --format=%P` parent-counting
-- repository information a --message-file/stdin caller never has.
--commit and --range populated it; --message-file and stdin silently
left it undefined, so the identical excuse applied to one path and not
the other for byte-identical text. Shape itself does not need to know a
commit has multiple parents: the signal `nonTrailerParagraph` actually
wants -- "this paragraph is platform-generated, not an attempted record"
-- is recoverable from the message's own first line, since that is
exactly what git's merge templates and GitHub's PR-merge button write
there on their own.

Reconciled by replacing the parent-count signal with a message-only one:
looksLikeMergeTitle matches the first line against those templates
(`Merge pull request #N from …`, `Merge branch '…'`, `Merge
remote-tracking branch '…'`, `Merge tag '…'`), so every input mode
computes the same excuse the same way. readCommitSource no longer
fetches %P at all -- MessageSource does not carry a merge flag anymore.

Closes #90

Ruled-out: keeping the parent-count signal and also computing it for --message-file via .git/MERGE_HEAD | that only exists while a merge is in progress (the commit-msg hook's own scenario), not when re-validating an already-made merge commit's extracted message -- which is the issue's own reproduction and stays unreconciled under that approach
Warn: a message whose first line happens to start with one of these templates on a non-merge, non-CommitLore commit would also be excused if its trailer-shaped final paragraph were entirely unknown-key -- the same shape of risk bug-issue-76's original parent-count gate carried for genuine merge commits, now traded for one that is at least computable the same way everywhere
Blast: module
Undo: easy
Certainty: firm
Verified: 57/57 in test/validate.test.ts, including the new bug-issue-90 test asserting --commit and --message-file report the identical checks[0] and stderr for the same merge commit's message
Verified: gitseed reproduction now reports `shape ok · references ok` through both --commit and --message-file
Verified: 315/315 across test/stale.test.ts, test/query.test.ts, test/trailers.test.ts (unaffected)
Evidence: test/validate.test.ts
Evidence: spec/SPEC.md
Record-Id: r-fix90shape
Provenance: authored
CommitLore-Version: 2.0.0
…validate too

Continuing bug-issue-89's finding, which fixed `parse` and flagged the
rest as open: core/stale.ts's findIdCollisions only fired when a
notes-sourced record disagreed with a commit's own content -- a group
with no notes record in it, which is what two same-message commit
blocks are, never reached it. `context --json` showed one clean record
and `validate` returned 0 for a message parse already refuses, so three
commands disagreed about the same message.

findIdCollisions now also flags a Record-Id claimed by two commit-sourced
records that share a sha -- declared by the same message, not a later
commit re-declaring the id over time (a legitimate SPEC S5 lifecycle
update, which stays unflagged: it never shares a sha). A note cleanly
mirroring its own commit always shares that commit's sha too, so the
new check only counts commit-sourced entries -- bug-issue-74's
divergent-note case stays gated on payload drift exactly as before.

validate's checkReferences built its collision-check array by pairing
repositoryRecords -- which already carries the single last-paragraph
record collectRecords derives for the commit being checked -- with a
per-block candidate, so checking the message's own last block paired
that block with a second copy of itself and an earlier block was never
placed beside a different one at all. Rebuilt to pass the message's own
blocks once each (ownRecords), plus any notes record already found for
that sha so bug-issue-74 stays covered, alongside prior.

Also fixed, without which the fix above could not be observed through
`commitlore context <path>` -- the shape a user actually runs:
core/query.ts's collectRows deduplicated rows fetched across aliases by
sha+source+seq alone. seq restarts at 0 within every record block (SPEC
S2.4), so a commit with two blocks has a seq:1 row in each -- collectRows
was silently dropping the second block's rows as "already seen," which
is what let a scoped-path query show one clean record instead of a
blocked collision. Fixed by keying on block too, matching the trailers
table's own unique index (commit_sha, source, block, seq).

Closes #92

Ruled-out: flagging every duplicate sha in a group regardless of payload, matching parse's labelRecordBlocks exactly | validate's own collision-check array pairs repositoryRecords (already carrying the message's last block once) with a per-block candidate; an unconditional duplicate-sha rule would flag the message's own single last block against its own re-derived copy, a false positive fixed instead by building ownRecords once per message rather than loosening the predicate past correctness
Ruled-out: reverting core/query.ts's collectRows dedup key back to sha+source+seq once findIdCollisions was fixed | that key was already wrong on its own terms (it does not match the trailers table's unique index), and leaving it in place would keep silently dropping a commit's second record block from any scoped-path context query, collision or not
Limit: cross-references between two blocks declared by the same commit (a Follows:/Supersedes: naming a sibling block's id) are still reported as dangling rather than resolved against the sibling -- unchanged from before this fix, and called out in validate.ts's own comment as future work
Blast: module
Undo: easy
Certainty: firm
Verified: 379/379 across test/validate.test.ts, test/stale.test.ts, test/query.test.ts, test/trailers.test.ts, including new tests for the same-sha collision (core/stale.ts unit, validate --commit, and commitlore context --json at a scoped path)
Verified: commitlore context --json <path> on a synthetic two-block same-Record-Id commit now reports identityCollision:true and trust:blocked, matching the existing divergent-note case; before this commit it reported one clean, unflagged record
Evidence: test/stale.test.ts
Evidence: test/validate.test.ts
Evidence: test/query.test.ts
Record-Id: r-fix92dupid
Follows: r-fix90shape
Provenance: authored
CommitLore-Version: 2.0.0
@github-actions

Copy link
Copy Markdown

CommitLore — record lint

Trailers: clean — 3 commits in origin/dev..7f41a6ed31ad7391901bee87c72b8301289a53c5
Active constraints: 32 limits · 86 ruled-out · 47 warnings — from 57 records over 16 changed paths

Active constraints for the paths this PR touches

Limits (32)

  • r-fix92dupid 7f41a6e — cross-references between two blocks declared by the same commit (a Follows:/Supersedes: naming a sibling block's id) are still reported as dangling rather than resolved against the sibling -- unchanged from before this fix, and called out in validate.ts's own comment as future work
  • 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-seabin39 9e9cd0e — doctor's PreToolUse hook runtime check still shells to scripts/commitlore-run.sh via bash for its own probe; a binary install with the Claude Code plugin hook already wired reports a plain ENOENT-style fail there rather than trying the binary directly -- not one of B-09 · Single static binary — remove the Node runtime dependency #39's six required commands, not fixed here
  • 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-det058 695cdf6 — the suite must need no model, agent, network or uncommitted benchmark input
  • 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-4a8e15 49e12c7 — git's grammar requires a subject before a trailer block, so a serialized block is not by itself a parseable message
  • r-1b7d94 736ef92 — git reads ambient configuration, so a suite that does not neutralise it is testing the developer's machine as much as the code
  • r-7e5f02 e5f5e00 — npm installs through an engine mismatch, so the ecosystem's own signal cannot be relied on to stop anything
  • 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
  • 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 (86)

  • r-fix92dupid 7f41a6e — flagging every duplicate sha in a group regardless of payload, matching parse's labelRecordBlocks exactly | validate's own collision-check array pairs repositoryRecords (already carrying the message's last block once) with a per-block candidate; an unconditional duplicate-sha rule would flag the message's own single last block against its own re-derived copy, a false positive fixed instead by building ownRecords once per message rather than loosening the predicate past correctness
  • r-fix92dupid 7f41a6e — reverting core/query.ts's collectRows dedup key back to sha+source+seq once findIdCollisions was fixed | that key was already wrong on its own terms (it does not match the trailers table's unique index), and leaving it in place would keep silently dropping a commit's second record block from any scoped-path context query, collision or not
  • r-fix90shape 7f5766d — keeping the parent-count signal and also computing it for --message-file via .git/MERGE_HEAD | that only exists while a merge is in progress (the commit-msg hook's own scenario), not when re-validating an already-made merge commit's extracted message -- which is the issue's own reproduction and stays unreconciled under that approach
  • 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-seabin39 9e9cd0e — mainFormat: "module" (an ESM SEA main) | verified to fail both blob generation and runtime on this Node line, not merely documented as unsupported (see above)
  • r-seabin39 9e9cd0e — pkg / nexe | third-party bundlers embedding a separate, forked Node runtime this project does not control the patch cadence of; pkg is archived upstream. Trades the Node runtime dependency this ticket removes for a different, less-maintained one
  • r-seabin39 9e9cd0e — Deno compile / Bun compile | a different runtime. node:sqlite, the TypeScript, and NodeNext resolution are all Node-specific; retargeting them is a second runtime port, not a build step, and issue B-09 · Single static binary — remove the Node runtime dependency #39's own first option ("Node SEA -- no source rewrite") needs none
  • r-seabin39 9e9cd0e — reimplement in Go/Rust | issue B-09 · Single static binary — remove the Node runtime dependency #39's own second option, and a real one via spec/fixtures + spec/contract-cases, but an order of magnitude more work than this ticket and not needed to solve either problem (latency, no-Node-on-PATH) this ticket opens with
  • r-seabin39 9e9cd0e — committing dist/commitlore next to dist/commitlore.mjs | breaks ADR-0011's committed-dist/-matches-src/ invariant at ~115 MiB per platform/arch, and a pushed blob that size is not removable from git history again
  • r-seabin39 9e9cd0e — Windows (commitlore.exe) in this PR | Node's docs describe a signtool path this repository has no CI runner to verify; shipping an unverified platform claim is what this project's numbers-or-silence discipline exists to refuse. classifyBinTarget and the resolution order are written so it is a small additive follow-up, not a redesign
  • 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-det058 695cdf6 — add the 100k history suite to CI | CI budget and deterministic local measurement are separate decisions
  • 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-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-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-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-6d92c4 b058de2 — documenting grep -a as the convention | it leaves every future reader one forgotten flag away from the same silent empty result, and the cause takes six characters to remove
  • 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-1b7d94 736ef92 — pass -c flags everywhere | they cover the test's own git calls and miss every call made by the code under test
  • r-1b7d94 736ef92 — give notes.ts a fallback identity | it would hide a real misconfiguration in a user's repository behind a fake author
  • 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-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
  • 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 (47)

  • r-fix90shape 7f5766d (claim) — a message whose first line happens to start with one of these templates on a non-merge, non-CommitLore commit would also be excused if its trailer-shaped final paragraph were entirely unknown-key -- the same shape of risk bug-issue-76's original parent-count gate carried for genuine merge commits, now traded for one that is at least computable the same way everywhere
  • r-seabin39 9e9cd0e (claim) — node:sea is "Active development" per Node's own docs; its schema or CommonJS-only constraint could change between Node versions. core/paths.ts's readInstalledFile/isSea split and build-binary.mjs's asset map are the one place that assumption is absorbed, same posture as ADR-0012 already committed to for node:sqlite
  • 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-det058 695cdf6 (claim) — guard precision is against the frozen archived labels, not a new hand-adjudication
  • 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-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-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-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-6d92c4 b058de2 (claim) — keep the NUL separator when touching these keys — a printable delimiter can appear inside a trailer value and would collide
  • 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-1b7d94 736ef92 (claim) — keep the GIT_CONFIG_* overrides in vitest.config.ts -- removing them makes the suite pass on developer machines and fail in CI, with a message that points at git rather than at the test
  • 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-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
  • 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 16 paths; query one path at a time to follow its rename chain

withheld the content of 2 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.

@MongLong0214
MongLong0214 merged commit ca52bc9 into dev Jul 28, 2026
7 checks passed
MongLong0214 added a commit that referenced this pull request Jul 28, 2026
Resolve conflict in dist/commitlore.mjs (generated) by rebuilding with npm run build && npm run bundle.

Incoming changes from dev (PR #97):
- npm surface removal
- Shape verdict fix
- Record-Id collision fix

Merge complete.
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