Multi-Jurisdiction Extensibility
On this page
- Overview
- Step 1 — Create the bundle crate and implement
StateBundle - Step 2 — Fill in the
BundleContributionaxes and federal mapping - Step 3 — Theme (ADR-036)
- Step 4 — Terminology (ADR-034)
- Step 5 — Seed data (
SeedContribution/RateTableSpec) - Step 6 — Register in the workspace and wire the consumers
- Step 7 — Join the feature matrix
- Step 8 — Activate the jurisdiction at deploy time
- Verification
- Related decisions
CRAIG is jurisdiction-neutral. The product code carries no Georgia (or any other state’s) assumptions; everything jurisdiction-specific lives inside a state bundle — a small crate implementing one trait. This guide walks an engineer through adding a brand-new jurisdiction, from an empty crate to a deployment that boots against it.
Overview
A CRAIG deployment activates exactly one state bundle. This is the
one-deployment-one-jurisdiction rule (ADR-032 §2.7): a running instance serves a
single jurisdiction, and which one is chosen at boot by the
CRAIG__ACTIVE_STATE_BUNDLES environment variable, selected from the bundles
that were compiled in via state-* Cargo features.
The seam works in two layers:
-
Compile time — each bundle-consuming crate enables zero or more
state-<name>features. Each feature pulls in one optionalcraig-state-<name>dependency and registers it in a#[cfg]-gated candidate list. A build with no state feature fails with acompile_error!; the default feature set keeps the Georgia build byte-identical. -
Boot time — every service reads
CRAIG__ACTIVE_STATE_BUNDLESonce and callsresolve_active_bundle(), which fails fast if the value is missing, names an unknown bundle, or names more than one. The single matched bundle’scontribute()output is merged into the per-axis registries, and its theme
terminology are materialized bycraig-web.
This is the State Bundle Pattern — Plan S Phase 3, implemented by Plan U. The
authoritative trait + contribution types live in the craig-state-bundle crate;
the production reference bundle is craig-state-ga (Georgia, full); the minimal
neutrality proof is craig-state-tx-stub (Texas, stub). This guide uses
craig-state-tx-stub as the worked minimal example throughout — it is the
smallest crate that satisfies the contract, so it is the best template for a new
jurisdiction.
|
"Five axes" is a high-level grouping of the engineering surface a bundle plugs
into: (1) partners (includes the |
Step 1 — Create the bundle crate and implement StateBundle
Create crates/craig-state-<name>/ (e.g. crates/craig-state-tx-stub/). The
crate needs only two dependencies: craig-state-bundle for the trait and
contribution types, and craig-reference for the FederalPartnerCategory enum.
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
name = "craig-state-tx-stub"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
craig-state-bundle = { workspace = true }
craig-reference = { workspace = true }
[lints]
workspace = true
The bundle itself is a zero-sized unit struct implementing the four REQUIRED
StateBundle trait methods (name, jurisdiction_code, contribute,
federal_mapping). Three further methods are DEFAULTED: subsidy_policy() and
uas_codes() (the #1072 subsidy/UAS reference seams — the stub inherits both
defaults, keeping craig-financial fail-closed under it) and search_schemes()
(the #1025 per-jurisdiction search-scheme declaration — the stub OVERRIDES this
one as the reference Opaque declaration for persons.ssn_last_four, activated by
#1398). A bundle with none of the corresponding policy inherits all three
defaults untouched:
use std::collections::HashMap;
use craig_reference::FederalPartnerCategory;
use craig_state_bundle::{
BundleContribution, CompositionContribution, PRODUCT_PARTNER_TYPES, PartnerTypeMeta,
SchemeName, SearchSchemeSpec, SeedContribution, StateBundle, TerminologyContribution,
ThemeContribution,
};
/// The minimal Texas reference bundle — proves the pattern carries no
/// Georgia-specific assumptions.
pub struct TxStubBundle;
impl StateBundle for TxStubBundle {
/// Stable bundle identifier — the token `CRAIG__ACTIVE_STATE_BUNDLES`
/// and duplicate-detection error messages name the bundle by. Must be
/// unique across all compiled-in bundles.
fn name(&self) -> &'static str {
"tx-stub"
}
/// The jurisdiction code the bundle is shaped for.
fn jurisdiction_code(&self) -> &'static str {
"texas"
}
fn contribute(&self) -> BundleContribution {
// ... see Step 2 for the full walkthrough of each field.
BundleContribution {
adapters: vec![],
audit_codecs: vec![],
mock_routes: vec![],
partner_types: PRODUCT_PARTNER_TYPES
.iter()
.map(|token| PartnerTypeMeta { token })
.collect(),
seed_data: SeedContribution::default(),
theme: ThemeContribution::default(),
terminology: TerminologyContribution::default(),
compositions: CompositionContribution::default(),
}
}
fn federal_mapping(&self) -> HashMap<&'static str, FederalPartnerCategory> {
// ... see Step 2
HashMap::new()
}
fn search_schemes(&self) -> Vec<SearchSchemeSpec> {
// #1025 (activated #1398): the reference override — Texas stores
// `persons.ssn_last_four` as unsearchable Opaque ciphertext where
// Georgia's default is the equality-searchable BlindIndex.
vec![SearchSchemeSpec {
entity: "persons",
column: "ssn_last_four",
scheme: SchemeName::Opaque,
}]
}
}
|
|
Step 2 — Fill in the BundleContribution axes and federal mapping
contribute() returns a BundleContribution with eight fields, and
federal_mapping() is a separate trait method (it is per-bundle-identity, not a
mergeable entry list, so it is deliberately not a contribution field). A
minimal bundle leaves almost everything at its .default() value:
| Field | Minimal value | May be empty? |
|---|---|---|
|
|
Yes — a jurisdiction may register zero adapters |
|
|
Yes |
|
|
Yes |
|
map |
No — must contribute the shared taxonomy |
|
|
Yes — empty rate-table set |
|
|
Yes — |
|
|
Yes — empty = no overlay |
|
|
Yes — no composable surfaces (Plan X, ADR-035 §8) |
Here is the stub’s complete contribute() and federal_mapping():
fn contribute(&self) -> BundleContribution {
BundleContribution {
// No Texas typed partner crates exist — a jurisdiction may
// legitimately register zero adapters / codecs / mock routes.
adapters: vec![],
audit_codecs: vec![],
mock_routes: vec![],
// Reuse the neutral product taxonomy — jurisdiction-agnostic,
// shared with every bundle.
partner_types: PRODUCT_PARTNER_TYPES
.iter()
.map(|token| PartnerTypeMeta { token })
.collect(),
// A stub declares no jurisdiction policy: empty rate-table set.
seed_data: SeedContribution::default(),
// Inherit the product-default palette; ship no terminology overlay.
theme: ThemeContribution::default(),
terminology: TerminologyContribution::default(),
// Declare no composable surfaces (Plan X, ADR-035 §8).
compositions: CompositionContribution::default(),
}
}
fn federal_mapping(&self) -> HashMap<&'static str, FederalPartnerCategory> {
// Minimal illustrative mapping over the shared taxonomy tokens.
// Absent tokens carry no federal responsibility (checked at
// REPORT-EMIT time, never at boot).
HashMap::from([
("tanf", FederalPartnerCategory::Tanf),
("medicaid", FederalPartnerCategory::Medicaid),
])
}
partner_types — reuse the shared taxonomy
partner_types is the one field a bundle must populate. Every bundle reuses
the same 14-token neutral taxonomy, craig_state_bundle::PRODUCT_PARTNER_TYPES,
without modification (Georgia and the stub do the identical .iter().map(…)):
pub const PRODUCT_PARTNER_TYPES: [&str; 14] = [
"state_agency", "court_system", "federal_agency", "tribal_authority",
"private_provider", "cwca_provider", "financial", "medicaid",
"child_abuse_registry", "tanf", "child_support", "external_data",
"education", "health_agency",
];
This list is sourced from the chk_exchange_partners_partner_type CHECK
constraint, so it is the same taxonomy the database enforces.
federal_mapping — the per-bundle federal seam
federal_mapping() maps a subset of the partner-type tokens to a
FederalPartnerCategory. Absent keys mean "no federal report responsibility";
completeness is checked at report-emit time, never at boot, so a stub may map two
tokens (as above) or none at all (HashMap::new()). The full Georgia bundle maps
five: tanf, medicaid, child_support, education, child_abuse_registry.
adapters, audit_codecs, mock_routes — optional partner wiring
A jurisdiction with typed partner integrations contributes adapter factories,
audit codecs, and (under the mock feature) mock routers, all keyed by an
adapter-kind token. The Georgia bundle ships ten of each. Texas has no typed
partner crates yet, so all three are empty — and that is a fully valid bundle.
Step 3 — Theme (ADR-036)
Themes are contributed as a fully-materialized Palette (light + dark mode token
sets, optional high-contrast, plus branding). There are two authoring paths;
neither requires a runtime toml dependency, and neither touches craig-web.
- Named-palette reuse (the Georgia approach)
-
Call a pre-built palette function from
craig-state-bundleand override only the branding via struct-update syntax. The token values were already baked intocraig-state-bundleat build time (itsbuild.rsparsestheme/simple-statehouse.tomlinto generated Rust). Your crate ships notheme.tomland nobuild.rs.fn georgia_theme() -> ThemeContribution { ThemeContribution { palette: Some(Palette { branding: ThemeBranding { agency_name: "Georgia DHS", logo_path: None, }, ..simple_statehouse_palette() }), } } - Custom palette
-
Ship
crates/craig-state-<name>/theme/theme.tomlwith[branding],[color.light], and[color.dark]sections (schema in Token Schema), plus abuild.rsthat parses it withtoml/serdebuild-dependencies only and generates&'static strtoken pairs. The generated Rust istoml-free at runtime (ADR-036 §2 no-toml-leak invariant).
Minimal bundles skip this step entirely: ThemeContribution::default() carries a
None palette and inherits the product default.
|
|
Step 4 — Terminology (ADR-034)
Terminology overrides are flat Fluent (.ftl) catalogs, embedded into the binary
via include_str! (no runtime parser dependency). Overridable vocabulary uses
the term- message-name prefix and must be *flat messages — never Fluent
-terms.
Ship a bilingual pair under crates/craig-state-<name>/terminology/:
term-admin-unit = County
term-role-caseworker = Social Services Case Manager
Embed both locales into the contribution:
fn georgia_terminology() -> TerminologyContribution {
TerminologyContribution {
catalogs: vec![
TerminologyCatalog {
lang: "en",
source: include_str!("../terminology/en/worker.ftl"),
},
TerminologyCatalog {
lang: "es",
source: include_str!("../terminology/es/worker.ftl"),
},
],
}
}
|
A non-empty terminology contribution must cover both |
Step 5 — Seed data (SeedContribution / RateTableSpec)
Seed data is the jurisdiction’s rate schedule. SeedContribution currently
carries one field, rate_tables: Vec<RateTableSpec>, where each RateTableSpec
is:
pub struct RateTableSpec {
pub payment_type: PaymentType, // payment program this rate applies to
pub age_min: i32, // inclusive lower bound (years)
pub age_max: i32, // inclusive upper bound (years)
pub daily_rate: &'static str, // fixed-point decimal, e.g. "15.50"
}
The Georgia bundle contributes five rows (three foster-care age bands plus
adoption- and guardianship-assistance bands). A stub contributes
SeedContribution::default() — an empty rate-table set, declaring no policy.
At seed time, craig-seed resolves the active bundle from
CRAIG__ACTIVE_STATE_BUNDLES (the same selector the services use) and reads both
jurisdiction_code() and seed_data.rate_tables from it. There is no
--jurisdiction flag — the active bundle is the single source of truth for which
jurisdiction is seeded.
Step 6 — Register in the workspace and wire the consumers
First, register the crate in the root Cargo.toml:
[workspace]
members = [
# ...
"crates/craig-state-tx-stub",
]
[workspace.dependencies]
craig-state-tx-stub = { path = "crates/craig-state-tx-stub" }
Then add a state-<name> feature and an optional dependency to each consuming
crate, and register the bundle in that crate’s #[cfg]-gated candidate list. The
consumers form the feature-matrix contract (Step 7):
-
services/craig-exchange/— partner/adapter registry boot -
services/craig-reporting/— federal-mapping registry boot -
services/craig-web/— theme + terminology materialization -
services/craig-intake/— edge theming (ADR-036 §4 amendment, #711) -
services/craig-financial/— subsidy reference boot (policy + UAS vocabulary, #1072) -
tools/craig-seed/— rate-table + jurisdiction-code seeding -
tools/craig-mock-server/— mock partner routes
Per-consumer Cargo.toml pattern:
[features]
default = ["state-ga"]
state-ga = ["dep:craig-state-ga"]
state-tx-stub = ["dep:craig-state-tx-stub"] # NEW
[dependencies]
craig-state-tx-stub = { workspace = true, optional = true } # NEW
Each consumer holds a thin candidate_bundles() wrapper whose body is the
shared craig_state_bundle::candidate_bundles! macro (1072) — the [cfg]
blocks expand against the CONSUMER’s features, so the contract crate never
depends on the concrete bundle crates. Registering a new bundle means adding
its #[cfg] block to the macro (one place), not to every consumer:
fn candidate_bundles() -> Vec<Box<dyn StateBundle>> {
craig_state_bundle::candidate_bundles!()
}
Each consumer also carries a compile guard so a build with no jurisdiction fails loudly:
#[cfg(not(any(feature = "state-ga", feature = "state-tx-stub")))]
compile_error!("no state bundle selected: enable a `state-<name>` Cargo feature");
Keeping default = ["state-ga"] means the default build stays Georgia and
byte-identical to before your change.
Step 7 — Join the feature matrix
The seven consumers above form the feature-matrix contract: each one must
declare every state-* feature and compile cleanly in isolation. The contract is
codified in xtask/src/cmd/feature_matrix.rs:
const FEATURE_MATRIX_CRATES: &[&str] = &[
"craig-exchange",
"craig-reporting",
"craig-mock-server",
"craig-web",
"craig-seed",
"craig-intake",
"craig-financial",
];
A fingerprint test
(matrix_crate_list_matches_crates_declaring_state_tx_stub) discovers every
workspace member that declares the state-tx-stub feature and asserts the set
equals FEATURE_MATRIX_CRATES. If you add a new bundle and forget to wire a
consumer — or wire one that is not in the list — this test fails, guarding
against silent drift.
Run the matrix locally before considering the bundle live:
cargo xtask feature-matrix
This runs cargo check -p <crate> --no-default-features --features <feature> for
each consumer × each state-* feature, plus a default-all cargo check
--workspace. The same job runs in CI.
Step 8 — Activate the jurisdiction at deploy time
A deployment selects its single jurisdiction with the
CRAIG__ACTIVE_STATE_BUNDLES environment variable (deployment-global; no
per-service prefix). The value is a single bundle name():
CRAIG__ACTIVE_STATE_BUNDLES=georgia
In the devstack docker-compose.yml, a single YAML anchor reads the host
environment (defaulting to georgia) and is injected into every service — no
per-service override, enforcing one-deployment-one-jurisdiction at the compose
layer:
x-active-state-bundles: &active_state_bundles ${CRAIG__ACTIVE_STATE_BUNDLES:-georgia}
# ... then per service:
# CRAIG__ACTIVE_STATE_BUNDLES: *active_state_bundles
At boot, each service calls resolve_active_bundle(), which fails fast:
-
Missing (
ActivationError::Missing) — the var is unset, empty, or whitespace. -
Unknown (
ActivationError::Unknown) — names a bundle not compiled in via astate-*feature; the error lists the available names. -
Multiple (
ActivationError::Multiple) — names more than one bundle, violating one-deployment-one-jurisdiction.
There is no fallback or default at the resolver — production services halt boot on
any of these. So a new jurisdiction is only reachable at runtime once its feature
is enabled in the build and its name() is the activation value. To activate
the worked example, build with --features state-tx-stub and set
CRAIG__ACTIVE_STATE_BUNDLES=tx-stub.
Verification
Run these to prove a new bundle builds state-neutral and boots:
# 1. The bundle crate builds on its own.
cargo check -p craig-state-tx-stub
# 2. Each consumer compiles against the NEW bundle in isolation
# (state-neutrality: no Georgia content required to build).
cargo check -p craig-exchange --no-default-features --features state-tx-stub
cargo check -p craig-reporting --no-default-features --features state-tx-stub
cargo check -p craig-web --no-default-features --features state-tx-stub
cargo check -p craig-seed --no-default-features --features state-tx-stub
cargo check -p craig-mock-server --no-default-features --features state-tx-stub
cargo check -p craig-intake --no-default-features --features state-tx-stub
cargo check -p craig-financial --no-default-features --features state-tx-stub
# 3. The default (Georgia) build is unchanged.
cargo check --workspace
# 4. The full feature-matrix contract + fingerprint test.
cargo xtask feature-matrix
cargo nextest run -p xtask matrix_crate_list_matches_crates_declaring_state_tx_stub
# 5. Bundle-level invariants (e.g. bilingual terminology coverage).
cargo nextest run -p craig-state-tx-stub
# 6. Boot fail-fast: an unknown bundle halts startup with a listed-names error.
CRAIG__ACTIVE_STATE_BUNDLES=ohio cargo run -p craig-exchange # expect ActivationError::Unknown
# 7. The compile guard fires when NO jurisdiction is selected.
cargo check -p craig-exchange --no-default-features # expect compile_error!("no state bundle selected")
Confirm the registration touched every required surface:
# The new crate is a workspace member + workspace dependency.
grep -n "craig-state-tx-stub" Cargo.toml
# All seven consumers declare the feature.
grep -rn "state-tx-stub" services/ tools/
# The feature-matrix crate list includes every consumer (no drift).
grep -n "FEATURE_MATRIX_CRATES" -A8 xtask/src/cmd/feature_matrix.rs
Related decisions
-
ADR-032: Multi-Jurisdiction Partner Registry and Transport Abstraction — the
StateBundletrait, partner-type taxonomy, and one-deployment-one-jurisdiction rule (§2.7). -
ADR-034: Per-Jurisdiction Terminology Resolution via Bundle-Overlaid Fluent Catalogs — the terminology overlay axis (Step 4).
-
ADR-036: Token + Theme Export via Bundle-Provided CSS Custom Properties — the theme axis and
GET /assets/theme.cssrendering (Step 3). -
Token Schema — the canonical light/dark token set a custom palette must satisfy.
-
The Five Engineering Contracts — the design framing of the five contribution axes.
-
Plan S — Multi-Jurisdiction Foundation — the umbrella plan sequencing ADR-032 + ADRs 033-037 and Plans T/U/V/W/X/Y.
-
Plan U — State Bundle Pattern — the plan that shipped the state-neutral bundles, theme, and terminology described here.