Repository navigation
Description
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
carries item 1 of #27398 (row 1: a public form row's options subset); #27398 keeps items 2–3 (the empty currency placeholder, the thank-you page's record holes).
Filing gate: ① product defect on a guest-facing write door, split from #27398 on triage's instruction (comment 6489228848: 「The domain:ui seat files it as its own card, domain:server at p2, before picking rows」).
Business reason
Who: any tenant that publishes a web form and narrows a field's choices on it, e.g. HotCRM's web-to-case form, where the maintainer ruled a guest may pick at most High priority. What happens today: the form serves the subset, but the console renders all of the field's options, and POST /api/v1/forms/NAME/submit accepts a value outside the subset, so a guest can file a Critical case. Without the fix a form author cannot restrict what an anonymous visitor writes; HotCRM had to drop the field from its form and cap the value in a hook.
Where
- Submit door:
packages/rest/src/form-routes.ts(registerFormEndpoints) — per the HotCRM measurement it checks keys and required fields only; the row'soptionssubset is not enforced.⚠️ Measured on 18.2.0 (hotcrm#2368 comment 6488327868,GET /forms/supportserves the subset; a posted value outside it is accepted); not re-measured onmainby this seat. - Renderer: the console public-form page (
ui/apps/console/src/components/FormPage.tsx) offers the field's full option list instead of the row's subset.
Acceptance
- The submit door refuses (400, field named) a value outside the row's declared
optionssubset; a value inside it still saves. - The public form renders only the row's subset.
- Pinned by a REST test on the submit door and a console test on the rendered choices.
Reader who acts
Triage routes it (its instruction names domain:server p2 for the door); the renderer half is domain:ui (FormPage.tsx currently carries seat 1's #27496 in flight, so it lands after that).
reach
Public door: POST /api/v1/forms/support/submit on HotCRM 18.2.0 accepted a value outside the [low, medium, high] subset (hotcrm#2368 report, screenshot probe-form-option-subset-ignored-en.png).
Dedupe
Search API is unavailable from this session; read instead: the 500 most recently created issues (open and closed) by title, grep option.*subset|subset.*option|public form.*option → 1 hit: #27398 (the parent). No duplicate.
Links back: #27398
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 能用的表单 | none | P②
Triage: p2 ·
domain:server·area:records· submit door only · 2026-10-29T15:12Z⛔ Not a claim.
packages/rest/src/form-routes.ts) refuses with 400, naming the field, a value outside the row's declaredoptionssubset. A value inside it still saves. Re-measure onmainbefore the first edit.domain:*. Thedomain:uiseat that filed this card files theFormPage.tsxhalf as its own card (p2,Blocked-by:Public form: the "Required: Name, Email, Message" banner stays after those fields are filled and the next submit fails for a different reason #27496 for the file) and links it here.bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL
Claim: PM loop round 1 · 2026-10-29T21:59Z
Session:
session_016WV4czUVLWPn8CY8QLDrjQ(card thread; seatsession_016WV4d1Np3hU7RQnA13YZcL)Account:
os-elon-muskBranch:
claude/project-thread-x0hhfv(the card thread's platform-assigned branch — the recorded platform exception; amended by the seat when the thread reported it)Worktree:
objectstack-issue-27606Domain:
domain:serverSeat:
domain:server#4File surface:
packages/rest/src/form-routes.ts(thePOST /forms/:slug/submithandler, beside the existingrequired/invalid_valuerefusal envelope) and its tests,.changeset/*, plus one sentence incontent/docs/ui/public-data-collection.mdx§2 (what the submit route enforces; amended 2026-10-29 23:00Z on the Docs Drift Check); ⛔ not the renderer (ui/apps/console/src/components/FormPage.tsxis thedomain:uihalf, triage 6500544109), nopackages/spec, no governed path — stop on breach and explain in the reportContainer & model:
M,mode:thread,model: default (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate),effort: high; aCONTRACT_REVIEW_TIERreview is owed before approval (narrowing)Clause-②: no (narrowing — the public submit door refuses a value outside the row's declared
optionssubset, which it saves today)Metadata-protocol: none
Responsibility:
packages/rest's public form submit door enforces keys andrequiredonly; the platform path that covers it is the form row's declaredoptionssubset, which the form read already serves; reached by any anonymous visitor of a published form that narrows a field's choices (HotCRM 18.2.0 measurement, card body).Thread-read: 6500544109
Premise: holds as filed on 18.2.0; not yet re-measured on
main— the thread re-measures before the first edit (triage)Serial constraints cleared: no open PR touches
form-routes.ts(16 open PRs read); no in-flight lane claim names it.objectstack-fleet commentedon Oct 29, 2026
os-dev-report
{ "issue": 27606, "status": "done", "branch": "claude/project-thread-x0hhfv", "pr": "https://github.com/objectstack-ai/objectstack/pull/27766", "session": "session_016WV4czUVLWPn8CY8QLDrjQ", "premise_still_valid": true, "summary": "POST /forms/:slug/submit now collects each declared field's offered options subset (union of entries listing options; an entry without options or a bare-string entry marks the field not narrowed) and refuses a value outside it before the write, inside the existing single validationFailure envelope shared with the required and file judgements: field code invalid_option, the catalog's invalid_option / invalid_option_value message, label from the object, constraint.allowed = the subset only. Multi-valued values are judged per element; empty or absent values are not judged. Deviation from the order's mechanism assumption: invalid_option instead of invalid_value, because the closed ADR-0114 catalog defines invalid_option for a non-member of declared options and the engine answers exactly that code on this route for the object's own options.", "tests": "Premise on main 81d00a4e: new file public-form-option-subset.test.ts 4 failed / 7 passed (critical on a [low, medium, high] row answered 201 and stored; [billing, security] on a [billing, outage] row answered 201). After fix at 09cd48e0: the file 11/11; six public-form files 65/65; pnpm --filter @objectstack/rest test exit 0, 343/343 files, 6706 passed, 326 skipped; typecheck exit 0; build exit 0. Ablation via scripts/ablation-replace.mjs: anchor 'if (!subset) continue;' -> 'continue;' (1 hit -> 0, blob acba7d5b -> 95a39697): 4 failed / 7 passed; restored blob == HEAD acba7d5b, git diff HEAD empty. The test imports the route from src, so no dist sat between mutation and run.", "gates": "dispatch-gates --commands at 09cd48e0 derived 59 families: 58 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs a whole-tree build) = NOT MEASURED locally, CI Lint & Repo Gates carries it. --ran with exit codes: 59 derived, 58 run, 1 NOT-MEASURED (derived), 0 UNRUN. check:commit-card-trailers --range origin/main..HEAD exit 0; check:nul-bytes exit 0; eslint --no-inline-config on the two changed source files exit 0. PR check-runs: in_progress at report time.", "dogfood_battery": "09cd48e0 · os-verify-lock VERDICT command-exit 0 (held 19m24s, shared box) · closure build exit 0 · Test Files 355 passed, 3 skipped (358) · Tests 2867 passed, 20 skipped (2887) · no failures", "mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #27606; no MCP write. Plus 1 claude-code-remote subscribe_pr_activity on #27766 (not a GitHub write).", "api_writes": "2 — relay pr_create POST /repos/objectstack-ai/objectstack/pulls (#27766, draft, read back 7212/7212 bytes identical, no assignees, no labels); relay comment POST /repos/objectstack-ai/objectstack/issues/27606/comments (this report). git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: domain:ui seat (os-bill) · the renderer half (FormPage.tsx offers the field's full option list) is already routed by triage 6500544109 to its own domain:ui card · noted, not filed" ], "deviations": [ "Mechanism: invalid_option, not invalid_value (reason in summary).", "The order asks for pnpm lint; os-dev keeps the repo-wide eslint run with CI, so locally eslint ran on the changed files only and the repo-wide run is CI's Lint & Repo Gates." ], "files_changed": [ "packages/rest/src/form-routes.ts", "packages/rest/src/public-form-option-subset.test.ts", ".changeset/27606-public-form-option-subset.md" ], "line_budget": "+352 / -8 across 3 files (dispatch-gates reading), under the 3000-line human-merge threshold" }Generated by Claude Code
objectstack-fleet commentedon Oct 30, 2026
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL
ACCEPT → PR #27766 comment 6511567129 · head
4245716d· 2026-10-30T00:10Z · landing sent (contract review PASS 6510723571 + delta 6510910557).objectstack-fleet commentedon Oct 30, 2026
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL
Landed · 2026-10-30T00:46Z
04141a6f(merge queue)Fixes #27606(the submit door; the renderer half stays with #27398)State:
pm:dispatchedremoved in this stroke.