Multi-Jurisdiction Extensibility

On this page
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 optional craig-state-<name> dependency and registers it in a #[cfg]-gated candidate list. A build with no state feature fails with a compile_error!; the default feature set keeps the Georgia build byte-identical.

  • Boot time — every service reads CRAIG__ACTIVE_STATE_BUNDLES once and calls resolve_active_bundle(), which fails fast if the value is missing, names an unknown bundle, or names more than one. The single matched bundle’s contribute() output is merged into the per-axis registries, and its theme
    terminology are materialized by craig-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 adapters, audit_codecs, mock_routes, and partner_types fields), (2) theme, (3) terminology, (4) seed_data, and (5) federal_mapping. The partner-type taxonomy (partner_types field) is the one required field; adapters, audit_codecs, and mock_routes are optional wiring for typed integrations. For the design rationale behind that surface, see The Five Engineering Contracts.

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.

crates/craig-state-tx-stub/Cargo.toml
# 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:

crates/craig-state-tx-stub/src/lib.rs (the trait impl)
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,
        }]
    }
}

name() and jurisdiction_code() are distinct. name() is the activation token (what an operator types into CRAIG__ACTIVE_STATE_BUNDLES); it must be unique across every bundle compiled into a binary. jurisdiction_code() is the canonical jurisdiction identifier used downstream (e.g. by craig-seed to stamp seeded rows). They can differ — the stub uses "tx-stub" for the former and "texas" for the latter.

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?

adapters

vec![]

Yes — a jurisdiction may register zero adapters

audit_codecs

vec![]

Yes

mock_routes

vec![]

Yes

partner_types

map PRODUCT_PARTNER_TYPESPartnerTypeMeta { token }

No — must contribute the shared taxonomy

seed_data

SeedContribution::default()

Yes — empty rate-table set

theme

ThemeContribution::default()

Yes — None palette inherits product default

terminology

TerminologyContribution::default()

Yes — empty = no overlay

compositions

CompositionContribution::default()

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-bundle and override only the branding via struct-update syntax. The token values were already baked into craig-state-bundle at build time (its build.rs parses theme/simple-statehouse.toml into generated Rust). Your crate ships no theme.toml and no build.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.toml with [branding], [color.light], and [color.dark] sections (schema in Token Schema), plus a build.rs that parses it with toml/serde build-dependencies only and generates &'static str token pairs. The generated Rust is toml-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.

craig-web (the BFF) owns rendering — the bundle author never writes a route. At boot, craig-web takes the active bundle’s ThemeContribution, validates structural completeness (all required tokens present; light/dark declare the same roles), and materializes a CSS string served from GET /assets/theme.css. A structurally-incomplete palette fails boot.

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/:

crates/craig-state-ga/terminology/en/worker.ftl (excerpt)
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 en and at least one es catalog — craig-web’s `validate_bilingual_coverage fails boot otherwise. An empty default (TerminologyContribution::default()) is exempt: it simply inherits the product-default catalogs with no overlay, which is what the stub does. At boot, craig-web’s `I18n::load applies the overlay with add_resource_overriding (jurisdiction wins over product default) and pre-resolves every key into an immutable table — the jurisdiction dimension is baked at boot, not consulted per-request.

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 a state-* 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
Edit this page · latest