Skip to content

Story 5.4: the engine gets a return type — and still no algebra - #52

Merged
guycorbaz merged 2 commits into
masterfrom
story-5.4-decision-and-ruleset-version
Jul 29, 2026
Merged

Story 5.4: the engine gets a return type — and still no algebra#52
guycorbaz merged 2 commits into
masterfrom
story-5.4-decision-and-ruleset-version

Conversation

@guycorbaz

Copy link
Copy Markdown
Owner

What lands

crates/opencmdb-core/src/identity/cascade.rs gains five types and no behaviour:

type what it is
Verdict D13's five verdicts, spelled as architecture.md:964 spells them
RuleVerdict { rule, verdict, evidence: Vec<ObsId> } the (rule, verdict, evidence) triple that is the explanation
RulesetVersion(u32) D14's mandatory version
Conclusion { Match | NoMatch | Abstained } D13's three-way decision
Decision { conclusion, verdict_vector, ruleset_version } what the cascade returns

Plus Verdict::all() and Decision::rule(). Nothing combines a verdict set, no rule produces a Verdict, nothing produces a Decision — the combining function is story 5.4b's, inserted into epics.md at this story's contexting.

Decision is a struct and Conclusion an enum so ruleset_version rests on one declaration rather than three variants agreeing. Measured: removing the field gives five E0560 plus one E0609.

Measurements

The four mutations

  • M1 drop ruleset_version → 6 errors (5× E0560, 1× E0609 at the assertion that reads it).
  • M2 rule() returns None for NoMatch2 assertion panics, both new. The only one of the four that reds an assertion rather than a name.
  • M3 delete Verdict::Disqualifyingerror[E0599] at three sites and NEVER E0308 — this story's first draft predicted E0308; the validation agent compiled the case and refuted it before dev, and the measurement agrees with the correction.
  • M4 drop evidence → 4 errors, plus an unpredicted side effect recorded rather than smoothed: it orphans the ObsId import, which the CI form of clippy makes a hard error.

⚠️ Two of the four reds are compiler-carried, so this does not close deferred-work.md:1045-1053 — owner stays 5.5. the_verdict_vector_carries_the_whole_triple_in_order has no possible behavioural mutation: a pub field has no code to break.

Docs and register

Six doc locations changed, five because this story falsified them — including trap.rs's RuleId doc, which predicted Epic 5 would close the enum: the corpus writes seven rule names and five are l2-*, Epic 6's.

deferred-work.md:1073-1076 is closed (this story was its named owner — its AC8 ends at the PR, and the merge is a separate act). Three entries annotated , none struck. New ## Deferred from: story-5.4 section: fifteen items — thirteen name a story, two name the condition that would produce an owner, and the preamble says so.

Scope held

No decide(), no table, no xtask gate (5.4b) · no rule, no join, no producer (5.5) · no From<Decision> for Outcome, VerdictVectorEntry still uninhabited (5.7) · no serde (5.9) · no Display, no cause() (5.14) · RuleId not closed into an enum (Epic 6).

🤖 Generated with Claude Code

guycorbaz and others added 2 commits July 29, 2026 16:51
`identity/cascade.rs` gains five types and no behaviour: `Verdict` (D13's five,
spelled as architecture.md:964 spells them), `RuleVerdict { rule, verdict,
evidence }`, `RulesetVersion`, `Conclusion` and `Decision`. Nothing combines a
verdict set, no rule produces a `Verdict`, nothing produces a `Decision` — the
combining function is story 5.4b's, inserted at this story's contexting.

`Decision` is a struct and `Conclusion` an enum so that `ruleset_version` rests
on one declaration rather than three variants agreeing (D14: without it,
improving the engine is "a silent data migration — the worst kind"). Measured:
removing the field gives five E0560 plus one E0609.

267 → 271 tests. Four mutations run. M3 is the one worth naming: deleting a
`Verdict` variant gives `error[E0599]` at three sites and NEVER the `E0308` this
story's first draft predicted — the validation agent compiled the case and
refuted the prediction before dev, and the measurement agrees with the
correction. M4 turned up an unpredicted side effect, recorded rather than
smoothed: dropping `evidence` orphans the `ObsId` import, which the CI form of
clippy makes a hard error.

Two of the four reds are compiler-carried, so this does NOT close
deferred-work.md:1045-1053 — owner stays 5.5.

Six doc locations changed, five because this story falsified them, including
`trap.rs`'s `RuleId` doc: it predicted Epic 5 would close the enum, and the
corpus writes seven rule names of which five are `l2-*`, Epic 6's.

Nothing under fixtures/ moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three parallel layers (Blind Hunter, Edge Case Hunter, Acceptance Auditor),
none failed. The SCOPE held and no behavioural defect was found in the five
types: no decide(), no table, no rule, no producer; VerdictVectorEntry still
uninhabited, trap.rs:38 byte-unchanged, fixtures/ xtask/ opencmdb-bin/ and
Cargo.lock untouched. All four mutation records re-derive against the tree —
no under-reporting of a red set, which is what the last three reviews caught.

The residue was in the prose the story wrote, and twice it is a lesson the
story quotes about itself:

- HIGH. "TWO of the four reds are COMPILER-carried ... M1 and M2 are not" is
  false. M1 (delete ruleset_version) produced SIX compile errors and ZERO
  executed assertions — five E0560 at construction sites, the exact class the
  same sentence assigns to M4 — and it contradicted the M2 bullet six lines
  above, which already called M2 the only one that reds an assertion. THREE of
  four are compiler-carried; only M2 reds an assertion. Two layers measured it
  independently. The weaker true sentence shipped.

- HIGH. Every line citation into deferred-work.md written by this branch used
  PRE-commit numbering while the commit's own insertions shifted the file by
  +21. Three layers converged. That is inherited lesson #1 — "a check that its
  own commit falsifies is worse than no check" — committed by the commit that
  quotes it. Fixed by citing entries BY TITLE, which does not rot; post-review
  positions recorded once in the Completion Notes.

- MEDIUM. The register preamble said "fifteen items: thirteen name a story".
  Counted bullet by bullet: twelve named a story, one named Epic 6, two named
  a condition — an epic called a story in the sentence forbidding exactly that.

- MEDIUM. The #[non_exhaustive] rationale claimed the attribute would destroy
  the E0004 protecting story 5.4b's table. It would not: #[non_exhaustive] is
  inert within the defining crate and 5.4b's table lives in this one. The real
  loss is downstream exhaustiveness in opencmdb-bin. AC1 carries the same
  error, so the code faithfully copied a false spec.

Also: "folded into the existing entry" was never folded (now performed); the
test-placement entry Task 6 declared closed was byte-untouched (now closed,
naming the convention and both cases it decides); AC5's four-name table lacked
the owner column it binds and its heading was untrue of two of its four rows;
the Verdict <-> TrapVerdict cross-reference was one-way; "five tests" where
four shipped; "evidence in four places" where the citations give five.

One patch is more than a doc edit: the vector test's message claimed "the three
lists are distinct" while asserting nothing of the kind. The fix ADDS the
pairwise-distinctness assertion rather than deleting the clause, because
distinctness is what stops that test passing on three identical evidence lists
— the vacuous shape Task 6 banned when it refused Uuid::nil() three times.

Measured, not predicted: replacing RuleVerdict's derived PartialEq with a total
eq() leaves the crate green, so the_verdict_vector_carries_the_whole_triple_in_
order pins a SHAPE and no behaviour. Its doc now says so.

Two findings were handed to story 5.4b's contexting rather than answered here
(Guy's call), and epics.md was NOT edited — writing 5.4b's criteria from inside
5.4 would be the act 5.4 refused when it declined to write decide():
- this commit introduces the workspace's FIRST f32/f64 token (1 on this branch,
  0 on master) — a QUOTATION of D13's own refusal at cascade.rs:42, sitting in
  exactly the subtree 5.4b's line-grep gate is specified to scan;
- three documents name 5.4b owner of the conclusion<->verdict_vector invariant
  while its six criteria cover neither it nor what decide() returns.

Three findings were REFUTED by measurement and dropped: "five sha256-locked
trap files" (measured: exactly five of ten name an l2-* rule), the missing
#[must_use] (the workspace carries one in total), and M1's "elided" sites
(Task 7 asked for the first error and the count, and got them).

Register: 15 -> 18 items in the story-5.4 section, fifteen naming a story, one
naming an epic, two naming a condition — counted mechanically after the last
edit. One item deferred under a new code-review section (the six Decision
literals with an empty verdict_vector that 5.4b will have to rewrite).

Gate re-run WHOLE after the last edit: fmt clean; clippy --all-targets clean;
clippy WITHOUT --all-targets clean (the CI form); 135 + 94 + 42 = 271 tests,
zero failures; cargo xtask ci five gates green with views-hash STALE exit 0,
NOT regenerated (issue #50); git status fixtures/ empty; cargo doc the same
three pre-existing warnings, so no new intra-doc link is broken.

The review's own patches falsified a count the story had measured, and it was
re-measured rather than carried: cascade.rs is 690 lines, first #[cfg(test)] at
:457 -> 456 CODE lines (was 660 / :445 / 444), ceiling 2000.

Story stays at `review`. `done` is the MERGE's business in this project — the
code-review workflow's default of setting it here is wrong and was not applied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@guycorbaz
guycorbaz merged commit da87b62 into master Jul 29, 2026
1 check passed
@guycorbaz
guycorbaz deleted the story-5.4-decision-and-ruleset-version branch July 29, 2026 19:12
guycorbaz added a commit that referenced this pull request Jul 29, 2026
…idated (#53)

Bookkeeping only — **no code changes**. Two acts, because the second
depends on the first.

## Story 5.4 → `done`

PR #52 squash-merged as `da87b62`, CI green on both commits (the story
and its 22 code-review patches). `done` is the **merge's** business in
this project, so the `code-review` workflow's default of setting it at
the end of the review was again not applied. Master re-verified after
the merge: **135 + 94 + 42 = 271**, zero failures.

## Story 5.4b → `ready-for-dev`

`decide(Vec<RuleVerdict>, RulesetVersion) -> Decision` as a **total**
pure function over D13's six rows plus the one input class the table
leaves uncovered, and the `cargo xtask ci` gate that refuses a float
under `identity/`.

**The design decision**, which answers a gap 5.4's review handed
forward: `decide` returns a `Decision`, **not** a bare `Conclusion`. The
returned `verdict_vector` *is* the input, so *"a `Match` with an empty
vector"* and *"a conclusion naming a rule absent from its own vector"*
become **unrepresentable through the function** rather than merely
unenforced — which is what makes the invariant testable at all.

## What validation actually caught

Two fresh-context agents (fact-check + gap-hunt): **9 HIGH + 12
MEDIUM/LOW applied**. Four changed the *design*, and every one was
**measured, not argued**:

- **M2 would have been a silent no-op.** The gap-hunt agent *compiled*
all three arm shapes the story permitted and found that with an
`if`-chain, deleting the arbitration arm *"compiles and changes 0 of 16
input classes"*. The arms are now bound to a `match` on the presence
tuple — the only shape that turns the deletion into `error[E0004]`.
- **The permutation test was vacuous.** Three mixed verdicts conclude
`Abstained`, which names no rule, so no tiebreak can change it — and the
32-subset walk gives that AC zero coverage, since one `RuleVerdict` per
verdict makes `min()` a singleton.
- **A wrong implementation passed every specified test.** `min()` over
the whole vector is deterministic and order-independent, yet returns
`NoMatch { rule }` naming the rule that argued *for* the merge.
- **The float gate was built and run.** The specified word-boundary
match misses `0.85f64` and `let confidence = 0.85;` — the likeliest
shapes a weight takes — and reds on a block comment, a **false
positive**, the one direction the draft said could not happen.

Bookkeeping corrected before dev rather than after: the register set is
**eight** entries across **four** sections, not six in one; **eleven**
doc blocks are falsified, not five; and the story cited `cascade.rs:42`
for the `f64` token when it is `:52` — inherited from the register
without re-measuring, which is this project's own lesson #1 committed by
the story that quotes it.

## One deviation, recorded rather than smoothed

5.4b is the **first** Epic 5 story contexted while its predecessor's PR
was still open; the four before it were each merged first. Every
`cascade.rs` line number in it was measured on the post-review tree, and
the deviation is stated at the top of the story. PR #52 has since
merged, so the prerequisite it states is now satisfied.

Docs current before push: `sprint-status.yaml`,
`docs/project-context.md` and `CLAUDE.md` all name 5.4 `done` and 5.4b
`ready-for-dev`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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