feat(verdicts): a cross-base candidate collision is reported, never dropped in silence

[skip-docs] — the invariant row for this plan lands in Step 13, after the mutations.

Co-Authored-By: Claude <claude-opus-5>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 21:15:50 +02:00
commit 2edd3dce2b
3 changed files with 214 additions and 0 deletions

View file

@ -110,6 +110,27 @@ def verdict_key(features: ProposalFeatures) -> str:
return _mint_id(features)
@dataclass(frozen=True)
class VerdictCollision:
"""Two knowledge bases described ONE candidate, and the second verdict was dropped.
``VerdictStore.add`` is first-write-wins per id and ``_mint_id`` excludes the corpus BY
CONSTRUCTION, so this drop is CORRECT one candidate, one learning key but it used to be
silent, and a base whose finding never entered the store looked exactly like a base that found
nothing (operator decision D2).
A DIAGNOSTIC, never an aggregate over bases: it reports that two bases described one candidate
and never merges, ranks or sums them, which is why it needs no combination rule. ``add`` itself
is unchanged it is called from inside ``run_project``/``run_portfolio``, where a drop can
equally be against a Step-7 inbox verdict or a bundle seed, so only the dispatcher holds the
id -> base map that makes "a SECOND base" a statement worth making.
"""
verdict_id: str
first_bundle_id: str
second_bundle_id: str
def capture_verdict(features: ProposalFeatures, decision: str, rationale: str) -> Verdict:
"""Layer-2 out-of-band verdict constructor: mint a stable content-hash id (the
learning-loop key) and build the ``Verdict`` to persist in the store."""