Repository navigation
authoring-validation-not-persisted: a flat view body is accepted, published and reported valid, then expands to nothing — the write door judges by the wire union, not the strict ViewSchema #7741
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 11, 2026 Moved
pm:queue→needs-user-decisionby the spec-lane seat (sessionsession_01JY2Q5Xto1u8YHADgrZDTnk), executing the maintainer's lane-triage authorization (2026-08-11, 「你的车道你可以执行 分类改标」). The card's own text asks for exactly this: "Neither is safe to guess. Please rule before this is dispatched" — a queue label on a card whose body demands a ruling is the #4829 shape in reverse.Four-lens recommendation (for the ruling; per #7498's decision-card requirement):
- 实际业务需求 — nobody benefits from a stored view that renders nothing while reporting
valid:true; the measured harm is the silent dead row, not the union's arity. Both directions fix that; neither adds capability. - 项目长远合理性 — the deeper fact (shared with sibling view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736) is that the runtime write door has NO expansion step, so no shape it accepts can produce a servable view. A schema-side tightening (B) fixes THIS card's shape but leaves view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736's container shape dying at the same door. If the door gains an expand-or-refuse step, both cards close structurally.
- 防 AI 写错 — B (inline arm requires an object binding) turns the silent dead row into a located refusal at the authoring door — the strongest anti-footgun shape. A alone (fix diagnostics/expansion, accept the shape) keeps a lenient wire arm that
defineViewrefuses, i.e. the two doors keep disagreeing in front of the same author. - Startup scope — B is a tightening of a published union arm (spec/ui: ViewMetadataSchema 的 union 无判别式且容器成员未导出——消费方做失败诊断只能按成员序索引嵌套 errors #6391 named the members deliberately;
view的 spec 校验闸门形同虚设:saveMetaItem({ item: { nope: 1 } })返回 success 并把{"nope":1}存成一个 active view #5599/ViewItemSchema同时是授权形状和 Studio 往返的 wire 成员 —— 拆成两个 schema 还是保持宽松?(挡住 #4001 批 18 最后 2 站点) #5074 behind it) — a contract action needing your word either way.
Recommendation: B, plus the honest half of A — require the object binding on the inline wire arm (located refusal, message mirroring
defineView's existing guidance), AND stop stampingvalid:trueon any stored view that expands to[](the diagnostics lie is real under every direction). If you'd rather treat the door's missing expansion step as the root fix, rule that instead and this card re-routes todomain:metadataalongside #7736.Awaiting your ruling — the card dispatches the moment one lands.
Generated by Claude Code
- 实际业务需求 — nobody benefits from a stored view that renders nothing while reporting
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsMaintainer ruling — 2026-08-12
裁定:方向 B —— inline arm 在运行时写门必须携带 object 绑定,拒绝时给出与
defineView同款的定位指路。要点:
- 一个无法展开、无法被任何读路径服务的行,不允许被存储并盖上
valid:true—— 那是给作者(尤其是 AI 作者)的假收据。 - 写门的拒绝复用 build 路径已有的 guidance(「Wrap it:
defineView({ list: { … } })」),让作者在犯错现场拿到正确拼法。 - draft 与 active 同样适用;若 Studio 将来出现「先存未绑定草稿」的实测需求,再对 draft 单独放宽 —— 现在没有这个证据,不预留。
- spec/ui: ViewMetadataSchema 的 union 无判别式且容器成员未导出——消费方做失败诊断只能按成员序索引嵌套 errors #6391 的 union 成员契约保持:收紧的是 inline arm 的绑定要求,不是砍掉这个 arm。
- 相邻的 view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736(container 体同样存而不展开)在实现时一并对照 —— 两卡病灶同源:运行时写门缺展开步骤。
裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话
session_01GZKbx4xyF7U5WXj6ch49BM代笔落卡。转pm:queue。
Generated by Claude Code
- 一个无法展开、无法被任何读路径服务的行,不允许被存储并盖上
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsClaim: PM loop round 1 (duty-division session, seat post #6017)
Session:session_0123k4cam2jEAkPmbJeoaY3r
Branch:claude/issue-7741-view-door-binding
Worktree:objectstack-issue-7741
Domain:domain:spec
File surface:packages/spec/src/ui/view.zod.ts(wire union /assertViewIdentityregion),packages/spec/src/kernel/metadata-type-schemas.ts, their tests; regeneratedreferences/**ride. (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: fable(claude-fable-5— mandatory clause: accept/reject change)
Serial constraints cleared: in-flight spec PRs #7892 (ui/dashboard.zod.ts), #7972 (ui/action.zod.ts), #7985 (data/driver/memory.zod.ts+ debt ledger) — none touchesview.zod.tsormetadata-type-schemas.ts; seat-post hot-file queue shows no live claim on either file. os-regen relay: this PR takes a slot after #7851/#7974 per the seat-body order.Dispatching against the maintainer's 2026-08-12 ruling on this thread (direction B — inline arm requires object binding at the runtime write door, refusal reuses
defineView's guidance; draft and active alike; union membership preserved).
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actions{ "issue": 7741, "status": "done", "branch": "claude/issue-7741-view-door-binding", "pr": "https://github.com/objectstack-ai/objectstack/pull/8089", "premise_still_valid": true, "summary": "Direction B implemented as ruled: the two inline (flattened-overlay) arms of ViewMetadataSchema now require the object binding at the runtime write door, draft and active alike, with located refusals that reuse defineView's wrap guidance through one shared prose constant (no fork). PM mechanism assumption 2 was executed by measurement: 'binding' resolved to the PAIR object + viewKind, because both object-bound read paths (GET /meta/view?object= in rest-server.ts and getViewsByObject() in metadata-manager.ts) filter on v.viewKind && v.object — requiring object alone would refuse the repro and then instruct authors into a second, switcher-invisible dead row. Platform personalization PUTs keep passing because normalizeViewMetadata/viewIdentityPatch (#2555) inherit the pair from the shadowed registry entry before validation; the refused body is the baseline-less one, i.e. the card's dead row. Union membership (#6391) preserved: four arms, same order, anyOf face identical in both io directions. #7736 untouched and not foreclosed (container arm byte-identical, pinned).", "tests": "spec: new view-inline-object-binding.test.ts (repro body REFUSED via getMetadataTypeSchema('view') with located guidance at paths object/viewKind naming the inline shape + 'Wrap it: defineView({ list: ... })'; half-bindings refused; bound inline body, ViewItem record, container accepted byte-identically; JSON-Schema required face pinned both io directions). Reverse verification, direction predicted first, fix committed first: on origin/main's schema the 8 refusal-side pins go RED (repro parses success:true) while all 5 acceptance-side pins stay green — plain acceptance direction, no inversion. Suites (DOWNSTREAM consumers of @objectstack/spec, run per package): pre-merge all green after fixture triage — spec 10116, metadata-protocol 1094, metadata 603, rest 1555, platform-objects 347, objectql 3356, runtime 2171; example apps app-crm/app-showcase/app-todo 'objectstack validate' exit 0. Post-merge with origin/main@f46e987e9: spec full suite 384 files / 10165 tests green, spec typecheck green, check:generated all 13 artifacts current (api-surface regenerated byte-identical; overlay members mint no reference/manifest entries per #6391, so no content/docs/references diff exists to ride). Post-merge consumer suite re-run in_progress at report time (running under the shared verify lock; incoming merge commits touch none of this PR's files — CI validates the same merge state). Gates all PASS: check:adr-anchors, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:doc-formula-expressions, check:docs-audit-scope, check:i18n, check:merge-driver, check:release-body, check:spec-parsed-alias, check:nul-bytes. Changeset: @objectstack/spec minor (no skip-changeset — this PR releases).", "open_questions": [ { "question": "The ruling's letter names 'object 绑定'; the PM's binding definition (require exactly what the read paths need, measured) yielded the pair object+viewKind, and I implemented the pair. Confirm the pair reading, or the viewKind half comes back out in a patch round.", "options": ["A - keep the pair (implemented): every accepted inline row is actually servable by the switcher filter v.viewKind && v.object, and the refusal teaches both keys at once", "B - object only: matches the ruling's literal word but re-admits a row bound by object yet invisible to GET /meta/view?object=, violating the ruling's motivating clause (no unservable row stamped valid:true)"], "recommendation": "A, because the ruling's core clause is only satisfied by the pair; measured at rest-server.ts and metadata-manager.ts, two call sites, one predicate." }, { "question": "Draft-path residue: the pin lives at the schema layer, which saveMetaItem consults for BOTH draft and publish modes (ADR-0005 gate in metadata-protocol). packages/rest was not touched per the file surface; if any transport saves view drafts WITHOUT saveMetaItem's schema gate, that path is outside this PR's reach.", "options": ["A - accept the schema-layer pin as complete (the card's own repro reached the gate via mode=draft, so the draft door demonstrably consults this schema)", "B - commission a rest-layer probe as a follow-up card"], "recommendation": "A." } ], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsPost-report addendum (same session
session_0123k4cam2jEAkPmbJeoaY3r, supplements the os-dev-report comment above — the report's onein_progressitem has since completed): the post-merge consumer suite re-run againstorigin/main@f46e987e9finished green —metadata-protocol,rest,objectql,runtime,metadata,platform-objectsall exit 0. PR #8089's Verification section is updated to the final state. Everything else in the report is unchanged.Generated by Claude Code
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsACCEPT — PR #8089 (draft), reviewed per step 7 against GitHub.
- Scope verified on the diff: exactly two production files (
ui/view.zod.ts,kernel/metadata-type-schemas.ts— the claimed surface), one changeset, and 38 test files across spec/metadata-protocol/objectql/rest/runtime — the dispatch-mandated fixture triage, with zero consumer production code touched. No generated files changed (overlay members mint no reference entries per spec/ui: ViewMetadataSchema 的 union 无判别式且容器成员未导出——消费方做失败诊断只能按成员序索引嵌套 errors #6391, verified bycheck:generated13/13) ⇒ no relay slot needed; lands independently. - Ruling B executed with the guidance reuse the ruling required: one shared
VIEW_WRAP_REMEDYconstant feeds both the build door and the new write-door refusals — no prose fork; container guidance byte-identical. Union membership (spec/ui: ViewMetadataSchema 的 union 无判别式且容器成员未导出——消费方做失败诊断只能按成员序索引嵌套 errors #6391) preserved and pinned in both io directions; view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736 untouched and not foreclosed (container arm byte-identical, pinned). The two deliberate fixture inversions are pinned AS inversions with the[#7741]marker — the honest way to move an acceptance face. - Reverse verification: direction predicted first; restoring the fields to optional turns exactly the 8 refusal-side pins red with all 5 acceptance-side pins green. Consumer sweep pre- AND post-merge with real numbers; examples validate ×3 green.
Open questions ruled by this seat (recorded for the maintainer's veto window):
- Binding = the measured pair
object+viewKind— confirmed (option A). This is the dispatch's own mechanism definition resolved by measurement ("require exactly what the read paths need — no more"): both object-bound read paths filter onv.viewKind && v.object(two call sites, one predicate), soobjectalone would mint a second, switcher-invisible dead-row class — recreating precisely what the ruling's motivating clause forbids (「一个无法展开、无法被任何读路径服务的行,不允许被存储并盖上valid:true」). The pair is the minimal binding satisfying the ruling; the letter's 「object 绑定」 named the intent, not a one-key ceiling. If the maintainer reads it otherwise, theviewKindhalf comes out in a patch round — the refusal paths make that a two-line change. - Draft-path coverage = schema layer suffices — confirmed (option A). The card's own repro reached the gate through
mode=draft, which is measured evidence the draft door consults this schema; commissioning a rest-layer bypass probe without any evidence of a bypass is speculative surface (startup-scope discipline). If a bypass transport is ever measured, that is its own card.
Residue acknowledged as the ruling's intent, not a defect: pre-existing unbound stored rows now read back
valid: false— the badge stops lying; noted for operators in the PR body.Landing: independent (no os-regen faces). Flip + auto-merge PM-driven on verified gate-job conclusions on head
fad22ec2b.
Generated by Claude Code
- Scope verified on the diff: exactly two production files (
- added a commit that references this issue
on Aug 17, 2026
Symptom
A flat view body — a single view's config written where the container belongs — is accepted silently by the runtime metadata door.
PUT /api/v1/meta/view/<name>?mode=draftwith{name, type:'grid', columns:[…], data:{…}}→ 200state:'draft'(and 200state:'active'on the direct door)_diagnostics:{valid:true}Reproduced on 3 names. The guidance for this exact body exists and fires elsewhere:
defineView(body)on the same body throws a located message — "Unrecognized key(s) on this view container:type,columns,data. •typebelongs to a single VIEW, not to the container. Wrap it:defineView({ list: { … } })".Consequence measured:
expandViewContainer(...)→[], andGET /meta/view?object=…omits it, whileGET /meta/viewlists it as a bare unbound row. Registered, reported valid, renders nothing — precisely the outcome the strict container shape was closed to prevent.Root cause
Two schemas judge the same body, and the write path is on the lenient one.
getMetadataTypeSchema('view')resolves toViewMetadataSchema(packages/spec/src/kernel/metadata-type-schemas.ts:102), a 4-member wire union (packages/spec/src/ui/view.zod.ts, ~line 2941 on the run's build). Its preconditionassertViewIdentitypasses anything thatspeaksViewVocabularyaccepts — and the message it would otherwise emit names "an inline view config (type,columns,sections,filter, …)" as a legitimate arm. A body carryingtype+columnstherefore never reaches a rejection.ViewSchema(view.zod.ts:2327onorigin/main; line 2358 on the run's build) is only on thedefineView/build path.Verified by hand during the run:
ui.ViewSchema.safeParse(body).success = falsewith the guidance;getMetadataTypeSchema('view').safeParse(body).success = true.Stale-premise check: re-verified on
objectstackorigin/main(00e9196). The union, the inline-config arm and theassertViewIdentityprecondition are all present; the strictViewSchemais still build-path only.The inline arm looks deliberate — #6391 named the union's members precisely so a consumer could reference them contractually, and #5599 / #5074 sit behind the current shape. The defect as measured is not "the union has four members"; it is that the door accepts a
viewit then cannot expand, and stamps it valid. Two candidate directions, and picking wrong re-opens a settled contract:view, then the bug is that it is reportedvalid:truewhile being unreachable, and the fix is on the diagnostics/expansion side, not the schema.Neither is safe to guess. Please rule before this is dispatched.
Related, not duplicate: #5599 (closed) is the ancestor of the current union — it reported that
saveMetaItem({item:{nope:1}})stored a junk active view. That hole was closed by the identity precondition; this card is the residue the precondition deliberately lets through, because a flat view config does speak view vocabulary.Adjacent and worth reading together: #7736 (this run) — a container body reaches the same door and is stored without ever being expanded either. The two cards meet at the same place: the runtime write door has no expansion step, so neither shape can produce a servable view.
Reproduction
PUT /api/v1/meta/view/<name>?mode=draftwith{name:'<name>', type:'grid', columns:[…], data:{…}}→ 200state:'draft'.GET /api/v1/meta/view/<name>→_diagnostics:{valid:true}.GET /api/v1/meta/view?object=<obj>→ the view is absent;GET /api/v1/meta/viewlists it as a bare unbound row.defineView(body)on the identical body — it throws the located "Wrap it:defineView({ list: { … } })" guidance.Routing note
Filed
domain:specrather thandomain:metadata: both located files are inpackages/spec(ui/view.zod.ts,kernel/metadata-type-schemas.ts), and every candidate fix direction above edits a schema or its diagnostics. If the ruling lands on A and the work turns out to be expansion-side, re-route todomain:metadataat that point.Source
Extracted from the QA run #7695 (framework 92f26f7, console 09987b680).