Plan: Stateless Intake — craig-cases Owns Report Lifecycle, craig-security Owns Partner Identity
On this page
- Status
- Context
- Scope
- Design
- Steps
- Step 1 — Plan + ADR-Accept + issue hygiene
- Step 2 — craig-security partner identity
- Step 3 — craig-cases reports + screening-decisions schema
- Step 4 — craig-cases report/screening/convert endpoints
- Step 5 — Data migration (REMOVED)
- Step 6 — craig-intake refactor
- Step 7 — craig-web partner-admin pages
- Step 8 — craig-web caseworker screening/review/convert + chain view
- Step 9 — SDK compatibility sweep
- Step 10 — Event rename (one-shot)
- Step 12 — Plan completion audit + archive
- Critical Files
- Verification (whole plan)
- Documentation Updates
|
Plan adopted 2026-04-22 from ADR-017. Supersedes |
Status
Status column reflects plan adoption at 2026-04-22.
| Step | Description | Status |
|---|---|---|
1 |
Plan file + ADR-017 status flip + GitLab issue hygiene |
Done (2026-04-23) — MR !137 — plan and ADR drafted, ADR-017 status flipped to Accepted, parent issue + per-step issue tree created. |
2 |
craig-security: partner identity schema + admin CRUD + |
Done (pre-ADR-030) — migration |
3 |
craig-cases: reports + screening_decisions + disposition_follow_ups schema + migrations |
Done (pre-ADR-030) — migration |
4 |
craig-cases: report/screening/convert endpoints (write path + internal transitions) |
Done (pre-ADR-030) — 8 new endpoints under |
5 |
Data migration: intake reports → cases; intake partner keys → craig-security |
Done (2026-04-25) — Removed — pre-v0.1 system, no production data exists. The |
6 |
craig-intake refactor: drop DB, add cases-forwarding sink, partner auth via craig-security, read-through proxy |
Done (pre-ADR-030) — intake retired its DB layer (deleted |
7 |
craig-web: partner-admin pages peer to user-admin |
Done (2026-04-25) — MR !147 — |
8 |
craig-web: re-point caseworker screening/review/convert UI at cases; add unified chain view |
Done (2026-04-25) — MR !148 — |
9 |
SDK compatibility sweep (Python + TypeScript) |
Done (2026-04-25) — MR !149 — fixed |
10 |
Event rename: |
Done (pre-ADR-030) — Step 4 already published |
12 |
Plan completion audit + archive + close CQR April Step 5b |
Done (pre-ADR-030) — audit subagent reconciled Steps 1–10 against actual codebase. Discrepancies fixed: Step 1 row updated; stale references cleaned in |
Step 11 (originally "remove deprecated intake.report_converted after consumer cutover") was collapsed into Step 10 — pre-v0.1 deployment, no live consumers, so the dual-publish window was never needed. Numbering preserved for stable cross-references in MR descriptions and the CHANGELOG.
|
Issues: #197 (CQR April — Step 5b; superseded by this plan on completion of Step 12)
Branch prefix: feat/stateless-intake-*
Context
See ADR-017 for the architectural rationale and the seven open questions raised at decision time. This plan pins concrete answers to those questions so implementation can proceed without re-litigating the direction:
| Open question (from ADR) | Resolution for this plan |
|---|---|
Local-outbox pattern for air-gapped partners |
Deferred. Air-gapped standalone retains the config-file |
I&R modeling (sibling disposition vs distinct entity) |
Sibling disposition. I&R reports live in the same |
Migration strategy (one-shot vs dual-write) |
One-shot with dry-run. Idempotent script; can re-run safely. Production deployment is small enough that downtime for the migration is negligible (measured in seconds). Dual-write adds complexity without buying anything. |
Read-through proxy caching on intake |
No cache initially. Stateless proxy for |
Decision-chain schema shape |
Pinned below (Step 3 design). |
Deprecation window for |
Resolved at implementation time. Pre-v0.1, no live consumers — Step 4 published |
Rate-limit scope (per-instance vs global) |
Per-instance preserved. Partner rate limits continue to apply per intake process, same as today. A shared-store-backed global limiter is a separate feature if needed. |
Scope
In scope:
-
Relocating
intake.reportsschema + all associated state (screening, disposition, conversion, referral back-pointer) tocraig-cases -
Introducing neutral multi-party decision-chain schema in cases (
screening_decisions,disposition_follow_ups) with jurisdictional override support via craig-rules -
Relocating partner identity (
partner_api_keys, optional JWS pubkeys) and per-partner rate limits tocraig-security -
Net-new partner admin UI in craig-web (CRUD partners, issue/revoke keys, attach/rotate JWS pubkey)
-
Stripping
craig-intaketo a domain-stateless edge service: schema validation, CAPTCHA, rate limiting, partner auth via craig-security, optional JWS verification, forward-to-cases sink,GET /reports/{id}read-through proxy to cases -
Re-pointing craig-web’s caseworker screening/review/convert UI at cases endpoints
-
Cross-family/chain unified view in craig-web (single screen showing a family’s intakes + referrals + investigations + cases)
-
Event rename:
intake.report_converted→case.report_converted(one-shot — pre-v0.1, no live consumers) -
One-shot idempotent data migration from intake → cases + craig-security
Out of scope (tracked as follow-ups if adopted):
-
Local-outbox pattern for flaky-link or air-gapped partners with submission buffering (ADR-009 precedent; separate plan if adopted)
-
Global (cross-instance) rate limiting — per-instance is preserved
-
Read-through proxy caching on intake
-
Retiring craig-intake as a service and merging into craig-cases (OpenStack nova-api-within-Nova pattern) — rejected in the ADR; remains rejected for this plan
-
Advanced chain-visualization UI (graph of related families, multi-generation case history) — MVP is tabular chain view; visualization is a future enhancement
Design
Reports + screening-decisions schema (cases-side)
-- Reports: the submission itself. Immutable body after creation.
CREATE TABLE reports (
id UUID PRIMARY KEY DEFAULT uuidv7(),
partner_id UUID NOT NULL REFERENCES security.partners (id),
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
received_ip_hash TEXT, -- hashed; forensic value, not plaintext
received_request_id UUID NOT NULL, -- idempotency key surfaced from intake edge
reporter_type TEXT NOT NULL,
reporter_name TEXT,
reporter_phone TEXT,
reporter_relation TEXT,
admin_unit TEXT NOT NULL,
narrative TEXT NOT NULL,
children JSONB NOT NULL, -- typed per craig-intake-sdk::types::Child
adults JSONB NOT NULL, -- typed per craig-intake-sdk::types::Adult
raw_submission JSONB NOT NULL, -- verbatim partner payload for audit/replay
-- Indices on (partner_id, received_at), (admin_unit, received_at),
-- received_request_id UNIQUE for idempotency.
UNIQUE (received_request_id)
);
-- Screening decisions: the chain. Supports multi-party overrides.
-- The "current effective disposition" is the latest row where supersedes_id
-- is NULL OR where no later row supersedes it.
CREATE TABLE screening_decisions (
id UUID PRIMARY KEY DEFAULT uuidv7(),
report_id UUID NOT NULL REFERENCES reports (id) ON DELETE RESTRICT,
actor_role TEXT NOT NULL, -- 'cicc', 'county_supervisor', 'caseworker', etc.
-- values sourced from jurisdiction ruleset.
actor_sub UUID NOT NULL, -- keycloak sub of the deciding user
disposition_kind TEXT NOT NULL, -- 'screened_in', 'screened_out',
-- 'screened_out_and_referred', 'i_and_r'.
-- values sourced from jurisdiction ruleset.
rationale TEXT,
decided_at TIMESTAMPTZ NOT NULL DEFAULT now(),
supersedes_id UUID REFERENCES screening_decisions (id),
CHECK (supersedes_id IS NULL OR supersedes_id <> id)
);
CREATE INDEX idx_screening_decisions_report_decided
ON screening_decisions (report_id, decided_at DESC);
-- Follow-up actions for dispositions that involve caseworker work despite
-- being screened-out at the disposition level (screened-out-and-referred, I&R).
CREATE TABLE disposition_follow_ups (
id UUID PRIMARY KEY DEFAULT uuidv7(),
decision_id UUID NOT NULL REFERENCES screening_decisions (id) ON DELETE RESTRICT,
action_kind TEXT NOT NULL, -- 'external_provider_referral',
-- 'le_assistance', 'info_packet_sent', etc.
target TEXT, -- provider name / LE agency / etc.
notes TEXT,
performed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
performed_by UUID NOT NULL -- keycloak sub of the caseworker
);
CREATE INDEX idx_disposition_follow_ups_decision
ON disposition_follow_ups (decision_id);
-- Referrals (existing): gains FK to the authorizing screening decision.
ALTER TABLE referrals
ADD COLUMN intake_report_id UUID UNIQUE REFERENCES reports (id),
ADD COLUMN authorizing_decision_id UUID REFERENCES screening_decisions (id);
Effective-disposition view: a SQL view returns the current effective disposition per report by resolving the supersedes chain. Used by UI rendering + NCANDS export + convert-eligibility checks. No stored "current" column — always computed from the chain to prevent drift.
Jurisdiction-configurable enums: actor_role and disposition_kind are TEXT columns populated from the jurisdiction ruleset at runtime. craig-rules validates submitted values against the active jurisdiction’s allowed set. Schema does not enum-constrain (would bake Georgia’s vocabulary into the schema).
Partner identity schema (security-side)
CREATE TABLE partners (
id UUID PRIMARY KEY DEFAULT uuidv7(),
name TEXT NOT NULL,
kind TEXT NOT NULL, -- 'hospital', 'school', 'le', 'other'
status TEXT NOT NULL DEFAULT 'active', -- 'active', 'suspended'
contact_email TEXT,
contact_phone TEXT,
rate_limit_rpm INTEGER NOT NULL DEFAULT 60,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID NOT NULL
);
CREATE TABLE partner_api_keys (
id UUID PRIMARY KEY DEFAULT uuidv7(),
partner_id UUID NOT NULL REFERENCES partners (id) ON DELETE RESTRICT,
key_hash TEXT NOT NULL UNIQUE, -- bcrypt or argon2; see craig-crypto
label TEXT, -- operator-facing description
jws_public_jwk JSONB, -- optional; ADR-010
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID NOT NULL,
revoked_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ
);
CREATE INDEX idx_partner_api_keys_hash ON partner_api_keys (key_hash)
WHERE revoked_at IS NULL;
POST /v1/security/partners/verify contract
Request (from intake, service-to-service signed):
{
"credential_kind": "api_key" | "jws_detached",
"credential": "..." // raw key, or the JWS header+signature for lookup of the key id
}
Response (200):
{
"partner_id": "018f1234-...",
"partner_status": "active",
"rate_limit_rpm": 60,
"jws_public_jwk": { ... } // present if credential_kind == 'jws_detached' or if the key has an attached JWK
}
Response (401): ApiError::Unauthorized per RFC 9457.
Latency budget: sub-millisecond p50, <5 ms p99 on warm pool.
Intake caches the { partner_id, rate_limit_rpm, jws_public_jwk } triple keyed on the credential hash for 60 s with an in-process cache (no shared cache). Cache miss goes to craig-security; cache hit is a single hashmap lookup.
craig-intake trimmed binary
After Step 6, craig-intake contains:
-
HTTP handler for
POST /reports(public) andPOST /api/internal/reports(integrated-auth) — both forward to cases after validation -
HTTP handler for
GET /reports/{id}— read-through proxy to cases -
Schema validation (craig-intake-sdk::types compatible)
-
CAPTCHA verification for public endpoint
-
Rate limiting (governor, per-partner)
-
Idempotency middleware (craig-api shared, unchanged)
-
Partner auth client for craig-security (with 60 s TTL cache)
-
Optional JWS verification (pubkey from craig-security response)
Removed:
-
services/craig-intake/src/store/(entire module — reports, api_keys, hashing) -
services/craig-intake/src/sink/database.rs -
services/craig-intake/src/sink/forwarding.rs(replaced by the cases-forwarder) -
services/craig-intake/src/sink/mapping.rs(body mapping moves to cases where it’s persisted) -
services/craig-intake/src/transitions.rs(state machine — moved to cases, expressed via craig-rules) -
services/craig-intake/src/api/internal.rs::convert_report(handler replaced by a BFF route that calls cases directly) -
Intake’s DB migrations (the
reportsandapi_keystables are dropped post-migration)
Expected LOC reduction in intake: ~2,500 lines → ~800 lines.
craig-web routing changes
| Old route | New destination |
|---|---|
| /intake/reports (caseworker dashboard) | cases /v1/cases/reports |
| /intake/reports/{id}/screen | cases /v1/cases/reports/{id}/screening-decisions (POST) |
| /intake/reports/{id}/convert | cases /v1/cases/reports/{id}/convert (internal cases transition) |
| /intake/partners/ (admin — did not exist; partner management was config-only) | craig-security /v1/security/partners/ |
| /intake/submit (public-facing report submission) | unchanged — still hits intake; intake forwards |
| /intake/reports/{id}/status (partner status check) | unchanged — intake read-through proxy to cases |
Steps
Each step is one logical unit of work shippable as its own MR. Where mentioned, step N depends on step N-1 unless noted.
Step 1 — Plan + ADR-Accept + issue hygiene
Files: this plan file, docs/modules/ROOT/pages/adrs/adr-017-stateless-intake.adoc (Status: Accepted), docs/modules/ROOT/nav.adoc (Active entry).
-
Flip ADR-017 Status to Accepted (done in this commit).
-
Add this plan to
nav.adocunder** Active. -
Create a parent GitLab epic for the stateless-intake rollout.
-
Create one GitLab issue per Step 2–12. Each:
-
Prefix:
feat:for 2–9,chore:for 10–12 -
Links back to this plan
-
Labels:
P2-medium, pluscraig-security/craig-cases/craig-intake/craig-web/sdkas relevant -
Milestone:
2026 Q2 — Close Active Plans
-
Verification: cargo xtask check-docs passes. glab issue list --milestone "2026 Q2 — Close Active Plans" shows the 11 new issues + existing ones.
Step 2 — craig-security partner identity
Files: services/craig-security/migrations/XXX_partner_identity.sql, services/craig-security/src/store/partners.rs (new), services/craig-security/src/api/partners.rs (new), services/craig-security/src/events.rs.
-
Migration creates
partners+partner_api_keystables per the Design section. -
store::partners::{create, get, list, update, delete}CRUD. -
store::partner_api_keys::{issue, revoke, rotate_jwk, lookup_by_hash, touch_last_used}. -
API endpoints:
-
POST /v1/security/partners(admin-only) -
GET /v1/security/partners(paginated) -
GET /v1/security/partners/{id} -
PUT /v1/security/partners/{id} -
DELETE /v1/security/partners/{id}(soft-delete, setsstatus='suspended') -
POST /v1/security/partners/{id}/keys— returns plaintext ONCE -
DELETE /v1/security/partners/{id}/keys/{key_id}(revoke) -
PUT /v1/security/partners/{id}/keys/{key_id}/jwk(attach/rotate JWS pubkey per ADR-010) -
POST /v1/security/partners/verify— the hot path; intake calls this per request
-
-
Publish
security.partner_created,security.partner_key_issued,security.partner_key_revokedevents for the audit subscriber. -
Integration tests cover: create partner, issue key, verify returns correct metadata, revoke key rejects subsequent verify, JWK attach/rotate round-trip.
Verification: 15+ new integration tests. cargo nextest run -p craig-security passes. No changes to existing craig-security surface; backward compatible.
Step 3 — craig-cases reports + screening-decisions schema
Files: services/craig-cases/migrations/XXX_reports_and_screening.sql, services/craig-cases/src/store/reports.rs (new), services/craig-cases/src/store/screening_decisions.rs (new), services/craig-cases/src/store/disposition_follow_ups.rs (new), migration adding new columns to existing referrals table.
-
Migration creates
reports,screening_decisions,disposition_follow_upstables per the Design section. Addsintake_report_id+authorizing_decision_idcolumns toreferrals. -
Create the
v_effective_dispositionSQL view that resolves the supersedes chain per report. -
Model types in
store/models.rs:Report,ScreeningDecision,DispositionFollowUp,EffectiveDisposition. -
CRUD helpers (no endpoints yet — Step 4 adds those).
-
Jurisdiction-ruleset lookups for valid
actor_roleanddisposition_kindvalues: craig-cases borrows from craig-rules via the existing shared-client pattern. -
Unit tests for the CRUD helpers + the effective-disposition view.
Verification: cargo nextest run -p craig-cases --lib adds ~15 tests. Migration applies cleanly on a fresh DB and on the current seed fixture.
Step 4 — craig-cases report/screening/convert endpoints
Files: services/craig-cases/src/api/reports.rs (new), services/craig-cases/src/api/screening.rs (new), services/craig-cases/src/api/convert.rs (new — replaces intake’s convert handler), router updates.
Public (service-to-service, authenticated-intake-only):
-
POST /v1/cases/reports— submit a report. Validates partner context in the signed service header. Returns 201 + report row.
Internal (caseworker-authenticated):
-
GET /v1/cases/reports— list (paginated, filtered: by partner, admin_unit, disposition, date range) -
GET /v1/cases/reports/{id}— single report + effective disposition + follow-up actions -
GET /v1/cases/reports/{id}/decisions— full decision chain -
POST /v1/cases/reports/{id}/screening-decisions— record a new screening decision. Validatesactor_roleauthority via craig-rules (e.g., county can override CICC per Georgia ruleset; a caseworker can’t override a supervisor). Publishescase.report_disposition_recorded. -
POST /v1/cases/reports/{id}/follow-ups— attach a disposition follow-up action -
POST /v1/cases/reports/{id}/convert— atomic internal transition: requires effective disposition =screened_in; creates a referral; updates the effective view; publishescase.report_converted+case.referral_created(existing). Single DB transaction.
Verification: cargo nextest run -p craig-cases adds ~25 integration tests covering: submission, listing, single-party screening, multi-party override (CICC → county), unauthorized override rejection, disposition follow-ups on screened-out-and-referred, convert from screened-in, convert rejection from other dispositions, idempotent convert.
Step 5 — Data migration (REMOVED)
The migration tool was delivered, then deleted in the 2026-04-25 compat-strip cleanup. CRAIG is pre-v0.1 with no live deployments, so a one-shot migration tool that lives in-tree forever to support a hypothetical future cutover is net-negative on maintenance. If a real cutover is ever needed, write a fresh tool against the schema as it stands at that moment.
Step 6 — craig-intake refactor
Files: services/craig-intake/src/sink/cases_forwarder.rs (new), services/craig-intake/src/api/partner_auth.rs (new — replaces the old api_key_lookup), removal of services/craig-intake/src/store/, services/craig-intake/src/sink/database.rs, services/craig-intake/src/sink/forwarding.rs, services/craig-intake/src/sink/mapping.rs, services/craig-intake/src/transitions.rs, services/craig-intake/src/api/internal.rs::convert_report. Config updates.
-
Introduce
CasesForwarderSink— takes the validated submission, posts to{cases_url}/v1/cases/reportswith a service-to-service signed header containing{ partner_id, received_request_id }. Returns cases' response verbatim. -
Introduce
PartnerAuthClient— wraps a reqwest call to craig-security’s/partners/verifywith a 60 s in-process cache. -
Replace
api_key_auth::ValidatedApiKeywith the new partner auth flow. -
POST /reports+POST /api/internal/reportsbecome: validate + authenticate + forward. Handlers drop to ~30 lines each. -
GET /reports/{id}becomes a read-through proxy: fetch from cases, return the response. -
Remove the old sink switchboard entirely.
ReportSinkenum collapses to one variant. Consider removing the enum entirely. -
Delete
transitions.rs— state transitions now validated in cases. -
Delete
convert_reporthandler — craig-web BFF calls cases directly after Step 8. -
Retire the
database_urlconfig field. Intake no longer needs a DB connection. -
Update `craig-intake’s Cargo dependencies: remove sqlx, migration crates. Keep craig-mq only if still publishing any events (probably yes — partner-level events like rate-limit-exceeded).
-
Integration tests updated: mock craig-security + craig-cases via wiremock; verify validation + forwarding paths.
Verification: cargo nextest run -p craig-intake passes. LOC drops from ~2,500 to ~800 (target). Binary size drops correspondingly. No database_url in craig-intake config.
Step 7 — craig-web partner-admin pages
Files: services/craig-web/src/routes/admin/partners.rs (new), services/craig-web/templates/admin/partners/*.html.jinja (new), nav updates.
-
New craig-web BFF routes under
/admin/partners:-
GET /admin/partners— list -
GET /admin/partners/new— create form -
POST /admin/partners— create (calls craig-security) -
GET /admin/partners/{id}— detail, including keys -
POST /admin/partners/{id}/keys— issue new key (shows plaintext ONCE on success) -
POST /admin/partners/{id}/keys/{key_id}/revoke— revoke -
POST /admin/partners/{id}/keys/{key_id}/jwk— attach/rotate JWS pubkey
-
-
Templates follow the existing user-admin visual pattern. Accessible nav entry under "Admin" section, peer to "Users" and "Admin Units".
-
RBAC: admin role only (same as user-admin).
-
E2E test: full CRUD flow (create partner, issue key, revoke key, attach JWK, suspend partner).
Verification: cargo xtask e2e — --grep "partner admin" — new specs pass. Existing specs unaffected.
Step 8 — craig-web caseworker screening/review/convert + chain view
Files: services/craig-web/src/routes/cases/reports.rs (new — replaces the intake-side reports routes), templates for reports list/detail/screening/convert, family-chain view route + template.
-
Re-point caseworker-facing reports UI from intake to cases endpoints.
-
Screening-decision form supports the multi-party override model: UI shows the decision chain; override button is conditional on the current user’s role + the active jurisdiction ruleset’s authority rules.
-
Disposition follow-up UI for screened-out-and-referred and I&R paths.
-
Family chain view — new page showing, for a given family (person/case), all intake reports, all referrals, all investigations, all cases on one screen, chronologically, with cross-links. Uses existing sort/search/pagination patterns.
-
Update CLAUDE.md Phase Status table reference to "craig-web: 25 pages → ~29 pages".
Verification: cargo xtask e2e — screening, convert, chain-view specs pass.
Step 9 — SDK compatibility sweep
Files: sdks/python/tests/.py, sdks/typescript/tests/.ts, sdks/*/README.md.
-
Verify Python + TypeScript SDKs see no API surface changes on submission (
POST /reportsresponse shape unchanged: 201 +{id, submitted_at, …}). -
Verify
GET /reports/{id}status-check continues to work (intake’s read-through proxy preserves the contract). -
Update SDK READMEs with a note that partner admin + key rotation are now done in craig-web’s admin UI, not via a config file (except for air-gapped mode).
-
Run full SDK test suites against an end-to-end deployed stack.
Verification: pytest sdks/python/tests + npm test --prefix sdks/typescript both pass unchanged.
Step 10 — Event rename (one-shot)
Files: services/craig-cases/src/events.rs, any consumers of intake.report_converted.
-
Rename
intake.report_converted→case.report_convertedin cases. No dual-publish — pre-v0.1, no live consumers to migrate. -
Update any in-tree subscribers (reporting, audit, etc.).
-
CHANGELOG entry under Unreleased.
Verification: grep workspace-wide for intake.report_converted — zero hits outside CHANGELOG historical entries.
Step 12 — Plan completion audit + archive
Files: this plan (status table → all Complete), docs/modules/ROOT/pages/plans/archive.adoc (new row), docs/modules/ROOT/nav.adoc (Active → Archive), docs/modules/ROOT/pages/plans/code-quality-review-2026-04.adoc (Step 5b → Superseded by stateless-intake plan).
-
Audit Steps 2–11 for residual findings (spawn a plan-completion-audit subagent per
delivery-protocol.md). -
Update the status table — all steps Complete.
-
Move this plan from Active → Archive in nav.
-
Add archive row under Architecture/Refactor section.
-
Close CQR April plan’s Step 5b with reference to this plan’s MRs.
-
Close the parent GitLab epic.
Verification: cargo xtask check-docs clean. Plan Completion Audit reports zero residual findings. glab issue list --milestone "2026 Q2 — Close Active Plans" --state opened shows no stateless-intake issues open.
Critical Files
| File | Purpose |
|---|---|
|
Step 2 — new partners + partner_api_keys tables |
|
Step 2 — admin CRUD + verify endpoint |
|
Step 3 — reports + screening_decisions + disposition_follow_ups |
|
Step 4 — report/screening/convert endpoints |
|
Step 6 — new HTTP forwarder sink |
|
Step 6 — craig-security partner auth client |
|
Step 7 — partner-admin BFF routes |
|
Step 8 — re-pointed caseworker UI |
|
Accepted status flipped in Step 1 |
|
Step 5b marked Superseded in Step 12 |
Verification (whole plan)
-
cargo nextest run --workspace --locked --profile integration— all tests pass including ~40 new integration tests across steps -
cargo xtask e2e— all existing + new specs pass -
cargo xtask dev reseed— seed data regenerates correctly with the new schema -
Full
cargo xtask validate+ pre-push gate green for every MR -
cargo xtask check-docsclean -
Manual QA: submit a report via the public form, screen it in (with a CICC → county override), attach a disposition follow-up, convert to referral, observe it in the family chain view
-
SDK test suites pass unchanged (proves no public contract drift)
-
Plan Completion Audit subagent reports zero residual findings
Documentation Updates
-
.claude/docs/services.md— update craig-intake stats (endpoint count drops, table count drops to 0), craig-cases stats (endpoint count rises, table count rises by 3), craig-security stats (+2 tables, +8 endpoints) -
.claude/docs/architecture.md— update service ownership diagram; add the chain-view lineage as a capability -
.claude/docs/local-dev.md— remove intake DB setup; note the new migration tool -
.claude/docs/services.md— update CLAUDE.md Phase Status table reference counts -
CHANGELOG.adoc— one entry per MR -
docs/modules/ROOT/pages/plans/archive.adoc— add stateless-intake row on completion (Step 12) -
docs/modules/ROOT/nav.adoc— move plan Active → Archive on completion (Step 12) -
docs/modules/ROOT/pages/plans/code-quality-review-2026-04.adoc— mark Step 5b Superseded -
sdks/python/README.md,sdks/typescript/README.md— partner admin moved to craig-web UI (Step 9)