You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
public forms: the submit door accepts a value outside a form row's declared options subset, and the renderer offers every option #27606
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's options subset is not enforced. ⚠️ Measured on 18.2.0 (hotcrm#2368 comment 6488327868, GET /forms/support serves the subset; a posted value outside it is accepted); not re-measured on main by 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 options subset; 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.
Claim: PM loop round 1 · 2026-10-29T21:59Z
Session: session_016WV4czUVLWPn8CY8QLDrjQ (card thread; seat session_016WV4d1Np3hU7RQnA13YZcL)
Account: os-elon-musk
Branch: 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-27606
Domain: domain:server
Seat: domain:server#4
File surface: packages/rest/src/form-routes.ts (the POST /forms/:slug/submit handler, beside the existing required / invalid_value refusal envelope) and its tests, .changeset/*, plus one sentence in content/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.tsx is the domain:ui half, triage 6500544109), no packages/spec, no governed path — stop on breach and explain in the report
Container & model: M, mode:thread, model: default (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate), effort: high; a CONTRACT_REVIEW_TIER review is owed before approval (narrowing)
Clause-②: no (narrowing — the public submit door refuses a value outside the row's declared options subset, which it saves today)
Metadata-protocol: none
Responsibility: packages/rest's public form submit door enforces keys and required only; the platform path that covers it is the form row's declared options subset, 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.
{
"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"
}
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.