Skip to content

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

@huangyiirene

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=draft with {name, type:'grid', columns:[…], data:{…}} → 200 state:'draft' (and 200 state:'active' on the direct door)
  • publish → 200
  • read-back carries _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. • type belongs to a single VIEW, not to the container. Wrap it: defineView({ list: { … } })".

Consequence measured: expandViewContainer(...) → [], and GET /meta/view?object=… omits it, while GET /meta/view lists 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 to ViewMetadataSchema (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 precondition assertViewIdentity passes anything that speaksViewVocabulary accepts — and the message it would otherwise emit names "an inline view config (type, columns, sections, filter, …)" as a legitimate arm. A body carrying type + columns therefore never reaches a rejection.
  • The guidance-carrying strict ViewSchema (view.zod.ts:2327 on origin/main; line 2358 on the run's build) is only on the defineView/build path.

Verified by hand during the run: ui.ViewSchema.safeParse(body).success = false with the guidance; getMetadataTypeSchema('view').safeParse(body).success = true.

Stale-premise check: re-verified on objectstack origin/main (00e9196). The union, the inline-config arm and the assertViewIdentity precondition are all present; the strict ViewSchema is still build-path only.

⚠️ This needs a direction ruling before implementation

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 view it then cannot expand, and stamps it valid. Two candidate directions, and picking wrong re-opens a settled contract:

  • A — re-word the checklist item / accept the shape as authored. If a bare inline config is a legitimate stored view, then the bug is that it is reported valid:true while being unreachable, and the fix is on the diagnostics/expansion side, not the schema.
  • B — make the inline arm require an object binding. An inline config that cannot name the object it attaches to cannot be expanded or served, so requiring the binding turns a silent dead row into a located refusal at the door — at the cost of tightening a published union arm.

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

  1. Boot the showcase with writable runtime packages.
  2. PUT /api/v1/meta/view/<name>?mode=draft with {name:'<name>', type:'grid', columns:[…], data:{…}} → 200 state:'draft'.
  3. Publish → 200; GET /api/v1/meta/view/<name> → _diagnostics:{valid:true}.
  4. GET /api/v1/meta/view?object=<obj> → the view is absent; GET /api/v1/meta/view lists it as a bare unbound row.
  5. Contrast: call defineView(body) on the identical body — it throws the located "Wrap it: defineView({ list: { … } })" guidance.

Routing note

Filed domain:spec rather than domain:metadata: both located files are in packages/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 to domain:metadata at that point.

Source

Extracted from the QA run #7695 (framework 92f26f7, console 09987b680).

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Moved pm:queue → needs-user-decision by the spec-lane seat (session session_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):

    1. 实际业务需求 — 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.
    2. 项目长远合理性 — 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.
    3. 防 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 defineView refuses, i.e. the two doors keep disagreeing in front of the same author.
    4. 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 stamping valid:true on 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 to domain:metadata alongside #7736.

    Awaiting your ruling — the card dispatches the moment one lands.


    Generated by Claude Code

  2. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    Maintainer ruling — 2026-08-12

    裁定:方向 B —— inline arm 在运行时写门必须携带 object 绑定,拒绝时给出与 defineView 同款的定位指路。

    要点:

    裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话 session_01GZKbx4xyF7U5WXj6ch49BM 代笔落卡。转 pm:queue。


    Generated by Claude Code

  3. self-assigned this
    on Aug 12, 2026
  4. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    Claim: 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 / assertViewIdentity region), packages/spec/src/kernel/metadata-type-schemas.ts, their tests; regenerated references/** 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 touches view.zod.ts or metadata-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

  5. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor
    {
      "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

  6. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    Post-report addendum (same session session_0123k4cam2jEAkPmbJeoaY3r, supplements the os-dev-report comment above — the report's one in_progress item has since completed): the post-merge consumer suite re-run against origin/main @ f46e987e9 finished green — metadata-protocol, rest, objectql, runtime, metadata, platform-objects all 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

  7. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    ACCEPT — PR #8089 (draft), reviewed per step 7 against GitHub.

    Open questions ruled by this seat (recorded for the maintainer's veto window):

    1. 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 on v.viewKind && v.object (two call sites, one predicate), so object alone 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, the viewKind half comes out in a patch round — the refusal paths make that a two-line change.
    2. 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

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions