isDuplicateClusterWinnerByClaim(pr: DuplicateClaimMember, openSiblings: DuplicateClaimMember[]): boolean (packages/gittensory-engine/src/duplicate-winner.ts:57) already exists, is fully tested (packages/gittensory-engine/test/duplicate-winner.test.ts), and is explicitly earmarked for this: the module's own header comment says "SECOND CONSUMER (#2278): this module is intentionally engine-hosted (not src/-only) because its election logic is reusable for the miner's own soft-claim adjudication — deciding which of several miners claiming the same issue proceeds. A future contributor wiring the miner's local claim ledger should import this module rather than reimplementing the election rule, so both the maintainer gate and the miner agree on exactly one winner by construction" (duplicate-winner.ts:24-28). The claim-ledger module's own comment points at the same gap from the other side: "This table ... does NOT itself decide who 'wins' a contested issue — that adjudication (reusing isDuplicateClusterWinnerByClaim from @jsonbored/gittensory-engine) is out of scope for this foundation-phase issue" (packages/gittensory-miner/lib/claim-ledger.js:81-83, referencing #3355 — the now-closed foundation issue that deliberately deferred exactly this work).
Today isDuplicateClusterWinnerByClaim has exactly one family of real callers, all maintainer-side: src/signals/engine.ts:4605, src/rules/advisory.ts:724, src/queue/processors.ts:7979 and :8967 — each builds a DuplicateClaimMember[] from real OPEN GitHub PRs that link the same issue and elects a winner among them.
Two concrete things worth flagging before this is picked up:
- The local claim ledger alone can't supply the adjudication input.
claim-ledger.js's own header says it is "a 100% client-side record ... The database only lives on this machine; this module never uploads, syncs, or phones home" (lines 6-10). One miner's local SQLite rows have zero visibility into what OTHER miner instances have claimed. So "adjudicate competing soft-claims" can't mean comparing rows within a single local ledger — the competing-claim signal has to come from something publicly observable, most plausibly the same kind of already-open-PR data the maintainer-side callers above already assemble (an issue with multiple open PRs linking it is itself the public signal of a contested claim). Sourcing that view is part of this issue's scope, not a solved prerequisite.
- Field-name mismatch to bridge.
DuplicateClaimMember needs linkedIssueClaimedAt (duplicate-winner.ts:33); the local claim ledger's rows expose claimedAt (packages/gittensory-miner/lib/claim-ledger.js:59-68, rowToClaim). Whatever assembles the election input needs to map one to the other explicitly — they are not interchangeable by accident of naming.
There's also a display-only helper, resolveDuplicateClusterWinnerNumber (duplicate-winner.ts:87-94), for naming the winner in messaging without driving the actual go/no-go decision — worth using if this surfaces winner info to the miner operator (e.g. "you lost this claim to PR #N").
Deliverables
References
isDuplicateClusterWinnerByClaim(pr: DuplicateClaimMember, openSiblings: DuplicateClaimMember[]): boolean(packages/gittensory-engine/src/duplicate-winner.ts:57) already exists, is fully tested (packages/gittensory-engine/test/duplicate-winner.test.ts), and is explicitly earmarked for this: the module's own header comment says "SECOND CONSUMER (#2278): this module is intentionally engine-hosted (notsrc/-only) because its election logic is reusable for the miner's own soft-claim adjudication — deciding which of several miners claiming the same issue proceeds. A future contributor wiring the miner's local claim ledger should import this module rather than reimplementing the election rule, so both the maintainer gate and the miner agree on exactly one winner by construction" (duplicate-winner.ts:24-28). The claim-ledger module's own comment points at the same gap from the other side: "This table ... does NOT itself decide who 'wins' a contested issue — that adjudication (reusingisDuplicateClusterWinnerByClaimfrom@jsonbored/gittensory-engine) is out of scope for this foundation-phase issue" (packages/gittensory-miner/lib/claim-ledger.js:81-83, referencing #3355 — the now-closed foundation issue that deliberately deferred exactly this work).Today
isDuplicateClusterWinnerByClaimhas exactly one family of real callers, all maintainer-side:src/signals/engine.ts:4605,src/rules/advisory.ts:724,src/queue/processors.ts:7979and:8967— each builds aDuplicateClaimMember[]from real OPEN GitHub PRs that link the same issue and elects a winner among them.Two concrete things worth flagging before this is picked up:
claim-ledger.js's own header says it is "a 100% client-side record ... The database only lives on this machine; this module never uploads, syncs, or phones home" (lines 6-10). One miner's local SQLite rows have zero visibility into what OTHER miner instances have claimed. So "adjudicate competing soft-claims" can't mean comparing rows within a single local ledger — the competing-claim signal has to come from something publicly observable, most plausibly the same kind of already-open-PR data the maintainer-side callers above already assemble (an issue with multiple open PRs linking it is itself the public signal of a contested claim). Sourcing that view is part of this issue's scope, not a solved prerequisite.DuplicateClaimMemberneedslinkedIssueClaimedAt(duplicate-winner.ts:33); the local claim ledger's rows exposeclaimedAt(packages/gittensory-miner/lib/claim-ledger.js:59-68,rowToClaim). Whatever assembles the election input needs to map one to the other explicitly — they are not interchangeable by accident of naming.There's also a display-only helper,
resolveDuplicateClusterWinnerNumber(duplicate-winner.ts:87-94), for naming the winner in messaging without driving the actual go/no-go decision — worth using if this surfaces winner info to the miner operator (e.g. "you lost this claim to PR #N").Deliverables
packages/gittensory-miner/lib/claim-adjudication.jsor similar) that buildsDuplicateClaimMember[]input for a contested issue from publicly-observable competing-PR data and callsisDuplicateClusterWinnerByClaimlinkedIssueClaimedAt— do not assume the field names line upclaim listflagging contested claims, orclaim claimwarning when the election would not currently favor this minerresolveDuplicateClusterWinnerNumber(display-only) if winner identity needs to be surfaced to the operator, keeping the actual go/no-go decision driven only byisDuplicateClusterWinnerByClaimisDuplicateClusterWinnerByClaimalready defines for sparse/invalid claim timestampsReferences
packages/gittensory-engine/src/duplicate-winner.ts:57(isDuplicateClusterWinnerByClaim)packages/gittensory-engine/src/duplicate-winner.ts:24-28(module header naming this as the intended second consumer)packages/gittensory-engine/src/duplicate-winner.ts:31-36(DuplicateClaimMembertype)packages/gittensory-engine/src/duplicate-winner.ts:87-94(resolveDuplicateClusterWinnerNumber, display-only helper)packages/gittensory-miner/lib/claim-ledger.js:6-10(local-only, no cross-miner visibility)packages/gittensory-miner/lib/claim-ledger.js:81-83(comment deferring this exact adjudication, citing Doc comment explicitly noting this table is a LOCAL bookkeeping primitive only and does NOT itself decide who "wins" a contested issue — that adjudication (reusingisDuplicateClusterWinnerByClaimfrom@jsonbored/gittensory-engine) is out of scope for this foundation-phase issue. #3355)packages/gittensory-miner/lib/claim-ledger.js:59-68(rowToClaim, theclaimedAtfield to map)src/signals/engine.ts:4605(maintainer-side real caller / pattern for assemblingDuplicateClaimMember[]from open PRs)src/queue/processors.ts:7979,:8967(further maintainer-side real callers)isDuplicateClusterWinnerByClaimfrom@jsonbored/gittensory-engine) is out of scope for this foundation-phase issue. #3355 (closed foundation-phase issue that explicitly deferred this adjudication)