fix: adjudicate received ParticipantStatus per dimension - #2280
Conversation
Closes #2235. A refused `rm` dimension no longer discards the accepted `vfd`/`pxa` values, and no longer aborts the Seam 1 Sequence before the `Add(CaseStatus)` self-emit that drives embargo teardown. - `status/nodes/dimension_filter.py` (new): `FilterParticipantStatusDimensionsNode` adjudicates `rm`, `vfd` and `pxa` independently and publishes a filtered `ParticipantStatus` carrying the participant's current value forward for each refused dimension. Read-only w.r.t. the DataLayer, so it runs as a precondition guard before the guarded commit (CLP-10-006). Refuses the update in full only when the accepted portion is indistinguishable from current state. - `status/add_participant_status_tree.py`: wire the filter in as Seam 1's second guard, replacing the all-or-nothing `CheckParticipantRMNotClosedNode`. - `case/nodes/lifecycle.py`: add the generic, ID-matched `ledger_payload_object_override` blackboard key so the canonical `CaseLedgerEntry` snapshots the accepted portion, not the raw assertion. - `status/nodes/append.py`: persist and append the filtered status; `ValidateRMTransitionNode` accepts an `rm` value refused and carried forward upstream, keeping its all-or-nothing semantics when used standalone. - `sync/nodes/participant_status_effect.py` (new): move `ApplyParticipantStatusFromLedgerNode` out of `effects.py` and add an RM ratchet — an `Announce(CaseLedgerEntry)` that would regress a replica's RM has that dimension carried forward at the local value (monotonic visibility). Lateral same-rank moves are applied unchanged. - `states/cs.py`: add `is_monotonic_vfd_forward` / `is_monotonic_pxa_forward` component-latch helpers. - `status/nodes/rm_validation.py` (new): move the RM guards out of `append.py`; both modules were at the BTND-07-004 500-line cap. - Docs: ADR-0060, spec group RSH-05-001..008, and both affected notes files. `em` is deliberately left to Seam 2 (#2256). Regression coverage in `test/core/behaviors/status/test_partial_accept_participant_status.py`. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
- Slowest legitimate test is 3.84s against the 5s `timeout` cap, so ~30% CPU contention is enough to abort the whole run. - Name the actual contention sources seen this session (concurrent pyright, graphify post-checkout rebuilds from freshen-branch.sh). Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Triage: #2280 — fix: adjudicate received ParticipantStatus per dimension
Linked issues: #2235 (Rejected status updates are dropped silently and all-or-nothing (violates liberal-accept))
Changed files: 26 files — core/behaviors, core/states, specs, notes, docs/adr
CI status: ❌ failing
Merge state: ✅ mergeable (UNSTABLE — failing checks only, no conflicts)
Base branch: main
Needs integration tests: yes
Findings
| # | Phase | Severity | Description | Outcome |
|---|---|---|---|---|
| phase5-ledger-snapshot-core-shaped-0 | spec-conformance | ❌ FAIL | dimension_filter.py:273 dumps the core ParticipantStatus into the canonical ledger payload override instead of as_ParticipantStatus.from_core(filtered) — root cause of both fcvcv Invariant Harness failures (CM-18-006, CLP-07-001, RSH-05-004) |
fix-now |
| phase8-ratchet-result-discarded-0 | code-review | ❌ FAIL | participant_status_effect.py:220 — the RM ratchet result is logged then discarded when the status already exists in the DataLayer; the un-ratcheted status is appended and the replica's RM regresses (RSH-05-007, SYNC-02-002) |
fix-now |
| phase8-absent-case-status-erases-pxa-em-0 | code-review | ❌ FAIL | dimension_filter.py:175 — an inbound status with no case_status erases the participant's known pxa/em and commits a ledger entry that RSH-05-005 requires be refused in full |
fix-now |
| phase10-no-snapshot-schema-assertion-0 | test-coverage | ❌ FAIL | The new suite's shape-agnostic accessors (lines 119-130) let 6/6 pass while CI fails; no test asserts the ledger snapshot's wire schema, the absent-case_status branch, or the pre-seeded-DataLayer ratchet path |
fix-now |
| phase8-closed-to-closed-mislabeled-refused-0 | code-review | _rm_is_acceptable (:138) reports CLOSED → CLOSED as a refused dimension, so the WARNING and the audit trail name a refusal where nothing was discarded (RSH-05-002) |
fix-now | |
| phase8-noop-path-leaves-stale-bb-keys-0 | code-review | dimension_filter.py:294 — the _require_datalayer() early return skips _publish(), leaving both output blackboard keys stale, contradicting the node's own docstring (BT-17-003/004) |
fix-now | |
| phase10-no-blackboard-leak-test-0 | test-coverage | No cross-run leak test for BB_DIMENSION_FILTER / BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE; the ID match at lifecycle.py:177 is untested in both directions |
fix-now | |
| phase10-monotonic-helpers-untested-0 | test-coverage | is_monotonic_vfd_forward / is_monotonic_pxa_forward have zero direct unit tests |
fix-now | |
| phase6-adr-number-collision-0 | adr-check | ADR 0060 is claimed by both this PR and older open PR #2275; neither is on main. Renumber this one to 0061 (+8 referencing files) |
fix-now |
Total: 4 FAIL · 5 IMPROVE · 0 NEW-ISSUE
The lead FAIL, in one paragraph
FilterParticipantStatusDimensionsNode._publish() writes the partial-accept
override with filtered.model_dump(...) on the core model. Reproduced
locally, the two paths diverge:
partial accept (override): [caseEngagement, caseStatus, context, cvdRole,
embargoAdherence, id, published, rm, type, updated, vfd]
full accept (no override): [@context, caseEngagement, caseStatus, context, cvdRole,
embargoAdherence, id, name, published, rmState, type,
updated, vfdState]
Nested rm/vfd dimension objects, no @context, no name, no
emConsentState. Every other canonical payloadSnapshot in the project is
wire-shaped, so this one entry is unreadable to the invariant harness
(_missing_fields_in_status_snap, cs_observations_from_snap) — and, more
importantly, to every participant it is replicated to via
Announce(CaseLedgerEntry). It also violates the AGENTS.md rule
"Core→Wire Conversion: Use wire_cls.from_core() — never model_dump() +
model_validate()."
CI baseline
fcvcv Invariant Harness is green on main (run 31632495576) and red here
with 2 failed, 46 passed in 1.85s — the assertions ran, so this is a genuine
regression. Already red on main and not attributable to this PR:
fvcv-handoff Invariant Harness, plus the Demo Integration jobs for
fvv / fccv-extension / fvcv-handoff / fvcv-extension / fcv / fcvcv.
This PR takes fcvcv Demo Integration from 7 → 4 failures.
Cleared
Cross-module refactor fidelity (all moved names re-exported, __all__ complete,
no symbol lost, no test monkeypatches an old path); BTND-07-004 500-line cap
(dimension_filter 393, append 341, rm_validation 280, participant_status_effect
256); CLP-10-006 guard/commit/effect ordering; mkdocs build --strict; notes
frontmatter (NF-06-001/002); commit trailers; PR body format; RM progress-scale
ordering and the ADR-0060 lateral carve-out; cs.py monotonicity logic;
embargo_adherence settability.
Triage artifact: .claude/pr-2280-triage.json
Next step: run /pr-execute or /pr-ship to apply fixes.
- phase5-ledger-snapshot-core-shaped: the canonical ledger snapshot for a partially accepted status was core-shaped (nested rm/vfd, no @context, emConsentState or cvdRole), which the fcvcv invariant harness rejects and every replica misreads. Core must not import the wire layer to rebuild the object (ADR-0009/0017), so the override is now a field *patch* keyed by wire alias, merged onto the snapshot's existing object by _merge_snapshot_object_fields. Shape preservation is structural: the override and non-override paths produce identical shapes. New spec RSH-05-009 states the requirement. - phase8-ratchet-result-discarded: ApplyParticipantStatusFromLedgerNode saved the ratcheted status only when the object was absent locally. Since the node appends what it reads *back*, an already-stored object silently discarded the ratchet and regressed the replica's RM while the ratchet's own warning claimed the opposite. Saved unconditionally now. - phase8-absent-case-status-erases-pxa-em: an inbound status with no caseStatus asserts nothing about pxa/em; it now carries the receiver's own case_status forward instead of blanking both. - phase8-noop-path-leaves-stale-bb-keys: update() clears both blackboard keys unconditionally, before the datalayer guard, so no no-op path inherits a previous execution's override (BT-17-003/004). - phase8-closed-to-closed-mislabeled-refused: addressed in part. The operator-facing WARNING now distinguishes "rewrote dimension(s) X" from "blocked dimension(s) X with no change to the asserted value". The reviewer's proposed reorder of _rm_is_acceptable was NOT applied: making CLOSED->CLOSED acceptable empties `refused`, which makes ValidateRMTransitionNode re-reject the transition (reintroducing #2235) and lets a pure no-op status be appended and ledger-committed. - phase10-no-snapshot-schema-assertion: assert the committed snapshot keeps the sender's wire shape (rmState/vfdState/emConsentState/cvdRole/@context, nested caseStatus, no core dimension dicts, no stale snake_case twins), plus omitted-caseStatus and already-stored-object coverage. - phase10-no-blackboard-leak-test: back-to-back executions, the commit node's ID match in both directions, resolve_dimension_filter's mismatch branch, and the datalayer-missing clear. - phase10-monotonic-helpers-untested: exhaustive table-driven coverage of is_monotonic_vfd_forward / is_monotonic_pxa_forward over every ordered pair, with a bitmask subset oracle independent of the implementation. - phase6-adr-number-collision: renumbered ADR-0060 -> ADR-0061; open PR #2275 (older) keeps 0060. Note that PR #2210 claims 0059, which is already taken on main and will need its own renumber. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`notes/flaky-tests.md` sent `fcvcv Demo Integration`, `fvcv-handoff Demo Integration` and `fvcv-handoff Invariant Harness` to #2216 and `fcv-reject Invariant Harness` to #2121 — both closed. Per pr-execute REFERENCE.md the stale entries are evicted and re-pointed; the live root cause for all of them is #2233 (engage-case 422: `SvcEngageCaseUseCase failed: TransitionParticipantRMtoAccepted`). Adds the two rows the catalog was missing (`fcvcv Invariant Harness`, `fv Invariant Harness`) and records that this row set is deterministic, not flaky: the 422 aborts the engage-case trigger before `GuardedCommitCaseLedgerEntryBT`, so `test_invariant_5_expected_event_types_ present[engage_case]` cannot pass on any scenario. Evidence for run 31656171944 vs the base-commit run 31655077093 is in #2233 (comment) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Execute: #2280 — fix: adjudicate received ParticipantStatus per dimension
Fixes applied: 9 findings in 1 commit (8cbfc79e), plus 1 catalog commit (b574e701)
Issues filed: 0
Deferred (awaiting your input): 0
Tests run: unit + integration
CI status after push: ⏳ pending — the new run started for b574e701; the previous full run is analysed below
Base sync: ✅ merged main @ d06619ca — 0 conflicts
Fixed
| Finding | Commit |
|---|---|
phase5-ledger-snapshot-core-shaped-0: adjudicated ledger snapshot was core-shaped |
8cbfc79e |
phase8-ratchet-result-discarded-0: RM ratchet discarded when the status object already existed locally |
8cbfc79e |
phase8-absent-case-status-erases-pxa-em-0: an omitted caseStatus blanked pxa and em |
8cbfc79e |
phase10-no-snapshot-schema-assertion-0: no test pinned the snapshot's wire schema |
8cbfc79e |
phase8-closed-to-closed-mislabeled-refused-0: CLOSED→CLOSED logged as a rewrite (addressed in part — see below) |
8cbfc79e |
phase8-noop-path-leaves-stale-bb-keys-0: no-op path left a stale blackboard override |
8cbfc79e |
phase10-no-blackboard-leak-test-0: no cross-execution leak test |
8cbfc79e |
phase10-monotonic-helpers-untested-0: _is_monotonic_{vfd,pxa}_forward untested |
8cbfc79e |
phase6-adr-number-collision-0: ADR-0060 collided with open PR #2275 → renumbered 0061 |
8cbfc79e |
Each of the four FAIL fixes was proven to be a real fix by reverting the source and watching the new test go red: KeyError: 'rmState', 'RECEIVED' == 'VALID' alongside a self-contradicting WARNING log, SUCCESS-instead-of-FAILURE, and a surviving stale override.
One finding addressed only in part
phase8-closed-to-closed-mislabeled-refused-0 proposed making _rm_is_acceptable return True for CLOSED→CLOSED. Not applied — that empties refused, which makes ValidateRMTransitionNode re-reject the status and reintroduces the exact silent drop this PR fixes (#2235), and it lets a pure no-op status be appended and ledger-committed. The audit-accuracy half was applied: the operator WARNING now says blocked dimension(s) … with no change to the asserted value when the recorded value matches the assertion, and reserves rewrote dimension(s) … for a real discard.
Tests
Full unit suite (rm -rf devlogs && uv run pytest -q -p no:randomly): 6726 passed, 370 skipped, 2 xfailed, 0 failed, exit 0. Counts are derived from the progress characters because this repo's reporter emits no final count line; there are zero FAILED/ERROR lines and no +++ Timeout +++ marker. The rm -rf devlogs prefix is required per #2274.
Directly on the changed behaviour — 148 tests, all passing:
| File | Tests |
|---|---|
test/core/behaviors/status/test_partial_accept_participant_status.py |
18 |
test/core/states/test_cs_monotonic_predicates.py (new) |
117 |
test/core/behaviors/status/nodes/test_append.py |
10 |
test/core/behaviors/sync/nodes/test_participant_status_effect.py |
3 |
xfail ratchet clean: the only two XFAILs reference #1991 and #1992, both open.
Pre-existing CI failures — tracked in #2233
Run 31656171944 @ d06619ca had six failing jobs. All six are one root cause, and all six fail identically on the exact merged base 06bf60c2 (run 31655077093):
| Job | Failure |
|---|---|
fcvcv / fvcv-handoff Demo Integration |
SvcEngageCaseUseCase failed: TransitionParticipantRMtoAccepted → 422 on trigger/engage-case, then Timed out waiting … {RM.ACCEPTED}; current=RM.VALID |
fcvcv / fv / fcv-reject Invariant Harness |
test_invariant_5_expected_event_types_present[engage_case] |
fvcv-handoff Invariant Harness |
same, plus test_fvcv_handoff_vendor2_rm_triage_observed |
The harness failures are downstream of the 422: it aborts the trigger before GuardedCommitCaseLedgerEntryBT, so no engage_case entry exists for the assertion #2266 made universal. Causality check against this diff: the 422 is logged ~95s before the first FilterParticipantStatusDimensionsNode line in the same job, so the new guard cannot be upstream of it. The base is in fact worse — six Demo Integration jobs fail there versus two here. Evidence handoff: #2233 comment.
The PR-attributable CI failure is fixed. fcvcv Invariant Harness previously failed the emConsentState/cvdRole snapshot-schema assertions that triage attributed to this PR; it is now 1 failed, 48 passed with only [engage_case] left — direct confirmation of the patch-merge fix.
notes/flaky-tests.md had routed four of these jobs to #2216 and #2121, both closed. Commit b574e701 evicts the stale pointers, adds the two missing rows, and records that this row set is deterministic rather than flaky.
Not done
- Review threads: none to resolve —
gh api repos/CERTCC/Vultron/pulls/2280/commentsreturns 0 inline comments; the only review is triage's own. - NEW-ISSUE findings: none — all 9 triage findings were
fix-now. - Separately noted, not this PR's problem: open PR #2210 claims ADR-0059, which is already taken on
main.
Execute artifact: .claude/pr-2280-execute.json
Next step: /pr-verify — running now as part of /pr-ship.
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Verify: #2280 — fix: adjudicate received ParticipantStatus per dimension
Overall verdict: ❌ GAPS-FOUND — blocking flag is UNVERIFIED-CI-FAILING
CI status: ❌ failing — 15 pass, 6 fail @ b574e701
Merge state: ✅ MERGEABLE (UNSTABLE — CI red, no conflict) — base main
Base sync in execute: ✅ merged @ d06619ca (0 conflicts resolved)
Integrity check: ✅ all 9 findings accounted for
Finding Verdicts
Every fix was checked at HEAD (b574e701), not just in its commit diff.
| Finding | Severity | Outcome | Verdict |
|---|---|---|---|
phase5-ledger-snapshot-core-shaped-0 |
❌ FAIL | fixed @ 8cbfc79e |
✅ CONFIRMED — _accepted_wire_patch + "fields" present at dimension_filter.py:77,334 |
phase8-ratchet-result-discarded-0 |
❌ FAIL | fixed @ 8cbfc79e |
✅ CONFIRMED — unconditional self.datalayer.save(status_obj) at participant_status_effect.py:226 |
phase8-absent-case-status-erases-pxa-em-0 |
❌ FAIL | fixed @ 8cbfc79e |
✅ CONFIRMED — carry-forward branch at dimension_filter.py:232 |
phase10-no-snapshot-schema-assertion-0 |
❌ FAIL | fixed @ 8cbfc79e |
✅ CONFIRMED — emConsentState/cvdRole/no-snake-twin assertions at test_partial_accept_participant_status.py:427 |
phase8-closed-to-closed-mislabeled-refused-0 |
fixed (in part) @ 8cbfc79e |
✅ CONFIRMED — rewrote/blocked wording split at dimension_filter.py:429; the _rm_is_acceptable half was deliberately declined |
|
phase8-noop-path-leaves-stale-bb-keys-0 |
fixed @ 8cbfc79e |
✅ CONFIRMED — unconditional self._publish((), None) at dimension_filter.py:353 |
|
phase10-no-blackboard-leak-test-0 |
fixed @ 8cbfc79e |
✅ CONFIRMED — TestLedgerOverrideDoesNotLeakBetweenExecutions (3 tests) at :723 |
|
phase10-monotonic-helpers-untested-0 |
fixed @ 8cbfc79e |
✅ CONFIRMED — test/core/states/test_cs_monotonic_predicates.py present, 117 tests |
|
phase6-adr-number-collision-0 |
fixed @ 8cbfc79e |
✅ CONFIRMED — docs/adr/0061-per-dimension-partial-accept.md at HEAD, no 0060 blob |
No conflict markers anywhere in the tree.
⚠️ Why the verdict is not READY-TO-MERGE
pr-verify Phase 3 is unconditional: integration_tests_run == true and the integration CI jobs are not green, so every finding carries UNVERIFIED-CI-FAILING and rule 2 fires. That is the verdict, and it is not being upgraded here. What the evidence actually shows, though, is worth stating precisely:
| Job | Failure | Base 06bf60c2? |
|---|---|---|
fcvcv Demo Integration |
SvcEngageCaseUseCase failed: TransitionParticipantRMtoAccepted → 422 |
❌ also fails |
fvcv-handoff Demo Integration |
same 422 | ❌ also fails |
fcvcv Invariant Harness |
test_invariant_5_expected_event_types_present[engage_case] — 1 failed, 48 passed |
❌ also fails |
fv Invariant Harness |
same | ❌ also fails |
fcv-reject Invariant Harness |
same | ❌ also fails |
fvcv-handoff Invariant Harness |
same + test_fvcv_handoff_vendor2_rm_triage_observed |
❌ also fails |
All six are one root cause — #2233, the engage-case 422 — and all six fail identically on the exact merged base (base run 31655077093 vs this run). The base is worse: six Demo Integration jobs fail there versus two here. Evidence handoff: #2233 comment.
The CI failure that triage did attribute to this PR is fixed: fcvcv Invariant Harness was failing the emConsentState/cvdRole payload-snapshot assertions and is now 1 failed, 48 passed with only [engage_case] left.
Unit suite locally: 6726 passed, 370 skipped, 2 xfailed, 0 failed (exit 0). 148 tests cover the changed behaviour directly. xfail ratchet clean (#1991, #1992 both open).
So the gap is not a defect in this branch — it is that main cannot currently produce a green Demo Integration run, which is exactly what #2251 is waiting on before required status checks can be enabled. Clearing this verdict needs a human call: merge on the documented base-parity evidence above, or land #2233 first.
Artifacts preserved — .claude/pr-2280-triage.json and .claude/pr-2280-execute.json remain for a re-run.
…e-silent-drop Resolves four conflicts with #2232 (ADR-0062, "Normalise Wire -> Core at Ingress, and Enforce It Again at the Persistence Boundary"). docs/adr/index.md, mkdocs.yml List-append false conflicts: both sides added an entry at the same position. Kept both, 0061 before 0062. vultron/core/behaviors/status/nodes/append.py Move-vs-edit. main edited ValidateRMTransitionNode and CheckParticipantRMNotClosedNode in place to use read_rm_states(); this branch had already relocated both classes to status/nodes/rm_validation.py for the 500-line cap (BTND-07-004). Kept the move (--ours) and hand-transplanted main's semantics into rm_validation.py, so all three of main's hunks survive at the new location. The PR's own _rm_was_carried_forward() carry-forward accept (RSH-05, #2235) still runs ahead of the terminal RM.CLOSED check. vultron/core/behaviors/sync/nodes/effects.py Import-set conflict. Kept participant_status_rm_state (main's new canonical RM reader, used by ApplyCloseCaseFromLedgerNode) and dropped main's ParticipantStatus name, which is unused here now that this branch moved ApplyParticipantStatusFromLedgerNode into sync/nodes/participant_status_effect.py. Semantic conflict, resolved in participant_status_effect.py This branch's _local_rm_state() read the replica's RM floor with getattr(current, "rm", None) and returned None when unreadable — exactly the degraded-default pattern main's ARCH-15-001/ARCH-15-002 sweep removed everywhere it existed. A None floor makes _ratchet_rm() a no-op, so a regressing ledger entry would be applied unchecked (the #2264 failure mode) while the ratchet stayed silent. Replaced with read_rm_states(), which distinguishes a shape mismatch (FAILURE) from a legitimate absence (participant with no recorded status yet — no floor to enforce). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The merge resolution replaced _local_rm_state()'s defaulting read of the replica's RM floor with read_rm_states(), so a non-core-shaped local record now yields FAILURE instead of a None floor that silently turned the ratchet into a no-op (ARCH-15-001, ARCH-15-002, ADR-0062, #2264). Verified red against the pre-fix accessor (SUCCESS, regressing entry applied) and green after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Execute: #2280 — fix: adjudicate received ParticipantStatus per dimension
Fixes applied: 9 findings (1 commit) + 1 merge resolution commit + 1 test commit
Issues filed: 0
Deferred (awaiting your input): 0
Tests run: unit + integration
CI status after push: ❌ failing — 6 jobs, all pre-existing (#2233)
Base sync: ✅ merged main @ 92935731 — 4 conflicts resolved (+ 1 semantic conflict)
Re-run of execute after the base moved: #2278 (#2232, ADR-0062) landed on
mainand put this branch into conflict. The 9 triage findings were already fixed at8cbfc79eand are unchanged; this pass resolved the merge.
Conflicts Resolved
| Path | Resolution |
|---|---|
docs/adr/index.md |
List-append false conflict — kept both entries, 0061 before 0062 |
mkdocs.yml |
Same treatment in the nav |
vultron/core/behaviors/status/nodes/append.py |
Move-vs-edit. main edited ValidateRMTransitionNode / CheckParticipantRMNotClosedNode in place to use read_rm_states(); this branch had already relocated both classes to status/nodes/rm_validation.py for the 500-line cap (BTND-07-004). Kept the move and hand-transplanted all three of main's hunks to the new location, so nothing was lost. The PR's own carry-forward accept (_rm_was_carried_forward, RSH-05) still runs ahead of the terminal RM.CLOSED check |
vultron/core/behaviors/sync/nodes/effects.py |
Import-set conflict — kept participant_status_rm_state (used by ApplyCloseCaseFromLedgerNode), dropped main's ParticipantStatus name, unused here since this branch moved ApplyParticipantStatusFromLedgerNode into its own module |
Semantic Conflict Resolved
Both sides applied cleanly in participant_status_effect.py, and the result was broken.
This branch's _local_rm_state() read the replica's RM floor with getattr(current, "rm", None) and returned None when unreadable — exactly the degraded-default pattern main's ARCH-15-001/ARCH-15-002 sweep removed everywhere it existed. A None floor makes _ratchet_rm() a no-op, so a regressing ledger entry would be applied unchecked (the #2264 failure mode), silently, because the ratchet only logs when it refuses something. main's ratchet test guards _NORMALIZE_WIRE_TO_CORE only, so nothing caught it.
Replaced with read_rm_states(), which distinguishes a shape mismatch (FAILURE) from a legitimate absence — a participant with no recorded status yet has no floor to enforce. Covered by a new test (a28328cf), verified red against the pre-fix accessor and green after.
Left as-is after review: append.py:266 (hasattr(status_obj, "rm") or hasattr(status_obj, "vfd")) is a "is this a ParticipantStatus at all" type check that already returns FAILURE, not a degraded default, so it is ARCH-15-conformant in outcome.
Fixed
| Finding | Commit |
|---|---|
phase5-ledger-snapshot-core-shaped-0: ledger payloadSnapshot must keep the sender's wire shape |
8cbfc79e |
| phase8-ratchet-result-discarded-0: ratcheted status not saved, so the read-back regressed the replica | 8cbfc79e |
phase8-absent-case-status-erases-pxa-em-0: absent caseStatus erased current pxa/em |
8cbfc79e |
| phase10-no-snapshot-schema-assertion-0: snapshot test asserted values, not key spelling | 8cbfc79e |
| phase8-closed-to-closed-mislabeled-refused-0: CLOSED→CLOSED narrative wording | 8cbfc79e |
| phase8-noop-path-leaves-stale-bb-keys-0: no-op path left stale blackboard keys (BT-17-003/004) | 8cbfc79e |
| phase10-no-blackboard-leak-test-0: no test for override leakage between executions | 8cbfc79e |
| phase10-monotonic-helpers-untested-0: monotonic CS predicates untested | 8cbfc79e |
| phase6-adr-number-collision-0: ADR 0060 collided with open PR #2275 | 8cbfc79e |
On phase8-closed-to-closed-mislabeled-refused-0 only the audit-accuracy half was applied. The reviewer's _rm_is_acceptable() suggestion was declined: making CLOSED→CLOSED acceptable empties refused, so ValidateRMTransitionNode re-rejects the snapshot and #2235 returns — and it would also let a pure no-op status be appended and ledger-committed.
Tests
| Suite | Result |
|---|---|
| Unit suite on the merged tree | 6805 passed, 370 skipped, 2 xfailed, 0 failed / 0 errors, no timeouts (exit 0) |
-m integration subset |
exit 0; 3 xfail/xpass, all referencing open issues |
| xfail ratchet | #1898, #1991, #1992, #1993, #1994, #2150 — all OPEN |
Counts are derived from progress characters: this repo's reporter (addopts = "-ra -q") emits no final count line. integration_tests/ collects nothing (exit 5) — it holds a README.md and a demo/ tree, no pytest modules; CI's Demo Integration jobs cover that path.
CI — 6 failing jobs, all pre-existing
fcvcv / fvcv-handoff Demo Integration and all four Invariant Harness jobs fail on one cause, tracked in #2233 (OPEN, Epic #2230):
SvcEngageCaseUseCase failed: TransitionParticipantRMtoAccepted
→ 422 on POST .../trigger/engage-case
→ no engage_case ledger entry
→ test_invariant_5_expected_event_types_present[engage_case] fails
→ Demo Integration times out on "CaseActor reflects <actor> at RM.ACCEPTED"
Byte-identical signature on base run 31655077093 @ 06bf60c2, where six Demo Integration jobs fail versus two here. No read_rm_states / shape-mismatch errors appear anywhere in the failing logs, and Tests (pytest), Lint (mypy) and Lint (pyright) all pass on the merged tree.
The one PR-attributable CI failure is fixed: fcvcv Invariant Harness previously failed on the emConsentState / cvdRole snapshot schema and now fails only on [engage_case] (1 failed, 48 passed).
Execute artifact: .claude/pr-2280-execute.json
Next step: /pr-verify.
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Verify: #2280 — fix: adjudicate received ParticipantStatus per dimension
Overall verdict: ❌ GAPS-FOUND — UNVERIFIED-CI-FAILING
CI status: ❌ failing — 6 jobs
Merge state: ✅ MERGEABLE (UNSTABLE — the failing checks below, not a conflict) — base main
Base sync in execute: ✅ merged @ 92935731 (4 conflicts resolved, + 1 semantic conflict)
Integrity check: ✅ all 9 findings accounted for
The merge conflict that sent this back through the pipeline is resolved. git grep finds no surviving conflict markers, and the live merge-state check is MERGEABLE. The verdict is held open by CI alone.
Finding Verdicts
All nine re-verified by reading each file at HEAD (git show HEAD:), not from the commit diff — the merge rewrote two of these files after the fixes landed.
| Finding | Severity | Outcome | Verdict |
|---|---|---|---|
| phase5-ledger-snapshot-core-shaped-0 | ❌ FAIL | fixed @ 8cbfc79e |
✅ CONFIRMED — _accepted_wire_patch at dimension_filter.py:77, used at :334 |
| phase8-ratchet-result-discarded-0 | ❌ FAIL | fixed @ 8cbfc79e |
✅ CONFIRMED — unconditional save(status_obj) at participant_status_effect.py:248 survived the merge |
| phase8-absent-case-status-erases-pxa-em-0 | ❌ FAIL | fixed @ 8cbfc79e |
✅ CONFIRMED — dimension_filter.py:231-235 carries the receiver's case_status forward |
| phase10-no-snapshot-schema-assertion-0 | ❌ FAIL | fixed @ 8cbfc79e |
✅ CONFIRMED — key-spelling assertions at test_partial_accept_participant_status.py:455-461 |
| phase8-closed-to-closed-mislabeled-refused-0 | fixed @ 8cbfc79e |
✅ CONFIRMED — rewrote/blocked wording at dimension_filter.py:429-431 |
|
| phase8-noop-path-leaves-stale-bb-keys-0 | fixed @ 8cbfc79e |
✅ CONFIRMED — every early return publishes ((), None) (:353, :363, :372, :378, :405) |
|
| phase10-no-blackboard-leak-test-0 | fixed @ 8cbfc79e |
✅ CONFIRMED — TestLedgerOverrideDoesNotLeakBetweenExecutions at :778 |
|
| phase10-monotonic-helpers-untested-0 | fixed @ 8cbfc79e |
✅ CONFIRMED — 14 tests in test_cs_monotonic_predicates.py |
|
| phase6-adr-number-collision-0 | fixed @ 8cbfc79e |
✅ CONFIRMED — docs/adr/0061-…md on disk; index.md:132 and mkdocs.yml:280 agree; no 0060 on this branch |
❌ CI — the only thing blocking
| Job | Status |
|---|---|
Tests (pytest), Lint (mypy), Lint (pyright), Lint (black), Lint (flake8), Spec Lint, docs-build-check, CodeQL, fv + fcv-reject Demo Integration |
✅ pass |
fcvcv / fvcv-handoff Demo Integration, all four Invariant Harness jobs |
❌ fail |
All six failures are the same pre-existing cause, tracked in #2233 (OPEN, Epic #2230): SvcEngageCaseUseCase failed: TransitionParticipantRMtoAccepted → 422 on POST .../trigger/engage-case → no engage_case ledger entry → test_invariant_5_expected_event_types_present[engage_case] fails, and the Demo Integration jobs time out on CaseActor reflects <actor> at RM.ACCEPTED.
Evidence that none of it is this PR's: the signature is byte-identical on base run 31655077093 @ 06bf60c2, where six Demo Integration jobs fail versus two here. No shape-mismatch or read_rm_states error appears anywhere in the failing logs.
The one CI failure that was this PR's is fixed: fcvcv Invariant Harness previously failed on the emConsentState / cvdRole snapshot schema and now fails only on [engage_case] (1 failed, 48 passed).
Verify has no carve-out for pre-existing CI failures — a red check is a red check — so the verdict stays GAPS-FOUND. Merging this PR is a judgment call for a human, since the gap is #2233, not #2280.
Post-merge suite
Run on the merged tree, not the pre-merge branch: 6805 passed, 370 skipped, 2 xfailed, 0 failed, 0 errors, no timeouts. -m integration subset green. xfail ratchet clean (#1898, #1991, #1992, #1993, #1994, #2150 all OPEN).
Artifacts preserved — re-run /pr-execute to address gaps (here: none actionable in this PR; the gap is #2233).
Summary
A received
ParticipantStatusis now adjudicated per dimension instead of as a unit, so refusing one dimension no longer discards the others and no longer aborts the Seam 1 → Seam 2 emit that drives embargo teardown.Changes
vultron/core/behaviors/status/nodes/dimension_filter.py(new):FilterParticipantStatusDimensionsNodeadjudicatesrm,vfdandpxaindependently and publishes a filteredParticipantStatusin which each refused dimension carries the participant's current value forward. It is read-only with respect to the DataLayer, so it runs as a precondition guard ahead of the guarded commit (CLP-10-006). An update whose accepted portion is indistinguishable from current state is refused in full — nothing appended, no ledger entry committed.vultron/core/behaviors/status/add_participant_status_tree.py: wires the filter in as Seam 1's second guard, replacingCheckParticipantRMNotClosedNode. That node's FAILURE was what aborted the enclosing Sequence beforeStatusUpdateGuard/EmitAddCaseStatusToSelfNode.vultron/core/behaviors/case/nodes/lifecycle.py: adds the generic, ID-matchedledger_payload_object_overrideblackboard key, next to its consumerCommitCaseLedgerEntryNode, so the canonicalCaseLedgerEntrysnapshots the accepted portion rather than the raw assertion. Opt-in, so the other receive trees are unaffected.vultron/core/behaviors/status/nodes/append.py:ResolveAndPersistStatusObjectNodepersists the filtered status;ValidateRMTransitionNodeaccepts anrmvalue that was refused and carried forward upstream, while keeping its all-or-nothing semantics when the append subtree is used standalone.vultron/core/behaviors/sync/nodes/participant_status_effect.py(new):ApplyParticipantStatusFromLedgerNodemoves here and gains an RM ratchet — anAnnounce(CaseLedgerEntry)that would regress a replica'srmon the progress scale has that dimension carried forward at the local value (monotonic visibility). Lateral same-rank moves (VALID↔INVALID,DEFERRED↔ACCEPTED) are the Case Actor re-adjudicating and are applied unchanged.vultron/core/states/cs.py: addsis_monotonic_vfd_forward/is_monotonic_pxa_forward, treating each VFD/PXA component as an independent one-way latch.vultron/core/behaviors/status/nodes/rm_validation.py(new):ValidateRMTransitionNodeand the now-deprecatedCheckParticipantRMNotClosedNodemove out ofappend.py. Bothappend.py(499) andsync/nodes/effects.py(495) sat within 5 lines of the BTND-07-004 500-line cap, so this fix could not land without splitting them.RSH-05-001throughRSH-05-008, plusnotes/received-status-authorization.mdandnotes/sync-ledger-replication.md.Scope
emis deliberately not adjudicated at Seam 1 — embargo state belongs to Seam 2, tracked in #2256. The sender still receives202 Acceptedregardless of outcome (#2255) and there is no outbound refusal message (#2259); per the agreed scope, the refusal is made visible through the canonical ledger, which every participant replicates.CheckParticipantRMNotClosedNodeis retained rather than removed (no breaking API changes), marked deprecated with an explicit "do not wire this back in" note.Verification
test/core/behaviors/status/test_partial_accept_participant_status.py: refusedrmwith acceptedvfd/pxa, survival of the Seam 2 emit, the ledger snapshot carrying the acceptedrm, anRM.CLOSEDparticipant still advancingvfd, whole-update refusal committing no entry, and the replica-side RM ratchet.black,flake8(incl. CC gate),mypy(1203 files) andpyright(0 errors, 0 warnings) all clean.markdownlint-cli2clean on all new and modified markdown.Note for reviewers: the guard→append handoff uses the py_trees blackboard, which is process-global and not cleared between BT executions. Both new keys are therefore written on every tick (with
Nonewhen inapplicable) and matched by object ID on read. This is recorded as a concern inplan/incoming/learnings/.🤖 Generated with Claude Code