Repository navigation
Form authoring: refuse an object-required field placed inside a predicate-gated section at save time (ruled option B of objectstack#13252) #6900
Description
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)
- Can this layer see the object's
requiredflag 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. - 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
claude commentedon Aug 30, 2026
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
zhuangjianguo commentedon Aug 31, 2026
⚖️ RULED — 维护者,2026-08-31,第 5 场总监席决裁批 #10,verbatim「同意」(采本席推荐)
裁定:先测 Q3(scope 等价性),成立 ⇒ D,不成立 ⇒ A(排在 #6776 后)。
requiredWhen(字段级、服务端强制,packages/types/src/form.ts:1045)能否表达 section 级visibleWhen谓词的常见 case」——重点核 dev 点名的 scope 差异(wizardPredicateScope:section 级与字段级谓词的求值宿主是否一致、record-scope 之外的谓词是否可降写)。产出:等价 / 不等价的逐形清单落卡。requiredWhen——dev 已起草的三修法拒绝文案以第 3 条(把 section 谓词写成字段requiredWhen)为标题修法,落进相应作者文档/schema describe;本卡以「既有契约已覆盖」收口,⛔ 不建 A 的新依赖边。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)。ModalFormcontentLayout: 'tabbed') cannot carry a sectionvisibleWhenat all —FormFieldTabdeclares no predicate slot #6237 语义 1(渲染器容忍 + 服务端响亮地板)与本卡(作者面栅栏)相容,三个 fixture 照旧——渲染器继续为漏网安排兜底,与作者面不再产出这种安排不矛盾。席位边界(裁不派):录裁 + 转
pm:queue(第一步测量即可派形),同笔摘needs-user-decision。裁决入本场追认摘要表。Generated by Claude Code
os-warren commentedon Aug 31, 2026
Claim: PM loop round 1 (⚠️ NOT ⚠️ that script's tree is ⚠️ Explicitly revisitable: the ruling says if step 1 lands you in option A, clause ② is judged then, by content ("条款②届时按内容判(真拒绝 ⇒ yes)"). Do not inherit this ⚠️ Option A's landing surface is
domain:uiseat, maintainer-forced takeover 2026-08-31)Session:
session_012wwHa4aaFybxXrfmfHioDM(GitHub identityos-warren—os-sam, the predecessor, who is following their own ten in-flight cards through themselves)Branch:
claude/issue-6900-requiredwhen-scope-equivalenceWorktree:
objectui-issue-6900Domain:
domain:uiFile 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 --tierreturns no path-derived mandate;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.
nointo 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 inplugin-detail.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:
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
requiredflag 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:
requiredWhenis 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)
packages/types/src/form.ts:1045for therequiredWhen/server-enforcement declaration. That citation is a day old — re-derive it.wizardPredicateScopeas where the scopes diverge. I have not read it.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: falseis 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
os-warren commentedon Aug 31, 2026
🔒 Dev claim — step 1 (scope-equivalence measurement) only
Session:
session_012wwHa4aaFybxXrfmfHioDM(GitHub identityos-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, offorigin/main@592acaf⛔ Assignee untouched, per dispatch.
Read before starting: card body plus all five comments — the
os-samclaim, the previous dev's fullos-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
visibleWhenpredicate be expressed as the field-levelrequiredWhenthe server already enforces, with attention to thewizardPredicateScopedivergence the previous dev named but did not measure. ⛔ Not implementing D's doc change, ⛔ not building A's wiring, ⛔ not touchingpackages/app-shell(live fence #6661 / #6776).Channel note for the seat: this session's GitHub App gate is closed —⚠️ that payload truncated at 15 of 23 timeline nodes with
GET /repos/objectstack-ai/objectui/issues/6900returns403 GitHub access is not enabled for this session. Reads went git + the zero-quota public-repo payload;backTimelineItemsempty, so the ruling (the newest comment) was not in it. One targeted MCPget_commentscall recovered it. Worth recording: on a card with a long timeline, the payload channel can silently omit the newest comments.Generated by Claude Code
os-warren commentedon Aug 31, 2026
os-dev-report
Step 1 delivered: scope equivalence measured. Q3 does NOT hold for the canonical case.
domain:uidev, sessionsession_012wwHa4aaFybxXrfmfHioDM, branchclaude/issue-6900-requiredwhen-scope-equivalence, worktree offorigin/main@592acaf. Zero edits —git status --porcelainempty, 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_useris contract-bound for a view-sidevisibleWhenand 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
⛔
wizardPredicateScopedoes not exist. Zero hits as a literal identifier, whole tree, tracked and untracked. Controls in the same query, same tool:PredicateScope308,visibleWhen1701,requiredWhen336. The real thing isWizardForm.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.tsrequiredWhencitation 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 fromobjectstack/packages/objectql/src/validation/rule-validator.ts:1794(source @ea0708c):Client, section-level
visibleWhen—plugin-formcopies the section predicate onto a pseudo-field (ObjectForm.tsx:1275-1279) andevalFieldPredicate(packages/core/src/evaluator/fieldRules.ts) evaluates it asExpressionEngine.evaluate(expr, { record, previous, extra: scope }), wherescopeis the host bag frombuildExpressionScope(app-shell/src/providers/ExpressionProvider.tsx):{ current_user, user, ctx: { user }, os: { user }, app, data, features }.Run through
buildScopeon 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):visibleWhenapp, ctx, current_user, data, features, os, previous, record, user(9)requiredWhenprevious, record(2)requiredWhen, master-detail resolvedparent, previous, record(3)The bound field-level set
record / previous / parentis confirmed four independent ways: mybuildScopemeasurement · the server validator's own call ·@objectstack/lint'sFIELD_RULE_BOUND_ROOTS· objectui's own authoring surfaceObjectFieldInspector.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
requiredWhenverbatimvisibleWhenshaperecord.*scalar / compare / membership / numeric / conjunctionrecord.status == 'sent',record.country in ['US','CA'],record.amount > 10record.*withhas()macrohas(record.owner_id)previous.*previous.status == 'draft'truestatus == 'sent'record.status == 'sent'status… Writerecord.status"data.*bound as the row aliasdata.type == 'list'record.type == 'list'datais the root of a METADATA form … Rewritedata.KEYasrecord.FIELD"Both are client-side tolerances with no server counterpart —
dataand 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
current_user.*and its ADR-0068 aliasesuser.*,ctx.user.*,os.user.*'admin' in current_user.positionscurrent_usera contract-bound root for view-sidevisibleWhen(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.features.*features.multiOrgEnabledapp.*app.locale == 'en'data.*bound as the host / data-source adapter (not the row alias of #6)data.viewKind == 'form'data"surface-dependent" atpage.zod.ts:242.page.VARNAMEpage statepage.selectedProjectId != ''page.zod.ts:242); no field-level counterpart exists at all.↩️ Reverse direction — server binds it, the section path does not
parent.*(master-detail header)requiredWhenbinds it since objectstack#4977; a sectionvisibleWhendoes not (measured: client faultsUnknown 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
visibleWhenthat faults fails open to visible — so an object-requiredfield inside it renders and blocks submit. ArequiredWhenthat faults fails open to not required — the server logs and skips (rule-validator.ts, objectstack#4977 deliberately declinedContainerSchema.maxWidth: false是声明了却读不到的值 ——container.tsx用||把它折成max-w-xl#4889's fail-closed carve-out). Same fault, opposite outcomes.requiredWhenis 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.{ ...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.
visibleWhenwith a resolvable enclosing literal, 27 section-shaped. Of those: 12record/previous(equivalent), 5 mechanical, 0 blocked, 10 non-predicates (i18n labels, legacy${...}, deliberate syntax-error fixtures).blocked: 0is 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.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
requiredWhenexpression, 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 theapp-shellfence.⭐ 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:
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.0with the right message (controls: bare identifier fires, plainrecordclean).⛔ 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_ROOTSmember sailing through the author gate and going inert at runtime, thedata.*signature from the row-predicate canon) and measured it directly — it does not occur forrequiredWhen. 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⚠️ Severity deliberately stated low — a downstream gate catches the followed advice (
*Whenreadingappgets the generic bare-reference prescription ("Writerecord.app"), a shape that binds on no layer; the #6290 misprescription class one root over.appis the one root objectui'sbuildExpressionScopebinds that is absent fromSCOPE_ROOTS, so it misses the good diagnostic's filter.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:
origin/main,pnpm installbuildScoperoot measurement,@objectstack/formula@17.2.0validateStackExpressionsgate probe, published@objectstack/lint@17.2.0, 13 casesgit status --porcelain592acafMODULE_NOT_FOUNDoccurred 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 (⚠️ that payload truncated at 15 of 23 timeline nodes with
GET /repos/objectstack-ai/objectui/issues/6900→ 403). Reads went git + the public-repo payload;backTimelineItemsempty, 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
os-warren commentedon Aug 31, 2026
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:uiseat, sessionsession_012wwHa4aaFybxXrfmfHioDM. Reviewed againstorigin/main@592acafand the siblingobjectstackcheckout — ⛔ 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 statusempty, 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:wizardPredicateScopePredicateScopevisibleWhenrequiredWhenIt is a garble of⚠️ 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.
const predicateScope = usePredicateScope()— confirmed verbatim atpackages/plugin-form/src/WizardForm.tsx:550, imported from@object-ui/reactat:23.⭐ 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:1045citation 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:117readsvisibleWhen: 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: 0was 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.The ruling sequenced option A "排在 #6776 后". #6776 closed
completedtoday 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 invalidateMetadataDraft, inside that package. The named blocker is spent; a new one is live.Housekeeping
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
os-warren commentedon Aug 31, 2026
决策:裁决的二分支落在了一个实测「部分成立」的答案上 —— 按字面取 A,还是取实测支持的 SPLIT?
domain:ui执行席,sessionsession_012wwHa4aaFybxXrfmfHioDM。⛔ 不代裁:本卡裁决是维护者的,且触及已发布契约形状。为什么这需要你一句话,而不是我按字面执行
裁决写的是干净二分支:「Q3 成立 ⇒ D,不成立 ⇒ A」。实测结果不是二值 —— 12 形里 4 形逐字等价、2 形经平台已规定的机械改写后等价、5 形不等价、1 形反向(server 绑
parent.*,section 路径不绑,所以两个面互不包含)。按字面读,「不成立」已满足 ⇒ A。这条路我可以直接走,不必问你。我把它拿上来,是因为实测同时带出一条契约事实,它高于执行席:
而三个已钉的 hazard fixture 3/3 都门在
cel("'sales_manager' in current_user.positions")(我复核过,AppContent.declaredVisibilityKeys.test.tsx:117)—— 正是那个不可表达的形。选项
validateMetadataDraft接对象 schema 解析器,覆盖全部 12 形@objectstack/lint在 build 期就响亮给出三条修法,已实测);7–11 行(身份 / feature / host / page-state)留给 A 的接线四棱
① 实际业务需求 —— 拉动一边倒且是实测的:现存 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 无论如何不采 —— 它实测交付不了本卡。
顺带两条时序更正(与选项无关,但影响排期)
app:launcherandnav: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
8 remaining items
objectstack-fleet commentedon Sep 25, 2026
Path: changing a running app without code | studio-authoring.authoring-validation-not-persisted | P1
Triage: first grade —
priority:p2·area:studio(keepsdomain:ui·pm:queue)Triage: lands in the scoped inspector's blocking-issues channel (ruling
5749269225) ⇒domain:ui; rationale: a ruled card — ruling5749269225(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 objectuiorigin/maind999617. 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
5749269225states, fallback a only on its premise (1).objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 3 —
domain:uiexecution seat 5Session:
session_01KUxVUa7e39aNjhkKi1gsoyBranch:
claude/issue-6900-required-in-gated-section-refusalWorktree:
objectui-issue-6900Domain:
domain:uiSeat:
domain:ui#5File 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 itsonBlockingIssuesChangereport), one helper module beside it if the check needs one, the refusal's keys inpackages/app-shell/src/views/metadata-admin/i18n.ts(every locale table that file carries), pins beside the existing inspectorcelGatetests, and one.changeset/6900-…md(stop on breach; explain in the report). ⛔clientValidation.ts(validateMetadataDraft) and the objectui#6980 pinResourceEditPage.schemaAdvisory.test.tsxstay 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.tsxorclientValidation.ts, and no livepm:dispatchedclaim does. PR objectui#10551 (seat 2, objectui#10448) editsmetadata-admin/i18n.tsin theengine.form.*rows; this card's keys go in their own rows, and the branch mergesorigin/mainbeforepr_create. This seat's objectui#10482 editspreviews/PagePreview.tsx, a different file. Open-PR file lists re-read in this act, at objectuiorigin/mainfee1da5.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.validateMetadataDraftstays advisory; the objectui#6980 pin (ResourceEditPage.schemaAdvisory.test.tsx) stays green and unchanged.」 Triage5829585992: 「Execution: exactly as ruling5749269225states, fallback a only on its premise (1).」requiredflag. 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.⇒
Fixes #6900.domain:uiseat 5 ·session_01KUxVUa7e39aNjhkKi1gsoy· claim · 2026-09-25T08:59Zobjectstack-fleet commentedon Sep 25, 2026
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
objectstack-fleet commentedon Sep 25, 2026
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 wordingdomain:uiseat 5,session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T11:37Z. This clarifies ruling5749269225(batch #197 item 3, letter c) on one point; nothing else in it moves.Provenance.
0461b3a:ResourceEditPagetitles every inspector-blocked Save withperm.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 pinResourceEditPage.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.What this changes.
metadata-admin/i18n.ts, titlesResourceEditPage's inspector-blocked Save.ResourceEditPage.celGate,.defaultGate,.schemaAdvisory(objectui#6980),.serverRefusalGate, and this PR's ownResourceEditPage.requiredInGatedSection-6900.validateMetadataDraftstays advisory.perm.cel.saveBlockedkeeps its CEL-only readers (PermissionMatrixEditor,ObjectHooksPanel).MetadataInspectorProps/MetadataDefaultInspectorPropsfor 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 byResourceEditPage.tsx(the inspector-blocked Save title only) and the five flipped test files, recorded here.objectstack-fleet commentedon Sep 25, 2026
✅ ACCEPT: objectui#10589 at
8fe6d58domain:uiseat 5,session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T12:19Z. Checked against the diff and CI, not against the report's own account.5749269225(letter c). The refusal is a second form-family term in the view inspector's existingonBlockingIssuesChangechannel, which already gates Save, on both hosts (scopedViewInspector, and homeViewDefaultInspector).validateMetadataDraftstays advisory, and no code path touches itvisibleWhenreadscurrent_user(and its aliases),features,apporpage(rows 7–11, roots read by@objectstack/formula'scollectCelRootIdentifiers, 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).datais 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 builtuseObjectFieldsalready readsobject.fieldsvia theMetadataClient, and now carriesrequired: trueontoObjectFieldInfo(an internal type). No new fetch, method or prop5831744213:ResourceEditPage's inspector-blocked Save title reads the neutralperm.inspector.saveBlocked. The objectui#6980 pin's asymmetry assertions (toBeEnabledon the advisory case,toBeDisabledon the CEL fault, the banner checks) are byte-identical; only its title string moved, with the four other flipped pins.PermissionMatrixEditorandObjectHooksPanelkeepperm.cel.saveBlockedrequiredin the catalog), and the editor pin on both hosts, with a not-required control proving the disabled Save is the refusalClause-②re-judged from the diff, as the ruling askspackages/app-shell/src/index.ts,MetadataInspectorPropsandMetadataDefaultInspectorPropsare untouched;ObjectFieldInfois 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 blockCONTRACT_REVIEW_TIERon this head, record5832244005on the PR. The seat read the reviewer transcript: every turn was at tier'@object-ui/app-shell': patch; all ten changeset sentences read true8fe6d58Fixes #6900; the body's patch-round note and its corrected opening paragraph were written by the seatViewConfigPanelgates neither CEL verdicts nor this refusal on Save; a{ group }section's object-sidefieldGroupspredicate is outside this fence. Stale comments left:ResourceEditPage'sinspectorBlockingJSX comment andViewInspector's "no such prop" lineState
readyand auto-merge (squash) go through the relay opspr_ready+automerge_enablein this act.objectstack-fleet commentedon Sep 25, 2026
Landed: PR objectui#10589 merged as
402a266through the merge queuedomain:uiseat 5,session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T12:46Z. Verified by content on objectuiorigin/mainac2d6f1, not by the sha:metadata-admin/inspectors/requiredInGatedSection.tsis present.ResourceEditPage.tsxreadsperm.inspector.saveBlocked(1), per the maintainer's ruling5831744213.previews/useObjectFields.tscarries the optionalrequired(2)..changeset/6900-required-in-gated-section-refusal.mdis present.Rulings executed:
5749269225(letter c, the scoped inspector's blocking-issues channel) and5831744213(letter B, the neutral Save title; the objectui#6980 pin's asymmetry assertions are unchanged). The two out-of-scope findings (the runtimeViewConfigPaneldoes not gate Save; a{ group }section's object-side predicate) are owed to the filing gate (seat post objectui#10488).The card closed
completedthroughFixes #6900.pm:dispatchedis removed in the same stroke.feat(app-shell): the view inspector refuses an object-required field …