Plan T1 — Foundations & Traits (sub-plan of Plan T umbrella)

On this page

Status

Step Description Status

1

[#560] T1.1 SHINES placement decision (lock macro shape). SHINES currently routes through services/craig-exchange/src/adapters/standard.rs::StandardAdapter (per Plan L Step 3 SHINES note). T1.1 LOCKS the decision: SHINES STAYS as the catch-all StandardAdapter inside services/craig-exchange. Rationale (corrected at T1.1 execution 2026-06-09 — the draft said "CSV-driven"; the infrastructure is JSON envelope-transform): StandardAdapter consumes the services/craig-exchange/src/adapters/mapping/ shared envelope-transform module (map_person, 96 LOC, pub(crate), marked STRUCTURAL-VALUE "by design, since the SHINES variant has no typed crate per Plan L §Step 3 SHINES note") that’s intrinsic to host-service shape; promoting SHINES to a typed crates/craig-partner-shines crate would require co-migrating that infrastructure across the crate boundary — out of T1 scope, and architecturally wrong: SHINES is CRAIG’s own legacy-envelope catch-all for partner rows without typed adapters, not an external partner spec. T1.1 deliverable: decision documented in T1.1 commit message (one paragraph in MR description body) + this Plan T1 body’s umbrella Status cell already records the outcome. T1.1 does NOT touch StandardAdapter::format: String at line 214 (per A1 axis-2 the field eventually goes per-call; that removal is Plan T2.1 scope when ExchangeAdapter::send legacy path goes away; T1 leaves the field intact + T1.7 just ADDS new send_with_format + audit_with_format direct methods alongside). T1.1 outcome decides which impl_erased_adapter! macro arm SHINES uses at T1.7 (the SHINES-passthrough arm per A1 line 402); T1.2 still implements BOTH arms (other states adopting CRAIG may use either pattern).

Done (2026-06-09) — !662 / a0c02714. Decision LOCKED: SHINES stays catch-all StandardAdapter; SHINES-passthrough macro arm at T1.7

2

[562] T1.2 ErasedAdapter trait + impl_erased_adapter! macro in craig-exchange-contracts. Per ADR-032 A1 + ADR-038 §1 Tier-O. Dep prep: T1.2 promotes serde_json from [dev-dependencies] to [dependencies] in crates/craig-exchange-contracts/Cargo.toml (current state at :21-25 has only serde/sqlx/strum/utoipa as normal deps + serde_json as dev-only) + adds thiserror = { workspace = true } (workspace dep at root Cargo.toml:thiserror = "2") + adds futures-util = { workspace = true } AND adds futures-util = "0.3" to root [workspace.dependencies] (NOT currently a workspace dep). Trait + macro: NEW crates/craig-exchange-contracts/src/erased.rs module hosts: (a) pub trait ErasedAdapter: Send + Sync + 'static with three methods — send_value(&self, endpoint: String, payload: Value, format: Option<&str>) → BoxFuture<', Result<Value, ErasedAdapterError>>; audit_value(&self, response: &Value, format: Option<&str>) → BoxFuture<', Result<Value, ErasedAdapterError>>; kind(&self) → &'static str; (b) pub enum ErasedAdapterError (thiserror-derived) with Serde([from] serde_json::Error) + Inner(Box<dyn ::std::error::Error + Send + Sync>) + AuditUnsupportedForKind { kind: &'static str } variants per ADR-032 A1 + ADR-038 §1; (c) pub use futures_util::future::BoxFuture; re-export at the crate root (so adopters reach it as craig_exchange_contracts::BoxFuture); (d) NEW declarative [macro_export] macro_rules! impl_erased_adapter with TWO matcher arms per A1: typed-partner arm ($adapter:ty, $kind:literal) + SHINES-passthrough arm ($adapter:ty, $kind:literal, passthrough). Macro hygiene: expansion uses ONLY $crate::ErasedAdapter, $crate::ErasedAdapterError, $crate::BoxFuture, $crate::ExchangeAdapter (the typed arm necessarily names the typed trait; list completed at T1.2 execution) for crate-local items; ::std::boxed::Box::pin(async move { …​ }) to produce BoxFuture (no FutureExt import needed — Box::pin over async move {} produces Pin<Box<dyn Future>> which coerces to BoxFuture directly); ::std::sync::Arc if needed; ::serde_json::from_value + ::serde_json::to_value for the Value/typed-Outbound/typed-Inbound boundary marshaling (mirrors the dispatch_send precedent at services/craig-exchange/src/adapters/mod.rs:296-311). Per-partner crates need NO futures-util dep — all paths route through $crate (which is craig_exchange_contracts) or std. The typed ExchangeAdapter trait at crates/craig-exchange-contracts/src/lib.rs:175-215 STAYS UNCHANGED per Plan L invariant; the erased seam is purely additive. Tests at crates/craig-exchange-contracts/src/erased.rs::tests: (i) [test] fn erased_adapter_is_object_safe() { fn _coerce(_a: ::std::sync::Arc<dyn ErasedAdapter>) {} } (compile-time check via _coerce’s presence); (ii) `[test] invoking the typed-partner macro arm on a stub TestAdapter: ExchangeAdapter + _coerce shape check; (iii) [test] exercising the SHINES-passthrough arm against a minimal stub with the new direct methods.

Done (2026-06-09) — !663 / 06a5edb7. Trait + error enum + both macro arms + BoxFuture re-export shipped; 5 new tests (3 happy / 2 sad, incl. the object-safety shape proof) + 2 existing green

3

[#563] T1.3 AuditCodec trait + 10 per-partner impls + struct exports in craig-partner-audit. Per ADR-032 A2 + ADR-038 §2 (trait-location via Rust orphan rule). NEW crates/craig-partner-audit/src/codec.rs module hosts: (a) pub trait AuditCodec: Send + Sync + 'static with fn encode(&self, payload: &serde_json::Value) → Result<PartnerAuditEvent, ErasedAdapterError> per A2 code block at ADR-032 line 422 (return type is ErasedAdapterError, NOT PartnerAuditDecodeErrorPartnerAuditDecodeError is the existing read-side decode enum at crates/craig-partner-audit/src/lib.rs:211 for decode_jsonb; AuditCodec::encode is the WRITE-side encoding seam). NEW crates/craig-partner-audit/src/codec/{caps,cprs,doe_slds,empi,ies,ions,smile,stars,tcm,wic}.rs (10 sibling files; each path uses snake_case partner name matching the existing serde tags at lib.rs:95-122) each define `pub struct <X>AuditCodec; impl AuditCodec for <X>AuditCodec { /* T1.3 ships with inline serde_json::from_value(payload.clone()).map(PartnerAuditEvent::<X>).map_err(ErasedAdapterError::Serde); T1.8 swaps the inline from_value for the per-partner audit_payload_from_value(payload.clone()).map_err(

e

ErasedAdapterError::Inner(Box::new(e))) typed decoder */ }`. Crate exports: lib.rs adds pub mod codec; + pub use codec::{AuditCodec, CapsAuditCodec, CprsAuditCodec, DoeSldsAuditCodec, EmpiAuditCodec, IesAuditCodec, IonsAuditCodec, SmileAuditCodec, StarsAuditCodec, TcmAuditCodec, WicAuditCodec}; so T1.6’s craig-state-default can construct Arc::new(CapsAuditCodec) etc. SHINES has no per-partner audit codec (per PartnerAuditEvent closed-aggregator at lib.rs:95-122 — Shines variant absent); no codec impl for it. Closed-aggregator limitation per ADR-032 A2 documented inline + cross-link to #558. Dep prep: craig-partner-audit likely already has serde_json as a normal dep (it imports serde_json::Value for decode_jsonb); T1.3 verifies + adds if missing. Tests at each codec file: per-partner round-trip happy + 1 @axis: sad (deserialize-fail surfaces ErasedAdapterError::Serde).

Done (2026-06-10) — !664 / b1f1a8a8. Trait + 10 codecs + crate exports shipped; 20 new tests (10 happy / 10 sad); B3a/B3b LOCKED

4

[564] T1.4 FederalPartnerCategory extension in craig-reference. Per ADR-032 §2.3 (unchanged by amendments) + D3. NEW crates/craig-reference/src/federal_partner_category.rs (sibling module to existing translate.rs + enums.rs; matches the per-domain-sibling convention) declares pub enum FederalPartnerCategory covering federal-report-emission classes — initial inventory (subject to T1.4 author refinement during drafting): Tanf, Ccwis, Afcars, Ncands, IvE, Icpc, Medicaid, EducationSlds, ChildSupport. Standard derive battery (Plan D F-022 style): Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumString, strum::EnumIter, serde::Serialize, serde::Deserialize, utoipa::ToSchema + [strum(serialize_all = "snake_case")] + #[serde(rename_all = "snake_case")]. NO sqlx::Type — this enum is consumed by StateBundle::federal_mapping() → HashMap<&'static str, FederalPartnerCategory> (signature lives in T1.5’s NEW StateBundle trait, NOT here), NOT by DB columns. T1.4 ships the enum + its serde/strum derives + tests; T1.5 declares the trait method consuming it. lib.rs gains pub mod federal_partner_category; pub use federal_partner_category::FederalPartnerCategory;. Tests at crates/craig-reference/src/federal_partner_category.rs::tests: round-trip serde + strum::Display + FromStr (3 standard tests matching the existing ExchangeAdapterKind pattern at crates/craig-exchange-contracts/src/lib.rs:222-252).

Done (2026-06-10) — !665 / 5e2f56e5. 9-variant enum + module wiring shipped; 3 tests (2 happy / 1 sad); IvE wire token is iv_e

5

[#565] T1.5 NEW crates/craig-state-bundle. Per ADR-032 A6 + §2.1 + D1. NEW workspace member at crates/craig-state-bundle/. Cargo.toml: craig-state-bundle; deps craig-exchange-contracts (for ErasedAdapter), craig-partner-audit (for AuditCodec + PartnerAuditEvent), craig-reference (for FederalPartnerCategory), axum = { workspace = true } (for MockRouteFactory Router type per A5), futures-util = { workspace = true } (BoxFuture re-export usage), reqwest = { workspace = true } (for BootContext { http_client: reqwest::Client }). Lib roots: standard Plan H Step 10 + Plan M Tier-3 lint denies (unreachable_pub + missing_docs + unused_crate_dependencies non-test). Workspace member added to root Cargo.toml [workspace] members list. Files:

* src/contribution.rs exports pub struct BundleContribution with 5 Phase-1-minimum fields per ADR-032 §2.1 lines 105-111 + §4 line 215: + [source,rust] ---- pub struct BundleContribution { pub adapters: Vec<(&'static str, ErasedAdapterFactory)>, // A4 factory shape pub audit_codecs: Vec<(&'static str, Arc<dyn AuditCodec>)>, // pre-materialized per A11 pub mock_routes: Vec<(&'static str, MockRouteFactory)>, // A5 pub partner_types: Vec<PartnerTypeMeta>, // §2.1 line 109 pub seed_data: SeedContribution, // §2.1 line 110 } ---- * src/bundle.rs exports pub trait StateBundle: Send + Sync + 'static with FOUR methods per ADR-032 §2.1 lines 99-103: fn name(&self) → &'static str;, fn jurisdiction_code(&self) → &'static str;, fn contribute(&self) → BundleContribution;, fn federal_mapping(&self) → HashMap<&'static str, FederalPartnerCategory>;. federal_mapping() returns the bundle’s typed-token → typed-category map per D3. * src/boot_context.rs exports pub struct BootContext { pub http_client: reqwest::Client } per A4 + A6. Lives in this crate per A6 (NOT in per-state crates); grows additively across Phase 2 plans per A11. * src/types.rs exports pub struct PartnerTypeMeta { pub token: &'static str } as Phase 1 minimum (Plan U Step 9 adds display labels per ADR-036 anticipated) + pub struct SeedContribution {} (or pub struct SeedContribution { pub _phase1_placeholder: () }) as Phase 1 empty placeholder (Plan U Step 10 adds fields per ADR-034 anticipated); empty placeholders cite §4 additive growth + ADR-038 §4 field-sourcing pre-flight. * src/registry.rs exports 5 immutable post-build registries — AdapterRegistry { entries: HashMap<&'static str, ErasedAdapterFactory> }, AuditCodecRegistry { entries: HashMap<&'static str, Arc<dyn AuditCodec>> }, PartnerTypeRegistry { entries: HashMap<&'static str, PartnerTypeMeta> } (keyed by token, value carries meta), FederalPartnerMappingRegistry { entries: HashMap<&'static str, FederalPartnerCategory> }, MockRouterRegistry { entries: HashMap<&'static str, MockRouteFactory> }. Each registry has pub fn from_bundles<I: IntoIterator<Item = Box<dyn StateBundle>>>(bundles: I) → Result<Self, BundleMergeError> that merges per-bundle entries + detects duplicates → typed BundleMergeError::DuplicateEntry { kind: &'static str, key: String }. FederalPartnerMappingRegistry::from_bundles aggregates StateBundle::federal_mapping() HashMaps from each active bundle (the trait method, NOT a BundleContribution field). * Type aliases (in src/types.rs or under src/contribution.rs): pub type ErasedAdapterFactory = Arc<dyn Fn(&BootContext) → Arc<dyn ErasedAdapter> + Send + Sync> per A4; pub type MockRouteFactory = Arc<dyn Fn() → axum::Router + Send + Sync> per A5. + Object-safety smokes at crates/craig-state-bundle/src/lib.rs::tests: fn _coerce_bundle(_b: Arc<dyn StateBundle>) {} + fn _coerce_codec(_c: Arc<dyn AuditCodec>) {} + fn _coerce_adapter(_a: Arc<dyn ErasedAdapter>) {}. Tests: bundle-merge happy path (2 non-overlapping bundles materialize all 5 registries cleanly); duplicate-detection sad path per registry kind.

Done (2026-06-10) — crate shipped (6 files); 7 tests (2 happy incl. factory materialization smoke / 5 duplicate-sad, one per registry). Dep deviations: futures-util OMITTED (nothing names BoxFuture — factories return Arc<dyn ErasedAdapter>; unused dep would trip machete) + thiserror ADDED (BundleMergeError). !666 / 00843cab

6

[#566] T1.6 NEW crates/craig-state-default seed bundle. Per ADR-032 D6. NEW workspace member. Cargo.toml: craig-state-default; deps craig-state-bundle (for trait + types), all 10 typed per-partner crates craig-partner-{caps,cprs,doe-slds,empi,ies,ions,smile,stars,tcm,wic} (for adapter constructors), craig-partner-audit (for <X>AuditCodec types exported via T1.3), craig-reference (for FederalPartnerCategory), craig-exchange-contracts (for ErasedAdapter). Lib root: src/lib.rs defines pub struct DefaultBundle; + impl StateBundle for DefaultBundle. Trait method outputs:

* name()"default". * jurisdiction_code()"georgia" (default bundle is Georgia-shaped; T3.6 isolates the hardcode to this single seed location per A8). * contribute() returns BundleContribution with: + (a) 10 adapter factory entries (typed partners only — SHINES NOT included). Each entry uses the ExchangeAdapterKind snake_case string as lookup key: `("caps", Arc::new(

ctx: &BootContext

Arc::new(CapsAdapter::new(ctx.http_client.clone())) as Arc<dyn ErasedAdapter>)), `("cprs", …​), …​, ("wic", …​). SHINES factory registration is HOST-SERVICE responsibility — added at the services/craig-exchange boot orchestrator (Plan T2.1 scope) where StandardAdapter is reachable. craig-state-default cannot reach StandardAdapter (which is pub(crate) inside services/craig-exchange/src/adapters/standard.rs:212) across crate boundaries; cleanly separating bundle contribution (per-partner crate factories) from host catch-all dispatch (SHINES factory at orchestrator) is correct per ADR-038 §3 + ADR-032 A6 dep-graph constraints. + (b) 10 audit codec entries("caps", Arc::new(CapsAuditCodec)) etc. via T1.3 exports. SHINES audit codec absent per closed-aggregator limitation (consistent with T1.3 omission). + (c) 14 partner_type tokens VERBATIM from services/craig-exchange/migrations/20260321100000_expand_partner_types.sql CHECK constraintPartnerTypeMeta { token: "state_agency" }, { token: "court_system" }, { token: "federal_agency" }, { token: "tribal_authority" }, { token: "private_provider" }, { token: "cwca_provider" }, { token: "financial" }, { token: "medicaid" }, { token: "child_abuse_registry" }, { token: "tanf" }, { token: "child_support" }, { token: "external_data" }, { token: "education" }, { token: "health_agency" }. Partner_type registry membership is ORTHOGONAL to adapter_kind registry membership; the same exchange_partners table has both columns. + (d) seed_data: SeedContribution::default() or struct-literal of the empty Phase 1 placeholder. + (e) mock_routes: vec![] placeholder per A5 (Plan T2 Step T2.3 populates when the per-partner mock Cargo feature lands). * federal_mapping() returns HashMap<&'static str, FederalPartnerCategory> populated ONLY for partner_types that emit to federal reports per A7 (absent keys mean "no federal report responsibility"; T3.4’s report-emit-time check returns ReportingError::MissingFederalMapping for rows emitting to federal with no mapping). Initial Phase 1 mapping (subject to T1.6 author refinement against services/craig-reporting/src/{afcars,ncands}/ row-emit usages): { "tanf" → Tanf, "medicaid" → Medicaid, "child_support" → ChildSupport, "education" → EducationSlds, "child_abuse_registry" → Ncands }. The 9 partner_types without entries (state_agency, court_system, federal_agency, tribal_authority, private_provider, cwca_provider, financial, external_data, health_agency) have no federal report responsibility under the default bundle.

Tests at crates/craig-state-default/src/lib.rs::tests: (i) DefaultBundle::contribute() returns expected counts (10 adapter factories — NOT 11 — explanation: SHINES is orchestrator-registered, not bundle-contributed; 10 audit codecs; 14 partner_types; 0 mock_routes); (ii) partner_types_match_migration_check_constraint lockstep test mirroring the precedent at services/craig-exchange/src/adapters/mod.rs:435::adapter_kind_inventory_matches_migration_check_constraint; (iii) federal_mapping_initial_5_entries (asserts the 5 mapped tokens present + categorized).

Done (2026-06-10) — crate shipped; 4 tests (counts / 14-token lockstep / federal-5 / factory token↔kind materialization sweep — bonus beyond spec). !669 / 0a6e9437

7

[567] T1.7 per-partner impl_erased_adapter! macro adoption (10 per-partner + 1 host-service invocations). 10 typed-partner crates crates/craig-partner-{caps,cprs,doe-slds,empi,ies,ions,smile,stars,tcm,wic}/src/adapter.rs each gain one craig_exchange_contracts::impl_erased_adapter!(<X>Adapter, "<kind>"); invocation (per-crate layout precedent verified at crates/craig-partner-caps/src/: adapter.rs hosts the <X>Adapter struct + impl ExchangeAdapter). 1 host-service invocation at services/craig-exchange/src/adapters/standard.rscraig_exchange_contracts::impl_erased_adapter!(StandardAdapter, "shines", passthrough); (SHINES-passthrough arm per A1 line 402; lives at host-service site because StandardAdapter is pub(crate) and reachable there but NOT from external crates). ADDS send_with_format + audit_with_format direct methods on StandardAdapter with signature async fn send_with_format(&self, endpoint: &str, payload: serde_json::Value, format: Option<&str>) → Result<serde_json::Value, StandardAdapterError> + async fn audit_with_format(&self, response: &serde_json::Value, format: Option<&str>) → Result<serde_json::Value, StandardAdapterError> (Plan L invariant 6 typed dispatch; respelled async fn at T1.2 execution — a T1.2 live probe showed the draft’s fn …​ → impl Future + async-block shape trips clippy::manual_async_fn under -D warnings; audit_with_format will additionally need [expect(clippy::unused_async, reason = …​)] if its body awaits nothing — mirror the PassthroughStub precedent in crates/craig-exchange-contracts/src/erased.rs::tests). Body uses format.unwrap_or(&self.format) to fall back to per-instance field when caller doesn’t override — keeps existing ExchangeAdapter::send callers (the AnyAdapter::send_serialized legacy path at services/craig-exchange/src/adapters/mod.rs:201) working unchanged. T1.7 does NOT remove StandardAdapter::format: String field at line 214; per-call format-override path coexists with per-instance field. Field removal moves to Plan T2.1 (when AnyAdapter::send_serialized legacy path goes away). Each typed-partner macro invocation expands to impl ErasedAdapter for <X>Adapter { fn send_value(…​) → $crate::BoxFuture<…​> { ::std::boxed::Box::pin(async move { …​ }) } …​ }. T1.7 does NOT modify the services/craig-exchange/src/adapters/mod.rs::adapter_for dispatch site (verified at :358-412); migrating dispatch to AdapterRegistry is Plan T2 Step T2.1 scope. Per-partner Cargo.toml needs no new deps (craig-exchange-contracts already present per Plan L Step 3); per-partner crates remain futures-util-free per macro-hygiene (all paths route through $crate or std). Tests at each crates/craig-partner-<kind>/src/adapter.rs::tests: object-safety smoke + kind() return matches (2 tests per partner × 10 partners = 20). Host-service test at services/craig-exchange/src/adapters/standard.rs::tests: SHINES-passthrough macro object-safety + kind() returns "shines" + format-override works (3 tests). Soft sequencing gate: F-065 / Plan G Step 6 / #462 SHOULD land before T1.7 per Plan G Step 6 gating clause. T1.7 MAY proceed if #462 has not landed at T1.6 completion, but reviewer SHOULD prefer waiting. Mechanical check at T1.7 MR open: glab issue view 462 --output json | jq -r .state — if closed, T1.7 unblocked; if open, T1.7 MR author either defers OR cites reviewer waiver in writing in the MR description.

Done (2026-06-10) — #462 OPEN at MR time; REVIEWER WAIVER granted in-session 2026-06-10 ("Waive — proceed now": T1.7 touches per-partner adapter.rs + standard.rs; #462’s cross-handler scan targets service handler modules — near-zero conflict surface); waiver cited verbatim in the MR description. 10 macro invocations + SHINES passthrough + send_with_format/audit_with_format direct methods (transform_outbound gains a format param; shared send_inner extracted); 23 new tests (20 partner + 3 host). !668 / 9c8916e6

8

[568] T1.8 per-partner audit_payload_from_value typed decoders + T1.3 codec swap. Each of the 10 typed per-partner crates ships pub fn audit_payload_from_value(payload: serde_json::Value) → Result<<X>AuditPayload, <X>Error> at crates/craig-partner-<kind>/src/lib.rs (or audit.rs sibling — per-crate author decides). Function body uses the existing <X>Error::MalformedResponse([from] serde_json::Error) variant (verified across crates/craig-partner-{caps,cprs,doe-slds,empi,ies}/src/error.rs — variant name is MalformedResponse, NOT AuditDecodeError): serde_json::from_value(payload).map_err(<X>Error::from) — the #[from] serde_json::Error impl converts to MalformedResponse(_). No new error variants needed. Pulled into the 10 <X>AuditCodec impls in craig-partner-audit (T1.3 destination) — each T1.3 impl swaps its inline serde_json::from_value call for `partner_crate::audit_payload_from_value(payload.clone()).map_err(

e

ErasedAdapterError::Inner(Box::new(e)))` to surface typed per-partner errors instead of bare serde_json::Error. SHINES has no decoder (closed-aggregator). Tests at crates/craig-partner-<kind>/src/lib.rs::tests (or sibling tests/audit_decode.rs): per-partner happy + sad (malformed value → Err(<X>Error::MalformedResponse(_))) (2 tests per crate × 10 partners = 20 tests). Sequencing: T1.3 lands FIRST with inline serde_json::from_value; T1.8 ships per-partner decoders + swaps the T1.3 impls in the same MR (or 10 sequential MRs per-partner if T1.8 author prefers; bundled MR is recommended for calendar alignment). Lockstep gate at T1.8 archive: every <X>AuditCodec in crates/craig-partner-audit/src/codec/<X>.rs invokes partner_crate::audit_payload_from_value (grep returns zero inline serde_json::from_value calls in codec impls).

Done (2026-06-10) — executed in DAG order (T1.8 before T1.7/T1.6 per §Step DAG). 10 decoders + 10 codec-body swaps in one bundled MR; 20 new tests (10 happy round-trip / 10 sad MalformedResponse); codec sad tests updated Serde → Inner; lockstep grep clean. !667 / 09fea89f

9

[#569] T1.9 plan-completion audit + archive (Plan T1). 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. Subagent verifies T1.1-T1.8 Status cells have concrete !MR / sha citations. Run cargo xtask docs plan-archive --dry-run (tool scans all candidates itself; no positional path); then run cargo xtask docs plan-archive to execute. nav.adoc § Plans § Active Plan T1 entry REMOVED; row ADDED under plans/archive.adoc § Architecture. .claude/CLAUDE.md § Phase Status row appended above Testing. Plan T umbrella Status cell Step 2 (Plan T1 executed) → Done (YYYY-MM-DD) per ADR-030 vocabulary. Plan S umbrella Status cell Step 5 (Plan T1 executed end-to-end) → Done (YYYY-MM-DD). Memory: EDIT the Plan S resume state to mark Plan T1 Done + set Plan T2 authoring as next pickup. Child epic &51 closed with cross-ref to Plan T1 body archive + 8 step issue cites (#560 + #562 through #568).

Done (2026-06-10) — this MR (audit + archive close-out). Audit subagent dispatched pre-archive; T1.6 cite backfilled (!669 / 0a6e9437)

Context

ADR-032 (Multi-Jurisdiction Partner Registry and Transport Abstraction) anchors Plan S Phase 1; ADR-038 (Trait-Object & Registry Patterns) codifies reusable principles. ADR-032 amendments A1-A11 (landed via !657) resolve the 8-round Plan T iteration findings.

Plan T1 lands the FOUNDATIONS the rest of Plan T (T2/T3) + the downstream Plans U/V/W/X/Y all build on:

  • ErasedAdapter trait in craig-exchange-contracts is the object-safe seam Plan T2’s AdapterRegistry dispatches through. The typed ExchangeAdapter trait at crates/craig-exchange-contracts/src/lib.rs:175-215 is UNCHANGED (Plan L invariant preserved per ADR-032 A1).

  • AuditCodec trait + impls in craig-partner-audit is the typed encode seam Plan T2’s AuditCodecRegistry consults. Distinct from PartnerAuditEvent::decode_jsonb at crates/craig-partner-audit/src/lib.rs:168 (read-side JSONB decoder). Closed-aggregator limitation inherited from Plan L Step 5 per ADR-032 A2 + #558.

  • FederalPartnerCategory in craig-reference is the typed enum that StateBundle::federal_mapping() returns. Federal-completeness check is REPORT-EMIT time (T3.4 surface), NOT boot — per ADR-032 A7.

  • craig-state-bundle NEW crate hosts BundleContribution aggregate (5 Phase-1 fields per §2.1 lines 105-111), StateBundle trait (4 methods per §2.1 lines 99-103), BootContext shared resource type, and 5 registries — per ADR-032 A6 + A4 + ADR-038 §3.

  • craig-state-default NEW seed bundle implements StateBundle for the 10 typed per-partners + 14 partner_type tokens verbatim per ADR-032 D6. SHINES factory NOT contributed by craig-state-default (host-service registers SHINES at orchestrator boot — Plan T2.1 scope).

  • Per-partner macro adoption wires the 10 typed-partner adapters + 1 host-service SHINES catch-all into the erased seam — F-065 SOFT gate.

  • Per-partner typed decoders swap T1.3’s inline serde_json::from_value for per-partner-typed error surfaces.

SHINES placement architecture (T1.1 outcome)

T1.1 locks "SHINES stays catch-all". Architectural consequence:

  • StandardAdapter REMAINS at services/craig-exchange/src/adapters/standard.rs::StandardAdapter (visibility pub(crate) — unchanged).

  • T1.7 adopts the SHINES-passthrough macro arm at the same site (where StandardAdapter is reachable). This is the ONLY impl_erased_adapter! invocation that lives OUTSIDE crates/craig-partner-*/src/adapter.rs.

  • T1.6’s craig-state-default::DefaultBundle::contribute() returns 10 adapter factories (typed partners only); does NOT include SHINES.

  • Plan T2.1 (registry migration) is where the boot orchestrator builds AdapterRegistry by MERGING bundle contributions (10 typed factories) + adding the host-service SHINES factory entry. Plan T2.1 body specifies this orchestrator-side SHINES injection.

  • No crates/craig-partner-shines is created; SHINES architecturally lives at host-service layer.

Decisions inherited (D1-D10 from Phase A artifact)

  • D1: BundleContribution + StateBundle in NEW crates/craig-state-bundle (T1.5 ships) — ADR-032 A6

  • D2: Factory-shaped adapter contribution + BootContext (T1.5 ships) — ADR-032 A4

  • D3: Typed federal_mapping() → HashMap<&'static str, FederalPartnerCategory> on StateBundle trait (T1.4 + T1.5 ship) — ADR-032 §2.1 lines 102 + 124-137

  • D4: F-065 / Plan G Step 6 / #462 hard-gates T2.1 (mentioned in T1.7 as SOFT gate during macro adoption — T1.7 may proceed if #462 open but reviewer prefers waiting)

  • D5: ExchangeAdapterKind enum DELETED in T3.1 — Plan T3 scope, NOT T1

  • D6: NEW crates/craig-state-default seed bundle (T1.6 ships 10 typed adapters; SHINES added by orchestrator at T2.1)

  • D7: AuditCodec trait + impls + struct exports BOTH in craig-partner-audit per Rust orphan rule (T1.3 ships)

  • D8: Mock partners self-state-bound; shared MockState deleted in T2.3 — Plan T2 scope (T1.6 ships mock_routes as vec![] placeholder)

  • D9: Temp hardcoded validators preserve 400→400 during T3.1 + T3.3 transition windows — Plan T3 scope

  • D10: T3.4 ships federal mapping seam only; reporting wire-up deferred (per A7) — Plan T3 scope

Hard sequencing gates within Plan T1

  • T1.1 (SHINES decision) → T1.2 (macro shape lock): T1.2 ships TWO macro variants per A1.

  • T1.4 (FederalPartnerCategory enum) → T1.5 (state-bundle crate).

  • T1.2 → T1.7 (macro adoption needs trait + macro to exist).

  • T1.3 → T1.8 (T1.3 ships impls with inline serde_json::from_value; T1.8 ships per-partner decoders + swaps the T1.3 impls).

  • T1.7 + T1.3 + T1.5 → T1.6 (state-default bundle constructs adapter factories referencing impl ErasedAdapter for <X>Adapter (T1.7) + audit codec types (T1.3) + bundle trait/types (T1.5)).

  • T1.9 (archive) — last step.

Calendar

Step Scope Anticipated MRs Calendar

T1.1

SHINES decision (lock to catch-all)

1

~0.5 day

T1.2

ErasedAdapter trait + impl_erased_adapter! macro + dep prep

1

~1 day

T1.3

AuditCodec trait + 10 per-partner impls + struct exports

1

~1 day

T1.4

FederalPartnerCategory enum extension

1

~0.5 day

T1.5

NEW craig-state-bundle crate

1

~1 day

T1.6

NEW craig-state-default seed bundle (10 typed adapter factories)

1

~1 day

T1.7

per-partner + host-service macro adoption (11 invocations)

1 (bundled — RECOMMENDED)

~1 day

T1.8

per-partner audit_payload_from_value decoders + T1.3 codec swap

1 (bundled — RECOMMENDED)

~0.5 day

T1.9

plan-completion audit + archive

1

~0.5 day

Total: ~9 MRs under bundled strategy; up to 29 MRs if T1.7 ships 11 per-invocation + T1.8 ships 10 per-partner. Bundled strategy strongly RECOMMENDED for calendar alignment + reviewer efficiency.

Step DAG

T1.1 (SHINES decision)         T1.4 (FederalPartnerCategory)
  ↓                              ↓
T1.2 (Erased trait + macro)    T1.5 (state-bundle crate)
  ↓                              ↓
  └─────┬───────────────┬────────┘
        ↓               ↓
T1.3 (AuditCodec inline from_value)
  ↓
T1.8 (per-partner typed decoders + T1.3 codec swap)
  ↓
T1.7 (per-partner + host-service macro adoption — F-065 SOFT gate)
  ↓
T1.6 (state-default seed bundle, 10 adapter factories)
  ↓
T1.9 (audit + archive)

Note: T1.7 and T1.8 internal deps are independent — T1.7 needs T1.2; T1.8 needs T1.3. They may execute in either order or in parallel before T1.6.

Threat model

  • Under-specified BundleContribution shape cascades downstream — Plan W/X/Y add plugins, compositions, field_ownership etc. as additive BundleContribution fields. If T1.5 ships a shape that constrains future additions (e.g. fixed-size struct), every Phase 2 plan inherits the defect. Mitigation: T1.5 uses Vec<(K, V)> + HashMap<K, V> shapes per ADR-032 §4; ADR-038 §4 field-sourcing pre-flight; T1.5 5-field struct verbatim from §2.1 lines 105-111.

  • AuditCodec dep cycle on per-partner crates — naive impl placing <X>AuditCodec in per-partner crates would cycle (craig-partner-audit ALREADY depends on per-partner crates per Plan L Step 5). T1.3 ships impls IN craig-partner-audit per ADR-038 §2 + ADR-032 A2 + Rust orphan rule. Mitigation: T1.3 reviewer cargo build -p craig-partner-audit pre-flight.

  • Macro hygiene gotchas in impl_erased_adapter! — declarative macro_rules! macros must use $crate-rooted paths to avoid call-site dep requirements. T1.2 macro uses $crate::ErasedAdapter, $crate::BoxFuture, ::std::boxed::Box::pin, ::serde_json::{from_value,to_value} — all reachable from craig-exchange-contracts root or std prelude. Per-partner crates remain futures-util-free. Mitigation: T1.2 macro tests + T1.7 first-partner adoption (caps) is smoke test before remaining 9.

  • T1.6 default-bundle 14-partner_type-token list drifts from migrationservices/craig-exchange/migrations/20260321100000_expand_partner_types.sql is SOURCE; T1.6 hardcodes them. Mitigation: T1.6 ships lockstep partner_types_match_migration_check_constraint test mirroring services/craig-exchange/src/adapters/mod.rs:435.

  • Closed-aggregator surfaces during non-georgia state adoptionPartnerAuditEvent is closed (per A2 limitation). Plan T inherits + accepts. #558 tracks decoupling.

  • F-065 / #462 timing slips T1.7 — Plan G Step 6 cross-handler DRY scan ideally lands before macro adoption. Mitigation: T1.1-T1.6 non-blocking; T1.7 gate-check via glab issue view 462.

  • SHINES catch-all dispatch double-registration risk — T1.6 ships 10 factories; orchestrator (T2.1) adds SHINES. If orchestrator + bundle BOTH register "shines", AdapterRegistry::from_bundles returns BundleMergeError::DuplicateEntry { kind: "adapter", key: "shines" } at boot. Mitigation: T1.6’s lockstep test asserts partner_types.count() == 14 && adapter_factories.count() == 10 && !adapter_factories.iter().any(|(k,_)| *k == "shines"). T2.1 body specifies its SHINES injection happens AFTER bundle merge.

Cross-cutting invariants

Invariant Enforced by Verification

ErasedAdapter is object-safe (Tier-O per ADR-038 §1)

crates/craig-exchange-contracts/src/erased.rs::tests::erased_adapter_is_object_safe

cargo check -p craig-exchange-contracts clean

AuditCodec is object-safe (Tier-O per ADR-038 §1)

crates/craig-partner-audit/src/codec.rs::tests::audit_codec_is_object_safe

cargo check -p craig-partner-audit clean

StateBundle is object-safe (Tier-O per ADR-038 §1)

crates/craig-state-bundle/src/lib.rs::tests::state_bundle_is_object_safe

cargo check -p craig-state-bundle clean

BundleContribution Phase-1 minimum carries 5 fields verbatim from ADR-032 §2.1

T1.5 struct definition matches §2.1 lines 105-111

ADR-032 §2.1 cited in T1.5 commit message

BundleContribution grows additively per ADR-032 §4 + ADR-038 §4

T1.5 uses Vec<(K, V)> / HashMap<K, V> shapes

ADR-032 §4 cited

StateBundle ships 4 trait methods per §2.1 lines 99-103

T1.5 trait + T1.6 DefaultBundle impl complete

ADR-032 §2.1 cited

Per-partner crates compile WITHOUT axum or futures-util dep at lib level

T1.7 macro adoption adds no axum/futures-util dep; mock-routes deferred to T2.3 per A5; macro hygiene routes through $crate per BLOCKING 4 fix

cargo tree -p craig-partner-caps shows no axum/no futures-util

Quality budgets monotonic

xtask validate [4i/14] quality-budgets

Pre-push gate

Axis coverage opt-out monotonic; new tests tag @axis:

xtask validate [4j/14] axis-coverage

Pre-push gate

fn-name-and rule on new helpers

xtask validate [4k/14] fn-name-and

Pre-push gate

New crate #![deny(unreachable_pub, missing_docs, unused_crate_dependencies)]

T1.5 + T1.6 lib roots

Plan H Step 10 + Plan M Tier-3 precedent

Workspace clippy lint set clean (17 denies + nursery group)

Pre-push battery

cargo clippy --workspace --all-targets — -D warnings

T1.6 partner_types match migration CHECK constraint

crates/craig-state-default/src/lib.rs::tests::partner_types_match_migration_check_constraint

Per-push test (precedent: services/craig-exchange/src/adapters/mod.rs:435)

T1.6 adapter factories DO NOT include "shines" (host-service responsibility)

crates/craig-state-default/src/lib.rs::tests::default_bundle_does_not_contribute_shines_adapter

Per-push test gate

T1.8 swap complete (no inline serde_json::from_value in codec impls)

T1.8 archive-time grep of crates/craig-partner-audit/src/codec/*.rs returns zero matches

T1.8 reviewer verify

Risk register

# Risk Impact Likelihood Mitigation

1

T1.2 macro hygiene defect propagates to T1.7 11-call adoption

11-call re-spin

Low

T1.2 macro uses $crate::-rooted paths; T1.7 first-partner adoption (caps) is smoke test

2

T1.5 BundleContribution shape constrains downstream Phase 2 plans

Architectural drift

Med

T1.5 uses additive Vec/HashMap; ADR-038 §4 pre-flight; T1.5 5-field struct verbatim from §2.1

3

T1.3 AuditCodec dep cycle blocks compile

T1.3 re-spin

Low

ADR-038 §2 + A2 dictate placement; T1.3 reviewer pre-flight cargo build -p craig-partner-audit

4

T1.7 F-065 / #462 timing slip

Schedule slip ~5 days

Med

T1.1-T1.6 non-blocking; T1.7 mechanical gate via glab issue view 462; reviewer waiver allowed

5

T1.6 14-token list drifts from migration

Boot registry mismatch

Low

T1.6 ships lockstep test mirroring mod.rs:435 precedent

6

Closed-aggregator (PartnerAuditEvent) surfaces during non-georgia adoption

Adoption friction

Low

#558 filed; T1.3 documents inline; decoupling deferred

7

T1.6 mock_routes vec![] placeholder forgotten by T2.3

Mock-server breakage

Low

T1.6 commit message references T2.3 follow-up

8

SHINES double-registration (bundle + orchestrator both register "shines")

Boot BundleMergeError::DuplicateEntry

Low

T1.6 lockstep test asserts SHINES excluded from bundle adapters; T2.1 body specifies orchestrator injection happens AFTER bundle merge

9

T1.2 dep additions miss workspace-level [workspace.dependencies] entry for futures-util

Cargo resolve fail

Low

T1.2 description explicitly calls out adding futures-util = "0.3" to root Cargo.toml [workspace.dependencies]

10

T1.7’s per-call format override path breaks existing AnyAdapter::send_serialized callers

SHINES regression

Low

T1.7 adds format.unwrap_or(&self.format) fallback; existing ExchangeAdapter::send impl unchanged; legacy callers route through unchanged path

  • ADR-032 — Multi-Jurisdiction Partner Registry; amendments A1/A2/A4/A6/A7/A11 anchor T1.2/T1.3/T1.5/T1.6.

  • ADR-038 — Trait-Object & Registry Patterns; §1 Tier-O cited by T1.2/T1.3/T1.5 object-safety smokes; §2 trait-location cited by T1.3 AuditCodec placement; §3 registry factory shape cited by T1.5 ErasedAdapterFactory; §4 field-sourcing cited by T1.5 BundleContribution + T1.6 mock_routes deferment.

  • ADR-030 — Status vocabulary on every cell.

  • Plan T umbrella — parent; this Plan T1 body sits at Plan T umbrella Step 1.

  • Plan S — grandparent; Plan T1 sits at Plan S Step 4 (T1 body filed) / Step 5 (T1 executed).

  • Plan G — F-065 / Plan G Step 6 / #462 SOFT-gates T1.7.

  • Plan L — Plan L Step 5 PartnerAuditEvent landed; T1.3 + T1.8 extend the pattern.

  • the pre-1.0 destructive-rebuild posture — N/A for T1 (no DDL changes).

  • the service-initialization pattern — orchestrator pattern referenced by future T2.1 bundle + SHINES injection wiring.

  • the quality-budget enforcement gate — B3a (serde_json::Value src budget) verification discipline applies to T1.2 + T1.3; STRUCTURAL-VALUE markers per-site.

  • #462 (Plan G Step 6 / F-065) — SOFT-gates T1.7.

  • #558 (closed-aggregator decoupling tracking) — known limitation inherited at T1.3.

Edit this page · latest