docs(form-builder): API.md Form Builder (Public), SCHEMA v2.1, ARCH §10.4, BACKLOG

S2c Phase 8.

- API.md: new **Form Builder (Public)** section documenting all 6
  public endpoints (GET schema + time-slots + sections; POST draft,
  PUT save, POST submit) with request/response examples, error codes,
  and the identity_match / schema_drift contracts. No PII-echo noted
  explicitly.
- SCHEMA.md bumped to v2.1:
  - changelog entry for S2c.
  - form_submissions table gains schema_version_at_open +
    identity_match_status columns; UNIQUE (form_schema_id,
    idempotency_key) replaces the composite index; a new composite
    index (form_schema_id, identity_match_status) landed for the
    organiser "pending-match" dashboard.
- ARCH-FORM-BUILDER.md bumped to v1.3 with new §10.4 "Public
  submission lifecycle — draft/save/submit split" documenting the
  three-endpoint contract, idempotency, schema-drift detection,
  access rules, the standardised error envelope, and the dependency
  data sub-endpoints.
- BACKLOG.md adds:
  - FORM-04 (grace_days configurable — current implementation still
    uses the hard-coded 7-day window)
  - DOC-01 (Scramble / OpenAPI generator for API.md to reduce the
    docs-drift effort going forward).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 23:07:26 +02:00
parent 9b1bf0e13d
commit 68d2c830a0
4 changed files with 325 additions and 9 deletions

View File

@@ -1,14 +1,15 @@
# ARCH — Universal Form Builder (v1.2)
# ARCH — Universal Form Builder (v1.3)
> **Source of truth** for Crewli's universal Form Builder architecture.
> Any discrepancy with SCHEMA.md is resolved in favour of this document
> during the refactor. SCHEMA.md is updated at the end of the refactor.
>
> **Status:** Approved — S2b in progress (service + API layer)
> **Version:** 1.2.1 (§31.10 FORM-02 contract rewritten authoritatively)
> **Previous version:** 1.2 April 2026 — per-purpose lifecycles, integration
> contracts, user guidance principles, documentation coverage requirements,
> in-app copy catalogue, and concrete gap fills from v1.1 review
> **Status:** Approved — S2c landed (public API completion)
> **Version:** 1.3 (§10.4 public submission lifecycle — draft/save/submit
> split with error envelope and drift detection)
> **Previous version:** 1.2.1 April 2026 (§31.10 FORM-02 contract),
> 1.2 April 2026 (per-purpose lifecycles, integration contracts, user
> guidance principles, documentation coverage, in-app copy catalogue)
> **Created:** April 2026
> **Owner:** Architecture doc; every session reads this before starting
>
@@ -1388,6 +1389,77 @@ When anonymised: IP replaced with `"[anonymised]"` and
Exception: if submission is flagged for investigation (organiser marks it),
anonymisation is paused until flag is cleared.
### 10.4 [v1.3] Public submission lifecycle — draft / save / submit split
S2c split the atomic "one-POST does everything" flow into three
REST endpoints so the portal can auto-save drafts without firing
submit-events on every keystroke:
```
POST /api/v1/public/forms/{public_token}/submissions
Body: { idempotency_key (required, 630 chars),
opened_at?, submitted_in_locale?,
public_submitter_name?, public_submitter_email? }
Creates a draft. Returns PublicFormSubmissionResource.
Duplicate POST with the same idempotency_key returns the existing
draft as HTTP 200 (vs 201 for fresh). Race-safe via UNIQUE
(form_schema_id, idempotency_key).
PUT /api/v1/public/forms/{public_token}/submissions/{submission_id}
Body: { values: { <slug>: <value>|<array>, ... }, first_interacted_at? }
Auto-save — partial updates allowed, only provided slugs are
written. Status stays 'draft'. auto_save_count increments per call;
FormSubmissionDraftUpdated event fires. Rule layer is relaxed
(nullable + type checks); service layer enforces
form_fields.validation_rules (min/max/regex/unique).
POST /api/v1/public/forms/{public_token}/submissions/{submission_id}/submit
Body: { values?: {...}, captcha_token? }
Final submit. Merges body values with already-saved values, runs
STRICT rule set (required, in:options, types, min/max) against the
merged map. On success: status draft → submitted, schema_snapshot
stored per form_schemas.snapshot_mode, schema_version_at_submit set,
fires FormSubmissionSubmitted (→ §31.10 tag sync, §31.1 identity
match). Rate-limited per
(public_token, ip) per hour via RateLimiter 'form-submit:TOKEN:IP'.
```
**Schema drift detection.**
`form_submissions.schema_version_at_open` is stamped at draft-create
time; `schema_version_at_submit` is stamped at submit. Any difference
(or — for active drafts — a difference against current `schema.version`)
surfaces in `PublicFormSubmissionResource.schema_drift: true` so the
portal can warn "this form has changed since you started".
**Access rules.**
- Submission must belong to the resolved schema (URL
`(public_token, submission_id)` pair must match).
- Submission must be `status = draft` on both PUT and POST-submit —
409 `SUBMISSION_ALREADY_SUBMITTED` otherwise.
- Rotated-token grace window applies the same way as the GET: resolve
via `public_token` first, then `public_token_previous` (rejected
with 410 `TOKEN_EXPIRED` if past the hard-coded 7-day grace).
Making the window configurable is BACKLOG FORM-04.
**Error envelope (D6).**
Every public form endpoint responds with the shared shape:
```json
{ "message": "...", "code": "...", "errors"?: {"values.slug": ["..."]} }
```
Codes: `SCHEMA_NOT_FOUND`, `TOKEN_EXPIRED`, `TOKEN_REVOKED`,
`SCHEMA_UNPUBLISHED`, `SUBMISSION_ALREADY_SUBMITTED`, `RATE_LIMITED`
(carries `Retry-After` header), `VALIDATION_FAILED`.
**Dependency-data endpoints.** The public GET `/{public_token}` embeds
`available_tags` per TAG_PICKER field. `AVAILABILITY_PICKER` and
`SECTION_PRIORITY` use sibling read endpoints:
- `GET /{public_token}/time-slots` — VOLUNTEER person_type only,
festival-parent query surfaces parent + children time slots.
- `GET /{public_token}/sections` — `show_in_registration=true +
type=standard`, dedup by name across festival children.
---
## 11. Migration plan