Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,14 @@ six-step checklist (enum → pattern → use-case → map → tests).
- **Inbox**: `vultron/adapters/driving/fastapi/routers/actors/` (package; `_routes.py` defines endpoints)
- **Errors**: `vultron/errors.py`
- **Demo**: `vultron/demo/cli.py` (entry point)
- **Case States**: `vultron/case_states/` — enums are authoritative
- **Case States**: `vultron/core/states/cs.py` — CS/VFD/PXA enums are
authoritative; `vultron/core/states/cs_invariants.py` holds the CS validity,
transition and history invariants (CSB-17). `vultron/core/case_states/` is the
legacy string-pattern reference model, retained as an independent oracle and
still imported by `states/cs.py` and `use_cases/query/action_rules.py`. Reach
for `cs_invariants.py` for new protocol-path work; the legacy module's only
remaining new-code use is as the oracle in the CSB-17 equivalence tests
(ADR-0060)

Full core-layer map → [`vultron/core/AGENTS.md`](vultron/core/AGENTS.md).
Full wire-layer map → [`vultron/wire/as2/AGENTS.md`](vultron/wire/as2/AGENTS.md).
Expand Down Expand Up @@ -486,16 +493,17 @@ See [notes/agents-md-structure.md](notes/agents-md-structure.md) for routing pol
and semantic-registry layers, and confirm against
`graphify explain "<function>"` call edges, before asserting absence.
CONCERN-2243 filed a Concern on this basis for an event emitted by all nine
scenarios.
See also ISSUE-1784 (tracking the script fix).
scenarios. *Source: CONCERN-2243*
- **`git rebase` "local changes would be overwritten" With a Clean Working Tree**
— this error can be a false positive when the rebased branch diverges far from
main and both sides touched the same files. Fix: cherry-pick onto a fresh branch
from `origin/main` (`git checkout -b temp origin/main && git cherry-pick <hash>`)
instead of rebasing. The error message is misleading — it is NOT evidence of
uncommitted work. See also: single large-commit branches with 70+ files trigger a
sequencer duplicate-pick bug; the cherry-pick workaround resolves both variants.
*Sources: ISSUE-1518, ISSUE-1504*
If `freshen-branch.sh` took this path and then hit a conflict, it can leave the
temp branch behind — delete it by hand (ISSUE-1784).
*Sources: ISSUE-1518, ISSUE-1504, ISSUE-1784*
- **Verify Issue ACs Against Current Code Before Starting** — an issue may already
be fully implemented by a prior PR that did not include a `Closes #N` footer.
Check current `main` against all ACs before writing any code; if satisfied, close
Expand Down
257 changes: 257 additions & 0 deletions docs/adr/0060-re-express-legacy-cs-invariants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
---
status: accepted
date: 2026-08-12
deciders: Allen D. Householder
consulted: []
informed: []
---

# Re-express the Legacy Case-State Invariants and Keep the Hypercube as Reference

## Context and Problem Statement

`vultron/core/case_states/` is the original implementation of the MPCVD
state-based model (CMU/SEI-2021-SR-021). It encodes the case-state (CS) rules as
**six-character strings and regular expressions** — `"vfdpxa"`, `"v..P.."` —
validated by `validations.py` and analysed by `hypercube.py` (a networkx /
numpy / pandas model that enumerates states, transitions, histories, and scores
them). The protocol implementation has since moved to typed enums (`CS`,
`CS_vfd`, `CS_pxa`), `pytransitions` machines, and per-machine dimension objects
(ADR-0036). The two worlds do not talk to each other.

The legacy module holds rules the current models do not enforce anywhere. The
most consequential is `is_valid_history`, which makes validity a **causal**
property of an entire event sequence rather than a point-in-time state check or
a wall-clock timestamp comparison — exactly what CONCERN #2181 asked for.
`VFDPXA` and `VFDXAP` visit only valid states, yet only the first is a case
history any real case could have produced; nothing in the current code can tell
them apart.

Two questions had to be settled together (issue #2237, which blocks #2236):

1. Which legacy rules are still valid, which have been superseded, and which are
wrong — and how should the survivors be expressed against the current models?
2. What is the legacy module's status: keep, retire, or archive?

Answering (2) required first checking the issue's stated premise that the module
is "completely orphaned". **It is not.** There are exactly two live importers
outside its own tree:

- `vultron/core/use_cases/query/action_rules.py` imports
`vultron.core.case_states.patterns.potential_actions.action`, reached from the
live `actors_get_action_rules` FastAPI endpoint.
- `vultron/core/states/cs.py` imports `ensure_valid_state` from
`validations.py` and decorates four string helpers with it.

The second is the load-bearing one: the *current* CS enum module depends on the
*legacy* validator, so "retire the legacy module" is not a delete — it is a
migration.

## Decision Drivers

- The rules, not the code, are the asset. The issue's own framing: treat
`case_states/` as the **specification of record**, and prefer a deliberate
line-by-line rewrite over `import and use`.
- Re-expression must be provably faithful. A rewrite that silently admits a
different rule set is worse than no rewrite, because it looks authoritative.
- Real case histories are usually **incomplete**; a rule family that only
validates all six events is not usable on live cases.
- Cross-machine rules (RM/EM × CS emit guards) belong to #2236, not here.
- `hypercube.py` pulls in networkx, numpy and pandas. Nothing on the protocol
path should acquire that dependency weight.
- The 500-line module guideline (CS-18-001): `cs.py` is already over the
guideline, so new code goes in a sibling module. `cs_invariants.py` itself
lands over it too; it is kept whole deliberately, because the rule family is a
single closed set of invariants whose only natural split — predicates apart
from the tables they read — would put a rule and its enforcement in different
files. Splitting is revisited if a second rule family lands here.

## Considered Options

- **A. Import and delegate** — have the current models call
`validations.is_valid_transition` / `is_valid_history`, converting enums to
strings at the boundary.
- **B. Re-express the surviving rules against the current enums; keep the legacy
module as the analytical model and as the test oracle.**
- **C. Re-express and retire** — rewrite, then delete `validations.py` and
`hypercube.py` in this change.
- **D. Archive the whole tree** — move `case_states/` out of `vultron/` to
`docs/` or a research repo, and rebuild any needed rule from the SEI report.

## Decision Outcome

Chosen option: **B — re-express the surviving rules in current idiom; keep the
legacy module, demoted to reference model and test oracle.**

### The rule inventory

Every rule in `validations.py` and the graph construction in `hypercube.py` was
assessed. No rule was found to be **wrong**; the verdicts split between *still
valid* and *superseded by structure*.

| Legacy rule | Verdict | Where it now lives |
|---|---|---|
| `is_valid_state`: `vF` and `fD` are impossible → 32 valid states | Still valid, but **structural** | `CS_vfd` has only 4 members, so the impossible combinations are unrepresentable. Expressed as a ratchet test, not a runtime predicate. CSB-17-001 |
| `is_valid_transition`: Hamming distance 1 | Still valid, unenforced | `cs_transition_event()`, CSB-17-002 |
| `is_valid_transition`: monotone (no UC→lc), same dimension | Still valid, partly enforced per-dimension | Delegated to `is_valid_vfd_transition` / `is_valid_pxa_transition`; compound-level check in `is_valid_cs_transition()`, CSB-17-002 |
| `TRANSITION_RULES[1]`: `...pX. → ...PX.` | Still valid | `required_next_cs_events()`. Partly covered by CSB-13-001 (entry cascade); CSB-17-003 generalises it to every successor of a `pX` state |
| `TRANSITION_RULES[0]`: `v..P.. → V..P..` | Still valid, asserted by SM-09-001 as a persistence-time normalization but with no CS-behavior rule of its own | `required_next_cs_events()`, CSB-17-003, which `refines` SM-09-001 by restating it as a trajectory rule |
| `is_valid_history`: causal event ordering (`V≺F≺D`, `P≺X`/`XP`, `V≺P`/`PV`) | Still valid — **the most valuable rule in the module** | `is_valid_cs_history()` / `is_valid_cs_history_prefix()` / `replay_cs_history()`, CSB-17-004 |
| `is_valid_pattern` and the whole `.`-wildcard regex pattern language | **Superseded** | Enum membership tuples (`VFD_VENDOR_AWARE`, `PXA_EXPLOIT_PUBLIC`, …) and the `is_*` predicates already do this, type-safely |
| `hypercube.py` scoring, tf-idf, pagerank, `DESIDERATA`, adjacency matrices | Out of scope — analytical, not normative | Stays in `hypercube.py`; no protocol path needs it |

The survivors are implemented in `vultron/core/states/cs_invariants.py` as
CSB-17, raising the current-idiom errors (`VultronInvalidStateTransitionError`,
`VultronValidationError`) rather than the legacy `CvdStateModelError` tree.

### Two re-expression choices worth recording

**Transitions delegate to the dimension machines instead of re-deriving
monotonicity.** `is_valid_cs_transition` identifies the single changed dimension
and hands the check to `is_valid_vfd_transition` / `is_valid_pxa_transition` —
the same tables `VfdDimension` / `PxaDimension` use. Monotonicity and the VFD
prerequisite chain then come for free and, more importantly, **cannot drift**
from what the dimension objects enforce. Only the two ephemeral rules are
genuinely new logic at the compound level.

**History validity is expressed as causal replay, not as ordering predicates.**
The legacy formulation compares permutation indices; the re-expression replays
the sequence through the transition rule from `CS.vfdpxa`. The two are provably
equivalent — for a permutation of `VFDPXA`, replay succeeds iff `V≺F`, `F≺D`,
`index(P)−index(X) ≤ 1`, and `index(V)−index(P) ≤ 1`, and the two ephemeral
rules can never be active simultaneously (one needs `P` set, the other needs it
unset). Replay was chosen because it **generalises to prefixes**: real cases are
in progress, and `is_valid_cs_history_prefix` validates what has happened so far
without demanding all six events. The index-comparison formulation cannot do
that. The equivalence is asserted directly in the tests, so the two formulations
cannot silently diverge.

### The legacy module's status: keep, demoted

`validations.py` and `hypercube.py` are **retained**, with a changed role:

- **Not** on the protocol path. `cs_invariants.py` is the runtime source of
truth for CS validity. New code MUST NOT import `case_states.validations`.
- **Reference model.** `hypercube.py` remains the derivation of the 32/58/70
figures and the home of the analytical tooling (scoring, desiderata, pagerank)
that has no protocol counterpart and no reason to acquire one.
- **Test oracle.** `test/core/states/test_cs_invariants.py` compares the new
implementation against the legacy string implementation over the whole space —
64 candidate states, 32×32 candidate transitions, all 720 permutations. This
is the strongest available evidence that the rewrite is faithful, and it only
works while both implementations exist.

Retirement is therefore **deliberately deferred, not forgotten**, and is gated on
two migrations that are out of scope here:

1. `vultron/core/states/cs.py` must stop importing `ensure_valid_state`. Its four
decorated string helpers (`vfd`, `pxa`, `state_string_to_enums`,
`state_string_to_enum2`) need an enum-native validator.
2. `vultron/core/use_cases/query/action_rules.py` must stop importing
`case_states.patterns.potential_actions` — which raises the separate question
of whether the `actors_get_action_rules` endpoint should be served from the
pattern language at all.

Option A was rejected because it would make the enum models depend on
string-and-regex validation permanently, and would let the legacy error tree leak
into protocol paths — the opposite of the issue's explicit instruction to prefer
rewriting. Option C was rejected because deleting the legacy module in the same
change that rewrites it destroys the only independent oracle proving the rewrite
correct, and because two live importers make it a migration rather than a
deletion. Option D was rejected for the same reason plus the loss of the
analytical model, which is genuinely useful and genuinely non-normative.

### Consequences

- Good: the `vP → VP` rule is now enforceable against the current enums for the
first time. SM-09-001 already required it, but only at the persistence
boundary and only with the legacy string-pattern module as its implementation;
CSB-17-003 gives it a CS-behavior expression, and CSB-17-005 settles what it
means for an in-progress history (a prefix may end in `vP`; a persisted state
may not be `vP`).
- Good: history validity is available as a causal check on complete *and*
partial histories, which is what CONCERN #2181 needs.
- Good: compound-transition validity cannot drift from the dimension objects,
because it delegates to their tables rather than re-deriving them.
- Good: the equivalence tests fail loudly if either implementation changes,
making the legacy module useful precisely as long as it is still present.
- Neutral: two implementations of the same rules coexist. Acceptable because one
is explicitly non-normative and the tests pin them together.
- Neutral: `cs_invariants.py` is a library, not an enforcement point. Wiring it
into emit/BT paths is #2236's job; nothing calls it on the protocol path yet.
- Bad: the retirement of `case_states/` is now a recorded intention with two
named prerequisites rather than a completed act, so it can still rot if those
prerequisites are not tracked.

## Validation

- `test/core/states/test_cs_invariants.py` — exhaustive equivalence
against the legacy implementation: the `CS` enum equals the legacy 32-state
set; the new transition rule admits exactly the legacy 58 edges; the causal
replay admits exactly the legacy 70 histories; and `is_valid_cs_history`
agrees with the legacy result on all 720 permutations.
- Ephemeral-state coverage: the 12 `vP`/`pX` states are identified
independently of the predicate under test, and each is asserted to have
exactly one valid successor.
- Prefix coverage: every prefix of every valid history validates, and the
causally impossible prefixes (`F` first, `XA`, `PA`) are rejected. The
converse is pinned too — the accepted set is exactly the prefix-closure of
the 70 valid histories, so no accepted prefix is a dead end (CSB-17-005).
- Input coercion: because `CSEvent` is a `StrEnum`, the single-letter strings of
the legacy API are accepted on every entry point and produce identical
verdicts; the bool-returning predicates answer `False` for non-events rather
than raising.

## Pros and Cons of the Options

### A. Import and delegate to the legacy validators

- Good, because it is the smallest change and cannot diverge from the legacy
rules by construction.
- Bad, because it makes the enum models permanently depend on string/regex
validation and on the legacy `CvdStateModelError` tree.
- Bad, because it cannot express prefix validity — `is_valid_history` requires
all six events, so live in-progress cases remain unvalidatable.
- Bad, because it contradicts the issue's explicit preference for a deliberate
rewrite over `import and use`.

### B. Re-express; keep the legacy module as reference and oracle

- Good, because the current models gain the rules in their own idiom, with their
own errors and their own types.
- Good, because the legacy implementation becomes an independent oracle that
proves the rewrite faithful over the entire state space.
- Good, because replay-based history validity generalises to prefixes.
- Neutral, because two implementations coexist until the migration completes.
- Bad, because retirement becomes a tracked intention rather than a done deed.

### C. Re-express and retire the legacy module now

- Good, because it leaves exactly one implementation of the rules.
- Bad, because it destroys the only independent evidence that the rewrite is
correct, at the moment that evidence is most needed.
- Bad, because two live importers make this a migration; `cs.py` itself depends
on `validations.ensure_valid_state`.
- Bad, because it would discard the analytical model (scoring, desiderata,
pagerank) that has no current-idiom replacement and needs none.

### D. Archive the whole `case_states/` tree out of `vultron/`

- Good, because it makes the non-normative status unmistakable.
- Bad, because it has all of C's problems plus the loss of the existing
`test/core/case_states/` suite.
- Bad, because the `actors_get_action_rules` endpoint would break immediately.

## More Information

The "completely orphaned" premise in issue #2237 is incorrect and the correction
matters for the decision — see `plan/incoming/learnings/`. Cross-machine
(RM/EM × CS) emit-guard enforcement is #2236, which this issue blocks.

Source model: Householder, A. D., and Spring, J. *A State-Based Model for
Multi-Party Coordinated Vulnerability Disclosure (MPCVD)*, CMU/SEI-2021-SR-021,
<https://doi.org/10.1184/R1/16416771>.

Generated spec requirements: `cs-behavior.yaml` CSB-17-001 through CSB-17-004.
Related: ADR-0036 (dimension objects), CSB-13-001 (pX→PX entry cascade),
CSB-16-001/002 (write-boundary transition validation).
1 change: 1 addition & 0 deletions docs/adr/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ General information about architectural decision records is available at <https:
- [ADR-0057 Rename `CVDRole.OTHER` to `CVDRole.OBSERVER` and Define Observer Participant Semantics](0057-observer-participant-role.md)
- [ADR-0058 Gate Demo Scenario Steps on Causal Preconditions, Not Temporal Order](0058-causal-gating-in-demo-scenarios.md) *(provisional)*
- [ADR-0059 Buffer Pre-Genesis `Announce(CaseLedgerEntry)` and Drain on Case Seed](0059-buffer-pre-genesis-ledger-entries.md)
- [ADR-0060 Re-express the Legacy Case-State Invariants and Keep the Hypercube as Reference](0060-re-express-legacy-cs-invariants.md)
- [ADR-0061 Adjudicate Received `ParticipantStatus` Per Dimension, Not as a Unit](0061-per-dimension-partial-accept.md)
- [ADR-0062 Normalise Wire → Core at Ingress, and Enforce It Again at the Persistence Boundary](0062-normalise-wire-to-core-at-both-ingress-and-persistence.md)

Expand Down
6 changes: 4 additions & 2 deletions docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ The **Case State (CS)** tracks awareness and readiness across six binary dimensi
| **A/a** | **A** | **a** | Active attacks have been observed / not observed |

Each transition from lowercase to uppercase represents an event; once uppercase,
it cannot revert. This forms a 64-state lattice (2^6 combinations).
it cannot revert. The 2^6 combinations give a 64-state lattice, of which only
**32 are reachable**: `vF*` (fix ready, vendor unaware) and `*fD*` (fix deployed,
fix not ready) are structurally impossible, per SM-09-002 and CSB-17-001.

---

Expand Down Expand Up @@ -210,7 +212,7 @@ it cannot revert. This forms a 64-state lattice (2^6 combinations).
- The **Case State (CS)** — the six-dimensional VfDpxa lattice model
- A **Case Status** — a snapshot of CS, RM, and EM at one moment
- An RM or EM state — a specific state machine location
- **Recommendation**: Prefer precise terms. Use "Case State" for the 64-node lattice; "Case Status" for a snapshot; "RM state" or "EM state" for a specific state machine location.
- **Recommendation**: Prefer precise terms. Use "Case State" for the lattice (64 combinations, 32 reachable); "Case Status" for a snapshot; "RM state" or "EM state" for a specific state machine location.

2. **"Participant" vs. "Actor"**:
- An **Actor** is any URI-identified federated peer (a role in the protocol).
Expand Down
Loading
Loading