Story 5.6: the blocker proposes every pair, and the floor is an integer - #59
Open
guycorbaz wants to merge 2 commits into
Open
Story 5.6: the blocker proposes every pair, and the floor is an integer#59guycorbaz wants to merge 2 commits into
guycorbaz wants to merge 2 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, sonew(a, b) == new(b, a)holdsbecause the two calls build the same value.
new(a, a)isNone: 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 DISTINCTobs_ids, TOTAL by decision. D13 disposes of the performance argument itself ("the blocker isnot there for performance — 90k pairs is noise on a NAS i5"), and it calls neither
joinnordecide_pair: proposing is not judging, and a blocker that consults a rule is that rule's echo.BLOCKING_RECALL_FLOOR_PER_MILLE: u32 = 999andblocking_recall_per_mille, returningOption<u32>—Nonefor an empty denominator, because a recall with no denominator isundefined, not perfect.
The float was typed, not avoided
D13 hands this story
blocking_recall >= 0.999, which is a bare float literal that reds thefloat-freegate — measured during story 5.5 and flagged forward on purpose. The gate is notweakened, skipped,
#[allow]ed or narrowed: it walks 4.rsfiles underidentity/now whereit 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, andbecomes 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'sfixtures.rstest module — nothing above#[cfg(test)]changed and no newpubitem appears inbin. Measured by the shipped assertions, not quoted: 24 traps, 23 naming a pair, 1 residue
(
example-must-abstain), 0 naming more than two, 10must-mergepairs, recall 1000‰.blocking_recall_above_999computes only the recall; the per-trap containment assertion is aseparate test. That split is load-bearing and was measured: in one test, a missing pair panics on
docker-veth-must-mergebefore 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
md5sumandgit status. Zero compiler-carried reds.l2_domainn*(n-1)/2count test correctly does NOT redSome(1000)on an emptyrequiredNonecase reds900M2 is the one that matters: all 23 committed trap pairs are same-scope, so a
l2_domain-blockedblocker 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_pairispub(crate), which is the crate and not the subtree), so it was verified and annotated rather thanrewritten. Register: R1 (the float) and R2 (the self-pair) disposed of by TITLE; three new
entries opened (D17's
dormantexclusion, 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
&strallocation one whose condition is measurednot met, since
candidatescalls 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 planningartifacts are byte-unchanged.
What this does NOT do
No verdict is produced, no
Decisionis built, anddecide/decide_pair/verdict_for_pairare notcalled.
trap_gate.rswas not opened; noFrom<Decision> for Outcomeappears (5.7). The blockerhas no production caller — it is reached from its own tests and from
fixtures.rs's test module,the same shape
decidewas in before story 5.5.🤖 Generated with Claude Code
https://claude.ai/code/session_01HEE7mz4xufqYW5xKfgJBaF