Plan T2 — Registry Migration (sub-plan of Plan T umbrella)

On this page

Status

Step Description Status

1

[#570] T2.1 AnyAdapter → registry dispatch + bundle orchestrator + ExchangeRegistries. Hard mechanical gate at MR open: F-065 / Plan G Step 6 / #462 must be closed — check glab issue view 462 --output json | jq -r .state; ALSO gated on Plan T1 archived (per Plan T umbrella Status rows 2-3). Scope:

* NEW services/craig-exchange/src/bundle_orchestrator.rs — Plan T scope activates craig-state-default::DefaultBundle HARDCODED (per ADR-032 A10; CRAIG__ACTIVE_STATE_BUNDLES env-var activation is Plan U Step 4). The orchestrator: (i) collects BundleContribution`s from active bundles; (ii) materializes the 5 registries via `<Registry>::from_bundles (per Plan T1 T1.5 crates/craig-state-bundle/src/registry.rs); (iii) materializes adapters ONCE at boot — calls each ErasedAdapterFactory with &BootContext { http_client } and stores HashMap<&'static str, Arc<dyn ErasedAdapter>> (per ADR-038 §3: factory shape exists for boot-resource threading; materialization happens once; immutable thereafter); (iv) injects the SHINES adapter AFTER bundle merge — constructs StandardAdapter (reachable here; pub(crate) at adapters/standard.rs:212) wrapped via the T1.7 passthrough impl ErasedAdapter, inserts under "shines" with an explicit duplicate check that fails boot if any bundle contributed "shines" (per Plan T1 body §SHINES placement architecture + risk row 8); (v) ships WITHOUT the DB membership check — T2.4 ADDS that check to this orchestrator (no stub/TODO placeholder; the orchestrator is complete-as-shipped at each step’s scope). * NEW ExchangeRegistries struct (in bundle_orchestrator.rs or sibling registries.rs) holding: adapters: HashMap<&'static str, Arc<dyn ErasedAdapter>> (materialized), audit_codecs: HashMap<&'static str, Arc<dyn AuditCodec>> (cloned from AuditCodecRegistry; consumed at T2.2), partner_types: PartnerTypeRegistry, federal_mapping: FederalPartnerMappingRegistry (carried for T3.4; unused here), noop: Arc<dyn ErasedAdapter> (the NoopAdapter erased once at boot). Wiring realizes the umbrella’s "ExchangeAppState extension" via the established Extension-layer pattern (Plan I F-026 precedent; verified: exchange/cases/security all use Extension layers, no custom State structs): Arc<ExchangeRegistries> layered as ONE axum::Extension at main.rs::build_router + passed directly to ExchangeSendWorker::new as a constructor field. * NEW dispatch helper ExchangeRegistries::resolve(&self, kind: ExchangeAdapterKind, endpoint_url: Option<&str>) → Result<Arc<dyn ErasedAdapter>, ResolveError> — the SINGLE site for: (i) noop:// sentinel check INLINE per ADR-032 A3 (if endpoint.is_some_and(|u| u.starts_with("noop://")) { return Ok(self.noop.clone()) } — mirrors the current adapter_for sentinel at adapters/mod.rs:364-366); (ii) registry lookup by kind’s strum snake_case token; (iii) typed `ResolveError::UnknownKind on miss (boot validation makes this unreachable for DB rows, but the typed error keeps the seam honest). Identity contract: downstream consumers that need the RESOLVED adapter’s identity (T2.2 audit encoding) MUST use adapter.kind() on the returned Arc<dyn ErasedAdapter> — NOT the partner-row ExchangeAdapterKind — because resolve may substitute the noop adapter (whose kind() is "noop") for any partner kind when the endpoint carries the sentinel. Keying on the row kind would mis-route a noop response into a real partner’s codec. * MIGRATE the 2 dispatch call sites: send_worker.rs:234 (adapter.send_value(endpoint, payload, Some(&partner.exchange_format)) — format threaded per-call per ADR-032 A1 axis-2) + api/partners.rs:296 (connectivity — VERIFY-OR-ADD: the Plan T1 body’s T1.2 spec defines 3 trait methods (send_value/audit_value/kind) with NO test_connectivity; T2.1 first VERIFIES whether T1 execution already added it (the T1 executor may have read this body), and if absent ADDS fn test_connectivity_value(&self, endpoint: String) → BoxFuture<', Result<(), ErasedAdapterError>> to the ErasedAdapter trait + both impl_erased_adapter! macro arms in crates/craig-exchange-contracts — additive trait change; the 11 T1.7 invocation-site impls regenerate automatically via the macro; delegates to the typed ExchangeAdapter::test_connectivity). Error-handling at both sites swaps AnyAdapterError match arms for ErasedAdapterError. * DELETE from services/craig-exchange/src/adapters/mod.rs: AnyAdapter enum + AnyAdapterError + adapter_for + dispatch_send + the per-variant adapter_for* test battery (replaced by registry-resolution tests). audit_typed + dispatch_audit_typed deletion is DEFERRED to T2.2 (T2.2 owns it; dispatch_audit_typed’s ONLY caller is `audit_typed — the pair dies together; sequencing keeps this MR reviewable). NoopAdapter at adapters/noop.rs gains impl_erased_adapter!(NoopAdapter, "noop"). * REMOVE StandardAdapter::format: String per-instance field at adapters/standard.rs:214 (the legacy ExchangeAdapter::send path that consumed it is gone) — StandardAdapter::new(config, client) loses the exchange_format param; per-call format arrives via send_with_format/audit_with_format from T1.7 (the T2.1 gate "Plan T1 archived" GUARANTEES T1.7’s direct methods exist before this MR opens). DECIDE at MR time whether impl ExchangeAdapter for StandardAdapter is retired (the erased passthrough path is its only remaining consumer) — retirement preferred if nothing else consumes it. * Tests: orchestrator boot happy-path — adapter map holds EXACTLY 11 entries (10 bundle-contributed + orchestrator-injected "shines"); "noop" ABSENT from the map (held as the ExchangeRegistries.noop sentinel field per A3); SHINES double-registration sad path (synthetic bundle contributing "shines" → boot error); resolve noop-sentinel + unknown-kind unit tests; existing send_worker + connectivity devstack integration tests (services/craig-exchange/tests/) stay green as the regression gate.

Done (2026-06-10) — MR !673 / 3ef30320. Gate verified at MR open (#462 closed + T1 archived). VERIFY-OR-ADD resolved to ADD: test_connectivity_value 4th trait method + both macro arms (+2 contracts tests; passthrough arm forwards to an inherent test_connectivity — see retirement note). Orchestrator + ExchangeRegistries + resolve shipped as specified (5 unit tests; boot log line consumes adapter_count). Execution deviations, all documented in the MR: (a) audit_typed + dispatch_audit_typed deleted HERE not T2.2 — they were AnyAdapter METHODS and could not outlive the enum (the body’s deferred-deletion sequencing assumed free functions); T2.2 scope adjusts to encode_audit seam-addition only; (b) config_for_kind + the 10 non-SHINES StandardAdapterConfig consts deleted HERE — T3.1’s collapse clause pulled forward per its own verify-at-MR-time note (registry pivot left them unreferenced; dead_code deny forces the call); (c) the 10 craig-partner-* deps DROPPED from services/craig-exchange/Cargo.toml (adapters now arrive via craig-state-default factories; machete gate [9b/14]); (d) impl ExchangeAdapter for StandardAdapter RETIRED per the DECIDE clause (erased passthrough was its only consumer; test_connectivity moved inherent); format: None falls back to DEFAULT_EXCHANGE_FORMAT = "json" (the DB column default); (e) orchestrator materializes 4 registries — MockRouterRegistry is the mock-server host’s at T2.3; (f) transactions.rs dead Extension<reqwest::Client> extractor removed with the replaced router layer. Tests: exchange 53 → 42 (−16 AnyAdapter-cluster tests / +5 orchestrator) + contracts 7 → 9. B3a ratchet 177 → 172 + B4 87 → 86 (deleted Value-marshaling helpers + an allow). MR cite backfilled at T2.2

2

[571] T2.2 AuditCodecRegistry wiring + audit seam deletion. Wire ExchangeRegistries.audit_codecs (populated at T2.1 from AuditCodecRegistry::from_bundles) as the audit-encode seam; DELETE the closed-enum audit machinery from services/craig-exchange/src/adapters/mod.rs: AnyAdapter::audit_typed (the Plan L Step 5 [cfg_attr(not(test), allow(dead_code))] seam at mod.rs:246-283) + dispatch_audit_typed helper (mod.rs:320-332) + their tests. Replacement seam: ExchangeRegistries::encode_audit(&self, adapter: &dyn ErasedAdapter, response: &serde_json::Value) → Result<PartnerAuditEvent, ErasedAdapterError> — keys the codec lookup on adapter.kind() (the RESOLVED adapter’s &'static str token, per the T2.1 resolve identity contract), NOT on the partner-row ExchangeAdapterKind: a noop-substituted dispatch carries kind() == "noop" → registry miss → fail-closed, exactly preserving the AnyAdapter fail-closed contract at mod.rs:277-282 (where Noop() + StandardConfigured() share the fail-closed arm). Registry miss (shines/noop have no codec per closed-aggregator A2) returns ErasedAdapterError::AuditUnsupportedForKind { kind } where kind: &'static str per the T1.2 error-enum spec (T1 body defines the variant as &'static str, NOT ExchangeAdapterKindadapter.kind() returns exactly that type; no T1 contract amendment needed). The cfg_attr(dead_code) hack does NOT carry over: encode_audit is exercised by unit tests + the storage layer remains deferred per Plan L Step 5 § Storage layer (deferred) — if clippy flags it, ONE #[cfg_attr(not(test), allow(dead_code))] with the same Plan L citation is acceptable until the storage MR lands. Tests: per-kind encode happy (caps fixture per crates/craig-partner-audit test pattern); SHINES fail-closed sad (adapter.kind() == "shines"AuditUnsupportedForKind { kind: "shines" }); noop-substituted fail-closed sad (resolve with noop:// endpoint, then encode_audit on the returned adapter → AuditUnsupportedForKind { kind: "noop" } — the identity-leak regression test); registry-driven inventory test (10 codecs present).

Done (2026-06-10) — scope narrowed (T2.1 deviation: audit_typed + dispatch_audit_typed already deleted as AnyAdapter methods; T2.2 ships only the registry replacement seam). ExchangeRegistries::encode_audit(&self, adapter: &dyn ErasedAdapter, response: &Value) keys on adapter.kind() per the resolve identity contract; [cfg_attr(not(test), expect(dead_code))] until the send-worker wire-up lands. audit_codecs field [expect(dead_code)] lifted. 4 new tests (caps happy, SHINES fail-closed sad, noop-substituted fail-closed sad, 10-codec inventory). craig-partner-caps added as dev-dep for the test fixture. T2.1 MR cite backfilled (!673 / 3ef30320). MR !674 / d0588d1d (cite backfilled at T2.4)

3

[572] T2.3 Mock-server → MockRouterRegistry + per-partner mock modules + MockState deletion. Per ADR-032 A5 + §2.5 + D8. Scope:

* NEW leaf crate crates/craig-mock-validationtools/craig-mock-server/src/validation.rs (136 LOC + 7 unit tests) moves with LOGIC UNCHANGED but NOT byte-verbatim: the new crate carries the standard lib-root denies (incl. missing_docs), and the current file has undocumented pub items (e.g. ValidationError’s `status/body fields at validation.rs:11-12) — the move ADDS /// doc comments to every public item to satisfy the gate; function bodies + signatures unchanged. Deps: axum + serde_json, unconditional (the crate is mock-only by definition). Rationale: mock modules in per-partner crates need these helpers; per-partner crates cannot prod-dep on craig-mock-server (REAL cycle — mock-server already prod-deps all 10 partner crates). * Mock partner modules migrate tools/craig-mock-server/src/{caps,cprs,doe,empi,ies,ions,smile,stars,tcm,wic}.rscrates/craig-partner-<kind>/src/mock.rs behind NEW mock Cargo feature per A5: per-partner Cargo.toml gains [features] mock = ["dep:axum", "dep:craig-mock-validation"] + [cfg(feature = "mock")] pub mod mock; in lib.rs. Moved inline tests: the modules' [cfg(test)] blocks use tower::ServiceExt::oneshot (e.g. mock-server caps.rs:82) — per-partner [dev-dependencies] gain tower = { workspace = true }; moved test modules gate on [cfg(all(test, feature = "mock"))]. These tests STILL RUN under plain cargo test -p craig-partner-<kind>: the round_trip suites dev-dep craig-mock-server, whose dep chain requires craig-state-default/mockcraig-partner-<kind>/mock, and Cargo feature unification enables mock on the partner lib within that test build graph. Signature normalizes from pub fn routes() → Router<MockState> to stateless pub fn routes() → axum::Router — verified 2026-06-09: ALL 10 modules take State(_state) underscore-unused; MockState’s 9 DashMaps have ZERO readers/writers; deletion is lossless. A5’s `Arc<RwLock<…​>> self-state-binding is documented in each mock.rs header as the pattern for WHEN a module later needs state (not retrofitted now). * DELETE tools/craig-mock-server/src/state.rs (MockState + MockStateInner) + the 10 per-partner module files + validation.rs (moved). * craig-state-default feature propagation per A5: [features] mock = ["craig-partner-caps/mock", …​, "craig-partner-wic/mock"] (all 10); DefaultBundle::contribute().mock_routes populates #[cfg(feature = "mock")] with the 10 entries ("caps", Arc::new(|| craig_partner_caps::mock::routes())), …​ (replacing T1.6’s vec![] placeholder; the non-mock build keeps vec![]). * tools/craig-mock-server rebuild: Cargo.toml drops the 10 direct per-partner deps, gains craig-state-bundle + craig-state-default = { features = ["mock"] }; lib.rs::router() becomes registry-driven — MockRouterRegistry::from_bundles([Box::new(DefaultBundle)]) then Router::new().nest(format!("/partner/{kind}"), factory()) per entry — the /partner/<kind>/…​ nesting path is VERBATIM from ADR-032 §2.5 line 151 (the current bare /{sys} prefixes were pre-contract; Plan U Step 3’s craig-state-tx-stub authors against the §2.5 contract, so T2.3 must land it exactly) + the existing /health route (system list derives from registry keys instead of the hardcoded array at lib.rs:121). router() loses its MockState param; spawn_for_test() + MockServerHandle + bin main.rs keep their shapes (port 9090, MOCK_PORT env). * Dev-dep cycle note (NOT a defect): post-migration the graph is mock-server →(prod) state-default(mock) →(prod) partner crates(mock) + partner crates →(dev) mock-server. Cargo permits dev-dependency cycles (serde/serde_derive precedent); feature unification means partner-crate test builds compile their own mock module — harmless. Plan L F-063 round_trip tests stay in per-partner tests/round_trip.rs consuming craig_mock_server::spawn_for_test() UNCHANGED (A5 option (b)). * UNIFORM path migration + 5 URL-surface audit (per §2.5 + A5 callout + umbrella threat model). ALL 10 mock routes move from bare /{sys} to /partner/<kind> (kind = adapter token; DOE’s token is doe_slds so /doe/partner/doe_slds; the other 9 are /X/partner/X): (a) docker-compose.yml:546-558 — unchanged (port 9090 + service name stable; paths live in URLs not compose); (b) tools/craig-seed/src/datagen.rs:1290-1385 — all 10 seeded endpoint URLs update from http://mock-server:9090/{sys} to http://mock-server:9090/partner/{kind}; pre-1.0 reseed per the pre-1.0 destructive-rebuild posture; (c) ALL 10 per-partner tests/round_trip.rs — hardcoded paths update (e.g. craig-partner-doe-slds/tests/round_trip.rs:69/98/112 {base}/doe{base}/partner/doe_slds); mechanical sweep in the same MR; (d) mock-server bin — unchanged; (e) /health systems list — registry-derived (assert in test). * Tests: registry-driven router smoke (10 /partner/<kind> prefixes respond); /health lists 10 registry keys; per-partner mock modules' existing inline tests move with them; full per-partner round_trip battery green post-path-sweep (the real gate).

Done (2026-06-10) — shipped per spec with ONE structural deviation: crates/craig-mock-validation NOT created — validation.rs DELETED instead. The cell’s premise ("mock modules need these helpers") was stale: Plan L Step 7’s typed migration removed every call; all 9 helpers had ZERO consumers (verified by name-level grep). Dead code does not get a new leaf crate (no-dead-code + YAGNI; T2.1’s config_for_kind deletion precedent); git history preserves the helpers if a future untyped mock needs them. Everything else as specified: 10 mock.rs modules moved (logic unchanged; State params dropped; /// docs added per partner-crate missing_docs_in_private_items deny — the mock-server root had allowed it) behind per-crate mock = ["dep:axum"]; craig-state-default mock feature fans out to all 10 + mock_routes() cfg-gated helper; mock-server rebuilt registry-driven (router() → Result<Router, BundleMergeError> — realization detail: the registry merge is fallible, spawn_for_test wraps the error as io::ErrorKind::InvalidData to keep its spec’d signature; 4 stale crate-level allow blocks lifted with the handlers that justified them); MockState + state.rs deleted (lossless per D8); paths VERBATIM /partner/<kind> (DOE = doe_slds): 30 round_trip URL sites + 7 seeded datagen URLs (the cell’s "all 10" overcounted — 7 of 11 seeded partners are mock-backed) + devstack.adoc; datagen tuple’s mock-route element collapsed to has_mock: bool (path now derives from adapter_kind). EXCHANGE seed hash re-blessed (f7df4abb…) — cases/placement/financial hashes verified UNCHANGED in the same bless run per hash-pinned regression tests. Mock-server tests 3 new (path smoke / registry-derived /health / legacy-path 404 sad); 30 inline tests moved into partner crates; B3a ratcheted 172 → 171. T2.4 MR cite backfilled (!676 / 0a427cdd). MR !677 / 9af1c9f0 (cite backfilled at T2.5)

4

[#573] T2.4 Drop adapter_kind CHECK constraint + boot membership validation. Per ADR-032 §1.3 (dual-site validation closes TOCTOU) + §2.4 as amended by A7 (boot validates REGISTRY MEMBERSHIP only; federal-mapping completeness is report-emit-time, T3.4 scope). Scope:

* NEW migration services/craig-exchange/migrations/<ts>_drop_adapter_kind_check.sqlALTER TABLE exchange_partners DROP CONSTRAINT chk_exchange_partners_adapter_kind; (pre-1.0 destructive OK; commit message cites the pre-1.0 destructive-rebuild posture). * Boot membership validation ADDED to the T2.1 orchestrator in bundle_orchestrator.rs (T2.1 shipped without it by design — no stub/TODO): SELECT DISTINCT adapter_kind FROM exchange_partners WHERE active → every value must be a key in ExchangeRegistries.adapters → typed boot error naming the offending kind(s) otherwise. (Write-path validation remains enum-enforced until T3.1 — serde rejects unknown adapter_kind values at the DTO layer since partners_dtos.rs:30/79 carry typed ExchangeAdapterKind; T3.1 swaps to registry-driven validation when the enum dies per D5.) * Lockstep test at adapters/mod.rs:435 (adapter_kind_inventory_matches_migration_check_constraint) — its premise (the CHECK constraint) is gone. REPLACE with registry_inventory_matches_adapter_kind_enum: assert ExchangeRegistries.adapters keys == ExchangeAdapterKind::iter() token set + "noop" handling documented (the noop adapter is NOT a registry entry — it’s the sentinel field; assert it is ABSENT from the map). This keeps enum ↔ registry lockstep until T3.1 deletes the enum (test retires there). * Tests: boot validation happy (devstack rows all registered); boot validation sad (synthetic unknown adapter_kind row → typed boot error; devstack test inserts a row bypassing the DTO layer via direct SQL since the CHECK constraint no longer blocks it — this IS the TOCTOU-closure proof per §1.3).

Done (2026-06-10) — shipped as specified, executed BEFORE T2.3 (steps mutually independent per the DAG). Migration 20260610181600_drop_adapter_kind_check.sql; ExchangeRegistries::validate_adapter_kind_membership(&self, pool) queries SELECT DISTINCT adapter_kind FROM exchange_partners WHERE active and delegates to a pure check_kind_membership (unit-testable without DB; T3.5 extends it with partner_type); typed OrchestratorError::UnregisteredAdapterKinds { kinds: Vec<String> } + MembershipQuery(sqlx::Error); called from main.rs after registry boot. has_adapter lifted from [cfg(test)] (production caller arrived). Lockstep test replaced as specified (registry_inventory_matches_adapter_kind_enum — registry keys == enum token set, "noop" absent). Siting note: craig-exchange is a binary crate, so the devstack-gated coverage ([ignore = "requires devstack postgres"]) lives in bundle_orchestrator.rs’s test module, not `tests/ (integration tests can’t reach bin modules — same constraint documented at tests/api/send_worker.rs). Deviation: the spec’d happy + sad devstack tests merged into ONE sequential test (@axis: evil; happy half asserted first, then the direct-SQL TOCTOU proof) — the validation asserts a GLOBAL table property, so two separate tests race each other’s synthetic rows under nextest concurrency (caught live on first run). Stale CHECK-constraint doc cite in craig-seed/src/model.rs updated. Exchange bin tests 46 → 49. T2.2 MR cite backfilled (!674 / d0588d1d). MR !676 / 0a427cdd (cite backfilled at T2.3)

5

[#574] T2.5 plan-completion audit + archive (Plan T2). Standard close-out per .claude/docs/delivery-protocol.md § Plan Completion Audit. Dispatch fresh plan-completion-audit Explore subagent per the plan-completion-audit bias; verifies T2.1-T2.4 Status cells carry concrete !MR / sha citations. Run cargo xtask docs plan-archive --dry-run then execute. nav.adoc § Plans § Active Plan T2 entry REMOVED; row ADDED under plans/archive.adoc § Architecture. .claude/CLAUDE.md Active Work row removed + plan name appended to § Completed Plans (per the 2026-06-10 leanness rule — no detailed rows). Plan T umbrella Status cell Step 4 (Plan T2 executed) → Done (YYYY-MM-DD); Plan S umbrella Status cell Step 7 → Done (YYYY-MM-DD). Plan T3 execution gate clears. Memory: EDIT Plan S resume state, mark Plan T2 Done. Child epic &52 closed with cross-ref to Plan T2 body archive + 4 step issue cites (#570-#573).

Done (2026-06-10) — this MR (audit + archive close-out). Audit subagent dispatched pre-archive: all 4 step cells verified against code reality (spot-checks PASS), 0 stale doc references outside the expected archive rewrites; T2.3 cite backfilled (!677 / 9af1c9f0). T2 executed in 5 MRs (!673/!674/!676/!677 + this one) over 2026-06-10 — T2.4 ran before T2.3 per the DAG’s mutual independence

Context

Plan T1 (body) lands the foundations: ErasedAdapter trait + impl_erased_adapter! macro (T1.2), AuditCodec + 10 codec impls (T1.3), craig-state-bundle with BundleContribution/StateBundle/BootContext/5 registries (T1.5), craig-state-default::DefaultBundle (T1.6), per-partner + host-service macro adoption (T1.7). Plan T2 MATERIALIZES those foundations into the running system:

  • T2.1 replaces the closed-enum AnyAdapter dispatch (the Plan L Step 2a wrapper at services/craig-exchange/src/adapters/mod.rs:41-78) with registry-resolved Arc<dyn ErasedAdapter> dispatch at the 2 production call sites. The bundle orchestrator boots DefaultBundle hardcoded (multi-bundle env-var activation is Plan U Step 4 per A10).

  • T2.2 does the same for the audit seam (audit_typedAuditCodecRegistry).

  • T2.3 makes the mock-server bundle-driven per A5/§2.5 — mock modules move to per-partner crates behind mock features; the shared MockState (verified unused) dies per D8.

  • T2.4 opens the DB closure: the adapter_kind CHECK constraint drops; boot registry-membership validation replaces it (dual-site per §1.3).

After Plan T2 archives: Plan T3 (open ExchangeAdapterKind + PartnerType enums, georgia seed removal, test hardening) executes; Plans U/V gain their trait+registry stability gate.

Decisions inherited

  • D4: F-065 / Plan G Step 6 / #462 mechanically hard-gates T2.1 (glab issue view 462).

  • D5: ExchangeAdapterKind enum survives Plan T2 UNCHANGED — deletion is T3.1. T2 dispatch converts enum → token via strum at the resolve seam.

  • D8: Mock partners self-state-bound; shared MockState deleted in T2.3. (Verified: zero state readers — modules ship stateless; self-state-binding documented as the future pattern.)

  • A3: noop:// sentinel INLINE at dispatch entry (ExchangeRegistries::resolve), NOT a registry entry.

  • A5: mock modules at crates/craig-partner-*/src/mock.rs behind mock Cargo feature; MockRouteFactory = Arc<dyn Fn() → axum::Router + Send + Sync>; feature propagation through craig-state-default.

  • A7: boot validates adapter_kind + partner_type REGISTRY MEMBERSHIP only; federal-mapping completeness is report-emit-time (T3.4).

  • A10: Plan T ships hardcoded DefaultBundle activation; CRAIG__ACTIVE_STATE_BUNDLES is Plan U Step 4.

  • Plan T1 §SHINES placement architecture: SHINES factory injection is ORCHESTRATOR-SIDE after bundle merge; craig-state-default contributes 10 factories; double-registration fails boot.

Sequencing gates

  • T2 execution start (T2.1 MR opens): Plan T1 archived AND #462 closed (mechanical check in T2.1 cell).

  • T2.1 → T2.2 (audit seam swap needs ExchangeRegistries + orchestrator).

  • T2.1 → T2.4 (boot validation extends the T2.1 orchestrator; constraint drop is safe only once registry dispatch is live).

  • T2.3 is independent of T2.2/T2.4 once T2.1 lands (mock-server consumes MockRouterRegistry via craig-state-default/mock; no exchange-service coupling) — may run in parallel.

  • T2.5 last.

Calendar

Step Scope Anticipated MRs Calendar

T2.1

Orchestrator + ExchangeRegistries + dispatch migration + AnyAdapter deletion

1

~1.5 days

T2.2

AuditCodecRegistry seam + audit machinery deletion

1

~0.5 day

T2.3

Mock migration (validation crate + 10 modules + features + mock-server rebuild)

1

~1.5 days

T2.4

CHECK constraint drop + boot membership validation

1

~0.5 day

T2.5

plan-completion audit + archive

1

~0.5 day

Total: 5 MRs / ~1 wk (sequential sum 4.5 working days ≈ 1 wk; T2.2/T2.3/T2.4 parallelism can compress the critical path to ~3.5 days — the ~1 wk figure is the sequential upper bound matching Plan T umbrella Calendar row 2 "~5 \| ~1 wk").

Step DAG

        [gate: Plan T1 archived + #462 closed]
                      ↓
        T2.1 (orchestrator + registry dispatch)
          ↓           ↓            ↓
        T2.2        T2.3         T2.4
        (audit)     (mock)       (DDL + boot validation)
          └─────────┬┴─────────────┘
                    ↓
        T2.5 (audit + archive)

T2.2 / T2.3 / T2.4 are mutually independent after T2.1; any order or parallel.

Threat model

  • Registry dispatch silently changes SHINES behavior — the legacy path threaded exchange_format at CONSTRUCTION (StandardAdapter::new(config, format, client) at mod.rs:405-409); the erased path threads it PER-CALL (send_value(…​, Some(&partner.exchange_format))). A threading bug sends SHINES payloads with the wrong format. Mitigation: T2.1 keeps the existing send_worker devstack integration tests green + adds a format-override unit test on the passthrough macro arm (extends T1.7’s 3-test battery).

  • Connectivity-test trait gap discovered lateErasedAdapter (T1.2 spec) ships without test_connectivity; T2.1 needs it at partners.rs:302. Mitigation: the T2.1 cell carries an explicit VERIFY-OR-ADD clause — verify whether T1 execution already added the method; if absent, add test_connectivity_value to the trait + BOTH macro arms (additive; the 11 T1.7 invocation-site impls regenerate automatically via the macro).

  • Mock migration breaks E2E / devstack silently — the §2.5 /partner/<kind> adoption changes ALL 10 mock route paths (a uniform, audited migration — not a silent drift). Mitigation: datagen (10 URLs) + all 10 round_trip suites updated in the SAME T2.3 MR; 5-surface audit in the cell; full e2e battery on T2.3’s pre-push.

  • Dev-dep cycle confuses future contributorspartner crates →(dev) mock-server →(prod, via state-default) partner crates looks like a cycle. Mitigation: T2.3 documents the legality (Cargo dev-dep cycle exemption; serde/serde_derive precedent) in tools/craig-mock-server/Cargo.toml comments + the body’s research note.

  • Boot validation bricks devstack on stale rows — post-T2.4 a seeded row with an unregistered adapter_kind fails service boot (intended fail-fast, but devstack reseed must precede). Mitigation: T2.4 ships with reseed in its verification steps; seeded kinds are the 11 registry-backed tokens already.

  • T1 execution deviates from the T1 body specs this T2 body cites — e.g. registry field shapes or macro arms change during T1 review. Mitigation: risk row 9; T2.1 MR opens only after T1 archived, so drift is visible at T2 execution start; body refresh is a small docs MR.

Cross-cutting invariants

Invariant Enforced by Verification

noop:// sentinel handled INLINE, never a registry entry (A3)

ExchangeRegistries::resolve is the single sentinel site; registry_inventory test asserts "noop" absent from adapter map

T2.1 + T2.4 tests

SHINES injected orchestrator-side AFTER bundle merge; double-registration fails boot

bundle_orchestrator.rs insertion order + sad-path test

T2.1 test battery

Audit encoding keys on the RESOLVED adapter’s kind(), never the partner-row kind

encode_audit(&self, adapter: &dyn ErasedAdapter, …​) signature + noop-substituted regression test

T2.2 test battery

Boot validates REGISTRY MEMBERSHIP only — no federal-mapping completeness at boot (A7)

T2.4 validation queries adapter_kind (+ partner_type at T3.5); no federal_mapping consultation

Code review + A7 cited in T2.4 commit

Per-partner crates compile WITHOUT axum at default features

mock feature gates axum + craig-mock-validation + mock module

cargo tree -p craig-partner-caps (no axum); cargo check -p craig-partner-caps --features mock (compiles)

craig-exchange-contracts default build stays runtime-dep-free (§3.1)

T2.1’s test_connectivity_value addition uses existing deps only (futures-util/serde_json from T1.2)

cargo tree -p craig-exchange-contracts

Typed ExchangeAdapter trait UNCHANGED (Plan L invariant)

T2 touches only the erased seam + dispatch host

git diff review on crates/craig-exchange-contracts/src/lib.rs:175-215

Quality budgets monotonic (B3a watch: serde_json::Value sites move, not multiply)

xtask validate [4i/14]; STRUCTURAL-VALUE markers move WITH the dispatch code

Pre-push gate

Axis coverage: new tests tag @axis:; T2.4’s TOCTOU sad-path tags @axis: evil

xtask validate [4j/14]

Pre-push gate

fn-name-and on new helpers (resolve, encode_audit are single-purpose names)

xtask validate [4k/14]

Pre-push gate

NEW crate craig-mock-validation carries standard lib-root denies

Plan H Step 10 + Plan M Tier-3 precedent

Crate lib.rs review

Mock routes nest at /partner/<kind> VERBATIM per ADR-032 §2.5 (post-T2.3)

Registry-driven nesting; /health registry-derived; Plan U Step 3 tx-stub authors against the same contract

T2.3 router smoke test

Risk register

# Risk Impact Likelihood Mitigation

1

SHINES per-call format threading regression

SHINES payload corruption

Med

Devstack send_worker integration tests + format-override unit test on passthrough arm

2

test_connectivity_value trait addition ripples through 11 macro invocation sites

T2.1 scope growth

Low

Macro-generated impls update automatically; only the macro definition + trait change by hand

3

§2.5 /partner/<kind> path migration misses a consumer

Hidden test failure

Low

5-surface audit in T2.3 cell covers datagen + all 10 round_trip suites + /health; grep mock-server:9090 + bare /{sys} paths workspace-wide at MR time

4

Dev-dep cycle confuses contributors or a future cargo version tightens rules

Build friction

Low

Documented in Cargo.toml comments; serde precedent; revisit if cargo changes

5

Boot validation bricks a devstack with stale seeded rows

Dev friction

Low

T2.4 verification includes reseed; pre-1.0 destructive posture

6

ExchangeRegistries Extension layering diverges from umbrella’s "ExchangeAppState" naming

Reviewer confusion

Low

Body documents the realization choice + Plan I F-026 Extension-pattern precedent; same intent (no craig_api::AppState pollution)

7

T2.3 mock-server rebuild breaks spawn_for_test consumers (10 round_trip suites)

Test breakage ×10

Med

spawn_for_test signature UNCHANGED; only router assembly changes; full round_trip battery on pre-push

8

MockState deletion loses state a module silently needed

Mock regression

Low

Verified 2026-06-09: zero readers/writers on all 9 DashMaps; round_trip battery is the regression gate

9

Plan T1 execution deviates from the T1-body specs this body cites (registry shapes, macro arms)

T2 body refresh needed

Med

T2 execution gated on T1 archive — drift visible before T2.1 opens; refresh is a small docs MR; D-decisions + ADR anchors make large drift unlikely

  • ADR-032 — §1.2 (AdapterRegistry) + §1.3 (dual-site validation / TOCTOU) + §2.4 (boot membership per A7) + §2.5 (mock-manifest contract) + A3 (noop sentinel) + A5 (mock_routes sourcing + feature gating) + A10 (hardcoded DefaultBundle activation).

  • ADR-038 — §3 factory shape + boot-time materialization rationale for T2.1.

  • ADR-030 — Status vocabulary.

  • Plan T umbrella — parent; this body fills Status row 3.

  • Plan T1 — foundations consumed: T1.2 trait/macro, T1.3 codecs, T1.5 bundle crate, T1.6 DefaultBundle, T1.7 macro adoption + §SHINES placement architecture.

  • Plan S — grandparent; Plan T2 sits at Plan S Step 6 (body filed) / Step 7 (executed).

  • Plan G — F-065 / #462 HARD-gated T2.1 per D4 (closed 2026-06-10 via !671; gate CLEAR).

  • Plan LAnyAdapter (Step 2a) + audit_typed (Step 5) + mock-server F-063 round_trip suites (Step 7) are the surfaces T2 migrates; typed ExchangeAdapter invariant preserved.

  • the pre-1.0 destructive-rebuild posture — T2.4 destructive constraint drop + T2.3 datagen URL change cite.

  • the service-initialization patternbundle_orchestrator boot wiring slots into the canonical bootstrap → deps → workers → router main shape.

  • the quality-budget enforcement gate — B3a discipline: Value sites MOVE with dispatch code (markers travel), no net-new Value surface.

  • #462 (F-065) — HARD gate on T2.1.

  • #558 (closed-aggregator) — T2.2’s fail-closed registry-miss path inherits the limitation; unchanged.

Edit this page · latest