Skip to Content
InternalDocsComplianceAdding Jurisdiction Packs

Adding Jurisdiction Packs

Source: docs/compliance/adding-jurisdiction-packs.md

# Adding New Jurisdiction Packs This guide walks through the process of adding a new jurisdiction pack to the Compliance Navigator. A jurisdiction pack consists of source data, an ingestion script, and the associated tests and UI integration. ## Pattern Overview Every jurisdiction pack follows the same structure: ``` data/sources/<pack-name>/ <pack-name>-obligations.json # Obligation snapshot (or eligibility criteria) <pack-name>-rules.json # Rule definitions (commodity-based packs only) <pack-name>-meta.json # Source URLs, version, SHA-256 checksums scripts/ ingest-<pack-name>.ts # Ingestion script packages/modules/trade/src/compliance/ <evaluator>.ts # Custom evaluator (entity-based packs only) ``` Before starting, decide which evaluation model your pack uses: | Model | When to Use | Examples | |-------|-------------|---------| | **Commodity-based** | Obligations depend on what is being imported (HS code, product category, origin) | APHIS CORE | | **Entity-based** | Obligations depend on who is importing (importer status, carrier registration, trader certifications) | CBSA CSA | Commodity-based packs use the shared `evaluateObligations()` function. Entity-based packs require a dedicated evaluator function (like `evaluateCsaEligibility()`). ## Step 1: Create Data Source Files Create a new directory under `data/sources/<pack-name>/`. ### Obligations JSON Snapshot For **commodity-based** packs, create `<pack-name>-obligations.json`: ```json { "version": "<pack-name>-v1.0", "source": "Human-readable source description", "obligations": [ { "obligationCode": "AUTHORITY-PROGRAM-NNN", "obligationName": "Human-readable name", "description": "What this obligation requires", "jurisdictionCode": "XX", "authorityCode": "AUTHORITY", "programCode": "PROGRAM", "requiredDocuments": ["DOC_TYPE_CODE"], "hsPrefixes": ["0603"], "productCategories": ["cut_flowers"], "originCountries": ["*"], "criteria": null, "sourceSection": "Source document, Chapter N, pp. XX-YY", "status": "ACTIVE", "effectiveFrom": "2024-01-01T00:00:00.000Z", "effectiveTo": null } ] } ``` For **entity-based** packs, create `<pack-name>-eligibility.json`: ```json { "program": { "code": "PROGRAM", "name": "Program Name", "jurisdiction": "XX", "authority": "AUTHORITY", "description": "Program description" }, "eligibilityCriteria": [ { "entityType": "IMPORTER", "obligationCode": "AUTHORITY-PROGRAM-ENTITY-CHECK", "name": "Human-readable name", "description": "What this check verifies", "requirements": [ { "field": "profileFieldName", "operator": "equals", "value": "EXPECTED" } ], "requiredDocuments": ["DOC_TYPE_CODE"], "sourceSection": "Source Guide, Chapter N" } ] } ``` Supported operators for entity-based requirements: `equals`, `after` (date comparison against `NOW`), `gte` (numeric greater-than-or-equal). ### Rules JSON Snapshot (Commodity-Based Only) Create `<pack-name>-rules.json`: ```json { "version": "<pack-name>-v1.0", "rules": [ { "ruleCode": "AUTHORITY_PROGRAM_NNN", "version": "YYYY.MM", "obligationCode": "AUTHORITY-PROGRAM-NNN", "module": "trade", "scopeType": "CORRIDOR", "scopeOrigin": ["*"], "scopeDestination": ["XX"], "priority": 90, "enabled": true, "level": "country" } ] } ``` Each rule must reference an existing `obligationCode` from the obligations file. Valid scope types: `UNIVERSAL`, `ORIGIN`, `DESTINATION`, `CORRIDOR`. Valid modules: `ship`, `trade`, `product`, `finance`. ### Metadata JSON Create `<pack-name>-meta.json`: ```json { "version": "<pack-name>-v1.0", "sourceLabel": "Human-readable source description", "sourceUrls": [ "https://official-source-url.gov/" ], "extractedAt": "2026-01-15T00:00:00Z", "obligationsSha256": "<sha256 of obligations JSON file>", "rulesSha256": "<sha256 of rules JSON file>" } ``` For entity-based packs, use `sha256` instead of separate obligation/rule checksums: ```json { "source": "AUTHORITY-PROGRAM", "version": "YYYY-QN", "description": "Pack description", "urls": ["https://official-source-url.gov/"], "extractedAt": "2026-01-15T00:00:00Z", "sha256": "<sha256 of eligibility JSON file>" } ``` Generate checksums with: ```bash shasum -a 256 data/sources/<pack-name>/<pack-name>-obligations.json shasum -a 256 data/sources/<pack-name>/<pack-name>-rules.json ``` ## Step 2: Write Ingestion Script Create `scripts/ingest-<pack-name>.ts`. Follow the patterns in the existing scripts. ### Required Behaviors 1. **Checksum validation**: Read the meta file, compute SHA-256 of each data file, and compare against the expected checksums. Fail immediately on mismatch. 2. **Structure validation**: Verify all required fields are present, check for duplicate obligation codes, validate authority/status/scope values. 3. **Transaction wrapping**: All database writes must happen inside `prisma.$transaction()`. Either all writes succeed or none do. 4. **Provenance records**: For each write, create corresponding entries: - `DataSource` (upsert by code) - `DataRelease` (upsert by sourceId + version) - `ComplianceObligation` (upsert by obligationCode) - `RuleDefinition` (find + update/create for nullable tenantId) - `DataChangeLog` (create for every obligation and rule upsert) 5. **Content hashing**: Compute SHA-256 of the normalized obligation payload and store in `contentHash`. Use `stableStringify()` (deterministic key ordering) to ensure consistent hashes. 6. **`--dry-run` flag**: When set, validate and print what would be written, but skip all database operations. ### Script Template ```typescript #!/usr/bin/env node import { createHash } from 'crypto' import { readFile } from 'fs/promises' import path from 'path' // ... type definitions ... function stableStringify(value: unknown): string { if (value === null || typeof value !== 'object') return JSON.stringify(value) if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]` const entries = Object.entries(value as Record<string, unknown>) .sort(([a], [b]) => a.localeCompare(b)) return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(',')}}` } function sha256Hex(input: string): string { return createHash('sha256').update(input, 'utf8').digest('hex') } async function main(): Promise<number> { const options = parseArgs(process.argv.slice(2)) // 1. Read source files // 2. Validate checksums // 3. Validate structure // 4. If --dry-run, print summary and exit // 5. Import prisma (lazy, so --dry-run doesn't need a DB) // 6. prisma.$transaction() with all upserts + changelog entries return 0 } main() .then((code) => process.exit(code)) .catch((error: unknown) => { console.error(`Ingest failed: ${error instanceof Error ? error.message : String(error)}`) process.exit(2) }) ``` ### Key Implementation Notes - Import `prisma` lazily (inside the non-dry-run branch) so that `--dry-run` works without a database connection. - The `changedBy` field on `DataChangeLog` should be `system:ingest-<pack-name>`. - For entity-based packs, set `hsPrefixes`, `productCategories`, and `originCountries` to empty arrays on the `ComplianceObligation` record, and store the entity criteria in the `criteria` JSON column. ## Step 3: Add Contract Tests ### Compliance Check Returns Correct Obligations Test that `POST /api/compliance/check` returns the expected obligations for known inputs: ```typescript it('returns APHIS APQ obligations for cut flowers from Colombia', async () => { const res = await request(app) .post('/api/compliance/check') .set('Authorization', `Bearer ${token}`) .send({ hsCode: '0603.11', originCountry: 'CO', destinationCountry: 'US', productCategory: 'cut_flowers', }) expect(res.status).toBe(200) expect(res.body.obligations).toEqual( expect.arrayContaining([ expect.objectContaining({ obligationCode: 'APHIS-APQ-CORE-301', }), ]) ) }) ``` ### Citation Metadata Present on All Results Every obligation in the response must include the citation block: ```typescript it('includes citation metadata on every obligation', async () => { const res = await request(app) .post('/api/compliance/check') .send({ hsCode: '0603', originCountry: 'CO' }) for (const obligation of res.body.obligations) { expect(obligation.citation).toEqual( expect.objectContaining({ source: expect.any(String), sourceSection: expect.any(String), releaseId: expect.any(String), contentHash: expect.any(String), }) ) } }) ``` ### Tenant Isolation (Entity-Based Packs) For entity-based packs like CSA, verify that one tenant's compliance profile does not leak into another tenant's evaluation: ```typescript it('evaluates CSA eligibility against the requesting tenant profile only', async () => { // Tenant A has a complete CSA profile // Tenant B has no CSA profile // Evaluate for Tenant B -> should get overallEligible: false }) ``` ### Ingestion Dry-Run Test that `--dry-run` exits cleanly without database writes: ```typescript it('dry-run validates without writing', async () => { const result = execSync( 'npx tsx scripts/ingest-<pack-name>.ts --dry-run', { encoding: 'utf8' } ) expect(result).toContain('Dry-run complete') }) ``` ## Step 4: Add to UI ### Jurisdiction Tab Add a new tab to the compliance navigator page in `apps/web/src/app/trade/compliance/`: 1. Add the jurisdiction to the tab list (e.g., "EU CCC" alongside "US APHIS" and "CA CSA"). 2. Wire the tab to filter obligations by `jurisdiction` query parameter. 3. Add authority filter options for the new jurisdiction's authorities. ### Obligation Display For commodity-based packs, the existing obligation table columns (HS prefixes, product categories, required documents) work as-is. For entity-based packs, the UI shows entity type grouping, requirement checks, and gap analysis instead of HS/category columns. ### Web Proxy Routes If adding new API endpoints beyond the existing four compliance routes, use `createProxyHandler`: ```typescript export const GET = createProxyHandler({ method: 'GET', path: '/api/compliance/<new-endpoint>', passQueryString: true, }) ``` ## New Pack Readiness Checklist Before merging a new jurisdiction pack: - [ ] **Source data files** created in `data/sources/<pack-name>/` with valid checksums - [ ] **Meta file** includes source URLs, version, extraction date, and SHA-256 checksums - [ ] **Ingestion script** validates checksums, uses `prisma.$transaction()`, supports `--dry-run` - [ ] **Ingestion dry-run passes**: `npx tsx scripts/ingest-<pack-name>.ts --dry-run` - [ ] **DataSource, DataRelease, ComplianceObligation, RuleDefinition** records upserted correctly - [ ] **DataChangeLog** entries created for every upsert - [ ] **Content hashes** computed with `stableStringify()` for deterministic ordering - [ ] **Contract tests** pass: correct obligations returned, citation metadata present, tenant isolation verified - [ ] **`POST /api/compliance/check`** returns new pack's obligations for matching inputs - [ ] **`GET /api/compliance/obligations?jurisdiction=XX`** lists new pack's obligations - [ ] **UI tab** added to compliance navigator page - [ ] **Authority filter** options updated for new jurisdiction - [ ] **All existing tests still pass**: `pnpm test` - [ ] **TypeScript compiles**: `npx tsc --noEmit` - [ ] **Linter passes**: `pnpm lint`