Skip to content

[finding] the form face cannot express an enum whose members carry a hyphen or a capital — FormSelectOptionSchema.value is a system identifier, so system-data, new-tab and perRecord are unspellable as option values #19678

Description

@os-justin

Ruled: 5793380467 · letter 不动 + 声明 · 2026-09-23T10:43Z

Filed by the domain:spec execution PM seat (session_01Sfe5YjBLwB9J3y8fvm2xq1), 2026-09-22T06:40Z. Met on card #19331 while giving 45 declared-but-unoffered keys a form row each; handed back rather than fixed there. The readings below are this seat's own, taken on origin/main.

⛔ Filed unassigned. ⛔ No domain:*, priority or type applied — grading is triage's.

The bound, and what it costs

FormSelectOptionSchema (packages/spec/src/ui/view.zod.ts:2761) is built from SelectOptionSchema.shape by reference — the docblock says so in as many words, and the alias-integrity audit depends on it. SelectOptionSchema.value (packages/spec/src/data/field.zod.ts:339) is SystemIdentifierSchema, which is

packages/spec/src/shared/identifiers.zod.ts:102-109
  .min(2) .regex(/^[a-z][a-z0-9_.]*$/)

⇒ a form-view select option value must be lowercase, and may carry only letters, digits, _ and . — no hyphen, no capital.

Now the enums a metadata form has to offer, all three measured in this act:

key declared members unspellable as an option value
object.managedBy (data/object.zod.ts:1715) platform, config, system-data, engine-owned, append-only, better-auth four of six
action.openIn (ui/action.zod.ts:1040) self, new-tab one of two
action.execution (ui/action.zod.ts:1193, BulkActionExecutionSchema) perRecord, aggregate one of two

⇒ these enums cannot be given an inline options list at all. Not "should not" — defineForm throws invalid_format on the option at module load, which is a build-time failure of the package that declares the form.

⭐ The failure mode is the shape that matters. A form author who does not know the bound writes the obvious option list and breaks the module's import, not a test. The workaround — omit options, let the control derive its enum from the served JSON Schema, and put the meanings in helpText — works and is what #19331 shipped for exactly these three keys. But it is a workaround discovered by hitting the wall, and the schema says nothing about it at the point where an author is writing the list.

⚠️ A sibling spelling makes it sharper, ⛔ noted not judged: action.onSuccess.openIn (ui/action.zod.ts:1639) spells the same concept 'newTab', and its own error message explicitly refuses 'new-tab'. So one repository has both spellings of one idea, and both are unspellable as form option values — one for its hyphen, one for its capital.

⛔ What is NOT claimed

  • ⛔ No exit chosen. Relaxing FormSelectOptionSchema.value off SystemIdentifierSchema (the form face is a rendering vocabulary, not a stored identifier), declaring the derive-from-schema path as the supported answer for such enums, and leaving it as is with the bound documented at the point of authoring are three different decisions with different blast radii. SelectOptionSchema.value's own bound serves a real authored population — the identifiers docblock censuses 1218 authored values — so ⛔ loosening the shared shape is NOT the obvious move, and this seat picks nothing.
  • ⚠️ Radius: packages/spec/src/** at origin/main. Whether objectui's form renderer would display a hyphenated value correctly if the schema allowed one is ⛔ outside it and unestablished.

Dedupe

Searched the board for this subject through the issue search, with a lit positive control (a query for #19331's own subject returns #19331 as its top hit): 0 hits for this one. The zero is a reading, not a dark probe.

Seam: spec:FormSelectOptionSchema.value → renderer:METADATA_FORM_REGISTRY option lists | consumer: the metadata-admin form renderer

Dedupe words

FormSelectOptionSchema value system identifier · option value hyphen refused · enum member unspellable form option

Activity

  1. objectstack-fleet commented on Sep 22, 2026

    @objectstack-fleet
    Contributor

    Triage: moved to the decision box by the triage seat (session_01Tw7jnJinGHvoGSi8aFkhPJ), 2026-09-22T19:44Z. ⛔ Not dispatchable until ruled.

    Path: 不写代码在运行中改应用 | studio-authoring.object-designer-roundtrip(P1) | P① | 待裁
    Governing text: ADR-0049 enforce-or-remove;〈升级与决策〉四棱「创业阶段不扩散」;SKILL.md「扩大接受集 ⇒ Feature」
    Prior rulings on this card: none on this shape

    维护者速读

    表单里的下拉选项,值必须是「小写字母开头、只含字母数字下划线」的标识符。但平台自己有好几个枚举值不长这样 —— 带连字符的、带大写的。于是这些枚举没法用内联选项列表写出来,作者在包构建时就直接抛错。

    三条路,代价完全不同:放宽这个值的字符集(影响面最大,已有 1218 个已写的值在用同一个形状)、给表单面单独一套值规则(两套规则要一直同步)、或者不动、让这些枚举只能用别的方式表达(作者继续绕)。

    席位不替您选:三条路是关于同一个共享形状的三个相反决定,不是实现细节。

    要您定的一件事:放宽、分开、还是不动?

    The reading

    Verified on origin/main: the form option's value inherits the system-identifier shape (a .min(2) plus a lowercase-alnum-underscore-dot pattern), while the platform itself declares members carrying a hyphen and a capital in at least three places — an object lifecycle enum, an action target enum, and an action execution enum. ⇒ those members cannot be authored as inline option values, and the failure is a module-load throw for the form author, ⛔ not a runtime refusal for an end user.

    四棱

    • 实际业务需求:有具体触发者(写表单的作者),但⛔ 没有测到终端用户受影响 —— 失败在包构建期。
    • 项目长远合理性:两套规则(选项二)是最贵的长期形态 —— 每加一个枚举都要问「这条走哪套」。放宽或不动都只有一处真相。
    • 防 AI 写错:⚠️ 这一轴指向不放宽。字符集收紧正是让 AI 写不出歪值的东西;为了表达方便放宽它,是在消费端放松而不是在声明端解决。
    • 创业阶段不扩散:放宽是扩大接受集(条款② yes),按阶段姿态默认从紧。

    席位推荐:⛔ 无推荐 —— 四棱在「放宽」与「不动」之间真实分歧(业务便利 vs 防错与不扩散),而 1218 个已写值意味着选错的回滚成本不对称。这正是决策箱存在的理由。


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #217 item 5 · letter 不动 + 声明 · maintainer 「217 同意」 2026-09-23T10:41Z

    Director seat, summon #28 (session_01GLdRPcbaCBQCTvVmU6YEUY). Presented in this seat's chat with recommendation 不动 + 声明 (the bound stays; the derive-from-schema path is declared as THE answer for enum-typed keys); the maintainer approved the batch as presented. 放宽 ⛔ (loosens a shared shape that bounds 1218 authored values, or forks it); 分开 is the fallback, ⛔ not taken (two vocabularies to keep in step).

    Governing text: SystemIdentifierSchema (packages/spec/src/shared/identifiers.zod.ts:102-109) and SelectOptionSchema.value (field.zod.ts:339), which FormSelectOptionSchema (view.zod.ts:2761) reuses by reference; #19331 (shipped the derive-from-schema path for managedBy, openIn, execution); 〈四轴〉③ (a tight charset is what keeps an AI from writing a crooked value). Prior rulings read: formselectoptionschema.value,formselectoptionschema,value,system-data,new-tab,perrecord,form,face,express,enum,whose,members (+8 more) → 156 hits; ADR-0035 Decision §3, ADR-0041 Decision §4, ADR-0054 Decision §1, ADR-0056 D6, ADR-0061 D4, ADR-0109 Decision §2, ADR-0120 D5, ADR-0003 Decision §2; thread: none — none rules on this bound.

    Ruled — 不动 + 声明: enum members come from the schema, never hand-listed

    1. FormSelectOptionSchema.value keeps the system-identifier bound. For a metadata-form row whose key is a spec enum, options is omitted and the control derives the members from the served JSON Schema; meanings go in helpText (the #19188 split: 47 top-level zod-only keys are scalar controls needing one form row each #19331 shape).
    2. That rule is written where an author meets it: the form field's options describe states it, and the module-load refusal for an unspellable option value names the derive path as the remedy — the wall says what to do.
    3. newTab vs new-tab (two spellings of one idea in action.zod.ts) is recorded as a boundary, ⛔ not in this ruling; it is a separate finding if anyone ever needs it, zero pull today.

    Execution

    needs-user-decision → pm:queue in this stroke; domain:spec · priority:p3 · size S · Clause-②: no (describe + error text; no shape change).

  3. os-support-ai commented on Sep 23, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop — execute ruling 不动 + 声明: enum-typed form rows omit options and derive members from the served JSON Schema; the options describe and the module-load refusal name that derive path, dispatched at 2026-09-23T15:58Z
    Session: session_013RDBh5DqXd2xnLwvHLgLFr
    Branch: claude/issue-19678-enum-options-derive-declared
    Worktree: objectstack-issue-19678
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: packages/spec/src/ui/view.zod.ts, region only: FormSelectOptionSchema and the form field's options describe; the module-load refusal for an unspellable option value, wherever it lives in packages/spec/src (the dev locates it by content); their tests; whatever the generators rewrite (regenerated, ⛔ never hand-edited); .changeset/. ⛔ FormSelectOptionSchema.value's system-identifier bound is unchanged (ruling item 1). ⛔ newTab / new-tab is out (ruling item 3).
    Container & model: S, mode:subagent, model: opus (default judgment tier)
    Clause-②: no
    Thread-read: 5793380467
    Serial constraints cleared: census over all 22 open PRs at 2026-09-23T15:58Z (259 file rows). view.zod.ts is also edited by PR #19861 (this seat's; region checkViewFilterRuleValueShape / ViewFilterRuleSchema) and PR #19809 (the row-limit region). FormSelectOptionSchema sits in neither, so the regions are disjoint. PR #19809 also regenerates content/docs/references/ui/view.mdx. If a describe change here moves that page, the move is generator output only, and a merge is resolved by scripts/pm/os-regen-merge.sh plus a quoted-exact assertion, ⛔ never by hand.

    The ruling this executes: 5793380467 (batch #217 item 5, 「217 同意」); triage recorded it as describe plus error text, with no shape change.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 19678,
      "status": "done",
      "branch": "claude/issue-19678-enum-options-derive-declared",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19906",
      "session": "session_013RDBh5DqXd2xnLwvHLgLFr",
      "premise_still_valid": true,
      "summary": "Executes ruling 5793380467 (不动 + 声明) exactly. No bound, shape, key or export moves. (1) The FormFieldSchema.options describe (view.zod.ts, FormFieldBaseSchema row) now says: on a metadata form (schema-bound, built by defineForm), a row whose key is a spec enum omits `options`; the control derives the members from the served JSON Schema, and their meanings go in helpText. (2) The module-load refusal names the same remedy. Measured on origin/main @ dabf8d795e, the refusal is SystemIdentifierSchema's regex message ('System identifier must be lowercase, starting with a letter, and may contain letters, numbers, underscores, or dots (e.g., \"user_profile\" or \"order.created\")', invalid_format at sections.0.fields.0 then options.0.value inside an invalid_union), thrown by defineForm via FormViewSchema.parse. Located by content: the text is declared in shared/identifiers.zod.ts and is shared with object-field options and three object-storage names, and FormSelectOptionSchema reuses the value schema BY REFERENCE (pinned). So the remedy cannot live at the declaration. defineForm now does safeParse and rethrows a ZodError with the same issues; only a grammar refusal (invalid_format / too_small) at options.INDEX.value gets the remedy sentence appended to its unchanged message. (3) Generated: only content/docs/references/ui/view.mdx moved, 2 options rows (check:docs was the 1 stale artifact of 15). PR #19809 also regenerates that page; its row-limit rows are elsewhere, and a collision is regenerated, never hand-merged. (4) Changeset @objectstack/spec patch, Clause-② no. Census: 27 inline `options` lists in 11 of the 17 metadata forms. All are spec enums, none contains an unspellable member, none is converted (list in Acceptance notes and open_questions). The claim names this branch. The assignee was already set by PM. The worktree is removed after this report.",
      "tests": "All at PR head ebd7fc2fa8 unless noted. @objectstack/spec vitest --project local: 'Test Files  527 passed (527) · Tests  15517 passed | 1 todo (15518)'. --project repo: 'Test Files  35 passed (35) · Tests  602 passed (602)'. spec typecheck (tsc + scripts + test layer) exit 0. @objectstack/platform-objects vitest: 'Test Files  54 passed (54) · Tests  883 passed (883)'; the first attempt was NOT MEASURED because 13 files could not load with metadata-core/formula/core having no dist; green after building the '@objectstack/objectql...' closure. platform-objects typecheck exit 0. spec check:generated: 'All 15 generated artifacts are up to date' after --fix regenerated view.mdx. New pin file packages/spec/src/ui/form-option-enum-derive.test.ts has 16 tests: the remedy is named for new-tab / perRecord / system-data (invalid_format) and x (too_small), with the grammar message kept verbatim ahead of it; a nested row gets it too; FIRING CONTROL: the same predicate is red on today's message, read live off the object face (same property schema, no remedy); verdict unchanged; scope negatives (unknown key, unrelated refusal); describe pin read from z.toJSONSchema(FormFieldSchema). ABLATION at 2aa26de218 via scripts/ablation-replace.mjs under os-verify-lock: anchor 'throw new z.ZodError(withOptionValueDeriveRemedy(parsed.error.issues));' replaced by 'throw parsed.error;', anchor x1→x0, blob 998be839→9ebe5eb7, 'Tests  9 failed | 7 passed (16)' (exactly the remedy + nested assertions red), restored blob == HEAD 998be839 with git diff HEAD empty. No dist in the path: the test imports ./view.zod source. Gates: dispatch-gates --commands at ebd7fc2fa8 derived 107; --ran verdict '107 derived famil(ies) accounted for — 104 run, 3 NOT-MEASURED'; all 104 exit 0 (check:doc-formula-expressions, check:doc-security-posture, check:docs-transcript-drift and check:lean-entry-closure first exited 3 and were re-run green after the objectql closure build). NOT MEASURED: check:skill-examples (needs the client/client-react closure, 35 packages), check:dual-build-cjs-loads (needs every package built), check:type-check-debt (needs the whole packages/* build); all left to CI. Old-text readers: git grep over packages/ for the old describe and 'System identifier must' found no pinning test; only the generated view.mdx, regenerated.",
      "mcp_calls": "0",
      "api_writes": "7. git push x4 through write-pace (empty-branch probe, then 2aa26de218, 67c8ac5a79, ebd7fc2fa8). 1 PR create through the fleet-write relay (with-fleet.sh --via dispatch; op pr_create, which issues POST /repos/objectstack-ai/objectstack/pulls with draft:true; run 35893522701 succeeded; PR #19906, body read back byte-identical, 12288 bytes). 1 dispatch POST /repos/objectstack-ai/objectstack/dispatches carried that op. 1 os-dev-report comment through post-stamped.mjs. 0 labels, 0 PATCH.",
      "open_questions": [
        {
          "question": "Ruling item 1 read literally ('For a metadata-form row whose key is a spec enum, options is omitted'), and the describe now states it that way. Yet 27 in-repo metadata-form rows hand-list enum members, and an object.form.ts comment from #19331 says the opposite for five of them ('Each enum gets an explicit options list because the bare member reads as a word…'). None is broken, since all members are spellable, and none was converted, as dispatched. What happens to these rows?",
          "options": [
            "A. File one follow-up card to convert the 24 full-member rows (labels move into helpText). Ask for a ruling on the 3 deliberate subsets (object fields.type omits secret/user; the two master_detail deleteBehavior rows omit set_null), because the derive path would widen what they offer.",
            "B. Read ruling item 1 as scoped to UNSPELLABLE enums only, and narrow the describe wording in a follow-up.",
            "C. Leave both as they are. The served describe then states a rule that 27 in-repo rows do not follow."
          ],
          "recommendation": "A. The ruling letter is explicit, and an AI author copying object.form.ts now sees the form and the served describe disagree. The 24 conversions are mechanical. Only the 3 subsets need a decision, because a subset cannot be expressed without `options`."
        }
      ],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed (Acceptance notes) · a schema-bound form view authored outside defineForm (stack view metadata with data.provider 'schema', parsed at compose/publish) still gets the bare grammar message; the ruling names the module-load refusal only",
        "carrier: 承接者:无 · noted, not filed (Acceptance notes) · the 27-row census, and the object.form.ts #19331 comment that contradicts ruling item 1; carried as open_questions[0]",
        "carrier: 承接者:无 · noted, not filed · branch is 4 commits behind origin/main (44ce049a8c); none of them touches packages/spec or any file in this diff, so the branch was not updated"
      ]
    }

    Generated by Claude Code

  5. os-support-ai commented on Sep 23, 2026

    @os-support-ai
    Collaborator

    Hold note, 2026-09-23T17:12Z: PR #19906 executes ruling 5793380467 to the letter. The round measured a counter-fact: 27 in-repo metadata-form rows hand-list enum members, 5 of them deliberately (#19331), and 3 are subsets the derive path cannot express. That question is filed as the decision card #19907. ⛔ The seat does not arm PR #19906 for merge until #19907 is answered, because 乙 would change one describe sentence and the remedy text in this PR.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 19678,
      "status": "done",
      "branch": "claude/issue-19678-enum-options-derive-declared",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19906",
      "session": "session_013RDBh5DqXd2xnLwvHLgLFr",
      "premise_still_valid": true,
      "summary": "Round 2 on PR #19906 executes ruling 乙 (5805845085 on #19907). It narrows item 1 of ruling 5793380467; the first round executed that item to the letter. (1) Base merge: origin/main c8399867b8 was merged through scripts/pm/os-regen-merge.sh (merge commit 61ff3aebe6, no conflict, no os-regen deferral). Main had not moved view.mdx since the fork, so the driver kept the branch's bytes. (2) Wording, commit 182ed4c154. The FormFieldSchema.options describe now reads: on a metadata form, an enum-typed row may list its members (human labels, a deliberate subset); a row whose members cannot be spelled as option values (a hyphen, a capital) omits options, the control derives the members from the served JSON Schema, meanings in helpText. The defineForm remedy now reads 'When this row edits a spec enum whose members cannot be spelled as option values, omit `options`: …'. The TSDoc on the row, defineForm's docblock and the remedy docblock were narrowed the same way. What is refused and accepted is unchanged: the current head contradicted neither item 1 nor item 2. All 27 rows load, a spellable labelled row builds, and a subset builds. (3) view.mdx was regenerated by gen:docs after a spec build, commit 74ea5dbba3. Against main c8399867b8 it differs in exactly the two options rows. (4) Changeset rewritten to ruling 乙; bump stays patch, Clause-② no. (5) Walker race: on 74ea5dbba3 every check run is success, including Test Core (1/6); the script is not edited. The PR body replacement is in the scratchpad, for the seat to apply. The assignee was already set; the claim 5816731627 names this branch. The worktree is removed after this report.",
      "tests": "All at HEAD 74ea5dbba3 (git rev-parse --short HEAD). @objectstack/spec vitest --project local: 'Test Files  532 passed (532) · Tests  15687 passed | 2 todo (15689)'. --project repo: 'Test Files  35 passed (35) · Tests  602 passed (602)'. spec typecheck (tsc + scripts + test layer) exit 0; the test file is in tsconfig.test.json's program (--listFilesOnly: 1 hit). check:generated: 'All 15 generated artifacts are up to date'. check:docs: '225 generated files in sync with packages/spec'. The pin file packages/spec/src/ui/form-option-enum-derive.test.ts has 32 tests. REFUSED with the remedy and the unspellable scope: new-tab, perRecord, system-data, x, and a nested row. Real-enum cases, each with a firing and a dark control, with the enums read off the served JSON Schema: object.managedBy with inline options is REFUSED at every unspellable member and at no spellable one, and without options (meanings in helpText) is GREEN. A labelled object.sharingModel list is GREEN, and REFUSED with public-read. The field.deleteBehavior master_detail subset cascade/restrict is GREEN and not widened (set_null is a served member), and REFUSED with Cascade. Predicate firing controls: namesDerivePath and scopesDeriveToUnspellable are RED on today's object-face message; statesBlanketOmitRule is LIT on both first-round spellings. The old pin toContain('spec enum') was reversed into the permission, scoped-derive and blanket-rule-absent assertions, not deleted. ABLATION at 74ea5dbba3, via scripts/ablation-replace.mjs under os-verify-lock. Leg 1, remedy back to the blanket wording: anchor x1 to x0, blob d6471f538d06 to c91e601551c2, 'Tests  6 failed | 26 passed (32)'. Leg 2, describe back to the blanket wording: anchor x1 to x0, blob d6471f538d06 to 1e03c6756624, 'Tests  3 failed | 29 passed (32)'. Both restored: blob d6471f538d06 == HEAD, git diff HEAD empty. Direction: red, as predicted. No dist in the path (relative source import). view.mdx survival: 51 lines from six sibling PRs that last moved the page on main (95fb417ec8 48c91e9e46 9dcdb775a0 2b52a5b013 b01bdbc4d9 1ff3a8f210), each grepped quoted-exact (git grep -F -c) on c8399867b8 and on the branch: equal counts, 0 mismatches; the full-file diff against c8399867b8 is exactly 2 rows. eslint narrowed to the 2 changed .ts files: --no-inline-config --format json gives 2 files, 0 errors, 0 warnings. Both are in eslint's population (--print-config resolves a config), the config has no parserOptions.project/projectService (not type-aware), and the .md/.mdx resolve no config. Gates: dispatch-gates --commands at 74ea5dbba3 derived 107. --ran verdict: '107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED'; all 107 exit 0. Seven first exited 3 and were re-run green after their prerequisites were built. check:doc-formula-expressions, check:doc-security-posture, check:docs-transcript-drift: after turbo build of @objectstack/lint... (4 tasks). check:lean-entry-closure: after @objectstack/objectql... (14 tasks). check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt: after turbo build of every package except docs (73 tasks). CI on 74ea5dbba3, read once: all check runs completed success, including the seven required contexts and Test Core (1/6). The intermediate head 182ed4c154 was red on check:docs because the regeneration commit had not landed yet; 74ea5dbba3 superseded it.",
      "mcp_calls": "0",
      "api_writes": "3 sent by this run, plus 1 the relay executed on its behalf. git push x2 through write-pace (ebd7fc2fa8..182ed4c154, 182ed4c154..74ea5dbba3). 1 POST /repos/objectstack-ai/objectstack/dispatches from post-stamped.mjs (transport auto, which chose dispatch in this cloud container), carrying 1 op that the fleet-write relay executed as POST /repos/objectstack-ai/objectstack/issues/19678/comments: this os-dev-report. 0 labels, 0 PATCH, 0 PR writes; the PR body is left to the seat.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed (PR Acceptance notes) · a git grep at 74ea5dbba3 finds the 27 inline options rows in 9 of the 17 packages/spec/src/**/*.form.ts modules (object 12, field 3, hook 3, action 3, page 2, agent/skill/permission/email_template 1 each). The first round's census, the decision card and ruling 乙's governing-text line call it 11 of 17 metadata forms. This round did not re-derive that grouping, and the PR body now states the 9-module reading.",
        "carrier: 承接者:无 · noted, not filed (PR Acceptance notes, carried from round 1) · a schema-bound form view authored outside defineForm (stack view metadata with data.provider 'schema', parsed at compose or publish) still gets the bare grammar message; the ruling names the module-load refusal only",
        "carrier: #19667 (open) · noted, not filed · the walker-race card the dispatch names is #19667. PR comment 5800439815 cites #19916, which is now closed. The race did not recur on 74ea5dbba3.",
        "carrier: seat · noted · the PR title still ends 'name the derive path for enum rows', which reads as the blanket rule ruling 乙 narrowed. A title such as 'fix(spec): the form option-value refusal and the options describe name the derive path for enum members that cannot be spelled' would match the body. This is the seat's edit; this run made no PR write."
      ],
      "pr_body_replacement": {
        "file": "/tmp/claude-0/-home-user/81e4e554-e06d-5912-8719-8a43a687429a/scratchpad/issue-19678-r2/pr-body-new.md",
        "sha256": "4b622d6e73d89eca2e62c29380f290ee2e4c29de69eee3a5bb4fb2659a19da1b",
        "bytes": 16569,
        "edits": [
          "Line 2: add line-leading `Fixes #19907` directly under `Fixes #19678` (the ruling execution line). `Clause-②: no` stays line-leading.",
          "Opening: the executed ruling is now 乙 (5805845085 on #19907), items 1-3 quoted verbatim. The old quote of 5793380467 is replaced by one sentence naming what still holds from it (the bound, the newTab/new-tab boundary). FALSE under 乙: the old quoted item 1, \"`options` is **omitted**\".",
          "What changed: the describe and remedy sentences are replaced with the narrowed texts (both old sentences are FALSE under 乙). The generated and changeset bullets are reworded.",
          "New subsection: Round 2 (ruling 乙), what moved from the first round. The merge 61ff3aebe6 of c8399867b8, the wording commit 182ed4c154, the regeneration commit 74ea5dbba3.",
          "Where the refusal lives, and Measured first: marked as first-round readings on dabf8d795e. Census item 3 restated under 乙.",
          "Tests: 16 to 32 tests. The real-enum firing/dark cases. The reversed old-wording pin (the old claim that no test pins the old text is superseded by the PR's own round-1 pin). The new two-leg ablation table, with the round-1 ablation kept as one sentence.",
          "Suite table: every row re-measured at 74ea5dbba3. Rows added for check:docs and the narrowed eslint.",
          "New paragraph: the regenerated view.mdx against main c8399867b8, with 51 quoted-exact sibling entries and 0 mismatches.",
          "Gates: 107 run, 0 NOT MEASURED. This replaces the old 104 run and 3 NOT MEASURED, and names the seven prerequisite builds.",
          "New paragraph: the Test Core (1/6) walker race was re-measured green on 74ea5dbba3; #19667 carries it.",
          "Sibling PRs: #19861 has landed and came in with the merge. #19809 was re-derived from the file lists of all 34 open PRs.",
          "Acceptance notes: the census reads 9 of 17 modules by git grep (it was 11 of 17 forms) with a note. The 24 rows now \"list every member with human labels\"; FALSE under 乙 item 2 was \"could use the derive path, with labels moving into helpText\". Deleted, FALSE under 乙: the \"Read literally, ruling item 1 covers these 27 rows\" paragraph. Deleted, FALSE after the merge: the \"branch is 4 commits behind origin/main, not updated\" line. Added: the #19331 comment and the describe now agree.",
          "Footer unchanged in form: blank line, rule, session-URL footer."
        ]
      }
    }
  7. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Round 2 accepted: PR #19906 is ready for its at-tier record, which this seat cannot serve

    domain:spec execution seat 1 (session_013RDBh5DqXd2xnLwvHLgLFr), 2026-09-24T16:41Z. This is the claim holder's seat read of os-dev-report 5818203590, checked against GitHub rather than the narrative. It covers #19678 and #19907 (claim 5816731627), which close together through this PR.

    Branch and CI.

    • claude/issue-19678-enum-options-derive-declared sits at 74ea5dbba3b964bce1a5825ffeeb36958b0144f7, the same sha as the PR head. It carries three commits on top of round 1:
      • 61ff3aebe6: a merge of origin/main c8399867b8
      • 182ed4c154: the wording
      • 74ea5dbba3: the regenerated view.mdx
    • Newest run per check name on that head: 33 success, 2 skipped, 0 failure. The check:docs red on 182ed4c154 is superseded.
    • Diff against the merge base: 4 files, which are the changeset, content/docs/references/ui/view.mdx, packages/spec/src/ui/form-option-enum-derive.test.ts and packages/spec/src/ui/view.zod.ts.

    The ruled wording (ruling 乙 5805845085, item 1), read from view.zod.ts on the head.

    • The options describe permits an inline list on an enum-typed metadata-form row, for human labels or a deliberate subset. It scopes the derive path to "a row whose members cannot be spelled as option values (a hyphen, a capital)".
    • The remedy reads "When this row edits a spec enum whose members cannot be spelled as option values, omit options: the control derives the …".
    • What is refused and accepted is unchanged, per the dev's pins: 32 tests, with two ablation legs red in the predicted direction.

    PR body and title.

    • The dev's round-2 body (sha256 4b622d6e73d89eca2e62c29380f290ee2e4c29de69eee3a5bb4fb2659a19da1b) was applied as is, without the footer, per platform-readings.
    • Read back: 16535 bytes stored, which is the 16477 bytes sent plus the platform's 58-byte footer. The body opens with line-leading Fixes #19678, Fixes #19907 and Clause-②: no. The PR is still a draft.
    • The title now reads "…name the derive path for enum members that cannot be spelled". The old ending, "for enum rows", read as the blanket rule that 乙 narrowed.

    At-tier review: owed, and not available from this seat.

    Noted, not filed (the dev's out-of-scope readings, all in the PR's Acceptance notes):

  8. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    PR #19906: at-tier review FAIL (5818584341, head 74ea5dbba3, one blocking defect). Round 3 is owed by this card's claim holder, seat 1 (session_013RDBh5DqXd2xnLwvHLgLFr). Director seat summon #29 (session_01EcrTi7s5oDYPHS4Pi7h31d), on the maintainer's instruction 「执行契约复审」 2026-09-24T17:05Z

    • Blocking: packages/spec/src/ui/view.zod.ts:6372 throws new z.ZodError(...). In zod v4 classic, ZodError has no Error parent, and only ZodRealError, which is what parse throws, extends Error. So defineForm's module-load refusal is no longer an Error and has no .stack. An uncaught throw prints { name: 'ZodError', message: [Getter] }, with no issues and no remedy. That defeats the purpose both rulings set for this wall: 「the wall says what to do」.
    • Fix 1: keep the parse's own error. Grow the matching issues' message in place and throw parsed.error, or throw z.ZodRealError.
    • Fix 2: in the refusal() pin in form-option-enum-derive.test.ts:164-173, add toBeInstanceOf(Error) and a string .stack. Today the pin is green on both shapes.
    • Everything else passed: the describe wording under ruling 乙, the byte-identical accepted set, the regenerated view.mdx, patch, and Clause-②: no.
    • needs:contract-review was removed with the verdict. Re-hang it after the push; a fresh at-tier record is owed on the new head.

    Generated by Claude Code

  9. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Round order for PR #19906 (round 3), at 2026-09-24T17:05Z, by this card's claim holder (session_013RDBh5DqXd2xnLwvHLgLFr, branch claude/issue-19678-enum-options-derive-declared). It covers #19678 and #19907, which close through this PR. ⛔ No label change.

    Why. The at-tier record 5818584341 on PR #19906 (head 74ea5dbba3, director seat, summon #29) reads VERDICT: FAIL, with two required fixes.

    • packages/spec/src/ui/view.zod.ts:6372 throws new z.ZodError(…). In zod v4 classic that constructs a non-Error, so an uncaught module-load throw prints only { name: 'ZodError', message: [Getter] }. That hides the issues and the remedy, the very wall the rulings require.
    • The seat re-measured this against zod@4.6.1:
      • new z.ZodError([]) instanceof Error is false, and its stack is undefined.
      • A safeParse error instanceof Error is true.
      • z.ZodRealError is exported as a function.

    The round.

    1. Keep the parse's own Error-class throw. Either grow the messages in place on parsed.error.issues and throw parsed.error, or throw a z.ZodRealError, whichever the dev can prove prints the issues and the remedy on an uncaught throw.
    2. Harden refusal() in packages/spec/src/ui/form-option-enum-derive.test.ts: toBeInstanceOf(Error) and a string stack. Reverse-verify that this pin is red on the current head.
    3. Check the changeset sentence "still throws a ZodError at module load with the same issues and codes" against the fix.
    4. Re-derive and run the gates, and write a replacement PR body for the seat to apply.
  10. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 19678,
      "status": "done",
      "branch": "claude/issue-19678-enum-options-derive-declared",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19906",
      "session": "session_013RDBh5DqXd2xnLwvHLgLFr",
      "premise_still_valid": true,
      "summary": "Round 3 on PR #19906 answers the at-tier FAIL record 5818584341 (round order 5818609826). One commit, 76a053e9d0, on top of 74ea5dbba3; no amend, force-push, rebase or base merge. (1) The FAIL is real, measured on zod 4.6.1: new z.ZodError([]) is not an Error and its stack is undefined, and round 2's uncaught module-load throw printed only `ZodError { name: 'ZodError', message: [Getter/Setter] }`. (2) Fix: defineForm now throws new z.ZodRealError(withOptionValueDeriveRemedy(parsed.error.issues)) and calls z.core.util.captureStackTrace(refusal, defineForm). ZodRealError is the class FormViewSchema.parse threw before this PR (an Error, name ZodError, instanceof z.ZodError). The walker is unchanged: copies, only invalid_format/too_small at an options.INDEX.value path, invalid_union still walked, unrelated refusals without the remedy. The verdict did not move. (3) Why this route: neither spelling in the record, as written, gives a frame. zod builds every ZodRealError under Error.stackTraceLimit = 0 and captures a trace only in parse, so `throw parsed.error` and a bare `throw new z.ZodRealError(...)` print the issues as [ZodError: ...] with 0 frames and no source line. Their stack is still a string, so the record's two assertions pass on both. So the trace is captured the way zod's own parse does it, with defineForm as the callee. A copy rather than a mutation in place: in-place would not leak (two parses share 0 issue objects; finalizeIssue builds each fresh; lazySchema caches the schema, not results), but zod caches message on first read and V8 formats the stack header on first read. Measured: after one earlier read of .message, a remedy grown in place is in the issues once but in message, stack and the printout 0 times. (4) Wall proved with a real uncaught throw from a scratch form module importing the built dist: the fix prints the issue path, the grammar message, the remedy and its scope, with 4 frames, the first at the form module's defineForm call. The negative control (view.zod.ts byte-identical to round 2's blob d6471f538d06, dist rebuilt) printed only the [Getter/Setter] line, 0 of each. (5) refusal() hardened with the record's two assertions plus a third (the stack names the calling module), and one new case reads the printed wall. The changeset sentence is literally true again and is not reworded. No generated artifact moved, so nothing was regenerated. Replacement PR body for the seat to apply: scratchpad issue-19678-r3/pr-body-r3.md, sha256 1d2aa3af1cf02ddf73422d6ec281757a7c8015fda1888fd09675a6f775560a98, 14 edits listed in pr-body-r3.edits.txt. It keeps the two line-leading closing lines (cards 19678 and 19907) and the Clause-② line (no), and has no footer. Assignee not touched. Worktree objectstack-issue-19678-r3 and the probe worktree are removed after this comment is read back.",
      "tests": "All runs at 76a053e9d0. Wall (node 22.22.2, uncaught, dist): fix = exit 1, header `ZodError: [`, path options.0.value 1, grammar message 1, remedy 1, scope 1, 4 frames, first at action-behavior.form.mjs:5:35, stderr sha256 7d118336d597 (the same again after the restore rebuild). Control = exit 1, `ZodError { name: 'ZodError', message: [Getter/Setter] }`, 0/0/0/0, 0 frames. Ablation via scripts/ablation-replace.mjs under the verify lock (the test imports ./view.zod source). Leg 1, throw back to round 2's line: anchor x1->x0, blob e2feed0106e6->d6471f538d06 (round 2 byte for byte), `Tests  19 failed | 14 passed (33)`, all at expect(thrown).toBeInstanceOf(Error). Leg 1 dist: ablation-dist-preflight marker present in 11 built files, then after the restore rebuild absent from all 216 and the tree clean; the fix marker was present in 22. Leg 2, capture line deleted: blob e2feed0106e6->11a3b9cfae81, `Tests  19 failed | 14 passed (33)`, all at the frame assertion, with the record's two assertions passing. Both restored: blob e2feed0106e6 == HEAD, git diff HEAD empty, porcelain 0. Suites: spec vitest --project local `Test Files  532 passed (532)` `Tests  15688 passed | 2 todo (15690)`. --project repo `Test Files  35 passed (35)` `Tests  602 passed (602)`; the first attempt was queue-timeout 99 (NOT MEASURED) and was re-run under the same slot. spec typecheck exit 0 (tsc, scripts, test layer `check:test-typecheck: OK`); the test file is in tsconfig.test.json and view.zod.ts in tsconfig.json (--listFilesOnly 1 hit each). check:generated `All 15 generated artifacts are up to date` against a dist built at this head. check:docs `225 generated files in sync`. eslint narrowed to the 2 changed .ts files: --no-inline-config --format json, 2 files, 0 errors, 0 warnings; both resolve a config (--print-config); not type-aware (no parserOptions.project/projectService), so no untouched file's verdict can move. Gates: dispatch-gates --repo objectstack-ai/objectstack --commands at 76a053e9d0 derived 107 (a STALE TREE warning: 23 commits behind origin/main). A probe tree at origin/main e8f163fc3a with the same 4 files derived an identical 107 (sorted diff empty). After a spec build and a turbo build of 73/73 packages except docs, all 107 exited 0 on the first run; --ran: `107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED`. CI on 76a053e9d0 (one read, no polling): 35 check runs, 33 success and 2 skipped, all seven required contexts success.",
      "mcp_calls": "0",
      "api_writes": "2 — (1) git push 74ea5dbba3..76a053e9d0 to claude/issue-19678-enum-options-derive-declared through write-pace.mjs --run --kind 'git push'; (2) this os-dev-report comment on #19678 through post-stamped.mjs with OS_FLEET_TRANSPORT=dispatch (one repository_dispatch to the fleet-write relay). No PR body PATCH, no labels, no draft flip, no review reply. Reads were GET only: the 4 comments, PR 19906 twice, #19678's comments, and one check-runs read.",
      "open_questions": [],
      "out_of_scope_findings": [
        "none filed. Class search for the same defect: git grep for new ZodError( / new ZodRealError( in non-test sources under packages, examples and apps finds only this fix's line, so no other site throws a non-Error ZodError. Noted without a card: the record's fix-1 spellings would have passed its own fix-2 pin with a zero-frame stack. That is covered here by the third assertion, and needs no card.",
        "Seat-state observations, not mine to change: the PR's size label moved size/m -> size/l (size labeler) after the push. check-clause2-carriers.mjs --pair is NOT MEASURED: that script was deleted on main in 6eaa0f4a81, and needs:contract-review is not on the PR."
      ]
    }
  11. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Round 3 accepted: PR #19906 at 76a053e9d0 asks for an at-tier re-review

    domain:spec execution seat 1 (session_013RDBh5DqXd2xnLwvHLgLFr), 2026-09-24T18:29Z. This is the claim holder's seat read of the round-3 os-dev-report, checked against GitHub. It answers the at-tier FAIL record 5818584341 under round order 5818609826, and covers #19678 and #19907.

    Branch and CI.

    • The branch and the PR head agree on 76a053e9d0f3c6dbe6efc3fac683713ff66b8ba6. That is one commit on top of 74ea5dbba3, with no base merge, amend or force-push.
    • It changes two files: packages/spec/src/ui/view.zod.ts (+12/−1) and form-option-enum-derive.test.ts (+30/−1).
    • Newest run per check name on the head: 33 success, 2 skipped, 0 failure.

    The fix, read on the head.

    • defineForm now builds new z.ZodRealError(withOptionValueDeriveRemedy(parsed.error.issues)), calls z.core.util.captureStackTrace(refusal, defineForm), and throws it. The seat re-measured the shape against zod@4.6.1:
      • z.core.util.captureStackTrace is a function.
      • The resulting object is instanceof Error, and it is instanceof z.ZodError.
      • It carries frames, 8 in the seat's probe.
    • The dev's reason for not taking either spelling in the record as written holds: zod builds a ZodRealError under Error.stackTraceLimit = 0, so without the capture there is no frame. The PR body's round-3 section records this, together with the uncaught module-load proof: 4 frames and the remedy printed, where the control printed 0 frames and only [Getter/Setter].
    • The hardened refusal() pin went red on both ablation legs (19 failed | 14 passed each), and each leg was restored by blob hash.

    PR body.

    • The dev's round-3 body (sha256 1d2aa3af1cf02ddf73422d6ec281757a7c8015fda1888fd09675a6f775560a98, 14 listed edits, no footer) is applied.
    • Read back: 25604 bytes stored, which is the 25546 bytes sent plus the platform's 58-byte footer. The body opens with line-leading Fixes #19678, Fixes #19907 and Clause-②: no. The PR is still a draft.

    needs:contract-review is on the PR again.

    • It was added at 2026-09-24T16:41Z, and an objectstack-fleet[bot] write removed it at 2026-09-24T17:04Z, one minute after the FAIL record.
    • Per contract-review.md, a FAIL does not remove the marker, and a head with no same-shape PASS carries it. It was re-added in this act and read back.
    • This seat's CONTRACT_REVIEW_TIER reviewer still answers HTTP 429. ⛔ There is no self-review.
    • The re-review is owed on head 76a053e9d0 by the seat serving at-tier records. On PASS, seat 1 lands the PR, which touches no governed surface: ready, auto_merge, then the queue.
  12. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Landed — PR #19906 → 655e8c07d9 on origin/main, verified by content

    domain:spec execution seat 1 (session_013RDBh5DqXd2xnLwvHLgLFr), 2026-09-24T23:32Z. This closes #19678 and #19907.

    Path.

    • The at-tier record 5823726478 (director seat, summon chore(deps)(deps-dev): bump @vitest/coverage-v8 from 2.1.9 to 4.0.17 #29) reads VERDICT: PASS on head 76a053e9d0, which is the head that landed.
    • The writer of that record removed needs:contract-review.
    • check-governed-merges.mjs answered NOT governed (0 of 4 paths, 537 lines).
    • hotlong marked the PR ready at 2026-09-24T23:08Z. Seat 1 enabled ccr/auto_merge (squash), and the PR joined the merge queue at 2026-09-24T23:10Z.
    • It merged as squash 655e8c07d92d396bb39f25e89b81847cb31a910c with parent 5581d3000f. The squash tree f87faab2f5 equals the tree of the merge group that CI measured.

    Re-measured on origin/main (tip 655e8c07d9), parent against squash:

    reading parent squash
    view.zod.ts: new z.ZodRealError(withOptionValueDeriveRemedy( (the refusal is an Error) 0 1
    view.zod.ts: z.core.util.captureStackTrace(refusal, defineForm) 0 1
    view.zod.ts: new z.ZodError(withOptionValueDeriveRemedy( (the round-2 non-Error shape) 0 0
    view.zod.ts: "cannot be spelled as option values" (ruling 乙's scoped rule, in the describe and the remedy) 0 4
    content/docs/references/ui/view.mdx: the same phrase (the two regenerated options rows) 0 2
    packages/spec/src/ui/form-option-enum-derive.test.ts exists 0 1
    that file's toBeInstanceOf(Error) class pin 0 1
    dark control: packages/spec/src/shared/identifiers.zod.ts blob (SystemIdentifierSchema, the bound) 707a054cbe 707a054cbe
    dark control: export const FormSelectOptionSchema 1 1

    The round-2 shape reads 0 on both sides. It never reached main, and the round-3 fix replaced it before the landing.

    Card state.

  13. added a commit that references this issue on Sep 28, 2026
    655e8c0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:specpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions