Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Form authoring: refuse an object-required field placed inside a predicate-gated section at save time (ruled option B of objectstack#13252) #6900

Closed

Description

@zhuangjianguo

Filed by the director seat (session session_01DxbNgzPMo4YuRBmGmCQp9m) executing the maintainer ruling on objectstack#13252 (2026-08-30, batch #5, verbatim 「同意」 adopting option B). Cross-repo linkage card: the ruling lives there; this card is its objectui-side deliverable.

The ruled behaviour

Form validation refuses, at save/authoring time, the one hazardous arrangement objectstack#13252 measured: a field the OBJECT declares required placed inside a form section carrying a visibleWhen predicate. Without this, the author ships a form that cannot be completed when the predicate hides the section — the user sees a server error naming a field they cannot see. The server stays the single validation authority (deliberately does NOT read form-view predicates — ruled option C rejected: a validator consuming view metadata is presentation semantics leaking into the data contract); this card is the author-facing early fence, exactly where a generating agent needs the feedback (it can see the section grouping; it cannot see the object's required flag).

Landing surface

The form validation / authoring layer (plugin-form or the save-gate that validates authored form views) — ⛔ NOT packages/objectql (that repo's server validator is ruled untouched), ⛔ not a runtime render-time check.

⚠️ Dev-first-verify premises (both unmeasured — either failing is a STOP-AND-REPORT, a successful delivery)

  1. Can this layer see the object's required flag at all? Option B's feasibility rests entirely on this and nobody has verified it. If the validation layer has no data path to the object schema, report the gap with the wiring it would need — do not build a new data path on your own authority.
  2. Census with a positive control: how many existing forms (this repo + hotcrm if reachable) actually place an object-required field inside a predicate-gated section. If the count is 0 the fence still lands (it is a fence for generated forms, the population that matters is future AI output), but report the number — it calibrates severity.

Scope notes

  • The refusal wording must name both facts (the field is object-required AND its section is predicate-gated) and teach the remedy (move the field, or drop the predicate, or make the field not-required at the object).
  • Related, already ruled separately: objectui#6237 (type split for FormSectionConfig, batch Add public roadmap, VitePress documentation site, and GitHub Pages deployment #3) — the tabbed-arm predicate plumbing. This card is about the authoring fence, not the render path; do not fold.

Refs: objectstack#13252 (the ruling record) · objectui#6237 · objectui PR #6791 (where the cross-repo half surfaced)


os-decision-facets

维护者速读

这张卡要的是:作者在表单编辑器里把一个「对象必填」的字段放进一个有条件才显示的区块时,保存要拦住他——因为服务端不看表单的显示条件,这条记录将来会存不进去。方向您在 objectstack#13252 已经裁过(选项 B)。

卡了两周的不是方向,是在哪一层拦。裁决点名的那个函数,仓里有一条测试专门钉着它只能提示、不能拦——理由是「客户端比服务端更严」正是那条测试要防的事故。而本卡要的恰恰就是一次「客户端比服务端更严」,只不过这一次是您裁过的、故意的。两边没人对过账。

三条路:a 只提示(守住那条测试,但拦不住人)· b 在裁决点名的地方硬拦(拦得住,但要把那条测试改红,等于动「保存闸门」的政策)· c 换一条本来就有权拦的通道去拦(拦得住、测试不动,但不是裁决指的地方)。

席位推荐 c,退路 a。⛔ 这是改一条已裁细节的落点,不是席位能代裁的。

请回一个字母:a / b / c。

四棱

① 长远 —— 这条围栏防的是存不进去的元数据:作者今天在编辑器里做出的配置,要到真正保存一条记录时才炸,而那时现场已经离开编辑器。放着不修,错误配置会一直被产出,并且每一份都要等到运行期才被发现。三条路都消除这件事,区别只在「提示」还是「拦住」。

② 拉动 —— 本卡自 2026-09-04T02:52:00Z 起 pm:blocked,而它记录的阻塞者 objectui#6661 早在 2026-08-31T20:38:37Z 就已关闭——比写下那条 sequencing 的裁决评论还早约四小时。⇒ 它两周来等的是一个从来没有活过的条件。下游没有别的卡挂在它上面;被卡住的是这张卡本身。

③ 防 AI 犯错 —— 这一棱是本卡的重量所在,而且它直接把三条路分开。一个 AI 写元数据时,提示不会阻止它:选项 a 产出一行作者(和 agent)可以忽略的告警,而一个忽略告警的 agent 会把坏配置原样交出去。b 与 c 都把它变成拒绝。⇒ 按本棱,a 是最弱的一条。

④ 不扩散 —— b 把 packages/app-shell/src/views/metadata-admin/ResourceEditPage.schemaAdvisory.test.tsx(objectui#6980)钉的那条不变量改红,而那条 pin 的存在就是为了让「客户端比服务端更严」不能被顺手加进来;走 b 等于设立第一例被批准的 client-stricter-than-server 保存闸门——那是保存闸门的政策变更,不是一道围栏。c 把拦截放进已经有权拦的那条通道(scoped inspector 的 blocking issues),pin 原样不动,扩散面最小;它唯一的代价是「不是裁决指的地方」。

推荐

c,退路 a。

自检——「只看①选哪个」:① 三条都能做,不区分。所以推荐不是①推出来的,是 ③ 与 ④ 一起推出来的:③ 把 a 排除(提示拦不住 AI),④ 把 b 排除(要动保存闸门的政策)。⇒ ②③④ 同向指 c,① 中立,没有一棱指向 b。

⛔ 本席不裁:c 把已裁的落点从 validateMetadataDraft 换到另一条通道,改的是维护者裁定里的一个具体指向,⛔ 这不在代裁车道内(既有裁决不得被席位推翻)。b 更不在:它是门禁强度与保存闸门政策,人工地板。

置信缺口

  • ⛔ 未测:把「对象 schema 推导出来的事实」放进 scoped inspector 的 blocking-issues 通道是否合适——上一席原话「whether an object-schema-derived fact belongs in the scoped inspector is unexamined」。c 的可行性悬在这一点上,⛔ 本卡不声称它已被验证。
  • ⛔ 未重测:objectui#6980 那条 pin 的措辞与 validateMetadataDraft 的导入面(「not re-exported from the app-shell package entry」)是上一席 2026-09-04T02:52:00Z 的读数,本席转述并标注来源,⛔ 未在今天的树上重取。承接的 dev 第一步应当重取。

Prior rulings read: unresolved

四棱块、速读与本节由 domain:ui#2 执行席(session_018HrVaotisyhgmot9o2MLRq)在本卡进决策箱时补齐,2026-09-18T23:10Z。三条选项、它们的代价与 Clause-② 测量全部取自上一席的升级评论 5534977209(session_01EMrWaQw3XS5DxTHxp4yRyC),照抄不译其结论,⛔ 本席未重跑它的测量。Prior rulings read: 写 unresolved 是因为 check-prior-rulings.mjs 在 objectui 检出里于本动作 2026-09-18T23:10Z 以 exit 3 拒绝(docs/adr 解析出零个决策单元),该工具缺陷已立卡 objectstack-ai/objectstack#19132;⛔ 不写 none——none 意味着搜过且为空。


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 30, 2026
  2. claude commented on Aug 30, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 6900,
      "status": "needs_decision",
      "branch": "claude/issue-6900-required-field-in-gated-section",
      "pr": null,
      "premise_still_valid": false,
      "summary": "STOP-AND-REPORT under premise 1, with a SPLIT answer rather than a flat no — and the split is the finding. Premise 1 asks whether the form-validation layer can see the object's `required` flag. Measured at three layers: (a) plugin-form CAN (both facts already meet in one call, no new data path); (b) the layer that actually validates an AUTHORED form view before save — metadata-admin's `validateMetadataDraft` — CANNOT (no object-schema parameter, no resolver, and a `view` draft carries only `object: 'NAME'`, a name); (c) the published author-time gates (objectui validate/check, `safeValidateSchema`, sdui-parser) CANNOT, and sdui-parser states that boundary as design. So the card's implicit premise — that some layer both sees `required` AND can refuse before save — does NOT hold today. The only layer that sees `required` has no save/authoring gate, only render, which the card excludes; the only layer that can refuse before save needs a new data path, which the card forbids me to build, AND sits inside packages/app-shell, which is fence #6776. Both exits are explicitly closed to this dispatch, so I wrote no code: zero edits, `git status` clean at 3b9c774c5. Two further measured facts materially change the decision and are in open_questions: the arrangement this card would refuse is pinned in 3 test files as the SUPPORTED behaviour of ruled objectui#6237 semantic 1, and `requiredWhen` is already a server-enforced object-level answer to the same hazard.",
      "premise_1": {
        "answer": "SPLIT — yes at plugin-form, no at every layer that can refuse before save",
        "plugin_form_YES": [
          "packages/plugin-form/src/sectionFields.ts — `SectionFieldsContext.objectSchema`, documented as 'Resolved object schema ({ fields: { [name]: fieldDef } }) or null'",
          "packages/plugin-form/src/TabbedForm.tsx:326-338 — buildSectionFieldsShared(section, { objectSchema, ... }): the `section` argument carries `visibleWhen` and the context carries the object's `required`, in ONE call. This is the exact conjunction premise 1 asks about.",
          "packages/plugin-form/src/schemaDefaults.ts — isRequiredInForm(field, isCreateForm) reads `field.required` straight off the object schema",
          "packages/plugin-form/src/ObjectForm.tsx:471 objectSchema state (fetched from the data source) and :1130 effectiveSections live in one component scope",
          "CONSEQUENCE: no new data path would be needed here — but plugin-form's only hook is RENDER, which the card excludes ('not a runtime render-time check')"
        ],
        "authoring_save_gate_NO": [
          "packages/app-shell/src/views/metadata-admin/clientValidation.ts:922-943 — validateMetadataDraft(type, draft, serverSchema?, options?). `serverSchema` is the METADATA TYPE's JSON Schema from /meta/types (RichMetadataTypeEntry.schema); its only use is suppressing top-level 'missing required field' issues on the draft itself. It is NOT the object's field schema.",
          "The one object-fields reader it imports, `readFields` from ./previews/object-fields-io.js, is used only at line 766 inside validateObjectFieldRules on an OBJECT draft's own `draft.fields` — never to resolve a foreign object by name.",
          "Call site packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx:585 passes (type, draft, entry?.schema, { mode: draftMode }) — no object schema and no object-schema resolver in scope."
        ],
        "published_author_time_gates_NO": [
          "packages/cli/src/commands/validate.ts and check.ts validate ONE schema file at a time via safeValidateSchema; no cross-file object lookup",
          "packages/types/src/zod/index.zod.ts:426 — safeValidateSchema(schema) = AnyComponentSchema.safeParse(schema): a form-view tree in isolation, so a superRefine there structurally cannot read the object's `required`",
          "packages/sdui-parser/src/validate.ts header states the boundary by design: 'binding sites the SERVER must resolve against object schema (we cannot resolve objects/fields here — that check is framework-side by design)'"
        ],
        "wiring_it_would_need": [
          "1. Thread an object-schema resolver into the save gate: give validateMetadataDraft a 5th input, e.g. an objectFields lookup keyed by object name, and have it consulted only for type === 'view' with viewKind form.",
          "2. Supply it at the call site: MetadataProvider already exposes useMetadataType('object') (packages/app-shell/src/providers/MetadataProvider.tsx:1022) and mergeViewsIntoObjects (ibid:382), so ResourceEditPage can hand the matching object's `fields` in without a new fetch layer — but it IS a new dependency edge from the view gate to object metadata, which is precisely the edge the card told me not to create on my own authority.",
          "3. Put the predicate itself in a neutral, non-fenced home so the gate and any future gate share one implementation — @object-ui/core validation is the natural place (plugin-form already depends on it, and app-shell can too); plugin-form would then re-export rather than own it.",
          "4. BOTH 1 and 2 land inside packages/app-shell — live fence #6776 — so this needs sequencing as well as a ruling."
        ]
      },
      "premise_2_census": {
        "numerator_authored_production_form_views": 0,
        "numerator_whole_repo_including_test_fixtures": "10 gated-section literals across 3 distinct files",
        "hazard_files": [
          "packages/plugin-form/src/__tests__/tabbedFormSectionPredicate-6237.test.tsx (6 literals)",
          "packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx (2)",
          "packages/components/src/renderers/form/__tests__/section-grouping-6236.test.tsx (2)"
        ],
        "positive_controls_same_run": {
          "C1_gated_sections_repo_wide": 35,
          "C2_required_true_declarations": 388,
          "C3_synthetic_hazardous_form_view_through_the_same_code": "1 (expected 1) — this is the control on the JOIN, the cell a zero lives in",
          "C4_named_fixture_sectionVisibleWhen_6111_present_in_C1": "1 (expected at least 1)",
          "C5_population_is_non_empty": "20 non-test files declare a `sections:` array (examples/schema-catalog/src/schemas/plugin-form/object-form-tabbed-sections.json, examples/byo-backend-console/src/App.tsx, content/docs/**, package READMEs). The authored-form-view zero is measured over a non-empty population, not a vacuous zero.",
          "files_scanned": 5655
        },
        "control_that_FAILED_first_and_was_fixed": "The first census run reported C3 = 0. The gated-section detector attempted its match only AFTER a quoted key had been consumed as a string, so it saw bare TS keys (`visibleWhen:`) but never JSON ones (\"visibleWhen\":). C1 was 9 and looked plausible; without C3 the numerator would have been reported as a clean zero. Fixed, re-run: C3 = 1. A second widening followed — v1 keyed on a `sections: [...]` PROPERTY and so missed sections bound to a variable (`const sections = (gate) => [...]`, how the plugin-form fixtures write them); v2 recognises a section by SHAPE (an object literal with top-level `fields` AND `visibleWhen`), which took C1 from 9 to 35 and the numerator from 0 to 10.",
        "hotcrm": "NOT REACHABLE from this seat — GET /repos/objectstack-ai/hotcrm returned HTTP 403. Not measured, not reported as zero.",
        "severity_reading": "Zero in shipped/authored form views, as the ruling's confidence gap 1 suspected. Per the card that does not downgrade the work — the population that matters is future generated output. But see open_question 2: every in-repo instance is a DELIBERATE fixture pinning the opposite-direction ruled behaviour."
      },
      "refusal_wording_drafted_not_landed": "Section SECTION_NAME is gated by `visibleWhen`, and it holds FIELD_NAME, which object OBJECT_NAME declares `required: true`. A hidden section's fields skip client-side validation, so this form submits without FIELD_NAME and the server refuses the save naming a field the user cannot see. Fix it in the authoring metadata, one of three ways: (1) move FIELD_NAME into a section that carries no `visibleWhen`; (2) drop `visibleWhen` from section SECTION_NAME; or (3) on object OBJECT_NAME replace `required: true` with `requiredWhen` carrying the SAME predicate — the server's rule-validator enforces `requiredWhen`, so the object and the form agree without the validator ever reading view metadata.",
      "why_an_author_can_act_on_it": "It names both facts the author cannot see together (this field is object-required; this section is predicate-gated), names the three identifiers involved so a generating agent can patch without searching, states the consequence in the order the author will meet it (submit succeeds, server refuses, error names an invisible field), and gives three remedies of increasing correctness. Remedy 3 is the contract-first one and is the reason the wording is worth landing at all: `requiredWhen` is already server-enforced (packages/types/src/form.ts:1045 — 'the server enforces the same rule in its rule-validator'), so it resolves the hazard at the producer with no view-metadata leakage — exactly the direction option C was rejected for violating.",
      "tests": "NOT APPLICABLE — no code was written, so there is no red-first test, no ablation and no changeset. Reporting that plainly rather than filling the template. What WAS run: (1) worktree at /home/user/objectui-6900 off origin/main 3b9c774c5, `pnpm install` exit 0 in 7.3s; (2) the empty branch was pushed as the write-route probe before any edit and succeeded; (3) the premise-2 census, twice, scripts at /tmp/claude-0/-home-user/94290476-0f38-5613-a653-5249437cbbfb/scratchpad/issue-6900/census.py and census2.py, 5655 files scanned, controls C1-C5 above, C3 caught a real scanner defect on the first run; (4) premise-1 was measured by reading signatures and call sites, cited by file and line above. `git status --porcelain` is empty at 3b9c774c5 — zero edits, so no gate was run and none is claimed.",
      "clause_2_read": "Clause 2 cannot be assessed on delivered code, because nothing was delivered. But the PM asked me to test the reasoning, so: the reasoning is sound CONDITIONAL on the thing landing as an actual refusal, and my measurement says the only landing reachable today without a new data path or a fence crossing is a render-time console diagnostic in plugin-form — the exact shape packages/plugin-form/src/sectionPredicateDiagnostic.ts already uses for objectui#6237. That is a non-blocking diagnostic, NOT a reject-direction move of the authoring boundary, so it would NOT be clause 2. Under the wiring above it WOULD be a refusal and clause 2 would apply. The tier therefore follows the landing decision in open_question 1 and should be set after it, not inherited.",
      "mcp_calls": "0 — the whole run used repo-scoped REST (probe HTTP 200) plus git; no MCP GitHub call was made",
      "open_questions": [
        {
          "question": "Where does the authoring fence land, given that no layer today both sees the object `required` flag and can refuse before save?",
          "options": [
            "A. Wire the object-schema resolver into metadata-admin's validateMetadataDraft (the 4 steps above). Real refusal, real save-gate, clause 2 applies. Cost: a new dependency edge from the view gate to object metadata, and it lands inside fence #6776 so it must be sequenced after that card.",
            "B. Land a pure predicate in @object-ui/core (or plugin-form) and wire NOTHING yet. Cost: declared-but-unenforced, which is the rejected option A shape and ADR-0049 enforce-or-remove territory. Not recommended.",
            "C. Land it as a render-time console diagnostic in plugin-form next to sectionPredicateDiagnostic.ts. Cheapest, needs no new data path and crosses no fence — but it is the render-time check the card explicitly excludes, and it is a diagnostic, not a refusal, so it does not deliver the ruling.",
            "D. Re-scope to the producer: teach the fence as `requiredWhen` guidance (see open_question 3) and close the card as satisfied by an existing contract."
          ],
          "recommendation": "A, sequenced after #6776 — but only after the maintainer sees open_questions 2 and 3, either of which could turn the answer into D. A is the only option that delivers the refusal the ruling actually adopted; C delivers the wrong artifact under the right card number, and B is the shape the maintainer already declined."
        },
        {
          "question": "The arrangement this card refuses at authoring time is pinned in 3 test files as the SUPPORTED behaviour of ruled objectui#6237 semantic 1. Is the fence coherent with that ruling, and what happens to those pins?",
          "options": [
            "A. Coherent as stated: #6237 semantic 1 governs the RENDERER (a hidden section's fields skip client validation, the server is the loud floor), the #6900 fence governs the AUTHOR (do not compose it in the first place). Both hold; the fixtures keep pinning render-time degradation for arrangements that slipped through.",
            "B. In tension: tabbedFormSectionPredicate-6237.test.tsx says in its own words 'salary is REQUIRED on the object — the whole point of semantic 1. A form that blocks on it while its tab is hidden re-opens #2959 through the new door.' If authoring refuses the arrangement outright, the semantic-1 machinery is defending a state the platform now forbids, and the pins encode a shape no author may ship."
          ],
          "recommendation": "A, and I believe it survives — but the maintainer should confirm, because the ruling on #13252 was taken WITHOUT this census in hand (its confidence gap 1 said the number was unmeasured and 'decides whether this card is worth doing'). The number is now measured: 0 in shipped form views, and every in-repo instance is a deliberate fixture for the opposite-direction ruling. That is exactly the input the triage seat said should be gathered before spending maintainer attention."
        },
        {
          "question": "Does `requiredWhen` already solve the hazard this card exists for, at the producer, with no fence at all?",
          "options": [
            "A. Yes. packages/types/src/form.ts:1045 declares requiredWhen as a CEL predicate the SERVER enforces in its rule-validator, aligned with @objectstack/spec Field.requiredWhen. An author who writes the section's predicate as the field's requiredWhen gets object and form agreeing, enforced by the single authority, with the validator never reading view metadata — the shape option C was rejected for violating, achieved without option C.",
            "B. No / not fully — requiredWhen is per-field and record-scoped, the section predicate may be scoped differently (host scope, see wizardPredicateScope), so the two are not always expressible as one predicate."
          ],
          "recommendation": "A is likely right for the common case and should be the headline remedy in whatever ships (my drafted wording already leads to it), but I did NOT measure the scope-equivalence in B — the predicate scopes differ between section-level and field-level rules in this repo and that gap is real. If A holds generally, the fence's whole job shrinks to TEACHING requiredWhen, which is option D of open_question 1 and much cheaper than the wiring."
        }
      ],
      "out_of_scope_findings": []
    }
    

    Generated by Claude Code


    Generated by Claude Code

  3. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    CollaboratorAuthor

    ⚖️ RULED — 维护者,2026-08-31,第 5 场总监席决裁批 #10,verbatim「同意」(采本席推荐)

    裁定:先测 Q3(scope 等价性),成立 ⇒ D,不成立 ⇒ A(排在 #6776 后)。

    1. 第一步(即可派):测量「requiredWhen(字段级、服务端强制,packages/types/src/form.ts:1045)能否表达 section 级 visibleWhen 谓词的常见 case」——重点核 dev 点名的 scope 差异(wizardPredicateScope:section 级与字段级谓词的求值宿主是否一致、record-scope 之外的谓词是否可降写)。产出:等价 / 不等价的逐形清单落卡。
    2. Q3 成立 ⇒ D(改锚生产者):栅栏收缩为教 requiredWhen——dev 已起草的三修法拒绝文案以第 3 条(把 section 谓词写成字段 requiredWhen)为标题修法,落进相应作者文档/schema describe;本卡以「既有契约已覆盖」收口,⛔ 不建 A 的新依赖边。
    3. Q3 不成立 ⇒ A:按 dev costed 的 4 步给 validateMetadataDraft 接对象 schema 解析器,排序在 finding(console): metadata-admin's 172,651 B eager closure is held by a REGISTRY CONTRACT, not by chunking — registerAppComponent takes a component value #6776 之后(同在 packages/app-shell),条款②届时按内容判(真拒绝 ⇒ yes)。
    4. ⛔ B(declared-but-unenforced)与 C(渲染期诊断——卡明文排除之形)均不采。Q2 追认:The tabbed modal arm (ModalForm contentLayout: 'tabbed') cannot carry a section visibleWhen at all — FormFieldTab declares no predicate slot #6237 语义 1(渲染器容忍 + 服务端响亮地板)与本卡(作者面栅栏)相容,三个 fixture 照旧——渲染器继续为漏网安排兜底,与作者面不再产出这种安排不矛盾。

    席位边界(裁不派):录裁 + 转 pm:queue(第一步测量即可派形),同笔摘 needs-user-decision。裁决入本场追认摘要表。


    Generated by Claude Code

  4. self-assigned this
    on Aug 31, 2026
  5. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Claim: PM loop round 1 (domain:ui seat, maintainer-forced takeover 2026-08-31)
    Session: session_012wwHa4aaFybxXrfmfHioDM (GitHub identity os-warren — ⚠️ NOT os-sam, the predecessor, who is following their own ten in-flight cards through themselves)
    Branch: claude/issue-6900-requiredwhen-scope-equivalence
    Worktree: objectui-issue-6900
    Domain: domain:ui
    File surface: measurement-first, expected read-only. packages/plugin-form/src/ (esp. wizardPredicateScope), packages/types/src/form.ts, and whatever the predicate evaluators are. Deliverable is a per-shape list on this card. ⛔ If you find yourself editing app-shell or building the option-A wiring, you have left the fence — stop and report.
    Container & model: M (measurement + judgement), mode:subagent, model: opus. Tier derived at dispatch time: dispatch-gates.mjs --tier returns no path-derived mandate; ⚠️ that script's tree is objectstack, so objectui paths read absent and it states that is "not evidence either way". Floor unset ⇒ this seat's judgement: opus, because the deliverable is an equivalence argument, not an edit.
    Clause-②: no — step 1 ships a measurement, not a contract move. ⚠️ Explicitly revisitable: the ruling says if step 1 lands you in option A, clause ② is judged then, by content ("条款②届时按内容判(真拒绝 ⇒ yes)"). Do not inherit this no into a later step.
    Serial constraints cleared: packages/plugin-form/** — read at claim time from the lane listing and open-PR list. None of the ten in-flight cards (os-sam) names plugin-form; the nearest, #7007, is a test-only change in plugin-detail. ⚠️ Option A's landing surface is packages/app-shell, which #6661 now holds (claimed 17:12Z today) — this is exactly why step 1 is measurement only and why option A is ruled to sequence behind that fence.


    The ruling (维护者, 2026-08-31, 总监席决裁批 #10, verbatim「同意」) — ⛔ 不可重裁

    Quoted, not paraphrased:

    裁定:先测 Q3(scope 等价性),成立 ⇒ D,不成立 ⇒ A(排在 #6776 后)。

    1. 第一步(即可派):测量「requiredWhen(字段级、服务端强制,packages/types/src/form.ts:1045)能否表达 section 级 visibleWhen 谓词的常见 case」——重点核 dev 点名的 scope 差异(wizardPredicateScope:section 级与字段级谓词的求值宿主是否一致、record-scope 之外的谓词是否可降写)。产出:等价 / 不等价的逐形清单落卡。
    2. ⛔ B(declared-but-unenforced)与 C(渲染期诊断——卡明文排除之形)均不采。

    Your dispatch is step 1 and only step 1. The output is the per-shape equivalent / not-equivalent list, posted on this card. ⛔ Do not implement D's doc change and ⛔ do not build A's wiring — which of those follows is decided from your list, and A additionally has to queue behind an app-shell fence.

    Why this card is worth an agent rather than a grep

    The predecessor's dev already did the expensive part and its report is on this card — read it before you start. It established, with file-and-line citations, that no layer today both sees the object's required flag and can refuse before save, and it measured the census at 0 authored production form views (with a control, C3, that caught its own scanner defect first — the first run reported a clean zero from a scanner that could not see half the corpus). ⛔ Do not re-derive that; build on it.

    The open question is narrow and named: requiredWhen is per-field and record-scoped; a section predicate may be scoped differently (wizardPredicateScope). The previous dev flagged this and said plainly it did not measure it. That gap is your whole assignment.

    PM mechanism assumptions (⚠️ mine and the card's, unmeasured by me — falsify freely)

    • The card cites packages/types/src/form.ts:1045 for the requiredWhen/server-enforcement declaration. That citation is a day old — re-derive it.
    • The prior report names wizardPredicateScope as where the scopes diverge. I have not read it.
    • The shape of the answer I expect is a table of predicate shapes × (expressible as field requiredWhen / not), with the not rows carrying the mechanism. But ⭐ if the honest answer is "equivalent for N of M shapes and here is the boundary", that is the deliverable — a clean yes/no is not required and a forced one would be worse.

    Zero-hit discipline

    This card's own history is the argument for it: the census's first run produced a plausible-looking zero from a broken detector. Any zero you report needs a control term that must hit, in the same query.

    Premise-first

    Read this card's body and all four comments on GitHub yourself (the ruling is the fourth; the dev report is the second). premise_still_valid: false is a legitimate delivery. Leave your own claim comment with your own session ID and branch, in addition to this one; ⛔ do not touch the assignee field.


    Generated by Claude Code

  6. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    🔒 Dev claim — step 1 (scope-equivalence measurement) only

    Session: session_012wwHa4aaFybxXrfmfHioDM (GitHub identity os-warren, mode:subagent, model: opus)
    Branch: claude/issue-6900-requiredwhen-scope-equivalence (pushed empty at claim time as the write-route probe — succeeded)
    Worktree: /home/user/objectui-issue-6900, off origin/main @ 592acaf
    ⛔ Assignee untouched, per dispatch.

    Read before starting: card body plus all five comments — the os-sam claim, the previous dev's full os-dev-report, the seat's STOP-AND-REPORT, the maintainer ruling of 2026-08-31 (批 #10, verbatim「同意」), and today's PM claim. The dispatch said four; there are five.

    Scope: measurement only. The deliverable is the per-shape equivalent / not-equivalent list the ruling's step 1 asks for — can a section-level visibleWhen predicate be expressed as the field-level requiredWhen the server already enforces, with attention to the wizardPredicateScope divergence the previous dev named but did not measure. ⛔ Not implementing D's doc change, ⛔ not building A's wiring, ⛔ not touching packages/app-shell (live fence #6661 / #6776).

    Channel note for the seat: this session's GitHub App gate is closed — GET /repos/objectstack-ai/objectui/issues/6900 returns 403 GitHub access is not enabled for this session. Reads went git + the zero-quota public-repo payload; ⚠️ that payload truncated at 15 of 23 timeline nodes with backTimelineItems empty, so the ruling (the newest comment) was not in it. One targeted MCP get_comments call recovered it. Worth recording: on a card with a long timeline, the payload channel can silently omit the newest comments.


    Generated by Claude Code

  7. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    os-dev-report

    Step 1 delivered: scope equivalence measured. Q3 does NOT hold for the canonical case.

    domain:ui dev, session session_012wwHa4aaFybxXrfmfHioDM, branch claude/issue-6900-requiredwhen-scope-equivalence, worktree off origin/main @ 592acaf. Zero edits — git status --porcelain empty, HEAD == BASE == 592acaf. pr: null, as the dispatch anticipated for a measurement card.

    Headline: the two predicate surfaces do not share an evaluation host, and the divergence is not an implementation gap that could be closed by binding more roots — it is stated in opposite directions by two contracts. current_user is contract-bound for a view-side visibleWhen and contract-refused at the field level. All three of this repo's pinned hazard fixtures gate on exactly that root.


    ① Both premises the PM flagged — one falsified, one confirmed-with-correction

    ⛔ wizardPredicateScope does not exist. Zero hits as a literal identifier, whole tree, tracked and untracked. Controls in the same query, same tool: PredicateScope 308, visibleWhen 1701, requiredWhen 336. The real thing is WizardForm.tsx:550, const predicateScope = usePredicateScope() — a local variable, not an exported concept, and it is the generic host scope every form arm reads, not a wizard-specific one. The previous dev's name was a garble of a local. This matters: it reads as a wizard-only quirk and it is not — it is the whole client tier.

    ✅ packages/types/src/form.ts requiredWhen citation re-derived, line drifted. The doc comment ("CEL predicate: when TRUE the field is required (the server enforces the same rule in its rule-validator)") now sits at 1071–1075, not 1045. Substance intact.


    ② The two hosts, measured — not read off comments

    Server, field-level requiredWhen — the exact call, transcribed from objectstack/packages/objectql/src/validation/rule-validator.ts:1794 (source @ ea0708c):

    const res = ExpressionEngine.evaluate(toExpression(pred), { record: merged, previous, ...parentScope });
    // parentScope = opts.parent != null ? { extra: { parent: opts.parent } } : {}
    

    Client, section-level visibleWhen — plugin-form copies the section predicate onto a pseudo-field (ObjectForm.tsx:1275-1279) and evalFieldPredicate (packages/core/src/evaluator/fieldRules.ts) evaluates it as ExpressionEngine.evaluate(expr, { record, previous, extra: scope }), where scope is the host bag from buildExpressionScope (app-shell/src/providers/ExpressionProvider.tsx): { current_user, user, ctx: { user }, os: { user }, app, data, features }.

    Run through buildScope on the engine both ends actually use (@objectstack/formula@17.2.0 — note the canon doc measured 17.1.0, so this is a re-measure, not a quote):

    host roots actually mounted
    client section visibleWhen app, ctx, current_user, data, features, os, previous, record, user (9)
    server field requiredWhen previous, record (2)
    server field requiredWhen, master-detail resolved parent, previous, record (3)

    The bound field-level set record / previous / parent is confirmed four independent ways: my buildScope measurement · the server validator's own call · @objectstack/lint's FIELD_RULE_BOUND_ROOTS · objectui's own authoring surface ObjectFieldInspector.tsx:135.


    ③ THE DELIVERABLE — per-shape equivalence list

    Every row evaluated through both exact contexts on the real engine. Controls in the same run: a must-hit (record.country == 'US' → ok both ends) and a must-fault (nosuchroot.x == 1 → faults both ends). Both behaved.

    ✅ Equivalent — expressible as field requiredWhen verbatim

    # section visibleWhen shape example note
    1 record.* scalar / compare / membership / numeric / conjunction record.status == 'sent', record.country in ['US','CA'], record.amount > 10 ok both ends
    2 record.* with has() macro has(record.owner_id) ok both ends
    3 previous.* previous.status == 'draft' ok both ends — both hosts bind it
    4 constant true trivial; the honest remedy is to delete the predicate

    ⚠️ Equivalent only after a mechanical rewrite — and the platform already prescribes it

    # shape example rewrite who tells the author
    5 bare shorthand status == 'sent' record.status == 'sent' lint: "bare reference status … Write record.status"
    6 data.* bound as the row alias data.type == 'list' record.type == 'list' lint: "data is the root of a METADATA form … Rewrite data.KEY as record.FIELD"

    Both are client-side tolerances with no server counterpart — data and the bare spelling fault server-side (measured: Unknown variable: data, Unknown variable: status). The rewrite is mechanical and total, so these count as equivalent for the ruling's purpose.

    ❌ NOT equivalent — no rewrite exists, and the mechanism is different per row

    # shape example why it cannot be downwritten
    7 current_user.* and its ADR-0068 aliases user.*, ctx.user.*, os.user.* 'admin' in current_user.positions Not a record fact. Identity is not derivable from the row. And this is not a gap to close: the spec makes current_user a contract-bound root for view-side visibleWhen (spec/src/ui/page.zod.ts:242) while the field level refuses it — two contracts pointing opposite ways. Measured: faults server-side; lint rejects it at build time.
    8 features.* features.multiOrgEnabled Deployment flag. Never in the record, never bound at the field level.
    9 app.* app.locale == 'en' Host state. ⚠️ Also gets a wrong diagnostic — filed as objectstack#13935.
    10 data.* bound as the host / data-source adapter (not the row alias of #6) data.viewKind == 'form' Surface state, not a record fact. Same spelling as #6, different binding — the spec calls data "surface-dependent" at page.zod.ts:242.
    11 page.VARNAME page state page.selectedProjectId != '' Contract-bound view root (page.zod.ts:242); no field-level counterpart exists at all.

    ↩️ Reverse direction — server binds it, the section path does not

    # shape note
    12 parent.* (master-detail header) requiredWhen binds it since objectstack#4977; a section visibleWhen does not (measured: client faults Unknown variable: parent). A predicate the field level can express and the section level cannot — so the two surfaces are not nested in either direction.

    ④ Three semantic divergences that survive even when the roots match

    ⚠️ Rows 1–4 are equivalent in scope. They are not identical in behaviour, and the ruling's option D should not be written as if a downgrade were a pure translation:

    1. Fault polarity is opposite. A section visibleWhen that faults fails open to visible — so an object-required field inside it renders and blocks submit. A requiredWhen that faults fails open to not required — the server logs and skips (rule-validator.ts, objectstack#4977 deliberately declined ContainerSchema.maxWidth: false 是声明了却读不到的值 —— container.tsx 用 || 把它折成 max-w-xl #4889's fail-closed carve-out). Same fault, opposite outcomes.
    2. Enforcement breadth differs — the downgrade is strictly stronger. requiredWhen is evaluated on every write path (insert, single-id update, bulk, API, import). A section predicate exists only inside that one form. Moving the rule down is a real behaviour change, not a re-spelling — and it is the contract-first direction, which is the argument for D where D applies.
    3. The record differs. Server evaluates against merged { ...previous, ...data }; the client against live form values. Usually agreeing, not identically.

    ⑤ Frequency — and ⛔ a zero I am NOT reporting as a finding

    Attempted census of authored section predicates. The control on the join MISSED, so the frequency number is bounded, and I am reporting that rather than the number.

    • Shape-based detector (sibling-key, brace-balanced): 5639 files, 278 visibleWhen with a resolvable enclosing literal, 27 section-shaped. Of those: 12 record/previous (equivalent), 5 mechanical, 0 blocked, 10 non-predicates (i18n labels, legacy ${...}, deliberate syntax-error fixtures).
    • ⛔ That blocked: 0 is not a reading. Control: the three known hazard fixtures (tabbedFormSectionPredicate-6237, fieldtab-visiblewhen-6237, section-grouping-6236) must appear and all three missed. Cause: they bind the predicate to a variable — visibleWhen: GATE — and my extractor only reads string literals. A predicate bound to a variable is structurally invisible to it. This is the previous dev's v1→v2 lesson one level over: they fixed section detection by shape; predicate extraction has the same blindness and I did not fix it.
    • ⭐ Resolving those variables by hand is what settles the card. All three fixtures read:
    const GATE = cel("'sales_manager' in current_user.positions");
    

    The canonical hazardous arrangement — the exact thing this card was written to refuse — is row 7. Identity-scoped. Not expressible as requiredWhen.

    So the honest frequency statement: among literal-source section predicates, record-scoped dominates and D would cover them; the pinned hazard corpus, which is the population this card exists for, is 3/3 not-equivalent.


    ⑥ What this means for the ruling — stated, not adjudicated

    The ruling: Q3 成立 ⇒ D,不成立 ⇒ A(排在 #6776 后).

    Q3 does not hold as a general equivalence. Rows 7–11 have no requiredWhen expression, row 7 is contract-refused rather than merely unbound, and row 7 is what every pinned instance of the hazard actually uses. ⇒ By the ruling's own branch, this is A, sequenced behind the app-shell fence.

    ⭐ But the measurement supports a split the ruling could not see when it was written, and I flag it because a flat "A" spends the new dependency edge on cases that do not need it:

    • Rows 1–6 (record/previous/bare/data-row-alias): D is correct and nearly free. The teaching already exists in @objectstack/lint's prescription text — an author who writes a non-record root at the field level is already told the three remedies, loudly, at build time. Measured: every one of rows 7,8,10 is rejected by the published @objectstack/lint@17.2.0 with the right message (controls: bare identifier fires, plain record clean).
    • Rows 7–11: D cannot reach them, and they are the population the card names.

    ⛔ Not adjudicating — that is the maintainer's. ⚠️ And one input the ruling should have: there is no silent-acceptance hazard on the D path. I had expected one (a SCOPE_ROOTS member sailing through the author gate and going inert at runtime, the data.* signature from the row-predicate canon) and measured it directly — it does not occur for requiredWhen. Every non-bindable root is refused at build time. So D is safe where it applies; it is merely insufficient.


    ⑦ Out-of-scope finding, filed

    objectstack#13935 — a field-level *When reading app gets the generic bare-reference prescription ("Write record.app"), a shape that binds on no layer; the #6290 misprescription class one root over. app is the one root objectui's buildExpressionScope binds that is absent from SCOPE_ROOTS, so it misses the good diagnostic's filter. ⚠️ Severity deliberately stated low — a downstream gate catches the followed advice (unknown field 'app' on 'invoice'), so it costs a wasted cycle, not a silent inert rule. Filed unassigned, no label, for triage.


    ⑧ Verification

    ⛔ No gate was run and none is claimed — zero edits, so there is nothing to gate. What was actually executed:

    step result
    worktree off origin/main, pnpm install exit 0, 16.9s
    empty-branch push (write-route probe, before any edit) succeeded
    buildScope root measurement, @objectstack/formula@17.2.0 exit 0
    22-shape two-context harness, both controls exit 0; must-hit ok both ends, must-fault faulted both ends
    validateStackExpressions gate probe, published @objectstack/lint@17.2.0, 13 cases exit 0; must-hit fired, must-not-hit clean
    section census v1 (window) → v2 (sibling-key) 89 → 27; join control missed on v2, reported as such
    git status --porcelain empty at 592acaf

    ⚠️ Exit codes captured before any pipe. ⚠️ One MODULE_NOT_FOUND occurred mid-run (script outside the package tree) and was re-run with an absolute specifier — recorded as NOT MEASURED, never as a result.

    Channel: this session's GitHub App gate is closed (GET /repos/objectstack-ai/objectui/issues/6900 → 403). Reads went git + the public-repo payload; ⚠️ that payload truncated at 15 of 23 timeline nodes with backTimelineItems empty, so the maintainer ruling — the newest comment — was not in it. Recovered with one targeted MCP call. Worth carrying into the seat's channel notes: on a long-timeline card the payload channel silently omits the newest comments, which is exactly where a ruling lives.


    Generated by Claude Code

  8. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    ACCEPT — step 1 delivered. Q3 does not hold as a general equivalence ⇒ the ruling's branch selects A. Escalating one fork the branch could not see.

    domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM. Reviewed against origin/main @ 592acaf and the sibling objectstack checkout — ⛔ not against the report's self-description.

    The dispatch scoped this to step 1 and the deliverable is the per-shape list. It arrived: 12 shapes — 4 equivalent verbatim, 2 equivalent after a rewrite the platform already prescribes, 5 not equivalent, 1 reverse-direction. Zero edits, git status empty, worktree removed without --force.

    ⛔ A symbol this card has been reasoning about for two rounds does not exist

    wizardPredicateScope — named by the previous dev's report, carried into the maintainer's ruling as the thing to check, and repeated by me in the dispatch order. It is not in the repo. Re-measured here, with three controls in the same query so the zero is a reading:

    probe files
    wizardPredicateScope 0
    control PredicateScope 82
    control visibleWhen 216
    control requiredWhen 56

    It is a garble of const predicateScope = usePredicateScope() — confirmed verbatim at packages/plugin-form/src/WizardForm.tsx:550, imported from @object-ui/react at :23. ⚠️ That is the generic host scope every form arm reads, not a wizard-specific one — so the "the wizard has its own scope" framing that motivated the scope-divergence question was itself wrong, and the real divergence had to be measured from the two evaluators. Which is what happened.

    ⭐ This is the third round in a row on this lane where a citation was inherited rather than re-derived. The dev re-derived it and reported the drift instead of writing around it.

    Also confirmed: the form.ts:1045 citation has drifted to :1074–:1077 (requiredWhen?: string | { dialect?: string; source: string }, "Fails open"). Substance intact.

    The measurement I re-checked myself

    ✅ The hazard gate is identity-scoped, as claimed: packages/app-shell/src/console/__tests__/AppContent.declaredVisibilityKeys.test.tsx:117 reads visibleWhen: cel("'sales_manager' in current_user.positions").

    ⇒ That is row 7 — the one shape D provably cannot express. So the corpus that actually exists sits exactly where the cheap remedy does not reach.

    ⭐ The reported non-reading is the best thing in this report

    The census's v2 blocked: 0 was reported as not a reading, because the control on the join missed: the extractor reads string literals, and all three known hazard fixtures bind the predicate to a variable (visibleWhen: GATE). ⛔ A seat in a hurry would have banked that zero.

    This is the second consecutive dev on this card to catch its own census with a join-level control. It is now this lane's house standard, not a one-off.

    ⚠️ A sequencing fact the ruling could not have known

    The ruling sequenced option A "排在 #6776 后". #6776 closed completed today at 14:24:54Z (PR #6913 merged). ⇒ That fence is discharged.

    ⛔ But A is still not dispatchable, for a different reason that arose after the ruling was written: #6661 now holds packages/app-shell (claimed 17:12Z, in flight with the predecessor seat). A's wiring lands in validateMetadataDraft, inside that package. The named blocker is spent; a new one is live.

    Housekeeping

    • objectstack#13935 filed as an out-of-scope finding, severity deliberately stated LOW and not inflated (a downstream gate catches the followed advice, so it costs a correction cycle, not a silent inert rule). Dedup search ran with a control that hit.
    • ⚠️ Channel reading worth carrying: this session's repo-scoped REST returns 403, and the zero-quota payload channel truncated at 15 of 23 timeline nodes, twice, reproducibly — omitting the newest comment, which on this card is the maintainer's ruling. ⇒ On a long-timeline card, budget one MCP call for the tail. A ruling lives exactly where that channel silently stops.

    Disposition

    pm:dispatched → needs-user-decision, assignee cleared, in the same action as this comment. The fork is stated in the next comment with the four-axis analysis. ⛔ Not adjudicating it: the ruling is the maintainer's and the measurement landed somewhere its binary branch does not cleanly cover.


    Generated by Claude Code

  9. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    决策:裁决的二分支落在了一个实测「部分成立」的答案上 —— 按字面取 A,还是取实测支持的 SPLIT?

    domain:ui 执行席,session session_012wwHa4aaFybxXrfmfHioDM。⛔ 不代裁:本卡裁决是维护者的,且触及已发布契约形状。

    为什么这需要你一句话,而不是我按字面执行

    裁决写的是干净二分支:「Q3 成立 ⇒ D,不成立 ⇒ A」。实测结果不是二值 —— 12 形里 4 形逐字等价、2 形经平台已规定的机械改写后等价、5 形不等价、1 形反向(server 绑 parent.*,section 路径不绑,所以两个面互不包含)。

    按字面读,「不成立」已满足 ⇒ A。这条路我可以直接走,不必问你。我把它拿上来,是因为实测同时带出一条契约事实,它高于执行席:

    ⚠️ current_user 在 view 侧 visibleWhen 是契约绑定的(spec page.zod.ts:242),在字段级却是契约拒绝的。
    ⇒ 这是两份契约指向相反方向,不是「再多绑一点就能补上的缺口」。

    而三个已钉的 hazard fixture 3/3 都门在 cel("'sales_manager' in current_user.positions")(我复核过,AppContent.declaredVisibilityKeys.test.tsx:117)—— 正是那个不可表达的形。

    选项

    选项 成本
    A 按裁决字面。给 validateMetadataDraft 接对象 schema 解析器,覆盖全部 12 形 一次落地。但新依赖边同时花在 1–6 行上,而那 6 行并不需要它
    SPLIT 1–6 行走 D 的教学(⚠️ 该教学已大体存在 —— @objectstack/lint 在 build 期就响亮给出三条修法,已实测);7–11 行(身份 / feature / host / page-state)留给 A 的接线 两次落地;A 那半仍受围栏
    D only 最便宜 ⛔ 实测交付不了本卡:3/3 已钉 hazard 都是第 7 行,D 表达不了

    四棱

    ① 实际业务需求 —— 拉动一边倒且是实测的:现存 hazard 语料 3/3 是身份域,需求在 7–11 行;1–6 行大体已被 build 期 lint 服务。⚠️ 自陈弱点:该频率证据是有界的 —— 字面量普查看不见变量绑定的谓词(visibleWhen: GATE),而唯一能手工解析的语料 3/3 指向另一边。若你把该语料读作代表性而非测试夹具产物,A 更对,SPLIT 就是过度设计。

    ② 项目长远合理性 —— SPLIT 是 contract-first 的读法:1–6 行在生产者侧就解决了(对象自己的字段规则,由唯一权威在所有写路径上强制,含 API 与批量),只有 7–11 行真的需要一道新的作者期栅栏。⇒ 依赖边买在「契约侧没有答案」的地方,不买在别处。

    ③ 防 AI 写错 —— 这一轴反对 D-only 最硬,并且它决定问题:被教「把 section 谓词改写成 requiredWhen」的 agent 会把它用到 current_user 门上。平台的拒绝是 build 期 lint 报错、不是静默失效(已实测,不存在静默接受路径),所以失败是响亮的 —— 但那份指导仍在教一个对它被援引的那个 case 无效的动作。

    ④ 创业阶段不扩散 —— SPLIT 是更小的购买:它拒绝为「已有 lint 消息已经处理的大多数形」花掉一条通往 app-shell 的依赖边。

    推荐

    SPLIT,弱持。 四棱里 ②③④ 同向指 SPLIT,①在实测拉动上也指 7–11 行 —— 但①自带的有界性是真的,所以这不是四棱同向的高置信局面,故不代裁。若你更信 3/3 那份语料的代表性,**A(裁决字面)**是更好的一击,我不争。

    ⛔ D only 无论如何不采 —— 它实测交付不了本卡。

    顺带两条时序更正(与选项无关,但影响排期)

    1. 裁决把 A 排在 finding(console): metadata-admin's 172,651 B eager closure is held by a REGISTRY CONTRACT, not by chunking — registerAppComponent takes a component value #6776 后。finding(console): metadata-admin's 172,651 B eager closure is held by a REGISTRY CONTRACT, not by chunking — registerAppComponent takes a component value #6776 今天 14:24Z 已 closed completed(PR perf(app-shell): take metadata-admin out of the console eager closure, and re-baseline the ceiling with it #6913 合并)⇒ 该围栏已解除。
    2. ⛔ 但 A 仍不可立即派:Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26) #6661 现在持有 packages/app-shell(17:12Z 认领,在飞,前任席位)。A 的接线落在 validateMetadataDraft,就在那个包里。裁决点名的阻塞已消,新的阻塞已起。

    ⇒ 所以这张卡等你一句话不占用任何工期:无论选 A 还是 SPLIT,A 的那半都要等 #6661 落地。


    Generated by Claude Code

  10. 8 remaining items

  11. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Path: changing a running app without code | studio-authoring.authoring-validation-not-persisted | P1

    Triage: first grade — priority:p2 · area:studio (keeps domain:ui · pm:queue)

    Triage: lands in the scoped inspector's blocking-issues channel (ruling 5749269225) ⇒ domain:ui; rationale: a ruled card — ruling 5749269225 (batch #197 item 3, letter c, maintainer 「同意」) sets the landing site and the behaviour; it only lacked a level. The hazard ships a form that cannot be completed when the predicate hides the section ⇒ p2. This heals sweep disjunct ④.

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01CRZSc7dU8oDStbTbSwhuZe · 2026-09-25T08:46Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card and its full thread, and objectui origin/main d999617. Dedupe by this seat over 739 objectui cards (open, plus closed since 2026-09-18): n/a (ruled card; the thread was read to its last comment, the ruling).

    Execution: exactly as ruling 5749269225 states, fallback a only on its premise (1).

  12. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 3 — domain:ui execution seat 5
    Session: session_01KUxVUa7e39aNjhkKi1gsoy
    Branch: claude/issue-6900-required-in-gated-section-refusal
    Worktree: objectui-issue-6900
    Domain: domain:ui
    Seat: domain:ui#5
    File surface: the view form-variant's scoped-inspector blocking-issues channel: packages/app-shell/src/views/metadata-admin/inspectors/ViewVariantInspector.tsx (the issue computation and its onBlockingIssuesChange report), one helper module beside it if the check needs one, the refusal's keys in packages/app-shell/src/views/metadata-admin/i18n.ts (every locale table that file carries), pins beside the existing inspector celGate tests, and one .changeset/6900-…md (stop on breach; explain in the report). ⛔ clientValidation.ts (validateMetadataDraft) and the objectui#6980 pin ResourceEditPage.schemaAdvisory.test.tsx stay unchanged. ⛔ No new data path to the object schema (premise 1 below).
    Container & model: M, mode:subagent, model: opus (default judgement tier). dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」.
    Clause-②: no
    Thread-read: 5829585992
    Serial constraints cleared: no open PR's file list names ViewVariantInspector.tsx, ViewInspector.tsx, ResourceEditPage.tsx or clientValidation.ts, and no live pm:dispatched claim does. PR objectui#10551 (seat 2, objectui#10448) edits metadata-admin/i18n.ts in the engine.form.* rows; this card's keys go in their own rows, and the branch merges origin/main before pr_create. This seat's objectui#10482 edits previews/PagePreview.tsx, a different file. Open-PR file lists re-read in this act, at objectui origin/main fee1da5.

    Clause-②: no, as the 2026-09-04 seat measured for option c (5534977209): no spec schema, server validator or published export moves, and the server's accept set is unchanged. The Studio Save gate's existing blocking channel gains the one ruled issue kind. The ruling asks the claiming seat to re-judge this from the diff; the seat does so at ACCEPT.

    Scope

    Ruling 5749269225 (batch #197 item 3, letter c, maintainer 「同意」), verbatim: 「The author-time refusal ruled on objectstack#13252 (option B) and narrowed by the SPLIT ruling on this card (5486949502: rows 7–11 only — identity / feature / host / page-state predicates) lands as a blocking issue in the scoped inspector's channel, the channel already licensed to gate Save. validateMetadataDraft stays advisory; the objectui#6980 pin (ResourceEditPage.schemaAdvisory.test.tsx) stays green and unchanged.」 Triage 5829585992: 「Execution: exactly as ruling 5749269225 states, fallback a only on its premise (1).」

    • The behaviour: refuse at save/authoring time, name both facts (the field is object-required AND its section is predicate-gated), and teach the three remedies. Rows 1–6 stay served by the build-time lint teaching, ⛔ no fence for them.
    • Premise (1), STOP-AND-REPORT: the inspector channel can see the object's required flag. If it has no data path to the object schema, report the wiring it would need; ⛔ no new data path on the dev's authority.
    • Premise (2), STOP-AND-REPORT: the census with a positive control (last readings: 0 authored production form views; 10 literals in 3 fixture files pinning objectui#6237 semantic 1; those fixtures stay).

    ⇒ Fixes #6900.

    domain:ui seat 5 · session_01KUxVUa7e39aNjhkKi1gsoy · claim · 2026-09-25T08:59Z

  13. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 6900,
      "status": "done",
      "branch": "claude/issue-6900-required-in-gated-section-refusal",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10589",
      "session": "session_01KUxVUa7e39aNjhkKi1gsoy — subagent of the domain:ui seat 5 PM (parent session id; Claude-Session trailer on every commit)",
      "premise_still_valid": true,
      "summary": "Implemented ruling 5749269225 (letter c). The view inspector reports an object-required field inside a form section gated on a fenced root as a blocking issue, through the existing onBlockingIssuesChange channel, as a second form-family term beside the list-only CEL term. The fenced roots are step-1 rows 7-11: current_user/user/ctx/os/features/app/page. The roots are read by @objectstack/formula collectCelRootIdentifiers, loaded lazily. Save holds on both hosts, ViewInspector (scoped) and ViewDefaultInspector (home). The refusal names the field, says it is required on the object, names the section and its predicate, and lists the 3 remedies. validateMetadataDraft stays advisory; the objectui#6980 pin is byte-unchanged (git diff --stat from BASE 9cbe4db to HEAD is empty) and green. Premise (1) holds. GET /meta/object/NAME serves each field def whole: applyObjectSchemaMask drops unreadable fields whole and never a key inside a def. MetadataClient.get unwraps item, and readFields strips only indexed/referenceTo/isSystem, so required passes. The one surface widening is ObjectFieldInfo.required?, set only when def.required === true. Premise (2) was taken at BASE with an AST-backed, variable-resolving census. Authored non-test form views gated on a fenced root: objectui 0 (6105 files), objectstack 0 (7678), hotcrm 0 (532, shallow clone 4db37cd). The positive control hit: the 3 objectui#6237 fixtures resolve GATE via variable/param/call-site to current_user, 6+3+1 = 10 literals in 3 files. data.* is read as row 6 (no fence): no runtime tier binds data as the host adapter any more (objectui#8166, objectui#9308). Files (net of merge parent 6ea68e6): 8 changed, +906/-10. New: inspectors/requiredInGatedSection.ts (283), requiredInGatedSection.test.ts (143), ViewVariantInspector.requiredInGatedSection-6900.test.tsx (165), ResourceEditPage.requiredInGatedSection-6900.test.tsx (208), .changeset/6900-required-in-gated-section-refusal.md (21, patch app-shell). Edited: ViewVariantInspector.tsx (+69/-10), useObjectFields.ts (+12), i18n.ts (+15; 6 keys x en/zh). SPLIT comment 5486949502 answered 404 (control 5749269225 answered 200); the PR says so. Deviations: the seat's shared write-pace budget (40/h) refused pr_create, label-write and this comment at least once each (exit 10, nothing written); each was retried after the window reopened.",
      "tests": "Main pin, BASE leg: tree 08c15a2 (tests-only commit on 9cbe4db) with the host pin copied cmp-identical from 5a8db23. `Tests 4 failed | 10 passed (14)`, EXIT=1. The 4 main pins fail (toBeDisabled / expected +0 to be 1); the controls pass. AFTER leg 5a8db23: `Tests 39 passed (39)`, EXIT=0. FINAL 0461b3a (merged origin/main 6ea68e6), 7 files (the 3 new, ResourceEditPage.schemaAdvisory, ViewVariantInspector.celGate/.homeGate, ResourceEditPage.defaultGate): `Tests 49 passed (49)`, EXIT=0. vitest run packages/app-shell/src/views/metadata-admin/, sharded: shards 1-3 at 5a8db23 EXIT=0 each (76 files each; 893+1 skipped, 802, 1029 tests); shard 4/4 at 0461b3a EXIT=0 (76 files, 738 tests). The merge touched no metadata-admin file. pnpm turbo run type-check --filter @object-ui/app-shell: EXIT=0 at 5a8db23 and at 0461b3a (81f18df1e0c4c01c; tsconfig.test.json --listFilesOnly lists all 3 new tests). Dependency closure: turbo build --filter '@object-ui/app-shell^...' EXIT=0 (28 cached). eslint on the 7 own .ts/.tsx files at 0461b3a: EXIT=0, 7 files, 0 errors, 4 warnings, all pre-existing (the same 4 on BASE). check:i18n-keys/-drift/-dead-keys/-designer-parity, check:control-bytes, test-path-roots, vi-mock-specifiers/-inherit/-override-shape, new-line-citations, changeset-claims, pending-changeset-literals, unreferenced-sources, phantom-deps, designer-field-key-parity, handler-key-reads, metadata-write-doors, check-changeset-presence.mjs: EXIT=0 each at 0461b3a. NOT MEASURED: check:eager-closure (needs a console build dist; left to CI). CI at report time: 42 check-runs, 23 success, 3 skipped, 16 in_progress. All heavy runs went through os-verify-lock (VERDICT command-exit 0). One outer timeout (124) killed a run after turbo had printed 30 successful; the run was repeated to capture its exit code.",
      "mcp_calls": "1 — mcp__Claude_Code_Remote__add_repo(objectstack-ai/hotcrm, read): answered read_available, nothing attached. 0 GitHub MCP calls; no MCP write tool used.",
      "api_writes": "3 landed REST writes, all through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectui/pulls (draft), run 36123567863; (2) label-write assign POST /repos/objectstack-ai/objectui/issues/10589/assignees os-tesla, run 36123683297, read back MATCHES; (3) this os-dev-report comment POST /repos/objectstack-ai/objectui/issues/6900/comments via post-stamped. Plus refused attempts before each landed (write-pace exit 10, nothing sent): pr_create 1, label-write 1, this comment at least 1; the final JSON message carries the exact count. git push x3 (empty-branch probe, 5a8db23, 0461b3a) are not REST writes.",
      "open_questions": [
        {
          "question": "When the new refusal holds Save shut, the Save button tooltip still reads 'Fix the CEL syntax errors before saving.' (ResourceEditPage titles every inspector block with perm.cel.saveBlocked). ResourceEditPage.tsx was outside the claimed file surface, so it was not edited. Extend the surface for a follow-up patch round?",
          "options": [
            "A. A patch round on this claim, adding ResourceEditPage.tsx: the inspector-blocked title reads a neutral new key (e.g. 'Fix the issues shown in the inspector before saving.'). The existing celGate/defaultGate title asserts and the PermissionMatrixEditor/ObjectHooksPanel copies stay as they are.",
            "B. Leave it. The refusal copy is shown in the inspector beside the disabled Save; the tooltip is secondary."
          ],
          "recommendation": "A. Real business need: every author who hits the new refusal also sees a false tooltip. Long-term soundness: a channel carrying two kinds should not title itself with one kind's copy. Preventing AI mistakes: an agent reading the tooltip chases a CEL error that does not exist. No scope creep: one file, one key, no new gate."
        }
      ],
      "out_of_scope_findings": [
        "not filed · carrier: none (承接者:无) · the runtime ViewConfigPanel, a third host of ViewVariantInspector, passes no onBlockingIssuesChange, which is already true for CEL; its mapObjectFields also builds ObjectFieldInfo without required. Neither refusal renders or gates there. Observation only; noted in the PR Acceptance notes · dedupe words: ViewConfigPanel onBlockingIssuesChange, mapObjectFields required, runtime view editor save gate",
        "not filed · carrier: whoever next edits inspectors/ViewInspector.tsx · stale comment 'The default (home) path below has no such prop to forward' (MetadataDefaultInspectorProps has declared it since objectui#4527 phase 2); the matching ViewVariantInspector docblock was fixed in this PR · dedupe words: ViewInspector stale comment, default inspector blocking channel",
        "not filed · carrier: none (承接者:无) · a { group } form section inherits its visibleWhen from the object's fieldGroups, which this fence does not read (it would need fieldGroups plus field.group in the catalog, a second widening that was not authorized); likewise a field-level visibleWhen hiding an object-required field is the same hazard outside this card · dedupe words: form section group reference required, fieldGroups visibleWhen required field, field visibleWhen required hazard"
      ]
    }

    Generated by Claude Code

  14. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Maintainer ruling recorded: B. The inspector-blocked Save title becomes neutral copy; ruling 5749269225's 「stays green and unchanged」 binds the objectui#6980 pin's Save-gate asymmetry, not its tooltip wording

    domain:ui seat 5, session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T11:37Z. This clarifies ruling 5749269225 (batch #197 item 3, letter c) on one point; nothing else in it moves.

    Provenance.

    • Who: the maintainer. Where: this seat's session.
    • The fork, measured by the dev of PR objectui#10589 at head 0461b3a: ResourceEditPage titles every inspector-blocked Save with perm.cel.saveBlocked ("Fix the CEL syntax errors before saving."). The channel this card lands in now carries a non-CEL issue, so on the new refusal that tooltip is false. The objectui#6980 pin ResourceEditPage.schemaAdvisory.test.tsx, which the ruling says 「stays green and unchanged」, itself asserts that title string. So a neutral title cannot land without changing that pin's bytes.
    • The seat asked whether "unchanged" covers the pin's Save-gate asymmetry only, or its tooltip copy too. The maintainer chose, verbatim: 「B 允许改提示文案 (Recommended)」. The option text read: 「「不动」指保存闸门策略,不含提示文字。换一个中性 key,5 个测试里的提示字符串跟着翻,Client-side Zod validation errors do not gate Save, while inspector-reported errors do — one class of author-time error is advisory, the other blocking #6980 pin 守的不对称不变。」

    What this changes.

    • One neutral key, in every locale table of metadata-admin/i18n.ts, titles ResourceEditPage's inspector-blocked Save.
    • The pins that assert the old title string flip to the new one, with a note naming this ruling: ResourceEditPage.celGate, .defaultGate, .schemaAdvisory (objectui#6980), .serverRefusalGate, and this PR's own ResourceEditPage.requiredInGatedSection-6900.
    • The objectui#6980 pin's ASSERTIONS about the asymmetry are ⛔ unchanged: the Zod advisory pass stays advisory, and a server refusal still blocks. validateMetadataDraft stays advisory.
    • perm.cel.saveBlocked keeps its CEL-only readers (PermissionMatrixEditor, ObjectHooksPanel).
    • Rejected: A, a kind-aware blocking channel. It would widen the published MetadataInspectorProps / MetadataDefaultInspectorProps for tooltip copy. Also rejected: C, landing the false tooltip and deciding later.

    Execution. One patch round on PR objectui#10589 in this seat's live claim (5829731715). Its file surface widens by ResourceEditPage.tsx (the inspector-blocked Save title only) and the five flipped test files, recorded here.

  15. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: objectui#10589 at 8fe6d58

    domain:ui seat 5, session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T12:19Z. Checked against the diff and CI, not against the report's own account.

    Implemented-by:  claude/issue-6900-required-in-gated-section-refusal
    Reviewed-by:     session_01KUxVUa7e39aNjhkKi1gsoy
    
    item reading
    the landing site ruling 5749269225 (letter c). The refusal is a second form-family term in the view inspector's existing onBlockingIssuesChange channel, which already gates Save, on both hosts (scoped ViewInspector, and home ViewDefaultInspector). validateMetadataDraft stays advisory, and no code path touches it
    the behaviour refused: an OBJECT-required field inside a form section whose visibleWhen reads current_user (and its aliases), features, app or page (rows 7–11, roots read by @objectstack/formula's collectCelRootIdentifiers, lazily loaded). The copy names the field, that the object requires it, the section and its predicate, and the three remedies. Rows 1–6 and 12 draw nothing (pinned). data is read as row 6: no runtime tier binds it as the host adapter on this tree (verified at the head), so the row-10 fence is not built
    premise (1) met on the EXISTING data path: useObjectFields already reads object.fields via the MetadataClient, and now carries required: true onto ObjectFieldInfo (an internal type). No new fetch, method or prop
    premise (2) the census was re-taken at the base with an AST-backed, variable-resolving reader: 0 authored production views in objectui, objectstack and hotcrm. The positive control hit the three objectui#6237 fixtures (10 literals), which stay
    maintainer ruling B 5831744213: ResourceEditPage's inspector-blocked Save title reads the neutral perm.inspector.saveBlocked. The objectui#6980 pin's asymmetry assertions (toBeEnabled on the advisory case, toBeDisabled on the CEL fault, the banner checks) are byte-identical; only its title string moved, with the four other flipped pins. PermissionMatrixEditor and ObjectHooksPanel keep perm.cel.saveBlocked
    pins the unit rows (8 fenced, 7 unfenced plus unparsable), the inspector pin on both hosts (red at the base: no required in the catalog), and the editor pin on both hosts, with a not-required control proving the disabled Save is the refusal
    Clause-② re-judged from the diff, as the ruling asks no holds: packages/app-shell/src/index.ts, MetadataInspectorProps and MetadataDefaultInspectorProps are untouched; ObjectFieldInfo is internal; nothing in @objectstack/* or the server moves. What narrows is the designer's own Save gate, by one ruled term, on a channel already licensed to block
    contract review PASS at CONTRACT_REVIEW_TIER on this head, record 5832244005 on the PR. The seat read the reviewer transcript: every turn was at tier
    semver '@object-ui/app-shell': patch; all ten changeset sentences read true
    governed NOT GOVERNED (13 paths)
    CI 3 skipped, 40 success, 0 red on 8fe6d58
    closing keywords the only closing reference in the body is Fixes #6900; the body's patch-round note and its corrected opening paragraph were written by the seat
    owed to the filing gate the runtime ViewConfigPanel gates neither CEL verdicts nor this refusal on Save; a { group } section's object-side fieldGroups predicate is outside this fence. Stale comments left: ResourceEditPage's inspectorBlocking JSX comment and ViewInspector's "no such prop" line

    State

    ready and auto-merge (squash) go through the relay ops pr_ready + automerge_enable in this act.

  16. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Landed: PR objectui#10589 merged as 402a266 through the merge queue

    domain:ui seat 5, session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T12:46Z. Verified by content on objectui origin/main ac2d6f1, not by the sha:

    • metadata-admin/inspectors/requiredInGatedSection.ts is present.
    • ResourceEditPage.tsx reads perm.inspector.saveBlocked (1), per the maintainer's ruling 5831744213.
    • previews/useObjectFields.ts carries the optional required (2).
    • .changeset/6900-required-in-gated-section-refusal.md is present.

    Rulings executed: 5749269225 (letter c, the scoped inspector's blocking-issues channel) and 5831744213 (letter B, the neutral Save title; the objectui#6980 pin's asymmetry assertions are unchanged). The two out-of-scope findings (the runtime ViewConfigPanel does not gate Save; a { group } section's object-side predicate) are owed to the filing gate (seat post objectui#10488).

    The card closed completed through Fixes #6900. pm:dispatched is removed in the same stroke.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions