Plan: craig-intake edge public-form parity remediation (Phase 5 prerequisite, epic &60)

On this page

In one line: before Phase 5 deletes craig-web’s public /report form, make the edge’s NONE/cases public form capture craig-web’s full public field set (a 911 safety banner, the narrative/incident-story section, child is_victim, reporter address + relationships, demographics) — a form-only change; the wire contract and cases persistence already support every field.

Context — why this exists

Phase 5 (the approved Phase 5 plan) deletes craig-web’s public report form, on the premise that the edge is at parity (the L5 gate). A pre-deletion audit — run because the edge-gap rule demanded it — found that premise is false for the field set: Phase 3 scoped "parity" as i18n + a11y + wizard + attachments only (confirmed: L5 wording, ADR-042 §D10, the Phase-3 plan), so the edge’s NONE/cases public form is a reduced subset of craig-web’s. Deleting craig-web’s form now would regress a child-safety surface: public reporters would lose the "call 911 first" warning, the ability to flag a child victim, and the entire detailed-narrative section. The owner decision (2026-06-27): full parity remediation first — restore the field set on the edge, with e2e proof, before Phase 5 proceeds.

The key finding — this is form-only (no backend change)

Verified end-to-end against source: the data path already supports every field; only the edge form is short.

Layer Status Evidence

Wire contract

✅ carries all fields

SubmitReportRequest has reporter_street/city/state/zip/county (report.rs:49+), reporter_relationship_to_* (108-112), indian_heritage/safety_concerns (129-137), children/adults (145-152), narrative (157)

craig-web sends them

build_report_body (craig-web/src/routes/report.rs:254-315) populates all of them, incl. narrative: Some(…​) (307)

cases persists them

✅ unchanged

reports table stores children/adults JSONB (so is_victim+demographics) + raw_submission JSONB (narrative object, address, relationships) — exactly as craig-web’s submissions did

Edge form renders

❌ subset only

craig-web-public fields are SHINES-gated or entirely absent under NONE

Edge form payload

❌ gated

report-form.js:344 builds narrative + SHINES fields inside if (shines) — never sent under NONE

So the remediation is form-only (templates/report.html + static/js/report-form.js + a few i18n keys + edge e2e) — no craig-intake-contracts, craig-cases, or schema change. The craig-web-public fields are CORE-contract fields, not the SHINES ExtraFields of ADR-042 §D10 (primary_caretaker/foster_care/military/SSN/household-role — those stay SHINES-only; craig-web’s public form never had them). "Form-only" ≠ trivial: some fields are un-gated, others added.

Scope — the field-mapping table (drives implementation)

Authoritative field model = craig-web’s static/js/report-wizard.js (defaultAdult l.9-17, defaultChild l.18-23, the reporter/incident state l.30-45, the narrative object l.54-65). The remediation makes the edge NONE form’s field model match it field-for-field, minus the true SHINES-only ExtraFields (exclude list below). The implementer should diff report-wizard.js against the edge `report-form.js` rather than trust a hand-list — the table below is the at-a-glance map + R-step assignment, not the sole source.

Group Fields to (re)gain on the edge NONE form (per report-wizard.js) Edge status Action Step

Safety

the 911 banner (report-form-911-warning, key at public.ftl:19)

absent

add (render-only alert)

R1

Child / Narrative base

child is_victim; abuse_description (optional) + harm_description

SHINES-payload-gated (report-form.js:344)

widen

R1

Reporter

street/city/state/zip/county, relationship_to_child/_to_caregiver, is_self_reporting/is_primary_caregiver/is_household_member, maltreater_relationship

absent (only reporter_county exists, SHINES)

add

R2

Adult

phone, email (craig-web names — NOT the edge’s contact_number/alt_contact_info), is_primary_caregiver, gender, is_active_duty+military_branch (adult-level — craig-web PUBLIC, not the SHINES household trigger), maltreater_relationship_to_victim, county, dob_approximate, marital_status, language, race, ethnicity

mix: some SHINES-gated, several absent

widen/add

R2

Child

relationship_to_caregiver, dob_approximate, language, race, ethnicity; gender (see erratum: AFCARS male/female only)

SHINES-gated/reduced

widen/add

R2

Incident

indian_heritage(+details), safety_concerns (match craig-web input shapes), incident_datetime (see Behavioral parity)

SHINES-gated/downgraded

widen

R2

Narrative (deep)

educational_neglect+sub-fields, csec_trafficking+sub-fields, maltreatment_frequency, awareness_source, children_location

absent even under SHINES

add (+ NARRATIVE_KEYS)

R3

i18n: keys verified present in crates/craig-i18n/catalog/en/public.ftl (report-form-street/city/state/zip/county l.82-87, -relationship-to-child/-caregiver l.90-91, -child-is-victim l.253, the narrative + CSEC + educational set); add only a genuinely-missing flag label.

SHINES-only — DO NOT widen (these are the §D10 ExtraFields craig-web’s public form never had): primary_caretaker*, sec_caretaker*, first_hand_info, foster_care*, family_name, is_any_one_ill_comments, is_in_foster_care, is_previously_reported, the household-level is_in_military/is_in_military_parent, shines_household_role, per-person ssn, grade, is_other_member, rel_to_primary_caretaker. (Note the adult-level is_active_duty/military_branch above are craig-web-public and ARE in scope — distinct from the household trigger.)

Behavioral parity (NOT just fields — report-wizard.js is the spec)

  • abuse_description is OPTIONAL — craig-web enforces min-20 only when non-empty (report-wizard.js:149), and the in form.html is cosmetic. Match that: do *not make it required on the edge (that would be stricter than parity).

  • incident_datetime needs normalization — the contract is DateTime<Utc> (RFC3339); a raw datetime-local value won’t deserialize. craig-web normalizes before submit (craig-web/src/routes/report.rs:169). Mirror that in report-form.js (normalize the datetime-local string to RFC3339 / UTC before POST) — restoring the input is not enough.

  • Init + remove-guard — start with one adult + one child (report-wizard.js:48,51); addAdult/addChild guard < 10; removeAdult/removeChild keep ≥ 1 (>1, l.106-108). The edge today starts with [] and removes the last — bring it to craig-web’s behavior.

  • Adult conditional validationis_active_dutymilitary_branch required; is_alleged_maltreatermaltreater_relationship_to_victim required (report-wizard.js:132-137). NONE keeps only these (no SHINES required-when-relevant).

  • _clean — strip empty strings so optional fields deserialize as None (report-wizard.js:68-74); children/adults filter to entries with a first_name.

  • Review step (Step 6) — the edge review (report.html:452+) shows only reporter type/name/phone + incident + names
    additional_info. Extend it to display the restored fields (narrative, address, demographics) so they are reviewable before submit, matching craig-web’s review.

Status

Step Description Status

R0 plan + erratum MR

Commit this plan as .adoc (nav Active); append an erratum to the Phase 5 plan + consolidation L5 (parity was i18n/a11y only — the field set diverged; Phase 5 is gated on this remediation); file R1–R3 issues under epic &60 (not a code step)

Done (2026-06-27) — MR !833 (011a87f2); #742 closed; R1 #741 / R2 #739 / R3 #740 filed under epic &60

R1 safety + core narrative

911 banner; child is_victim under NONE; the core narrative (abuse_description optional min-20-if-filled + harm_description) under NONE; extend the review step for these; edge e2e for each

Done (2026-06-27) — 911 banner + is_victim (core child projection) + CORE_NARRATIVE_KEYS lifted out of the if (shines) payload gate; review step extended; 4 intake-ui e2e (POST-body asserted) + intake-ui-shines green

R2 reporter + people

Reporter address + relationships + caregiver/household/self flags; adult fields (phone/email names, is_active_duty+military_branch, is_primary_caregiver, maltreater_relationship_to_victim, demographics); child demographics + gender (AFCARS male/female — see erratum); dob_approximate; indian_heritage/safety_concerns; incident_datetime (normalize to RFC3339); start-with-one + remove-guard (≥1); extend the review step; edge e2e

Done (2026-06-27) — all fields added NONE-gated (SHINES rendering + wire byte-identical; projectPerson uses explicit NONE_*/SHINES_* key lists); incident_datetime normalized; start-with-one + remove-guard(≥1) + filter-by-first_name; 6 intake-ui e2e (POST-body asserted) + intake-ui-shines green. Adult-maltreater SHINES gap filed as a follow-up (out of scope)

R3 deep narrative

educational_neglect + CSEC sub-sections + maltreatment frequency + awareness source + children location (absent even from SHINES today); edge e2e

Done (2026-06-27) — educational-neglect + CSEC blocks added NONE-gated; maltreatment_frequency added; the general questions (maltreatment_last_occurred / awareness_source / maltreater_has_access / children_location / additional_comments) MOVED out of the SHINES-only block to render under both (CORE_NARRATIVE_KEYS); occur_again/child_danger stay SHINES-only; no new i18n keys (narrative is opaque JSONB — no 4xx risk); 3 intake-ui e2e (POST-body asserted) + intake-ui-shines green. Edge↔craig-web field parity now complete; Phase 5 unblocked.

Epic: &60 · Sequencing: R0 → R1 (safety) → R2 → R3 → (then Phase 5 P5.1/P5.2). R1 first so the safety-critical gaps close earliest. Each step is its own feature/{desc} branch + MR; full pre-push validate + a fresh J1–J8 pass. Phase 5’s strip stays blocked until R1–R3 land and the edge is a verified superset.

Design — the rules (per-field detail is in the table above)

SHINES-isolation (the R2 implementation decision, 2026-06-27). To keep the SHINES profile a frozen, separately- tested surface (the whole remediation exists because parity was mishandled once), R2 added the craig-web-public fields NONE-gated (x-show="$store.relevance.backend_profile !== 'shines'") rather than widening the SHINES gates in place. The existing SHINES per-person sub-blocks are untouched and kept before the new NONE blocks so the SHINES e2e’s positional .adult-entry select locators still resolve to shines_household_role. projectPerson switched to explicit per-profile key lists (NONE_*KEYS = craig-web’s set, dropping empties to match _clean; SHINES*_KEYS = the exact prior keys with the same omit-if-blank rule) so the SHINES wire payload is byte-identical and the NONE-only fields never leak to SHINES (and vice-versa). Shape-identical overlap fields (adult street/city/state/zip/dob/race/ marital_status/language) are rendered in both the NONE and SHINES blocks (per-person inputs carry no id, so no collision); the one id-bearing overlap (reporter_county) uses id="reporter_county_none" for the NONE copy. incident_datetime (NONE) and incident_date (SHINES) are split by profile (distinct ids). Result: zero SHINES rendering/wire/e2e change. R3’s deeper-narrative additions follow the same NONE-gated pattern.

Erratum (2026-06-27) — gender options

The plan’s "gender non_binary/unknown`" parity item rested on a false premise. The server’s `Gender enum (craig-reference) is AFCARS-bound male/female only; non_binary/other/unknown fail validate_gender (400). craig-web’s public form offers all five (a latent craig-web bug — those three 400 at the intake endpoint), and the edge’s pre-R2 child gender even carried a latently-400-ing other. R2 therefore offers only male/female for both adult and child gender (the server-valid set) and drops the stale other; every other R2 dropdown (relationship, maltreater, military, marital, language, race, ethnicity) was verified server-valid. The broader question — extend the Gender enum for non-binary support vs. trim craig-web’s options — is tracked in #744.

Rendering (templates/report.html). Apply the table’s Action column: widen = render the craig-web field under NONE (NONE-gated per the SHINES-isolation rule above) where the edge previously gated it to SHINES; add = new section
inputs mirroring craig-web’s form.html markup. Do not wire the SHINES required_when_relevant relevance rows into the NONE profile — NONE keeps craig-web’s lighter validation only (abuse_description optional, min-20-if-non-empty; the adult conditional rules — see Behavioral parity).

Payload (static/js/report-form.js) — the load-bearing detail. The submit builder gates narrative + the SHINES set inside if (shines) at l.344 (narrative assembled l.361-363). Rendering under NONE is not enough — a field isn’t sent unless captured outside that gate. So: lift the widen-row fields out of the if (shines) gate; for add rows, add to blankForm/blankChild/blankAdult + projectPerson + the payload (and educational_neglect/ csec_trafficking + sub-fields to NARRATIVE_KEYS). The edge already serializes the body to raw + forwards to cases unchanged, so captured fields flow through as before. Guard: only lift the table’s rows — never the §D10 ExtraFields.

i18n. Keys are verified present (the table’s i18n column); add only a genuinely-missing flag label. cargo nextest run -p craig-intake (Askama + the i18n render test) proves every |t key resolves; add to JS_I18N_KEYS (ui.rs) only if JS-looked-up. (Spanish #722 stays separate — en-only.)

E2E (tests/e2e/specs/intake-embedded-ui.spec.ts, the NONE intake-ui project). Assert each restored behavior is visible AND in the POST body under NONE (the POST-body assertion is what proves the payload gate was lifted) — sample per category:

  • 911 banner visible on the form.

  • narrative: when filled, POST body.narrative.abuse_description is present (it is optional — assert it’s sent when entered, and that an empty value does NOT block submit).

  • reporter: POST body.reporter_street non-empty (an add field).

  • child: POST body.children[0].is_victim === true (a widen field).

  • people: start-with-one (one adult + one child present on load), add up to 10, remove leaves ≥1; body.adults[0].phone uses the craig-web name; a datetime-local value round-trips as a valid incident_datetime.

  • SHINES regression: --project=intake-ui-shines still green (the shared report.html/report-form.js edits didn’t break the SHINES profile).

This is exactly the "port the gaps" work Phase 5’s P5.1 anticipated — doing it here satisfies it early.

No backend, contract, or schema changes (see The key finding).

Files touched

File Step

services/craig-intake/templates/report.html (render craig-web public fields under NONE; 911 banner; extend review step)

R1–R3

services/craig-intake/static/js/report-form.js (capture/send the fields under NONE; blankForm/blankChild/blankAdult/projectPerson/payload; init+remove-guard; datetime normalize)

R1–R3

crates/craig-i18n/catalog/en/public.ftl (+ services/craig-intake/src/ui.rs JS_I18N_KEYS if JS-looked-up) — verify/add keys

R1–R3

tests/e2e/specs/intake-embedded-ui.spec.ts (edge parity assertions — visible + in POST body)

R1–R3

services/craig-intake/static/css/intake-public.css (911 alert styling — token-only, no raw hex)

R1

plans/craig-intake-edge-public-form-parity-remediation.adoc (this plan) + nav.adoc (Active); committed Phase 5 plan + consolidation L5 (erratum); CHANGELOG.adoc

R0 + each

Untouched: craig-intake-contracts, craig-cases (+ schema), the SHINES ExtraFields architecture.

Risks & mitigations

Risk Mitigation

Fields render under NONE but the report-form.js:344 if (shines) payload gate still skips them → captured in the UI, never sent

The named fix: lift the craig-web-public subset out of that gate. E2E asserts the field is in the POST body under NONE, not just visible — proves the gate was lifted

Widening a SHINES gate accidentally shows a true SHINES-ExtraField (foster_care/SSN/household-role) under NONE

Re-add ONLY craig-web-public fields (the table rows); leave the §D10 ExtraFields SHINES-gated. Cross-check each field against craig-web’s report-wizard.js/form.html (the authoritative NONE field set)

A re-added field has no i18n key in the shared public.ftl → raw key renders

cargo nextest run -p craig-intake (Askama + the i18n render test) + verify each |t key resolves; add missing keys to public.ftl

Applying SHINES required-when-relevant rules to NONE → over-validation (e.g. making abuse_description required)

NONE keeps craig-web’s validation only — abuse_description optional (min-20-if-filled), + the adult conditional rules; do NOT wire SHINES required_when_relevant rows into NONE

Editing the shared report.html/report-form.js regresses the SHINES profile

Run --project=intake-ui-shines in verification (the existing SHINES e2e is the regression gate); widen gates additively, never remove SHINES branches

Adult phone/email widened from the edge’s contact_number/alt_contact_info → wrong field names in the POST body

Add phone/email as craig-web names (don’t rename/reuse the SHINES contact fields); e2e asserts body.adults[0].phone

incident_datetime (datetime-local) won’t deserialize as DateTime<Utc>

Normalize to RFC3339/UTC in report-form.js before POST (mirror craig-web routes/report.rs:169); e2e asserts a valid incident_datetime round-trips

Scope creep — "full parity" pulls in fields the project actually wanted dropped

The inventory + craig-web’s report-wizard.js is the bounded target; anything genuinely undesired is an explicit, recorded drop decision (not silent)

cases stores rich narrative/address only in raw_submission (not queryable columns)

Pre-existing (true for craig-web too) — out of scope; note as a possible separate enhancement, NOT a parity blocker

Delivery sequence

  1. R0 — plan + erratum MR: commit THIS remediation plan as a committed .adoc (plans/craig-intake-edge-public-form-parity-remediation.adoc, nav Active); annotate the committed Phase 5 plan
    L5 (parity was i18n/a11y; the field set diverged; Phase 5 gated on this remediation); file R1–R3 issues under epic &60 (type-prefixed, weighted, epic-linked).

  2. R1 (safety)R2R3 — one MR each (full validate + J1–J8; Closes #N + closing comment).

  3. Then resume Phase 5 (P5.1 strip → P5.2 final) on the now-true-superset edge; P5.1’s "port e2e gaps" is already satisfied by R1–R3’s assertions.

Verification (whole remediation)

cargo nextest run -p craig-intake          # Askama + i18n render test (every re-added |t key resolves)
cargo xtask dev reload && cargo xtask e2e --no-refresh -- --project=intake-ui --project=intake-ui-integrated --project=intake-ui-shines
# edge NONE + integrated public form: 911 banner, narrative, is_victim, reporter fields, demographics all present + asserted
cargo xtask check-docs && cargo xtask plan-lint

Bottom line: a public reporter on the edge NONE/cases form can do everything they could on craig-web’s form (safety warning, victim flag, full narrative, address) — verified by e2e — before Phase 5 removes craig-web’s form.

Edit this page · latest