Skip to content

Story 5.6: the blocker proposes every pair, and the floor is an integer - #59

Open
guycorbaz wants to merge 2 commits into
masterfrom
story-5.6-blocker-and-recall-assertion
Open

Story 5.6: the blocker proposes every pair, and the floor is an integer#59
guycorbaz wants to merge 2 commits into
masterfrom
story-5.6-blocker-and-recall-assertion

Conversation

@guycorbaz

Copy link
Copy Markdown
Owner

The candidate generator is now an explicit component with a measured recall floor, so a false split
cannot be born silently before any rule has had a chance to speak.

What ships

crates/opencmdb-core/src/identity/blocking.rs — the file the architecture already names
[architecture.md:3368] — holds:

  • CandidatePair, private fields ordered by its constructor, so new(a, b) == new(b, a) holds
    because the two calls build the same value. new(a, a) is None: the self-pair precondition,
    which decide_pair's doc left ownerless, finally has a holder — in the type, not in a comment.
  • candidates(&[Observation]) -> BTreeSet<CandidatePair> — every unordered pair of DISTINCT
    obs_ids, TOTAL by decision. D13 disposes of the performance argument itself ("the blocker is
    not there for performance — 90k pairs is noise on a NAS i5"
    ), and it calls neither join nor
    decide_pair: proposing is not judging, and a blocker that consults a rule is that rule's echo.
  • BLOCKING_RECALL_FLOOR_PER_MILLE: u32 = 999 and blocking_recall_per_mille, returning
    Option<u32>None for an empty denominator, because a recall with no denominator is
    undefined, not perfect.

The float was typed, not avoided

D13 hands this story blocking_recall >= 0.999, which is a bare float literal that reds the
float-free gate — measured during story 5.5 and flagged forward on purpose. The gate is not
weakened, skipped, #[allow]ed or narrowed: it walks 4 .rs files under identity/ now where
it walked 3. The value is D13's, unchanged; only its TYPE moved, on D13's own milli-units corollary
("an INTEGER in milli-units (0..1000), never REAL/DOUBLE") and on the architecture's ratified test
name blocking_recall_above_999, which already carried no float and is used verbatim.

Why this is not the recall gate D18 refuses by name

D18 puts pairwise recall in Tier 2, blocking nothing, because "a gate that cannot fall is
decoration"
. The module doc owes and carries the distinction: different subject (generator input
coverage, not engine output), different venue (a unit test over the frozen corpus, not a release
gate) — and the honest arithmetic that at a 10-pair denominator >= 999‰ IS zero-tolerance, and
becomes a real tolerance only past 1000 required pairs, where NFR4's "any fraction is theatre"
would bite. That day is registered, not inherited. Nothing here advances NFR4.

The corpus is the truth set, and its counts are asserted

Because D47 forbids the domain crate to read files, the corpus half lives in opencmdb-bin's
fixtures.rs test module — nothing above #[cfg(test)] changed and no new pub item appears in
bin. Measured by the shipped assertions, not quoted: 24 traps, 23 naming a pair, 1 residue
(example-must-abstain), 0 naming more than two, 10 must-merge pairs, recall 1000‰.

blocking_recall_above_999 computes only the recall; the per-trap containment assertion is a
separate test. That split is load-bearing and was measured: in one test, a missing pair panics on
docker-veth-must-merge before any recall is computed, and the number you would report is the panic.

Prove-to-red

Six mutations, run against a committed baseline, each restore verified by md5sum and
git status. Zero compiler-carried reds.

# mutation observed reds
M1 blocking on a shared L1 key 700‰ exactly, as measured at contexting 7 (3 bin, 4 core)
M2 blocking on l2_domain the whole corpus stays GREEN, 139/139 1 core
M3 the self-pair admitted 2 reds; the n*(n-1)/2 count test correctly does NOT red 2 core
M4 pair ordered by argument order corpus entirely green — corpus-invisible 3 core
M5 Some(1000) on an empty required the None case reds 1 core
M6 floor weakened to 900 only the independent-literal test reds 1 core

M2 is the one that matters: all 23 committed trap pairs are same-scope, so a l2_domain-blocked
blocker scores a full 1000‰ and is invisible to every corpus test. One synthetic cross-domain test
stands between that wrong blocker and green — which is why the story required it to be written first.

Three divergences from the predictions are stated rather than left implicit: M4's bin count is 139,
not the predicted 138 (same verdict, one more test); M1's bin red set is 3, not 2, the extra being
the containment test that did not exist at contexting; M1's core red set is 4, where the story
estimated "several".

Docs and register

The five falsified doc sites are corrected, plus a sixth found by re-reading and not by grep
identity/mod.rs's D54 paragraph, which turned out to be true (verdict_for_pair is
pub(crate), which is the crate and not the subtree), so it was verified and annotated rather than
rewritten. Register: R1 (the float) and R2 (the self-pair) disposed of by TITLE; three new
entries opened (D17's dormant exclusion, unimplementable because no lifecycle state exists —
measured; the quadratic universe; the floor's arithmetic past 1000 pairs); four entries belonging to
other stories read and left open, including the &str allocation one whose condition is measured
not met, since candidates calls no rule.

Numbers

309 → 332 tests (139 bin + 147 core + 46 xtask), re-counted mechanically after the last edit.
Six green gates plus ℹ views-hash STALE (exit 0 by design, issue #50). fixtures/ and the planning
artifacts are byte-unchanged.

What this does NOT do

No verdict is produced, no Decision is built, and decide/decide_pair/verdict_for_pair are not
called. trap_gate.rs was not opened; no From<Decision> for Outcome appears (5.7). The blocker
has no production caller
— it is reached from its own tests and from fixtures.rs's test module,
the same shape decide was in before story 5.5.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HEE7mz4xufqYW5xKfgJBaF

guycorbaz and others added 2 commits August 1, 2026 00:42
The candidate generator is now an explicit component. `candidates()` returns
every unordered pair of DISTINCT observation ids as a `BTreeSet<CandidatePair>`,
total by decision rather than by omission: D13 disposes of the performance
argument itself (90k pairs at 300 hosts is noise), and every exclusion a blocker
makes is a false split it can never be talked out of.

`CandidatePair` has private fields ordered by its constructor, so `new(a, b) ==
new(b, a)` holds by construction; `new(a, a)` is `None`, which gives the
self-pair precondition its first holder — `decide_pair` answers it today and
says only that the pair "arrives as an argument".

D13's `blocking_recall >= 0.999` ships as `BLOCKING_RECALL_FLOOR_PER_MILLE: u32
= 999`, on D13's own milli-units corollary and on the architecture's ratified
test name, which already carries no float. The `float-free` gate walks 4 files
under identity/ now, unweakened.

Measured against the committed corpus, in fixtures.rs's test module because the
domain crate may not read files (D47): 24 traps, 23 name a pair, 10 must-merge,
recall 1000 per-mille. The containment assertion and the recall value are two
tests on purpose — in one, a missing pair panics before any recall is computed.

309 -> 332 tests (139 bin + 147 core + 46 xtask). Six gates green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEE7mz4xufqYW5xKfgJBaF
The five falsified doc sites are corrected — identity/mod.rs, cascade.rs's
Decision, l1.rs twice, and lib.rs, whose stated reason for not flat-re-exporting
join ('its consumer, the candidate generator, does not exist yet') is exactly
what this story falsifies. A sixth site was found by RE-READING rather than by
grep: identity/mod.rs's D54 paragraph, which turned out to be TRUE
(verdict_for_pair is pub(crate) — the crate, not the subtree), so it was
verified and annotated instead of rewritten.

Register: R1 (the float) and R2 (the self-pair) disposed of by TITLE, three new
entries opened, and four belonging to other stories read and left open — the
&str allocation entry's condition is measured NOT met, since candidates calls no
rule at all.

All six mutations run against a committed baseline, every restore verified by
md5sum and git status. Zero compiler-carried reds. Three divergences from the
predictions are stated rather than left implicit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEE7mz4xufqYW5xKfgJBaF
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