Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions dist/commitlore.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 30 additions & 2 deletions dist/core/squash.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/core/squash.js.map

Large diffs are not rendered by default.

29 changes: 27 additions & 2 deletions dist/core/stale.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/core/stale.js.map

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions installer/canonical-artifact.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
"tsconfig.json",
"src"
],
"sha256": "755e78a532247fe89949a7f3307ae095cf3ba96fedda9a31cfaaa8840cbce736"
"sha256": "85d4e719a8a1a3302c30b080ad019d0c1dabc7bbf2284e685f20caed6aaa9ff2"
},
"artifact": {
"sha256": "33be2eacf99af27f8f9846b04156ff26d02ae5bf1766a68667d36619ac6b88e0",
"sha256": "148696fcc3cda54db7e539c2cb63ab40eebc325e7ff5c2ab7c82a8599ed75e8b",
"files": [
{
"path": "dist/cli.d.ts",
Expand Down Expand Up @@ -682,7 +682,7 @@
},
{
"path": "dist/commitlore.mjs",
"sha256": "db1595e3a073918cfceff43c108164e42c8aa3b9e9bdffddc05c32a517c5af0c"
"sha256": "9e3b770f9248d50e2abc86a5616de65bf195260cce9085faa7273036d5df5919"
},
{
"path": "dist/core/agent-configs.d.ts",
Expand Down Expand Up @@ -1122,23 +1122,23 @@
},
{
"path": "dist/core/squash.js",
"sha256": "92eb3e4045ab87466d35aa8e541a74f091ce4dfa550e662ee13755dd6a638731"
"sha256": "7562cfb49e5b81ddc26d6cbea1f589f45f1cd5c5ec1233cc3161c1e09516b113"
},
{
"path": "dist/core/squash.js.map",
"sha256": "3c7cd113a3dafc56dd3c243afaffbcf323f9e16576442f3fecb3e5f694978834"
"sha256": "5327efa6bd74cea468ea32d5e49f893f2f16ddca91baaa76d1a0f4dee11f4029"
},
{
"path": "dist/core/stale.d.ts",
"sha256": "f1662c45ba06e9904cdcc802673d8a0c29bd9e72eba54bac7f56f03348a8da69"
},
{
"path": "dist/core/stale.js",
"sha256": "498145626104341a009c506cb28b4103560d4a17e404d98a4bed5439145512eb"
"sha256": "1d336d4c7952c24c2fad7ce45c9629131d1fd5437789a278718df619332d933e"
},
{
"path": "dist/core/stale.js.map",
"sha256": "c50dca2f601b32d4e4bec89e405cb6167cd0477df1f65d0ce113a28f4e7537f3"
"sha256": "2e5a3ca1870b65bee58b747c4792cca569136bfab97194afec609a772987ce6c"
},
{
"path": "dist/core/sync.d.ts",
Expand Down
33 changes: 32 additions & 1 deletion src/core/squash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
CERTAINTY_VALUES,
SINGLE_VALUED,
UNDO_VALUES,
isCommitLoreKey,
type Trailer,
} from './types.js';

Expand Down Expand Up @@ -291,6 +292,36 @@ const recordIdOf = (record: CollectedRecord): string | undefined =>
const contentSet = (trailers: readonly Trailer[]): Set<string> =>
new Set(trailers.map((trailer) => `${trailer.key}${NUL}${trailer.value}`));

/**
* One commit's blocks reduced to the records among them: the trailers whose
* keys this protocol defines, and no block left without one.
*
* git's grammar decides what a trailer block is, and a message's last
* paragraph qualifies whatever its keys mean -- so a paragraph holding only
* `Claude-Session:` is a trailer block. It is not a record, and inheriting it
* wrote a key SPEC §3 does not define into the message the squash was about to
* commit, which `commitlore validate` then refused, blocking the merge
* `squash-preserve` had been run to protect (#1153).
*
* `types.ts isCommitLoreKey` already answers this question for the index,
* where the same paragraphs were served to agents as recorded decisions
* (#335). Here it is asked per trailer rather than per block, because what
* this module emits is a commit message that has to validate: a paragraph
* mixing `Limit:` with a foreign key is a real record, and dropping the whole
* block to be rid of the foreign key would lose it. `Signed-off-by:` goes the
* same way as any other key outside the vocabulary -- it is an attestation
* about the commit that carried it, and inheriting it would assert a sign-off
* on a commit nobody signed.
*
* Applied where both channels become candidate records, before they are
* matched (`mergeCommitBlocks`), so a message cannot reintroduce through its
* mirror what the mirror dropped, or the other way round.
*/
const recordsAmong = (blocks: readonly Trailer[][]): Trailer[][] =>
blocks
.map((block) => block.filter((trailer) => isCommitLoreKey(trailer.key)))
.filter((block) => block.length > 0);

/**
* Matches one commit's message blocks against its mirrored note blocks
* (SPEC §2.4), so a source commit that itself carries several record blocks —
Expand Down Expand Up @@ -433,7 +464,7 @@ export const collectRange = (range: string, opts: SquashOptions = {}): Collected
const noteBlocks =
cachedNote ?? (mirrored.has(sha) ? readRecordBlocks(sha, opts) : []);
if (cachedNote === undefined) opts.cache?.notes.set(sha, noteBlocks);
const blocks = mergeCommitBlocks(messageBlocks, noteBlocks);
const blocks = mergeCommitBlocks(recordsAmong(messageBlocks), recordsAmong(noteBlocks));

for (const trailers of blocks) {
if (trailers.length === 0) continue;
Expand Down
33 changes: 32 additions & 1 deletion src/core/stale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import {
RECORD_ID_RE,
SINGLE_VALUED,
isCommitLoreKey,
parseProvenance,
type Lifecycle,
type Record,
Expand Down Expand Up @@ -471,6 +472,36 @@ const payloadSignatureWithoutProvenance = (record: StaleRecord): string =>
.sort()
.join('\u0001');

/**
* `payloadSignatureWithoutProvenance` restricted to the keys this protocol
* defines (`types.ts isCommitLoreKey`).
*
* What an inherited copy carries is the record, not whatever else the origin's
* trailer paragraph happened to hold: `squash-preserve` drops the foreign keys
* so the message it composes passes `validate` at all (#1153), and a
* `Signed-off-by:` on the origin is an attestation about that commit which
* must not be copied onto another. Compared in full, every faithful copy of
* such a record would read as divergent and be reported as a `duplicate-id`
* -- the same false refusal #1148 removed for the provenance stamp, one key
* class over.
*
* Only the inherited-copy comparison uses this. Weakening it costs the case
* where a copy carries a *different* value for a foreign key than its origin
* did, which stops being a collision; the record's own content still has to
* match exactly.
*/
const recordPayloadSignature = (record: StaleRecord): string =>
record.trailers
.filter(
(trailer) =>
trailer.key !== RECORD_ID_KEY &&
trailer.key !== PROVENANCE_KEY &&
isCommitLoreKey(trailer.key),
)
.map((trailer) => `${trailer.key}\u0000${trailer.value}`)
.sort()
.join('\u0001');

/**
* Whether `message` is a fold of several records and `note` is one of them
* (#1116).
Expand Down Expand Up @@ -564,7 +595,7 @@ const collisionRivals = (group: StaleRecord[]): StaleRecord[] => group.flatMap((
if (isOwnCommitMirror(record, group)) return [];
const origin = inheritedOrigin(record, group);
if (origin === undefined) return [record];
if (payloadSignatureWithoutProvenance(record) === payloadSignatureWithoutProvenance(origin)) return [];
if (recordPayloadSignature(record) === recordPayloadSignature(origin)) return [];
return [{
...record,
trailers: [
Expand Down
Loading
Loading