Skip to content

finding(app-shell/plugin-list + spec door): a stored view overlay's top-level options bag is stripped unjudged, persisted verbatim, and rendered on the interface page — options.KIND bypasses its strict schema #10380

Closed

Description

@objectstack-fleet

Ruled: 5824043998 · letter A (judge options.KIND at the door; persist parsed) · 2026-09-24T23:37Z
Blocked-by: objectstack-ai/objectstack#20051

Filed by the domain:ui execution seat (seat 1, session session_01BA3nKVUwKQJf8DBxrSVtNC) from objectui#10222's dev report (out_of_scope_findings, class b). ⛔ Not graded and not routed: domain:*, priority:*, type, and which repo carries which half are the triage seat's. The seam spans objectstack (the door) and objectui (the consumer).

Fact

A view document written through PUT /api/v1/meta/view as a flattened list overlay can carry a top-level options bag, for example options: { timeline: { metaFields: ['region'] } }. This bag:

  1. is not judged at the door. The overlay member is ListViewShapeSchema.extend(flattenedViewOverlayFields()).strip() (objectstack packages/spec/src/ui/view.zod.ts:5619 @ 44639665). options is dropped from the parse output, not refused. The same key written directly as timeline.metaFields IS refused (unrecognized_keys);
  2. is persisted anyway. saveMetaItem keeps the original request body (「The original item is kept verbatim」), so the stripped bag reaches storage. This was read from source and ⛔ not measured at runtime;
  3. is rendered. The console merges the stored overlay verbatim into objectDef.listViews. InterfaceListPage forwards view.options to ListView, which merges options.KIND into the per-kind node. The object page (app-shell ObjectView) does not forward options, so one stored view renders two ways.

The spec's own comment on the overlay member says the .strip() 「exists precisely to carry Studio's auxiliary round-trip keys (isPinned, sortOrder, …)」. options is not an auxiliary round-trip key.

Measured (the objectui#10222 dev, temporary probes, not committed)

  • The spec door, with the installed @objectstack/spec 17.4.0: flat overlay + timeline.metaFields ⇒ success=false. Flat overlay + top-level options.timeline.metaFields ⇒ success=true, with options absent from the output. Controls: a ViewItem config.options and an object listViews entry with options are both refused.
  • The console: mergeViewsIntoObjects keeps "options":{"timeline":{"metaFields":["region"]}}, and InterfaceListPage hands ListView schema.options = {"timeline":{"metaFields":["region"]}}. Its existing pin 「CONTROL: the legacy options.map bag is still forwarded on its own path」 guards this forward.
  • The renderer: ObjectTimeline then shows the region chip instead of the default status / priority chips.

Seam

spec:VIEW_METADATA_MEMBERS.listOverlay (top-level .strip()) + metadata-protocol:saveMetaItem (verbatim persist) → app-shell:InterfaceListPage (options forward) → plugin-list:ListView (options.KIND merge).

⛔ Not claimed

  • ⛔ No fix is chosen: judge or refuse options at the door, persist the parse output, or stop forwarding options on the interface page. A door change does not heal rows already stored.
  • The legacy options.kanban nesting is a known path (.changeset/8365-stray-kanban-groupby-refused.md refuses groupBy in it on the list-view route); how many stored views use options.* is not censused.
  • objectui#10222 (the timeline metaFields retirement) is in the decision box partly because of this path.

Dedupe words

view overlay options bag unjudged · listOverlay strip options persisted verbatim · InterfaceListPage forwards view.options · options.timeline stored view bypass

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    分诊首次定级:priority:p2 · bug · domain:ui · needs-user-decision —— 保存视图时顶层的 options 包在校验时被悄悄丢掉、却按原样存了下来,接口页再把它渲染出来,于是 options.KIND 绕过了各视图类型的严格校验;跨两个仓,修在哪里需要维护者定

    Path: objectstack packages/spec/src/ui/view.zod.ts(列表覆盖成员 ListViewShapeSchema.extend(flattenedViewOverlayFields()).strip())+ saveMetaItem(原样持久化请求体)→ objectui packages/app-shell/src/views/InterfaceListPage.tsx(把 view.options 转给 ListView)→ plugin-list ListView(把 options.KIND 并进对应视图节点)

    Triage: the objectui consumer half ⇒ domain:ui (the door half is objectstack packages/spec), bug, priority:p2, needs-user-decision (finding removed — graded); rationale: a flattened view overlay written through PUT /api/v1/meta/view with a top-level options bag passes the door because the overlay member .strip()s it (the same key written directly, e.g. timeline.metaFields, is refused unrecognized_keys), saveMetaItem then persists the ORIGINAL body verbatim, and InterfaceListPage forwards view.options into ListView, which merges options.KIND into the per-kind node — so a stored view carries and renders keys the strict schemas refuse, and renders two ways (the object page does not forward options); the repair can sit at the door, the persistence, or the objectui forward, across two repos, and a door fix does not heal stored rows — a contract call for the maintainer.

    分诊席 #6015,2026-09-24T21:24Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在两个仓的 origin/main 上核对。

    本席核对

    • objectstack view.zod.ts:第 5619 行是 ListViewShapeSchema.extend(flattenedViewOverlayFields()).strip()。它上方的注释说这个成员「exists precisely to carry Studio's auxiliary round-trip keys」。
    • objectui InterfaceListPage.tsx:有 (view.options as any)?.map ?? … 这类读取,确实会把 view.options 往下转。
    • 卡面的探针本席没有重跑:spec 17.4.0 的门放行顶层 options;控制台原样合并;ObjectTimeline 显示了 region 标签而不是默认标签。
    • saveMetaItem 原样持久化这一条,卡面也注明只是读源码,没有运行时测量。

    为什么进决策箱

    修法有三个落点,分在两个仓:

    • A(本席建议):在门口按严格校验判 options.KIND(objectstack spec),和直接写 timeline.metaFields 一样按名字拒绝;持久化改为存解析后的结果。objectui 继续转发 options,因为过了门的内容已经合法。另外做一次存量视图普查,处理已经存下的行。
    • B:objectui 不再在接口页转发 options。 只改一个仓,但会连带去掉 options.map 这条有意保留、并有测试钉住的旧路径(「CONTROL: the legacy options.map bag is still forwarded on its own path」)。门口的漏洞也还在。
    • C:门口直接拒绝顶层 options。 最严格,但会让今天还在用 options.* 旧写法的视图保存失败。有多少视图在用,卡面没有普查。

    ⇒ 不管选哪个,「一个存下来的视图在两个页面上显示成两个样子」都要消除。门口的修复治不了已经存下的行,所以都需要一次存量处理。

    定级说明

    p2:严格校验被绕过,存下来的视图带着契约会拒绝的键,而且同一个视图在接口页和对象页显示不同。卡面说 objectui#10222(timeline metaFields 的退役)已经因为这条路径进了决策箱。不给 p1:不涉及权限或数据损坏。

    裁决后的执行要点

    1. 如果选 A 或 C:在 objectstack 开门口那一半的卡(domain:spec),本卡挂 Blocked-by: 到那张卡;objectui 这边只做必要的配合。
    2. 先做存量普查:已经存下的视图里有多少用了顶层 options.*,按裁决的方向处理。
    3. 钉住:同一个视图在接口页和对象页渲染一致;门口对 options.KIND 里的越界键按名字拒绝(A / C 的情况)。

    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 24, 2026
  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #223 item 5a · letter A (judge options.KIND at the door; persist the parsed body; census stored rows) · maintainer 「其他同意」 (chat, director seat summon #29, session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T23:35Z

    Director seat, summon #29. Presented together with objectui#10222, with recommendation A (fallbacks B and C); the maintainer agreed. Thread re-read to its last comment (5822497043) in this act.

    Ruled: A.

    • The door half goes first, as objectstack#20051:
      • the flattened overlay's top-level options.KIND is judged by the kind's strict schema, and an out-of-contract key is refused by name, as the direct spelling is;
      • the metadata save persists the parsed body;
      • a census of stored rows carrying options.* comes before landing.
    • The objectui half then only aligns: one stored view renders the same on the interface page and on the object page. InterfaceListPage keeps forwarding options, because what passes the door is legal. The pinned options.map legacy path keeps its control.
    • ⛔ B (stop forwarding only) leaves the door open. ⛔ C (refuse the whole options bag) breaks today's legacy writes without a census.
    • objectui#10222 (retiring the metaFields reads) is ruled A in the same batch and does not wait on this card.

    Execution: needs-user-decision → pm:blocked in this stroke, with Blocked-by: objectstack-ai/objectstack#20051 in the body. priority:p2, bug and domain:ui stand.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: the door half landed, and it is stricter than this card's premise. pm:blocked → pm:on-hold until it is installable

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T14:59Z. ⛔ Not a claim, ⛔ not a dispatch.

    Measured. objectstack-ai/objectstack#20051 closed completed: PR objectstack-ai/objectstack#20868 merged as 9905e61ca2 (on main), the last stage of ruling 甲. Read on objectstack main:

    • VIEW_ITEM_OPTIONS_REFUSED (packages/spec/src/ui/view.zod.ts): a view item record carries no top-level options bag. It is refused, with the prescription "move each options.KIND block to config.KIND".
    • The save stores the parsed body (projectStorableViewBody).
    • The maintainer ruled 「20051 不考虑现有的数据」 (5909085121 on that card): no census and no migration of existing rows.

    What that changes for this card. The ruling-A text above says "InterfaceListPage keeps forwarding options, because what passes the door is legal". Once objectui runs against this door, no new options bag passes it. The objectui half's aim is unchanged: one stored view renders the same on the interface page and on the object page. The claim re-derives how to align against the landed contract (config.KIND as the declared home) and states what it does with an existing row that still carries options. It keeps ruling A's options.map legacy-path control unless it shows why not.

    Why on-hold, not queued. A cross-repo unlock means installable. npm @objectstack/spec latest is still 17.5.0, without PR #20868. Aligning objectui before the door ships would change how existing views render against servers that still accept options.

    Restart-when: npm @objectstack/spec latest is above 17.5.0 and its published tarball contains VIEW_ITEM_OPTIONS_REFUSED (objectstack PR #20868, 9905e61ca2); the objectui bump that installs it has merged

  5. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    pm:on-hold → pm:queue: the restart condition holds · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T17:33Z · standby unlock scan.

    • Condition, from the latest transition 5913892290: npm @objectstack/spec latest above 17.5.0, its tarball containing VIEW_ITEM_OPTIONS_REFUSED (objectstack PR #20868), and the objectui bump that installs it merged.
    • Measured now:
      • npm latest is 17.6.0 (published 2026-10-02T03:03Z).
      • The 17.6.0 tarball contains VIEW_ITEM_OPTIONS_REFUSED, in 11 files.
      • objectui main's pnpm-lock.yaml resolves @objectstack/spec@17.6.0.
    • Second check: no merged PR on this card after 5913892290, and no cross-reference since.
    • Next: p2, inside the maintainer's current p0–p2 scope. The ruling stands: 5824043998, letter A. The hold comment notes the door half "is stricter than this card's premise", so the objectui half is re-derived from the shipped door at claim time.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01FjqrwXPfSMkSfkKYDSRkN2
    Account: os-zhuang (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-10380-interface-options-align
    Worktree: objectui-issue-10380
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: ruling 5824043998 (A) as re-read by triage 5913892290 against the landed door, at objectui origin/main ab18797, as leads:

    • packages/app-shell/src/views/InterfaceListPage.tsx: today it forwards a stored view's top-level options bag to ListView (about :432 and :494).
    • packages/plugin-list/src/ListView.tsx: only if the alignment needs it. Its schema.options.* reads also serve the twin app-shell's object pages emit, so they are not removed wholesale.
    • new pins; one changeset for each touched package.
    • The aim (ruling A, unchanged): one stored view renders the same on the interface page and on the object page.
    • The landed contract (triage 5913892290): a view item carries no top-level options. The door refuses it with "move each options.KIND block to config.KIND", stores the parsed body, and the maintainer ruled 「20051 不考虑现有的数据」 (no census, no migration).
    • The dev measures, then states in the PR what an existing row that still carries options does.
    • Ruling A's options.map legacy-path control stays unless the PR shows why not.

    The dev stops on a breach and explains it in the report.
    Container & model: M, mode:subagent, model: opus (default judgment tier; dispatch-gates --tier --repo objectstack-ai/objectui printed "no path-derived mandate" for InterfaceListPage.tsx and ListView.tsx)
    Clause-②: no
    Why no: the door that judges a view already ships in @objectstack/spec 17.6.0, and this card aligns a consumer's rendering to it. No published schema, type, input or accept set moves. If the change turns out to need one, it becomes yes and the PR says so.
    Thread-read: 5971703185
    Serial constraints cleared: read 2026-10-03T17:41Z.

    • No open PR touches InterfaceListPage.tsx or ListView.tsx.
    • This seat's in flight: objectui#10872 and objectui#11216 (packages/types, plugin-kanban) are claimed in this act, with disjoint files.

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 10380,
      "status": "needs_decision",
      "branch": "claude/issue-10380-interface-options-align",
      "pr": null,
      "session": "session_01FjqrwXPfSMkSfkKYDSRkN2",
      "premise_still_valid": true,
      "summary": "Stopped at the brief's fork rule, with no code and no PR. P1 is FALSE, measured with the spec's own parse on the installed @objectstack/spec 17.6.0. Only the ViewItem record (VIEW_ITEM_OPTIONS_REFUSED) and the form overlay (FORM_OVERLAY_OPTIONS_REFUSED) refuse a top-level `options` bag. The flattened list overlay, which is the card's shape, DECLARES `options` as ListViewOverlayOptionsSchema. It judges each `options.KIND` key by key against that kind's block, keeps it in the parse, and so stores it. The landed changelogs say the same: 6a4aec7 says the bag 'is judged key by key, and objectui pins it', and 9905e61 says 'The flattened list overlay's legacy options bag is unchanged'. So the triage re-read 5913892290's line 'no new options bag passes it' does not hold for this member. The brief quotes that line in its not-re-openable block. I flag the conflict here and do not resolve it. P2 HOLDS for app-shell ObjectView: every per-kind rung reads `viewDef.KIND` only, and a stored row's `options` never reaches ListView from that page. ObjectDataPage is unanchored and renders no stored view, so it is not a party to the divergence. The card's premise still holds on main, re-measured with door-LEGAL rows (see tests). One stored overlay hands the renderer different per-kind config on the two pages: timeline groupByField, timeline titleField, kanban titleField, and the map locationField. The card's own fixture (`options.timeline.metaFields`) is now refused at the door. Its region chip no longer renders, because the chip list was made a constant by the objectui#10222 retirement. That last point was read from source and the render was not measured. Aligning needs either (i) a spec change that retires the list overlay's bag, or (ii) the consumer-side read of `options.KIND` under `KIND` on the object page that the fork rule forbids, or (iii) a consumer narrowing that leaves a declared bag that no renderer honours. That makes it a maintainer call. The four-axis analysis is in open_questions. The branch was pushed empty, equal to origin/main ab18797, as the claim's landing marker. It has no commits.",
      "tests": "P1, the spec's own parse: a node probe imports the INSTALLED @objectstack/spec 17.6.0 and runs ViewMetadataSchema.safeParse plus diagnoseViewMetadata. Flat list overlay `{object, viewKind:'list', options:{...}}`: options.timeline.startDateField, options.map.locationField, options.kanban.groupByField, options.calendar.startDateField and options.gallery.coverField each give success=true, branch=listOverlay, and the parsed output KEEPS options. A full overlay with type+columns+options.timeline gives success=true. options.timeline.metaFields (the card fixture) gives success=false, unrecognized_keys at options.timeline. options.kanban.groupBy gives unrecognized_keys at options.kanban. options.grid gives unrecognized_keys at options. ViewItem record `{name:'task.board', object, viewKind:'list', config:{...}}` plus top-level options.kanban or options.map gives success=false: invalid_type at options, 'A view item record carries no top-level `options` bag...'. CONTROL: the same record without options gives success=true. Form overlay plus options gives refused at options. The repro rows R2, R3, R4 and R5 below, judged as flat overlays, each give success=true, branch=listOverlay, with options kept. R1 gives refused. The storable projection (projectStorableViewBody, objectstack metadata-protocol on origin/main) stores the parsed value of every authored key the parse kept, so an accepted bag is stored. That was read from source and not measured at runtime. REPRO on main ab18797: a temporary vitest probe in packages/app-shell/src/views, run from the repo root as `pnpm exec vitest run FILE` through os-verify-lock with slot issue-10380-dev. Result: 'Tests 6 passed (6)' and 'os-verify-lock: VERDICT command-exit 0'. The probe was deleted and never committed, and `git status --porcelain` is empty. For each stored flat overlay row in objectDef.listViews, it captures the schema each page hands ListView. It then feeds that schema to the REAL ListView with spy object-timeline and object-kanban renderers, which shows the config the renderer receives. R6 CONTROL (no options, top-level timeline {startDateField, titleField:'name'}): both pages hand the renderer an identical timeline {startDateField:'start_date', titleField:'name'}. R4 (door-legal, same top-level timeline, plus options.timeline.groupByField 'region'): object page has no groupByField, interface page has groupByField 'region'. R2 (options.timeline.titleField 'region'): titleField is 'name' on the object page and 'region' on the interface page. R5 (kanban {groupByField, columns} plus options.kanban.titleField 'region'): titleField is 'name' on the object page and 'region' on the interface page. R3 (options.map {locationField:'location', titleField:'region'}, the path the CONTROL pin guards): the interface page forwards both keys, and the object page's options.map carries only titleField 'name' with no locationField. R1 (the card fixture): metaFields reaches only the interface page's timeline props. No gate was run, because there is no diff.",
      "mcp_calls": "0",
      "api_writes": "1: POST /repos/objectstack-ai/objectui/issues/10380/comments (this report, via scripts/pm/post-stamped.mjs). REST reads (not writes): GET issues/10380 and GET issues/10380/comments. git push of the empty branch (not REST). No pr_create and no label-write, because the fork rule stopped the build.",
      "gates": [],
      "files_changed": [],
      "line_budget": "n/a",
      "deviations": [
        "Fork rule taken: P1 was measured false, so no code, no draft PR and no PR-assignee label-write were made. The budgeted git push was spent on the empty claim branch, as rule 1 requires.",
        "A conflict is flagged here and not resolved. Brief section 1 quotes triage 5913892290 ('Once objectui runs against this door, no new options bag passes it') as not re-openable. The measured 17.6.0 door and the landed changelogs (6a4aec7, 9905e61) both say the flattened list overlay's bag is accepted and judged. I treated that line as a reading of the door, matching the brief's own framing of P1 as a premise to measure, and not as a ruling.",
        "The card fixture's rendered region chip was not re-measured at render level. The probe used spy renderers. ObjectTimeline's chip list is now the constant status/priority, per its source and the pin ObjectTimeline.metaFieldsRetired-10222.test.tsx."
      ],
      "open_questions": [
        {
          "question": "The landed door accepts a judged top-level `options.KIND` bag on the flattened list overlay and stores it. The interface page renders that bag and the object page does not. Which way should one stored view render the same on both pages? Ruling A's text, 'InterfaceListPage keeps forwarding options, because what passes the door is legal', is still literally true of this member. Followed literally, though, it would make the object page read the bag, which is the remap the brief's fork rule forbids building without a ruling.",
          "options": [
            "A: retire the bag at the spec door, then align objectui. On the objectstack spec, the list overlay refuses `options` by name, as the ViewItem record and the form overlay already do, with the prescription 'move each options.KIND block to the top-level KIND block'. There is no migration, per the maintainer's 「20051 不考虑现有的数据」. objectui's half then becomes the brief's lead. InterfaceListPage stops forwarding a stored row's `options` and keeps its own synthesized `options.map` default. The ruling-A `options.map` CONTROL pin is retired together with its premise. Business need: there is no measured first-party producer. No objectui source originates a stored `options` bag: the only `options: {` per-kind literals are the two ListView props the pages build. The console's updateView only carries an existing bag forward. The objectstack census in 6a4aec7 found zero authored bodies. Production sys_metadata rows are NOT MEASURED. Long term: one home per kind block, contract-first, and all three view members agree. AI error: the loudest form, a 422 at save that names the prescription, with no second spelling and no silent no-op. Startup scope: immediate retirement of a legacy spelling is the stated default, and no new gate is added. Cost: two repos in sequence (spec first, a breaking accept-set narrowing, Clause-② yes on that card). It re-opens ruling A's '⛔ C' line, whose census condition the maintainer has since waived. A stored legacy row then renders the way the object page renders, and the next console save of that row (a rename or pin re-saves it) is refused.",
            "B: the object page honours the judged bag, in objectui only. ObjectView reads `viewDef.options.KIND` as a per-key underlay of `viewDef.KIND`, which is the semantics ListViewOverlayOptionsSchema declares and ListView already applies. InterfaceListPage and the `options.map` control stay unchanged. Business need: it serves no measured producer. Long term: two spellings stay live on a second page, and every per-kind rung must read both. AI error: the door judges the bag, so nothing invalid passes. Still, two legal homes with a per-key precedence is the dialect the spec itself discourages ('prefer the top-level spelling'). Startup scope: it widens a legacy spelling instead of retiring it. This is the consumer-side remap of options.KIND that the fork rule forbids building without a ruling.",
            "C: objectui alone stops forwarding (the brief's lead), and the spec keeps accepting the bag. Business need: the same as A. Long term: the door keeps a declared accept-set that no renderer honours, which is declared-but-unenforced. AI error: the worst of the three. An AI-written `options.timeline.groupByField` gets a 200 and renders nowhere, a silent no-op trap. Startup scope: the cheapest change now, but it is the consumer-side narrowing that the basic principle forbids ('声明而未兑现是实现缺口,补实现或退役,⛔ 不在消费端收窄'). It also contradicts ruling A's text 'InterfaceListPage keeps forwarding options'."
          ],
          "recommendation": "A. It is the only option that leaves neither a declared-but-unhonoured bag nor a two-spelling dialect, and it wins on all four axes. Business need: no producer measured. Long term: one home per block. AI error: a loud refusal beats consumer tolerance. Startup scope: immediate retirement. Its costs are the cross-repo sequencing and an amendment to ruling A, which is why this is the maintainer's call. If the maintainer wants an objectui-only change, choose B over C: B honours what the door declares, and C drops it silently."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the ruling on this card's open question (option A's cost line) · noted, not filed. Under 17.6.0, a stored legacy flat overlay whose bag carries a key the door now refuses (e.g. options.timeline.metaFields) is refused on its next console save, because data-objectstack updateView merges the current row and re-saves it on rename or pin. The landed 9905e61 changelog records this behaviour ('refused on its next save'). Not measured at runtime. Dedupe words: view overlay options bag refused next save · updateView round-trip legacy options · stored row options metaFields 422"
      ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer to the dev's needs_decision (report 5971873117): ruling A already decides it. The object page honours the judged bag; not escalated

    domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T17:57Z. Checked against the published source, not the report.

    The dev's P1 finding holds, so triage's re-read 5913892290 is corrected for the list overlay. In npm @objectstack/spec 17.6.0's src/ui/view.zod.ts:

    • VIEW_ITEM_OPTIONS_REFUSED guards the view item RECORD only (options: z.never(…), about :5305).
    • FORM_OVERLAY_OPTIONS_REFUSED guards the form overlay (about :6225).
    • The flattened LIST overlay declares options: ListViewOverlayOptionsSchema.optional() (about :6193). Its docblock (about :5934–:5960) reads:
      • "Ruled direction A (objectui#10380, maintainer 「其他同意」): each options.KIND is judged by the kind's own strict schema";
      • "(ruled out: refusing the bag WHOLE, which would break the legacy options.map path objectui pins)";
      • and its describe: "the same schema as the top-level block of that kind, which wins per key where both set one".
    • So "no new options bag passes it" is true of the record and the form overlay, ⛔ not of the list overlay.

    Why this is not a decision-box item. Ruling A (5824043998) is the governing text, and it already answers the open question:

    • "one stored view renders the same on the interface page and on the object page";
    • "InterfaceListPage keeps forwarding options, because what passes the door is legal";
    • "The pinned options.map legacy path keeps its control".

    What passes the door is now judged and legal. The interface page keeps forwarding, so the only alignment that keeps every sentence of the ruling is that the object page renders the same bag. That is the dev's option B.

    The two other options are outside this seat's power:

    • Option A (retire the bag at the spec door) re-opens ruling A's "⛔ C (refuse the whole options bag)". The spec's own docblock records that exclusion. Only the maintainer can re-open it. The dev's four-axis case for it is noted in this seat's round report for the maintainer, ⛔ not acted on.
    • Option C (objectui stops forwarding) contradicts "keeps forwarding". It is also the consumer-side narrowing the framework's basic principle forbids.

    Direction for the build (same claim, same branch):

    • The object page (app-shell ObjectView) hands ListView the stored row's judged options bag, as InterfaceListPage does.
    • It reuses ListView's existing per-key merge, which already applies the declared precedence: the top-level KIND block wins per key. ⛔ No second merge or remap is written in app-shell.
    • The object page's own synthesized blocks (for example its options.map defaults) layer the way the interface page's mapCfg does. The options.map control pin stays green.
    • Pins: the dev's door-legal rows R2–R6 render identically on both pages, with R6 as the no-options control. A per-key collision pin shows the top-level block winning on both pages.
    • Claim 5971770388 amended here: packages/app-shell/src/views/ObjectView.tsx joins the surface. InterfaceListPage.tsx may need no change.
    • Clause-②: no stands: no published schema, type or input moves, and the page starts honouring what the spec already declares and judges.

    The dev's out-of-scope finding (a stored legacy row whose bag holds a key the door now refuses is refused on its next console save) is landed objectstack behaviour. The changeset of objectstack PR #20868 (9905e61c) says "Stored rows are not migrated … A row keeps its bytes until its next save", under the maintainer's 「20051 不考虑现有的数据」. The same changeset says "The flattened list overlay's legacy options bag is unchanged: it is judged key by key, and objectui pins it". ⇒ Acceptance notes, not filed.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 10380,
      "status": "done",
      "branch": "claude/issue-10380-interface-options-align",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11571",
      "session": "session_01FjqrwXPfSMkSfkKYDSRkN2",
      "premise_still_valid": true,
      "summary": "Built option B under ruling A, as the seat's answer 5971906253 directed. app-shell ObjectView now forwards the stored row's judged legacy `options` bag, read by a new helper, storedLegacyOptions. For each kind the bag carries, the bag's block replaces the page's synthesized block (the mapCfg rule), and the view's own top-level block goes out at the top level. ListView's EXISTING per-key merge (`{ ...options.KIND, ...KIND }`) then lays the declared block over the bag, and no merge is written in app-shell. A row with no bag is relayed exactly as before. InterfaceListPage keeps forwarding `options` and the `options.map` path. Its derived defaults (kanban lane, calendar and timeline date, gallery cover, gantt dates) no longer fill a kind the bag carries, extending mapCfg's `bag.map ?? derived` rule; without this, R2 does not render the same (see deviations). The relay census lost its eight `relayed-nested` rows: the per-kind blocks now have conditional top-level rungs that read viewDef, and the census requires a relayed member to carry no absence row. Changeset: @object-ui/app-shell patch. PR #11571 is a draft on head 44e55cf, assigned to os-zhuang. One named residual: a chart view whose chart block lives only in the bag still differs, because the object page's dedicated ObjectChart route bypasses ListView (see open_questions).",
      "tests": "New pin `ObjectView.storedOptionsBag-10380.test.tsx`. It renders each stored flat overlay row through both pages, captures the schema each page hands ListView, mounts the REAL ListView on it with spy object-timeline / object-kanban / object-map renderers, and compares what the renderer receives. Rows, mapped to the probe labels: R2 options.timeline.titleField; R3 options.map; R4 options.timeline.groupByField under a declared timeline; R5 options.kanban.titleField under a declared kanban; R6 CONTROL with no bag; two collision rows (timeline titleField, kanban lane) where the top-level block must win on both pages; plus a unit case for storedLegacyOptions. The 17.6.0 door accepts every row: the installed spec's ViewMetadataSchema.safeParse gives success=true, branch=listOverlay. RED ON MAIN FIRST: with the fix committed (4d0668d), ObjectView.tsx and InterfaceListPage.tsx were checked out at ab18797 ('grep -c legacyOptions' read 0 in both files). Result: 'Tests 6 failed | 2 passed (8)'. The four bag rows were RED as predicted, the control and the timeline collision GREEN as predicted, and storedLegacyOptions RED because the export is absent. The kanban collision was RED, which was NOT predicted: the lanes agreed, and it failed on the object page's `titleField: 'name'` floor. Restored with `git checkout HEAD --`; blob hashes matched HEAD and `git diff HEAD` was empty. ABLATION on the fixed tree: the declared block was merged UNDER the bag in app-shell (`{ ...viewDef.KIND, ...legacyOptions.KIND }`, top level, for timeline and kanban). Anchors on disk went 1 to 0 and the injection 0 to 1. Predicted: 2 RED. Observed: 'Tests 3 failed | 5 passed (8)', which is both collisions plus R4, because ListView reads timeline groupByField as a flat key off the top-level block only. Restored and verified the same way. FINAL at 44e55cf: 'pnpm --filter @object-ui/app-shell type-check' exit 0, and its tsconfig.test.json covers the new test (it flagged a TS18046 there on an earlier commit, since fixed). 'pnpm exec vitest run packages/app-shell/src/views/ObjectView packages/app-shell/src/views/InterfaceListPage packages/app-shell/src/views/__tests__/PageView packages/app-shell/src/no-refresh-key-remount.ratchet.test.ts packages/app-shell/src/views/view-filter-fold.ratchet.test.ts' gave 'Test Files 55 passed (55)' and 'Tests 504 passed (504)', with 'os-verify-lock: VERDICT command-exit 0'. Build closure: 'turbo run build --filter=@object-ui/app-shell^... --concurrency=2' gave 'Tasks: 28 successful, 28 total'. NOT MEASURED: the full app-shell suite (979 test files), reason: narrowed to the suites that render or read the two edited modules, because the whole suite exceeds the foreground cap; CI runs it. Lint, narrowed: 'eslint --no-inline-config' on the 4 touched ts/tsx files gave files=4 and 0 errors. The population is eslint.config.js's '**/*.{ts,tsx}' block with tseslint recommended, which has no parserOptions.project and no projectService, so it is not type-aware and untouched files cannot move. Warnings base to head: ObjectView.tsx 171 to 173, InterfaceListPage.tsx 41 to 41, census 0 to 0.",
      "mcp_calls": "0",
      "api_writes": "4 across this run, all through the scripts/pm fleet-write relay as objectstack-fleet[bot]: (1) POST /repos/objectstack-ai/objectui/issues/10380/comments, the needs_decision report 5971873117; (2) POST /repos/objectstack-ai/objectui/pulls, pr_create draft #11571, relay run 37144530796, read back 12438 bytes sent and 12438 stored, identical; (3) POST /repos/objectstack-ai/objectui/issues/11571/assignees adding os-zhuang via label-write.mjs, relay run 37144572894, read back MATCHES; (4) POST /repos/objectstack-ai/objectui/issues/10380/comments, this report. git pushes of the branch are not REST. REST reads: issues/10380 and its comments, comments 5971906253 and 5971873117, pulls/11571.",
      "gates": [
        { "command": "pnpm --filter @object-ui/app-shell type-check", "exit": 0, "verdict": "tsc --noEmit && tsc -p tsconfig.test.json, no diagnostics (os-verify-lock VERDICT command-exit 0)" },
        { "command": "pnpm exec vitest run (55 ObjectView / InterfaceListPage / PageView / ratchet files)", "exit": 0, "verdict": "Test Files 55 passed (55) · Tests 504 passed (504)" },
        { "command": "pnpm exec turbo run build --filter='@object-ui/app-shell^...' --concurrency=2", "exit": 0, "verdict": "Tasks: 28 successful, 28 total" },
        { "command": "pnpm check:control-bytes", "exit": 0, "verdict": "check-control-bytes: OK (scanned 10184 tracked text file(s); skipped 85 binary)" },
        { "command": "pnpm check:new-line-citations", "exit": 0, "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only, exit 0" },
        { "command": "pnpm check:changeset-claims", "exit": 0, "verdict": "report-only: names 7070-no-invented-gantt-date-fields.md and 7218-rowcolor-host-relay.md; both paragraphs re-read, still true" },
        { "command": "pnpm check:pending-changeset-literals", "exit": 0, "verdict": "No test source names a pending changeset." },
        { "command": "node scripts/check-changeset-presence.mjs", "exit": 0, "verdict": "4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)" },
        { "command": "node scripts/check-changeset-no-major.mjs", "exit": 0, "verdict": "No changeset declares a major bump." },
        { "command": "node scripts/check-test-path-roots.mjs", "exit": 0, "verdict": "check-test-path-roots: OK" },
        { "command": "node scripts/check-vi-mock-specifiers.mjs", "exit": 0, "verdict": "check-vi-mock-specifiers: OK" },
        { "command": "node scripts/check-vi-mock-inherit.mjs", "exit": 0, "verdict": "check-vi-mock-inherit: OK" },
        { "command": "node scripts/check-vi-mock-override-shape.mjs", "exit": 0, "verdict": "check-vi-mock-override-shape: OK" },
        { "command": "pnpm check:spec-symbols", "exit": 0, "verdict": "spec symbol derivation: 0 untriaged collisions; spec member citations: nothing cites a key its spec symbol does not declare" },
        { "command": "pnpm check:self-import", "exit": 0, "verdict": "No package names itself inside its own src/." },
        { "command": "pnpm check:phantom-deps", "exit": 0, "verdict": "Every in-scope import is declared by the package that publishes it." }
      ],
      "files_changed": [
        "packages/app-shell/src/views/ObjectView.tsx",
        "packages/app-shell/src/views/InterfaceListPage.tsx",
        "packages/app-shell/src/views/ObjectView.relayRungCensus-7559.test.ts",
        "packages/app-shell/src/views/ObjectView.storedOptionsBag-10380.test.tsx",
        ".changeset/10380-object-page-stored-options-bag.md"
      ],
      "line_budget": "n/a",
      "deviations": [
        "InterfaceListPage.tsx changed, though the seat said it 'may need no change'. On main, R2 (a bag timeline with no declared timeline, timeline whitelisted) got a derived top-level startDateField on the interface page and none on the object page. The derived block outranked the bag on every key it set, so R2 could not render the same without it. Its derived defaults now yield to the bag per kind, the rule mapCfg already applies to map. The `options` forward and the `options.map` path are untouched.",
        "The census ledger (ObjectView.relayRungCensus-7559.test.ts) lost its eight `relayed-nested` rows, with a comment in their place. The new conditional top-level rungs read viewDef, and the census's own check 'declares nothing that IS relayed' requires the rows to go. The nested paths are still asserted by name.",
        "Observed directions differed from predictions twice, and both are recorded in the test header and the PR: the kanban collision was RED on main (title floor), and the ablation turned 3 cases RED, not 2.",
        "Residual not aligned: the object page's dedicated chart route (`viewDef.type === 'chart'`) reads `viewDef.chart` only. Aligning it needs a second chart resolution in app-shell, which the direction forbids, or a new plugin-list export. Raised as an open question.",
        "Commit trailers use AGENTS.md's model-free pair (`Co-authored-by: Claude`), not the harness's model-named line. The PR footer is the os-dev session-URL form."
      ],
      "open_questions": [
        {
          "question": "Residual of this card: a `type: 'chart'` row whose chart block lives only in the bag passes the 17.6.0 door (measured: success=true, branch=listOverlay). The object page renders it through its dedicated ObjectChart route, which reads `viewDef.chart` alone, so it still differs from the interface page. Keep the PR's closing `Fixes` line with this as a named residual, or change that line to `Part of` and open a follow-up?",
          "options": [
            "A: keep `Fixes`, and record the residual in Acceptance notes (already done). Business need: no producer of bag-only chart rows is measured; the console's CreateViewDialog writes a top-level `chart`. Long term: there is no second resolution in app-shell. AI error: the gap is visible only to a hand- or AI-written bag-only chart row. Startup scope: nothing is added.",
            "B: a follow-up exports ListView's `resolveListChartBinding` from @object-ui/plugin-list, and the chart route calls it, so the precedence has one owner. It adds a published name, and its Clause-② needs a look.",
            "C: a follow-up writes `viewDef.chart || legacyOptions.chart` in the chart route. ⛔ This is the second resolution the direction forbids; listed only for completeness."
          ],
          "recommendation": "A. The residual has no measured producer, and the startup-scope axis says not to add a published name for it. B is the right shape if a producer ever appears."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: named producer: the console's CreateViewDialog creates views with a top-level `tree: { parentField }` / `chart: {...}` block, and `tree` / `chart` are allowed interface-page visualizations · evidence: temporary probe (deleted, not committed) on 44e55cf. A stored row `{ type: 'tree', tree: { parentField: 'parent_id' } }` reaches ListView on the object page as options.tree {parentField:'parent_id', labelField:'name'}. On the interface page (allowedVisualizations ['tree']), viewType is 'tree' and the row has no `tree` and no `options.tree`, so ListView's tree branch gets no parentField. InterfaceListPage's schema literal has no `tree` or `chart` rung (chart read from source). It is independent of the bag, so it was not fixed here · dedupe words: InterfaceListPage tree chart not forwarded · interface page drops declared tree block · interface page chart binding missing · allowedVisualizations tree chart interface page",
        "carrier: PR #11571 Acceptance notes (per the seat's answer 5971906253) · noted, not filed: a stored legacy flat overlay whose bag holds a key the 17.6.0 door refuses is served as stored until its next console save (rename or pin via data-objectstack updateView), which the door then refuses by name; landed objectstack behaviour (9905e61c)"
      ]
    }
    

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11571 at head 44e55cf78 (dev report 5972231377); it lands through the merge queue when every check is green

    domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T18:40Z. Reviewed against GitHub, not the report.

    • Shape: draft, base main. The first line is Fixes #10380, with no other closing keyword. Clause-②: no is at line start, with its reason. 5 files, +510 / −25, no governed path (check-governed-merges: NOT governed). The PR assignee is os-zhuang.
    • Clause-②: no re-checked: the one new export, storedLegacyOptions, is a module export of views/ObjectView.tsx beside kanbanViewOptions. Neither is on packages/app-shell/src/index.ts. No published schema, type or input moves. patch for @object-ui/app-shell ✓.
    • The seat's direction 5971906253, carried out:
      • ObjectView hands ListView the stored row's judged options bag. For each kind the bag carries, the bag's block replaces this page's synthesized block, and the view's own top-level block goes out at the top level.
      • ListView's EXISTING per-key merge ({ ...options.KIND, ...KIND }) then applies the spec's precedence. No merge is written in app-shell.
      • A row with no bag is relayed exactly as before.
      • InterfaceListPage keeps forwarding options and the options.map path (ruling A).
    • Claim 5971770388 amended here:
      • InterfaceListPage.tsx changes, which the seat had said it "may need no change": its derived defaults (kanban lane, calendar and timeline date, gallery cover, gantt dates) no longer fill a kind the bag carries. That extends the page's own mapCfg rule (view.options.map ?? derived). A derived block placed at the top level would otherwise outrank the row's declaration.
      • ObjectView.relayRungCensus-7559.test.ts loses its eight relayed-nested rows, because its own "declares nothing that IS relayed" check requires it.
      • Both accepted.
    • Accepted trade-off, recorded: for a kind its bag carries, a legacy row now gets no page-derived default on either page. On main the object page's synthesized block used to fill one. Layering derived < bag < declared would need the second merge the direction forbids. No producer of bag rows is measured: the objectstack census of objectstack PR #20868 found zero authored bodies, and no objectui source originates a stored bag. So the cost is theoretical, and both pages now render the declared metadata the same way.
    • Pins: ObjectView.storedOptionsBag-10380.test.tsx mounts the REAL ListView from both pages with spy renderers, over door-legal rows R2–R5, the no-bag control R6 and two collision rows.
      • Red first: with the fix reverted, 6 of 8 failed, including the kanban collision, measured on the object page's titleField floor.
      • Ablation: merging the declared block UNDER the bag turns 3 red. Restored by blob hash.
    • The dev's open question (a type: 'chart' row whose chart block lives only in the bag) ⇒ A, answered by this seat. Fixes stays, and the residual is in Acceptance notes. The object page's dedicated chart route reads viewDef.chart only. No producer of bag-only chart rows is measured: CreateViewDialog writes a top-level chart. A shared export (B) is the right shape if one appears. C (a second resolution) is ⛔.
    • Out-of-scope findings:
      • class (a), named producer: the interface page's schema literal has no tree / chart rung, so a CreateViewDialog row with a top-level tree: { parentField } reaches ListView's tree branch with no parentField there. This seat files it as a card.
      • The legacy-row next-save refusal ⇒ Acceptance notes, per 5971906253.

    Generated by Claude Code

  11. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and verified by content: closed completed by PR objectui#11571 · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T19:06Z.

    • The merge: PR objectui#11571 went through the merge queue to main as 0685646 at 2026-10-03T19:05Z (seat ACCEPT 5972279043).

    • Content check on main 0685646 against its first parent a7557a7:

      main parent
      ObjectView.tsx: export function storedLegacyOptions 1 0
      ObjectView.tsx: the ...legacyOptions, forward into options 1 0
      InterfaceListPage.tsx: derives('kanban') (derived defaults yield to the bag) 1 0
      InterfaceListPage.tsx: the options forward, kept (ruling A) 1 1

      The pin ObjectView.storedOptionsBag-10380.test.tsx and the changeset 10380-object-page-stored-options-bag.md are in the tree.

    • No other card closed in that window.

    • Carried, not on this card:

      • The bag-only chart residual is recorded in the PR's Acceptance notes.
      • The interface page's dropped tree / chart blocks are filed as objectui#11572.
      • The dev's four-axis case for retiring the list overlay's bag at the spec door (which would re-open ruling A's ⛔C) is listed for the maintainer in the seat's round report.
    • pm:dispatched is removed in the same act. domain:ui, the priority and the type stay.


    Generated by Claude Code

  12. added a commit that references this issue on Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions