Plan L: Partner Typed Schemas (destination architecture for partner integrations)
On this page
- Status
- Context
- Cross-cutting invariants
- Scope
- Plan refresh — 2026-05-25 (ADR-030 §3 substantiation)
- Decision 1 — Wire-protocol enum name + location
- Decision 2 — Fate of
exchange_partners.partner_typeString column - Decision 3 —
send_worker.rs:206runtime dispatch - Decision 4 — Per-partner route implications (auth + rate-limit + OpenAPI)
- Decision 5 — Codegen pilot partner selection (Step 6 pre-pick)
- Step 1 status row was stale
- Steps
- Step 2: F-058 trait redesign
- Step 3: F-059 per-partner crate scaffolding
- Step 4: F-060 schema versioning per partner
- Step 5: F-061 typed Audit payloads end-to-end
- Step 6: F-062 codegen pilot
- Step 7: F-063 mock-server adapter integration
- Step 8: F-064 retire partner-edge Value carve-out
- Step 9: Plan completion audit + archive
- Files Touched
- Verification
- Risks
- After this plan lands
Status
| Step | Description | Status |
|---|---|---|
1 |
Plan filing — body lands in a docs-only MR alongside the style-guide-driven refresh of Plans D/G/H/I/J/K. nav.adoc + CHANGELOG. Epic + step issues filed. No code changes. |
Done (2026-05-15) — Plan body landed via commit |
2 |
F-058 implementation: redesign |
Done (2026-06-07) — Step 2a Done (2026-05-25): trait redesign foundation + |
3 |
F-059 implementation: per-partner crate scaffolding. One workspace crate per partner system: |
Done (2026-06-06) — PILOT (2026-05-25) |
4 |
F-060 implementation: schema-versioning per partner. For each partner whose interface spec mentions explicit version numbers (STARS has v2.x; CPRS has dated revisions): introduce a versioned envelope |
Done (2026-06-08) — scope-narrowed by empirical reality. Pre-flight scan of all 10 interface spec docs confirmed every "Version X.Y" header is a document-revision marker, not a wire-protocol version marker — no partner spec carries a |
5 |
F-061 implementation: typed Audit payloads end-to-end. Choice locked = Option B (single audit table with |
Done (2026-06-08) — type-side delivered; storage layer deferred to a follow-up MR. Pre-flight scan confirmed the dispatch host has zero production callers of audit payloads today: |
6 |
F-062 implementation: codegen pilot for one partner schema. Pick STARS (largest spec; most variants; greatest value). Determine the source format (XSD vs OpenAPI vs prose-described — verify at Step 6 branch time by re-reading |
Deferred (no machine-readable spec exists for any partner; revisit when a partner publishes an XSD / OpenAPI / WSDL — 2026-06-08) — pre-flight scan per plan body’s explicit pre-check ran |
7 |
F-063 implementation: mock-server adapter integration. Today’s mock-server is "shipped but functionally orphaned" (memory |
Done (2026-06-08) — PILOT shipped (2026-06-08): |
8 |
F-064 implementation: retire the partner-edge |
Done (2026-06-08) — final sweep retires all 23 |
9 |
Plan completion audit + archive. |
Done (2026-06-08) — plan-completion audit dispatched via fresh Explore subagent (per the plan-completion-audit bias) caught 2 stale Status-cell items and 1 minor clarification: Step 1 date drift |
Epic: &34 (epic: Partner Typed Schemas (Plan L))
Issues: #441 (Step 2 — F-058 trait redesign) · #442 (Step 3 — F-059 per-partner crates) · #443 (Step 4 — F-060 schema versioning) · #444 (Step 5 — F-061 typed Audit) · #445 (Step 6 — F-062 codegen pilot) · #446 (Step 7 — F-063 mock-server de-orphan) · #447 (Step 8 — F-064 retire Value carve-out) · #448 (Step 9 — plan completion)
Branch prefix: feat/partner-typed-schemas- / refactor/partner-typed-schemas-
Milestone: TBD (depends on partner-integration priority; lower-priority until at least one production partner integration starts)
Context
CRAIG today has an ExchangeAdapter trait at services/craig-exchange/src/adapters/mod.rs:18-31 shaped:
pub trait ExchangeAdapter: Send + Sync {
fn test_connectivity(&self, endpoint_url: &str)
-> Pin<Box<dyn Future<Output = Result<String, String>> + Send + '_>>;
fn send(&self, endpoint_url: &str, payload: &serde_json::Value)
-> Pin<Box<dyn Future<Output = Result<serde_json::Value, String>> + Send + '_>>;
}
Every property of this signature violates the new style guide:
-
&serde_json::Valueparameter — "Abstract JSON objects and values (serde_json::Value) are not allowed." -
Result<_, String>error type — "Error types cannot be strings." -
Pin<Box<dyn Future…>>— manual async-trait shape;async_trait-or-RPITIT preferable.
This wasn’t bad design for a placeholder. The 17 partner-interface docs at docs/modules/ROOT/pages/interfaces/ define the target shapes per partner (STARS, CPRS, EMPI, TCM, DOE/SLDS, SMILE, IES, CAPS, IONS, WIC, SHINES, …). The trait was deliberately erased to Value while no real partner integration existed. With the style guide as the new bar, that placeholder is now technical debt with a defined exit.
This plan is also the destination cited by Plan I F-037’s "Option 2-as-roadmap" decision: F-037 takes the typed-everywhere-within-CRAIG slice now; the partner-edge Value carve-outs (raw_submission, partner audit envelopes, mock-server inbound JSON) stay as documented temporary state with a // PARTNER-EDGE-UNTYPED: see plans/partner-typed-schemas.adoc cite. This plan retires those carve-outs.
Cross-cutting invariants
-
No partner integration in production yet — destructive refactors are fine. Per the pre-1.0 destructive-rebuild posture — there is no real partner data in flight; the existing 11
NoopAdapterregistrations are devstack fixtures only. The trait can change shape freely. -
Per-partner crate isolation. Each partner gets a workspace crate (
crates/craig-partner-<name>). Cross-partner reuse goes throughcraig-exchangeor a newcraig-partner-coreshared types crate — NOT through directuse craig_partner_<a> as _;between sibling partner crates. Keeps each adapter independently versionable + replaceable. -
Source-of-truth =
docs/modules/ROOT/pages/interfaces/. Every typed shape must trace back to a section of one of the 17 interface docs. If the doc is ambiguous, file a docs MR fixing the spec first; the typed shape’s PR cites the docs MR. -
Codegen vs hand-written is per-partner. Some partner specs are machine-readable (XSD / OpenAPI); some are prose with example payloads. Step 6 pilots one partner; subsequent partners pick codegen-or-hand based on what their spec supports. Don’t force XSD codegen on a prose-described partner.
-
Send + Sync + 'staticpreserved. The new trait’s associated types must satisfy the bounds axum handlers + tokio-spawn require. Document the bound in the trait definition; verify at compile time per associated-type impl. -
No
Box<dyn>dispatch in business logic. Each handler dispatches to a concrete adapter type via the partner discriminator. The "lookup adapter by string and get aBox<dyn>`" pattern at `adapters/mod.rs:41-59is exactly what this plan replaces.
Scope
In scope (7 findings):
-
F-058
ExchangeAdaptertrait redesign (associated types; typed error) -
F-059 per-partner crate scaffolding
-
F-060 schema versioning per partner
-
F-061 typed Audit payloads end-to-end
-
F-062 codegen pilot for one partner schema
-
F-063 mock-server adapter integration (de-orphan)
-
F-064 retire partner-edge Value carve-out (closes Plan I F-037)
Out of scope:
-
Adding a new partner that’s not in the 17-doc corpus — out of scope; this plan covers known partners
-
Wire-protocol changes to existing partner endpoints — those are stakeholder decisions; this plan reshapes CRAIG’s internal types only
-
Federation / cross-state partner sharing — separate stakeholder decision; this plan covers intra-Georgia partners + the federal interfaces in the spec corpus
-
HTTP-client choice —
reqwest::Clientstays; trait shape only
Plan refresh — 2026-05-25 (ADR-030 §3 substantiation)
Re-review on 2026-05-25 surfaced 5 gaps where Step 2 was not executable for a contextless agent. Decisions locked here so the implementing MR has a clear spec rather than re-litigating architecture during code review.
Decision 1 — Wire-protocol enum name + location
The plan body’s Step 3 dispatch sketch (match partner_kind { PartnerKind::Stars ⇒ … }) collides with the existing craig_reference::PartnerKind enum, which encodes semantic categories (Le, Hospital, etc.) — NOT wire-protocol identifiers.
Locked: introduce a new enum ExchangeAdapterKind in services/craig-exchange/src/adapters/mod.rs (inline; not shared cross-service):
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumString,
strum::EnumIter, serde::Serialize, serde::Deserialize, utoipa::ToSchema,
sqlx::Type)]
#[sqlx(type_name = "text", rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ExchangeAdapterKind {
Caps, Cprs, DoeSlds, Empi, Ies, Ions, Shines, Smile, Stars, Tcm, Wic,
}
Existing craig_reference::PartnerKind stays untouched — it encodes a different concept (semantic category for compliance reporting / authz scoping). Step 3’s match partner_kind is updated to match adapter_kind referencing the new enum.
Decision 2 — Fate of exchange_partners.partner_type String column
Today the column is TEXT with a 14-value CHECK constraint (state_agency / court_system / financial / medicaid / tanf / etc.). These are semantic categories — same concept as craig_reference::PartnerKind — NOT wire-protocol identifiers.
Locked: ADD a new column adapter_kind (TEXT NOT NULL with a CHECK constraint listing the 11 ExchangeAdapterKind::* snake_case values). Backfill is destructive-reseed-safe per pre-1.0 doctrine (the pre-1.0 destructive-rebuild posture) — devstack rows are NoopAdapter fixtures only. Existing partner_type STAYS (covers semantic categorization, compliance reporting, audit grouping); the two columns serve orthogonal purposes.
Migration ships in Step 2’s MR (so dispatch logic + column land together). Migration file: services/craig-exchange/migrations/<TS>_add_adapter_kind.sql.
Decision 3 — send_worker.rs:206 runtime dispatch
Per-partner axum routes (lines 144-151) solve HTTP dispatch but not the services/craig-exchange/src/send_worker.rs:206 call. send_worker reads outbox rows from the DB + dispatches by partner — runtime-typed-on-partner, no compile-time information.
Locked: introduce a small enum wrapper enum AnyAdapter { Stars(StarsAdapter), Cprs(CprsAdapter), … 11 variants } (also in adapters/mod.rs) for the send_worker dispatch site. The wrapper exposes a send_serialized(&self, endpoint: &str, payload: serde_json::Value) → Result<serde_json::Value, AnyAdapterError> method that per-variant deserializes the JSON into Self::Outbound, calls the typed send(), then re-serializes Self::Inbound.
This preserves the typed-everywhere property inside each adapter while accommodating the outbox row’s String/Value transport at the worker boundary. The Value escape is contained to ONE function (AnyAdapter::send_serialized) — it does not leak back into adapter business logic. Step 8 (F-064 retire Value carve-out) explicitly notes this as the exception with rationale.
Audit-payload generation in send_worker also routes through AnyAdapter::audit_serialized(&self, inbound: serde_json::Value) → AuditPayload.
Decision 4 — Per-partner route implications (auth + rate-limit + OpenAPI)
Adding 11 new routes (/v1/exchange/<partner>/send) needs explicit handling for:
-
Auth: existing
auth_middlewareapplies to every route under/v1/exchange/*automatically. No per-route changes. -
Partner rate-limit (Plan C F-006): per-partner rate-limit middleware buckets on
partner_id(resolved from the request body/auth context), NOT on route. New routes inherit the existing middleware without modification. -
OpenAPI: each handler gets its own
#[utoipa::path]attribute. ~30 LOC of boilerplate per partner; aggregate adds ~330 LOC to the OpenAPI surface. Worth the cost — each partner contract is independently documented + versionable. -
Per-partner test fixtures: existing
craig-test-lib::ExchangeClient(16 methods per Plan I F-035d split) gets a new method per partner route. Plan I F-054 lint stays clean as long as ExchangeClient ≤16 methods. Verify at Step 2 implementation that adding 11 send-methods doesn’t push ExchangeClient past 16; if it does, split per Plan I §F-035 pattern (e.g. oneExchangePartnerClientper wire protocol).
Decision 5 — Codegen pilot partner selection (Step 6 pre-pick)
Plan body §Step 6 defers the codegen-or-handwritten partner selection to branch time. Pre-pick locked here based on the interface-spec corpus survey done 2026-05-25:
-
STARS has 2 docs (
stars-detailed-design.adoc,stars-integration-architecture.adoc) but neither contains a machine-readable XSD or OpenAPI — prose-described JSON examples only. STARS is NOT suitable for codegen pilot. -
EMPI has 3 docs (
empi-inquiry.adoc,empi-interface.adoc,empi-registration.adoc) — needs branch-time inspection but federal interface lineage suggests possible HL7 / OASIS XSD. -
IES has Medicaid eligibility lineage; likely XSD or fixed-position file format.
Locked: Step 6 implementer MUST start by running the spec-format probe (lines 260-269 below) against ALL 10 partner crates (shines excluded per §Step 3 SHINES note) and producing a decision-table MR-first commit. The codegen pilot then targets the partner with the cleanest machine-readable spec — likely EMPI or IES, NOT STARS as the original plan body assumed.
Steps
Step 2: F-058 trait redesign
Files:
-
services/craig-exchange/src/adapters/mod.rs— replace existing trait with the following. Uses RPITIT (return-positionimpl Traitin trait, stable in Rust 2024), not#[async_trait::async_trait]— Plan L invariant 6 drops object-safety (noBox<dyn ExchangeAdapter>in business logic), so theBox-per-call overheadasync_traitimposes serves no purpose. Native async-fn-in-trait is cleaner:pub trait ExchangeAdapter: Send + Sync + 'static { type Inbound: serde::de::DeserializeOwned + Send + Sync + 'static; type Outbound: serde::Serialize + Send + Sync + 'static; type AuditPayload: serde::Serialize + Send + Sync + 'static; type Error: std::error::Error + Send + Sync + 'static; fn test_connectivity(&self, endpoint_url: &str) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send; fn send(&self, endpoint_url: &str, payload: Self::Outbound) -> impl std::future::Future<Output = Result<Self::Inbound, Self::Error>> + Send; fn audit(&self, response: &Self::Inbound) -> Self::AuditPayload; }Decision locked at plan-filing time (NOT MR-time): no
connect_timeout/ per-call config methods on the trait. Per-adapter config is constructor-injected (StarsAdapter::new(client, timeout, …)) — keeps the trait surface minimal. If a future partner truly needs trait-level config, file as Plan L errata. -
services/craig-exchange/src/adapters/standard.rs+noop.rs— update to new trait.NoopAdapterbecomes generic over a<C: NoopConfig>so its associated types are concrete per test. -
All call sites in
services/craig-exchange/src/api/**that today take&dyn ExchangeAdapter— convert to per-partner dispatch. -
axum-handler dispatch strategy (locked: per-partner-routed endpoints): the current
POST /exchange/sendendpoint that runtime-switches onpartner_type: &str(today’sadapter_foratadapters/mod.rs:41-59) gets split into per-partner routes:// Before .route("/v1/exchange/send", post(send_handler)) // where send_handler reads partner_type from request body and dispatches via Box<dyn> // After .route("/v1/exchange/stars/send", post(send_stars_handler)) .route("/v1/exchange/cprs/send", post(send_cprs_handler)) .route("/v1/exchange/empi/send", post(send_empi_handler)) // ...one per partnerEach handler embeds the concrete adapter type via
State<Arc<StarsAdapter>>. Trade-off vs the alternative (enum-of-concrete-adapters + hand dispatch): more routes (~10), more handler boilerplate, but no enum + no manual dispatch + each handler is independently versionable. Choice locked per the 2026-05-15-pm assessment.
Branch: refactor/partner-typed-schemas-step2-trait-redesign
MR title: refactor(craig-exchange): redesign ExchangeAdapter trait with associated types + typed error [Step 2 of partner-typed-schemas]
Verification:
-
cargo build --workspace --all-featuresclean after trait change -
cargo nextest run -p craig-exchange— adapter unit tests + integration tests green -
grep -rn "serde_json::Value" services/craig-exchange/src/adapters/returns zero matches
Step 3: F-059 per-partner crate scaffolding
SHINES note: enum variant kept, per-partner crate intentionally excluded
The ExchangeAdapterKind::Shines enum variant exists for catch-all-fallback routing — it is NOT an external partner system. SHINES (Georgia Statewide Automated Child Welfare Information System) is the legacy CCWIS that CRAIG is replacing, not a partner CRAIG calls. The interface doc at docs/modules/ROOT/pages/interfaces/shines-integration-mapping.adoc opens with an explicit [NOTE] block: "Legacy SHINES reference document … It documents the system being replaced, not CRAIG behavior." Its body is a SHINES↔STARS mapping (how the legacy system used to talk to the STARS partner), not a spec for a SHINES partner.
What SHINES actually does in the dispatch table:
-
Backfill default:
services/craig-exchange/migrations/20260525213241_add_adapter_kind.sqlsetsDEFAULT 'shines'so legacyexchange_partnersrows with no explicitadapter_kindroute somewhere reasonable. -
Catch-all for unmapped semantic categories: seed data assigns
Shinestopartner_typevalues where no modern partner protocol exists —cwca_provider,tanf,external_data,tribal_authority, sister-state CCWIS systems (all annotated "no SHINES equivalent"). -
Permanent route through
StandardAdapter:ExchangeAdapterKind::Shineswill NEVER get anAnyAdapter::Shines(ShinesAdapter)dispatch arm or acrates/craig-partner-shinescrate. It stays on the placeholder dispatch surface forever.
The originally-planned batch-2 third slot of craig-partner-shines was substituted with craig-partner-wic in !456 after this audit landed. Net effect: Step 3 ships 10 per-partner crates, not 11.
Per-partner crates (10 after SHINES exclusion)
Files (per partner crate; 10 crates after dedup of the 17 interface docs into 11 source partner systems then subtracting SHINES per the note above):
The 10 partner systems (from docs/modules/ROOT/pages/interfaces/):
-
caps (
caps-referral-ies.adoc) -
cprs (
cprs-court-order-report.adoc,cprs-fcc-case-plans.adoc,cprs-inv-ong-stage-data.adoc) -
doe-slds (
doe-slds-detail.adoc,doe-slds-interface.adoc) -
empi (
empi-inquiry.adoc,empi-interface.adoc,empi-registration.adoc) -
ies (
ies-medicaid-eligibility.adoc) -
ions (
ions-outbound.adoc) -
smile (
smile-financial.adoc) -
stars (
stars-detailed-design.adoc,stars-integration-architecture.adoc) -
tcm (
tcm-medicaid-claims.adoc) -
wic (
wic-referral-ies.adoc)
Per partner:
-
crates/craig-partner-<name>/Cargo.toml(NEW) -
crates/craig-partner-<name>/src/lib.rs(NEW) -
crates/craig-partner-<name>/src/types.rs(NEW —InboundEvent,OutboundCommand,AuditPayloadtyped structs/enums; initial set traces to that partner’s interface-spec file(s)) -
crates/craig-partner-<name>/src/adapter.rs(NEW —pub struct <Name>Adapter; impl ExchangeAdapter for <Name>Adapter { … }) -
crates/craig-partner-<name>/src/error.rs(NEW —#[derive(thiserror::Error)] pub enum <Name>Error { … })
(Repeat for each of the 10 partner crates.)
* Cargo.toml (workspace) — add each new crate to members
* services/craig-exchange/Cargo.toml — add path deps on each new crate
* services/craig-exchange/src/adapters/mod.rs::adapter_for — replace the string-match dispatch with a typed match adapter_kind { ExchangeAdapterKind::Stars ⇒ stars::StarsAdapter::new(…), … }. Note: ExchangeAdapterKind is the NEW enum introduced at Step 2 per refresh Decision 1; it does NOT collide with the existing craig_reference::PartnerKind (which encodes semantic categories like Le / Hospital, not wire-protocol identifiers).
Architectural decision (PILOT — 2026-05-25): The ExchangeAdapter trait + ExchangeAdapterKind enum live in a NEW leaf crate crates/craig-exchange-contracts (NOT in services/craig-exchange, where they would stay pub(crate)). Per-partner crates depend on craig-exchange-contracts only; the host services/craig-exchange re-exports them via pub(crate) use craig_exchange_contracts::{…}; so its handler / store / worker call sites compile unchanged. AnyAdapter enum + *Error types + adapter_for STAY in the host service — those are implementation glue, not contract. This is the "Choice 2" architecture from the Step 3 brief; it’s the right shape for the 10-partner-crate ecosystem (ExchangeAdapterKind retains 11 variants because Shines stays as the catch-all routing fallback — see §Step 3 SHINES note) because each partner crate stays a strict leaf with no service deps.
Branch (per partner): feat/partner-typed-schemas-step3-<partner> (~10 MRs)
MR title (per partner): feat(craig-partner-<name>): typed adapter crate per partner-typed-schemas [Step 3 of partner-typed-schemas]
Verification (per partner):
-
cargo build -p craig-partner-<name>clean -
cargo build --workspaceclean (workspace integration) -
cargo nextest run -p craig-partner-<name>— type-system smoke tests (round-trip serde on each typed variant)
Step 4: F-060 schema versioning per partner
Files: per partner — within each crates/craig-partner-<name>/src/types.rs, introduce versioned envelope where the interface doc mentions explicit version numbers:
#[derive(Deserialize)]
#[serde(tag = "schema_version")]
pub enum StarsEnvelope {
#[serde(rename = "2.3")]
V2_3(v2_3::Shape),
#[serde(rename = "2.4")]
V2_4(v2_4::Shape),
}
-
Versioned-partner crates gain
src/v<X>/mod.rsper version. -
Single-version partners: no envelope wrapper; the bare
Shapeis the inbound type.
Branch: feat/partner-typed-schemas-step4-schema-versioning
MR title: feat(craig-partner-{stars,cprs,…}): introduce schema-version envelopes per interface-doc revisions [Step 4 of partner-typed-schemas]
Verification:
-
Round-trip serde test per versioned variant
-
Unknown version → typed
UnrecognizedSchemaVersion { received: String }variant (NOT aValuefallback)
Step 5: F-061 typed Audit payloads end-to-end
Choice locked = Option B (unified table + typed read API). Option A is the documented fallback.
-
Option B (locked):
partner_audit_events(partner_kind ENUM, payload JSONB). Read-side store function returnsVec<PartnerAuditEvent>enum, neverValue. Pros: cross-partner queries trivial; one migration. Cons: payload column is JSONB internally; need to keep the read-side typed-enum-only discipline. -
Option A (fallback): per-partner audit table
stars_audit_events,cprs_audit_events, etc. Use ONLY if Option B’s read-side enum becomes unmaintainable (~20 variants). Step 5 MR justifies the switch in body if invoked.
Files (Option B):
-
services/craig-security/migrations/<TS>_typed_partner_audit_events.sql(NEW) -
services/craig-security/src/store/audit.rs— typed read API returningPartnerAuditEvent -
services/craig-security/src/api/audit.rs— handlers return typed enum, no Value escape
Branch: feat/partner-typed-schemas-step5-typed-audit
Verification:
-
Migration applies clean (devstack)
-
Read-side returns typed
PartnerAuditEventfor every audit row -
grep -rn "serde_json::Value" services/craig-security/src/{store,api}/audit*returns zero matches
Step 6: F-062 codegen pilot
Pick STARS (or first partner whose spec is machine-readable). Source-format determination + tooling choice happens at branch time.
Pre-check at branch time — verify STARS spec is machine-readable:
# Look for XSD / OpenAPI / Swagger markers
grep -E '<xs:schema|openapi:|swagger:' docs/modules/ROOT/pages/interfaces/stars-*.adoc
# Look for JSON example fenced blocks (suggests prose-described JSON shapes)
grep -c '```json' docs/modules/ROOT/pages/interfaces/stars-*.adoc
# Look for actual XSD or OpenAPI spec files attached to interface dir
find docs/modules/ROOT/attachments -name '*stars*' -name '*.xsd' -o -name '*.yaml' -o -name '*.yml' 2>/dev/null
If none of the above produces machine-readable schema for STARS, fall back to a partner that does (CPRS, EMPI, and the federal interfaces — IES / TCM — most likely candidates per federal interface lineage). Decision memo + the codegen-or-handwritten determination committed alongside this Step’s MR.
Files:
-
crates/craig-partner-<chosen>/build.rs(NEW) — invokes codegen from spec source -
crates/craig-partner-<chosen>/src/generated/(NEW — checked-in generated output) -
crates/craig-partner-<chosen>/Cargo.toml— adds codegen-time build-dep -
Spec source under
docs/modules/ROOT/pages/interfaces/— may need a fixture extraction step to make it consumable by tooling
Branch: feat/partner-typed-schemas-step6-codegen-pilot-<partner>
Verification:
-
cargo build -p craig-partner-<chosen>runs codegen, produces typed module -
Generated types pass round-trip serde tests
-
Decision memo committed: which other partners qualify for codegen vs hand-written
Step 7: F-063 mock-server adapter integration
Files:
-
tools/craig-mock-server/src/fixtures/(NEW subdirectory — does not exist today; current layout has flat<partner>.rssiblings ofmain.rs) -
tools/craig-mock-server/src/fixtures/<partner>/(NEW per partner) — canonical inbound + outbound fixture payloads -
tools/craig-mock-server/src/{caps,wic}.rs(NEW — 2 missing partner modules; today’s mock-server has 8 partner modules for cprs, doe, empi, ies, ions, smile, stars, tcm).shinesdoes NOT get a mock-server module per §Step 3 SHINES note (it’s the catch-all fallback, not a partner with its own wire shape). -
tools/craig-mock-server/src/main.rs— fixture-serving routes per partner kind -
crates/craig-partner-<name>/tests/round_trip.rs(NEW per partner — 10 total) — exercises the adapter against the mock-server
Branch (per partner): feat/partner-typed-schemas-step7-mock-roundtrip-<partner>
Verification:
-
cargo nextest run -p craig-partner-<name> --test round_trip— green per partner -
Mock-server
cargo run --bin craig-mock-serverboots clean against the new routes
Step 8: F-064 retire partner-edge Value carve-out
Files:
-
services/craig-cases/src/api/reports.rs+ adjacent — confirmraw_submissiontyped (per Plan H F-030 unwrap audit + this plan’s earlier steps) -
All
// PARTNER-EDGE-UNTYPED:comments cited in Plan I F-037 — remove -
Plan I F-037 sub-row Status update: "Done (closed by Plan L F-064)"
Branch: refactor/partner-typed-schemas-step8-retire-value-carveout
Verification:
-
grep -rn "serde_json::Value" services/ crates/— zero occurrences in non-test code (modulo any audit-payload internal-JSONB-column site; the read API still returns typed enum) -
grep -rn "PARTNER-EDGE-UNTYPED" services/ crates/— zero comments left
Files Touched
| File | Step | Change |
|---|---|---|
|
2,3 |
EDIT (trait redesign + per-partner dispatch) |
|
2 |
EDIT (update to new trait) |
|
3,4,6,7 |
NEW (one crate per partner; ~10 total) |
|
3 |
EDIT (members) |
|
3 |
EDIT (path deps) |
|
5 |
NEW |
|
5 |
EDIT (typed read API) |
|
7 |
NEW (per partner fixtures) |
|
7 |
EDIT (fixture routes) |
All sites currently carrying |
8 |
EDIT (comment + Value field removal) |
Risks
| Risk | Mitigation |
|---|---|
Step 3’s ~10 per-partner crates explode workspace member count + compile time |
Workspace already has 25 members; +10 brings it to 35. Compile time grows but each crate is independent. Use |
Step 4 versioned envelopes lock in version names that the partner later renames |
Use |
Step 5’s per-partner-table vs unified-table decision is bikeshed-prone |
Concrete decision required in Step 5’s MR body, not deferred. Default = Option B (unified + typed read). Owner makes the call + documents tradeoff |
Step 6 codegen for STARS turns out to require building an XSD-to-Rust toolchain from scratch |
Hand-written types are the fallback. The pilot’s deliverable is "decided how to codegen + at least one partner converted"; if STARS doesn’t support codegen, pick a partner that does (or accept hand-written everywhere) |
Step 7 mock-server adapter integration retroactively breaks the existing mock-server fixtures |
The mock-server is functionally orphaned per memory |
Plan L conflicts with Plans D/G/H/I sweeps over |
Plan L is the destination; Plans D/G/H/I are the daily-driver cleanup. Run Plans D/G/H/I first; Plan L picks up after they land |
After this plan lands
-
ExchangeAdaptertrait is associated-types-typed with a typed error type (noValue, noStringerror) -
~10 per-partner crates own their typed schemas + adapter impls + audit payloads
-
Schema versioning enforced at deserialize per partner that has explicit revisions
-
Audit log returns typed
PartnerAuditEventenum from read API (internal JSONB storage is opaque to consumers) -
Mock-server is no longer orphaned — every adapter has a round-trip integration test against canonical fixtures
-
Partner-edge
Valuecarve-out cited by Plan I F-037 retired; codebase has zeroserde_json::Valuein non-test, non-codegen code