feat: Phase-11 field-ownership application (ADR-037 §4–§7) — CWCA provider portal

Summary

The surface-bound field-ownership application (ADR-037 §4–§7), deferred from Plan Y to Phase 11 (the CWCA provider portal) — the field-ownership feature's real first consumer and second tenant.

Plan Y delivered the surface-agnostic evaluation core (ADR-037 §2/§3): AuthzEngine::resolve_field_permission(&Claims, ResourceRef, field, field_owner) -> Result<FieldPermission, ApiError> in crates/craig-authz (FieldPermission { Read, Write, Propose, None }, fixture-proven). This issue tracks the application that consumes it on a real multi-tenant edit surface.

Field ownership is intrinsically cross-tenant (state agency vs external provider). Building §4–§7 ahead of the portal would be either a fig leaf (a synthetic intra-agency surface that mis-fits the state/provider/shared taxonomy) or throwaway (a synthetic host the portal replaces). It lands with its consumer — exactly as craig-composition reads rulesets/ directly rather than consuming an unbuilt bundle axis.

ADR-037 is unedited and is satisfied incrementally: Plan Y = §2/§3, this issue = §4–§7. Epic &50 stays open as the durable tracker until this lands.

Acceptance criteria (ADR-037 §4–§7)

  • §4 — field_ownership bundle axis. Add the field_ownership: FieldOwnershipContribution field to BundleContribution, landing with the consumer surface (mirrors composition's rulesets/-direct pattern; no dormant axis ahead of its consumer).
  • §5 — per-surface ownership tables. rulesets/<jurisdiction>/<surface>_ownership.toml mapping each field to its owner classification (state / provider / shared), loaded + validated at boot on the OWNING backend service; boot fails closed on an unknown field/owner.
  • §6 — propose/accept/reject queue + field audit. pending_edits_<surface> table on the owning backend; a non-owner's write to a shared field is queued (not applied); the owner accepts (single transaction reusing Action::Approve) or rejects; field-attached audit on every read AND write via the existing per-service audit pipeline (ADR-022 outbox).
  • §7 — BFF reflection + Studio. A per-field permission-map endpoint the BFF calls; lock-icon reflection in the edit template (a reflection of the backend decision, not the source of truth, ADR-004 preserved); a Studio pending-changes view for the owner's accept/reject queue.
  • Owning-service rubric (resolve in Phase 11). Pin to services/craig-cases for case-attached fields, OR spin a NEW services/craig-cwca-provider if the CWCA boundary is substantial (field count, distinct session/auth boundary, >2 new tables) — see the rubric in the archived Plan Y body and ADR-037. A new service triggers the full devstack service-add obligation set (OIDC principal, audience mappers on all clients, ResourceType coverage, etc.).
  • The application calls resolve_field_permission unchanged (no new authz primitives needed — the evaluation core is done).

References

  • ADR-037 — docs/modules/ROOT/pages/adrs/adr-037-field-ownership-authz.adoc (the Phase-11 spec, incl. the verified pending_edits_<surface> shape, single-transaction accept, per-GET read audit, owning-service rubric, encryption-aware PII writes).
  • Plan Y (archived) — docs/modules/ROOT/pages/plans/archive/field-ownership-authz.adoc (§ "The Phase-11 hand-off").
  • Plan S umbrella — docs/modules/ROOT/pages/plans/multi-jurisdiction-foundation.adoc (the sequencing authority; service-boundary table).
  • Implementation guide — docs/modules/ROOT/pages/implementation-guide.adoc (Phase 11 — Constituent & Provider Portals).
  • The shipped evaluation core — crates/craig-authz (AuthzEngine::resolve_field_permission, FieldPermission); registered in docs/modules/ROOT/pages/shared-crates.adoc.
  • Epic &50 (this issue's parent; kept open as the durable tracker).

🤖 Generated with Claude Code