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 |
|
craig-web sends them |
✅ |
|
cases persists them |
✅ unchanged |
|
Edge form renders |
❌ subset only |
craig-web-public fields are SHINES-gated or entirely absent under NONE |
Edge form payload |
❌ gated |
|
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 ( |
absent |
add (render-only alert) |
R1 |
Child / Narrative base |
child |
SHINES-payload-gated ( |
widen |
R1 |
Reporter |
|
absent (only |
add |
R2 |
Adult |
|
mix: some SHINES-gated, several absent |
widen/add |
R2 |
Child |
|
SHINES-gated/reduced |
widen/add |
R2 |
Incident |
|
SHINES-gated/downgraded |
widen |
R2 |
Narrative (deep) |
|
absent even under SHINES |
add (+ |
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_descriptionis OPTIONAL — craig-web enforces min-20 only when non-empty (report-wizard.js:149), and theinform.htmlis cosmetic. Match that: do *not make it required on the edge (that would be stricter than parity). -
incident_datetimeneeds normalization — the contract isDateTime<Utc>(RFC3339); a rawdatetime-localvalue won’t deserialize. craig-web normalizes before submit (craig-web/src/routes/report.rs:169). Mirror that inreport-form.js(normalize thedatetime-localstring 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/addChildguard< 10;removeAdult/removeChildkeep ≥ 1 (>1, l.106-108). The edge today starts with[]and removes the last — bring it to craig-web’s behavior. -
Adult conditional validation —
is_active_duty⇒military_branchrequired;is_alleged_maltreater⇒maltreater_relationship_to_victimrequired (report-wizard.js:132-137). NONE keeps only these (no SHINES required-when-relevant). -
_clean— strip empty strings so optional fields deserialize asNone(report-wizard.js:68-74);children/adultsfilter to entries with afirst_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 |
Done (2026-06-27) — MR !833 ( |
R1 safety + core narrative |
911 banner; child |
Done (2026-06-27) — 911 banner + |
R2 reporter + people |
Reporter address + relationships + caregiver/household/self flags; adult fields ( |
Done (2026-06-27) — all fields added NONE-gated (SHINES rendering + wire byte-identical; |
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; |
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 |
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_descriptionis present (it is optional — assert it’s sent when entered, and that an empty value does NOT block submit). -
reporter:
POST body.reporter_streetnon-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].phoneuses the craig-web name; adatetime-localvalue round-trips as a validincident_datetime. -
SHINES regression:
--project=intake-ui-shinesstill green (the sharedreport.html/report-form.jsedits 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 |
|---|---|
|
R1–R3 |
|
R1–R3 |
|
R1–R3 |
|
R1–R3 |
|
R1 |
|
R0 + each |
Untouched: craig-intake-contracts, craig-cases (+ schema), the SHINES ExtraFields architecture.
Risks & mitigations
| Risk | Mitigation |
|---|---|
Fields render under NONE but the |
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 |
A re-added field has no i18n key in the shared |
|
Applying SHINES required-when-relevant rules to NONE → over-validation (e.g. making |
NONE keeps craig-web’s validation only — |
Editing the shared |
Run |
Adult |
Add |
|
Normalize to RFC3339/UTC in |
Scope creep — "full parity" pulls in fields the project actually wanted dropped |
The inventory + craig-web’s |
cases stores rich narrative/address only in |
Pre-existing (true for craig-web too) — out of scope; note as a possible separate enhancement, NOT a parity blocker |
Delivery sequence
-
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). -
R1 (safety) → R2 → R3 — one MR each (full validate + J1–J8;
Closes #N+ closing comment). -
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.