Skip to content

fix: adjudicate received ParticipantStatus per dimension - #2280

Merged
sei-ahouseholder merged 7 commits into
mainfrom
bug/2235-status-update-silent-drop
Aug 13, 2026
Merged

fix: adjudicate received ParticipantStatus per dimension#2280
sei-ahouseholder merged 7 commits into
mainfrom
bug/2235-status-update-silent-drop

Conversation

@sei-ahouseholder

Copy link
Copy Markdown
Contributor

Summary

A received ParticipantStatus is 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): FilterParticipantStatusDimensionsNode adjudicates rm, vfd and pxa independently and publishes a filtered ParticipantStatus in 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, replacing CheckParticipantRMNotClosedNode. That node's FAILURE was what aborted the enclosing Sequence before StatusUpdateGuard / EmitAddCaseStatusToSelfNode.
  • vultron/core/behaviors/case/nodes/lifecycle.py: adds the generic, ID-matched ledger_payload_object_override blackboard key, next to its consumer CommitCaseLedgerEntryNode, so the canonical CaseLedgerEntry snapshots the accepted portion rather than the raw assertion. Opt-in, so the other receive trees are unaffected.
  • vultron/core/behaviors/status/nodes/append.py: ResolveAndPersistStatusObjectNode persists the filtered status; ValidateRMTransitionNode accepts an rm value 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): ApplyParticipantStatusFromLedgerNode moves here and gains an RM ratchet — an Announce(CaseLedgerEntry) that would regress a replica's rm on the progress scale has that dimension carried forward at the local value (monotonic visibility). Lateral same-rank moves (VALIDINVALID, DEFERREDACCEPTED) are the Case Actor re-adjudicating and are applied unchanged.
  • vultron/core/states/cs.py: adds is_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): ValidateRMTransitionNode and the now-deprecated CheckParticipantRMNotClosedNode move out of append.py. Both append.py (499) and sync/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.
  • Docs: ADR-0060, spec group RSH-05-001 through RSH-05-008, plus notes/received-status-authorization.md and notes/sync-ledger-replication.md.

Scope

em is deliberately not adjudicated at Seam 1 — embargo state belongs to Seam 2, tracked in #2256. The sender still receives 202 Accepted regardless 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.

CheckParticipantRMNotClosedNode is retained rather than removed (no breaking API changes), marked deprecated with an explicit "do not wire this back in" note.

Verification

  • 6 new regression tests in test/core/behaviors/status/test_partial_accept_participant_status.py: refused rm with accepted vfd/pxa, survival of the Seam 2 emit, the ledger snapshot carrying the accepted rm, an RM.CLOSED participant still advancing vfd, whole-update refusal committing no entry, and the replica-side RM ratchet.
  • Full unit suite passes (0 failures); integration suite passes (0 failures).
  • black, flake8 (incl. CC gate), mypy (1203 files) and pyright (0 errors, 0 warnings) all clean.
  • markdownlint-cli2 clean 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 None when inapplicable) and matched by object ID on read. This is recorded as a concern in plan/incoming/learnings/.

🤖 Generated with Claude Code

ahouseholder and others added 2 commits August 12, 2026 21:18
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 sei-ahouseholder left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ⚠️ IMPROVE _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 ⚠️ IMPROVE 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 ⚠️ IMPROVE 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 ⚠️ IMPROVE is_monotonic_vfd_forward / is_monotonic_pxa_forward have zero direct unit tests fix-now
phase6-adr-number-collision-0 adr-check ⚠️ IMPROVE 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.

ahouseholder and others added 2 commits August 13, 2026 00:55
- 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 sei-ahouseholder left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/comments returns 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 sei-ahouseholder left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ⚠️ IMPROVE 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 ⚠️ IMPROVE fixed @ 8cbfc79e ✅ CONFIRMED — unconditional self._publish((), None) at dimension_filter.py:353
phase10-no-blackboard-leak-test-0 ⚠️ IMPROVE fixed @ 8cbfc79e ✅ CONFIRMED — TestLedgerOverrideDoesNotLeakBetweenExecutions (3 tests) at :723
phase10-monotonic-helpers-untested-0 ⚠️ IMPROVE fixed @ 8cbfc79e ✅ CONFIRMED — test/core/states/test_cs_monotonic_predicates.py present, 117 tests
phase6-adr-number-collision-0 ⚠️ IMPROVE 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.

ahouseholder and others added 2 commits August 13, 2026 13:14
…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 sei-ahouseholder left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 main and put this branch into conflict. The 9 triage findings were already fixed at 8cbfc79e and 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 sei-ahouseholder left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ⚠️ IMPROVE fixed @ 8cbfc79e ✅ CONFIRMED — rewrote/blocked wording at dimension_filter.py:429-431
phase8-noop-path-leaves-stale-bb-keys-0 ⚠️ IMPROVE fixed @ 8cbfc79e ✅ CONFIRMED — every early return publishes ((), None) (:353, :363, :372, :378, :405)
phase10-no-blackboard-leak-test-0 ⚠️ IMPROVE fixed @ 8cbfc79e ✅ CONFIRMED — TestLedgerOverrideDoesNotLeakBetweenExecutions at :778
phase10-monotonic-helpers-untested-0 ⚠️ IMPROVE fixed @ 8cbfc79e ✅ CONFIRMED — 14 tests in test_cs_monotonic_predicates.py
phase6-adr-number-collision-0 ⚠️ IMPROVE 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).

@sei-ahouseholder
sei-ahouseholder merged commit e722269 into main Aug 13, 2026
15 of 21 checks passed
@sei-ahouseholder
sei-ahouseholder deleted the bug/2235-status-update-silent-drop branch August 13, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 301+ diff lines or 7+ ACs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rejected status updates are dropped silently and all-or-nothing (violates liberal-accept)

2 participants