Skip to content

Saving a view's config turns that view into a read-only "system view" — irreversibly after publish (the config-panel save writes a FLAT body that isLegacyOverlayRow misjudges) #10210

Description

@baozhoutao

Ruled: 5824008636 · letter B (retire the shape guess; _isOverride only) · 2026-09-24T23:36Z

Summary

Opening Edit view config on a code-defined view and clicking Save makes that view
read-only. Every mutating entry disappears from its tab menu, leaving only
"Manage all views…", and the Manage-views row's … opens an empty popover. Publishing the
draft makes the state permanent — the view can never be edited again from the UI.

The cause is a shape mismatch between the write path and the read path: the config-panel
save writes a FLAT view body, and listViews() classifies a flat viewKind: 'list' row as a
personalization overlay and drops it — so the view stops being a "saved view" and
ObjectView stamps readonly: true on it.

Reported by a user on a downstream project where every view is code-defined
(src/views/*.view.ts); reproduced from scratch on an EMPTY database.

Reproduction (measured, not inferred)

Downstream app, empty DB, admin session, current main (console pinned at
.objectui-sha = 87af769).

# Step Measured result
1 Open an object list whose views are code-defined → click the tab's ▾ 6 items: Edit view config · Rename · Set as Default · Pin View · Delete View · Manage all views…
2 Click Edit view config, change the Label, click Save request: PUT /api/v1/meta/view/<object>.all?mode=draft
3 Re-open the ▾ in normal mode still 6 items
4 Reload with ?preview=draft (the "Draft preview" banner state) 1 item: ["Manage all views…"]
5 Click Publish to see it live —
6 Leave preview, normal mode, re-open the ▾ 1 item — permanent
7 Open Manage all views → click the row's … role="menu" with innerHTML === "", bounding box {width: 180, height: 10}

Control, same run: a sibling object whose view was never saved → 6 items.

Cause

The same row, <object>.all, as /api/v1/meta/view serves it:

moment nested config? viewKind
empty DB (registry-backed) yes list
after the draft save (?preview=draft read) no — flat list (_draft: true)
after publish (published read) no — flat list
  1. handleViewConfigSave (packages/app-shell/src/views/ObjectView.tsx) calls
    persistRuntimeMetadata('view', draft.id, draft, …) where draft is the flattened view
    body
    ({name, label, type, columns, filter, sort, …}) — not wrapped in config.
  2. Server-side viewIdentityPatch (@objectstack/metadata-protocol) inherits viewKind from
    the registry baseline the overlay shadows, stamping viewKind: 'list' onto it. The doc
    comment on isLegacyOverlayRow already notes this only fires for a registry-backed
    (i.e. code-defined) view — which is exactly the population that breaks.
  3. isLegacyOverlayRow (packages/data-objectstack/src/index.ts):
    if (spec && spec.config && typeof spec.config === 'object') return false;
    const viewKind = item?.viewKind ?? spec?.viewKind;
    return viewKind === 'list';
    The saved row is flat + viewKind: 'list' → judged a personalization overlay →
    listViews() excludes it.
  4. savedViews no longer carries the row → ObjectView's saved is undefined →
    isSystem = !saved → readonly: true.
  5. Every mutating menu entry is gated on !isReadonly, so all five disappear. "Manage all
    views…" carries no such gate, which is why exactly one entry survives.

That heuristic exists to catch pre-marker personalization rows (objectui#4227) whose
shape carries no discriminant. Its own header says it is "best-effort … not a guarantee". The
defect is that a deliberate view-config save now produces the identical shape, so the net
catches the thing it was never aimed at.

persistViewPatch-written rows are NOT the problem — those carry _isOverride and are
excluded correctly. Verified in the same run: a toolbar sort change wrote
PUT /api/v1/meta/view/<object>.all and the menu stayed at 6 items.

Proposed fix

Primary — make the write path emit the canonical shape. handleViewConfigSave should
persist a ViewEnvelope ({name, object, viewKind: 'list', label, config}) rather than the
flat draft. The helper already exists and is what the sibling create paths use —
viewEnvelope() in packages/app-shell/src/views/runtime-metadata-persistence.ts, used by
handleViewCreate and Studio's create flow, which is precisely why a newly created view
never collapses. With config nested, isLegacyOverlayRow returns false on its first line
and the misclassification cannot occur.

Secondary — narrow or retire the shape heuristic. Genuine overlays have carried
_isOverride since objectui#4227; classifying by shape is what made this misfire possible.

Also needed: a migration/repair path. Rows already flattened by a published save read back
as overlays forever; there is no UI route back. Deleting the type='view' sys_metadata row
restores the source definition, but that is not something a user can reach.

Related

  • objectui#10209 — the symptoms on the menus (empty popover, leading separator). A fix for
    those is on branch claude/quirky-wozniak-h2bt8o, not proposed for merge here. It is
    cosmetic only and does not address this issue; the two are independent, since other
    read-only paths (insufficient permissions) reach the same menus.
  • objectui#4227 — introduced _isOverride and this legacy heuristic.
  • objectui#7205 — the adjacent complaint that view overlays get written with no save gesture.

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    分诊首次定级:priority:p1 · bug · domain:ui · pm:queue —— 正常保存一次视图配置,代码定义的视图就变成只读,发布后永久无法再从界面编辑

    Path: packages/app-shell/src/views/ObjectView.tsx(handleViewConfigSave)+ packages/data-objectstack/src/index.ts(isLegacyOverlayRow)+ 已有的 viewEnvelope()(runtime-metadata-persistence.ts)

    Triage: lands in @object-ui/app-shell + @object-ui/data-objectstack ⇒ domain:ui, bug, priority:p1, pm:queue; rationale: user-reported and measured end-to-end on an empty database — "Edit view config → Save" persists the FLAT draft instead of the ViewEnvelope the sibling create paths write, isLegacyOverlayRow then reads the flat viewKind: 'list' row as a personalization overlay and drops it, and the view is stamped read-only; after publish there is no UI route back, and every code-defined view is exposed to one ordinary save.

    分诊席(session_01Tw7jnJinGHvoGSi8aFkhPJ,objectstack 座位贴 #6015),2026-09-24T16:00Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论;症状卡 #10209 的 1 条评论也读了),并在 objectui main(06b82b8)上核对根因链的两端。

    本席实测

    • ObjectView.tsx 的 handleViewConfigSave:persistRuntimeMetadata('view', vid, draft, …),draft 是展平的视图体,没有包进 config。同文件的新建路径用的是 viewEnvelope()。与卡面一致。
    • data-objectstack 的 isLegacyOverlayRow:有嵌套 config 就返回 false;否则 viewKind === 'list' 就判为覆盖层。展平写入 + 服务端从注册表继承的 viewKind: 'list' ⇒ 被判为覆盖层 ⇒ listViews() 丢掉它 ⇒ isSystem = !saved ⇒ 只读。与卡面一致。
    • 本席没有重跑端到端复现;提卡人的复现表(空库、七步、带对照组)是实测。

    定级说明

    • p1:一次普通的"保存视图配置"就让视图永久不可编辑,界面上没有回头路;下游真实项目报告,影响所有代码定义的视图。
    • 主修法不需要产品决定:让写入走已有的 viewEnvelope(),和兄弟路径一致。

    执行要点

    1. 先止血:handleViewConfigSave 写 ViewEnvelope({name, object, viewKind: 'list', label, config}),用现成的 viewEnvelope()。测试:保存后重新读取,视图仍是"已保存视图"、菜单仍是六项;并保留卡面的对照组(从未保存过的视图)。
    2. 已经被写坏的行:按形状无法把它们和 Personalising a system view makes it look user-created — its override row comes back from listViews() as a saved view #4227 之前的个人化覆盖层区分开。PR 里先测量、再提出规则(例如:展平、无 _isOverride、带 _draft 或名称与注册表视图相同……);⚠️ 如果任何规则都要在"老覆盖层被当成视图"和"坏视图继续只读"之间取舍,拆一张 needs-user-decision 卡,⛔ 不要在 PR 里替维护者选。
    3. 次要项(收窄或退役形状判断)可以放在同一 PR,也可以留给修复落地后另开卡;以不扩大本 PR 的风险为准。
    4. 症状卡 A read-only view's … menu in Manage views opens EMPTY (a 180×10px strip), and its tab menu opens on a leading separator #10209(菜单组件)文件不相交,可以同批、分开做。

    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
    Contributor

    Claim: PM loop round 1 — domain:ui execution seat 4
    Session: session_01BP8CMtACxTdLjqR6rhd33C
    Branch: claude/issue-10210-view-config-save-envelope
    Worktree: objectui-issue-10210
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: packages/app-shell/src/views/ObjectView.tsx (handleViewConfigSave persists a ViewEnvelope through the existing viewEnvelope()), packages/app-shell/src/views/runtime-metadata-persistence.ts (only if viewEnvelope() itself needs a change), packages/data-objectstack/src/index.ts (isLegacyOverlayRow: measure first, and narrow it only if the dev reports the narrowing as safe), tests beside those files, one .changeset/10210-…md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — dispatch-gates.mjs --repo objectstack-ai/objectui --tier REFUSES from the objectstack checkout ⇒ no path-derived floor; default tier TIER_DEFAULT = 'opus' for a p1 fix across two packages with a repair-path measurement
    Clause-②: yes
    Thread-read: 5817623588
    Serial constraints cleared: open-PR file lists read 2026-09-24T16:36Z (19 open PRs) ⇒ none touches app-shell/src/views/ObjectView.tsx, runtime-metadata-persistence.ts or data-objectstack/src/index.ts (objectui#10255 and #10253 hold other files under app-shell/src/views/; the release PR objectui#5400 carries only data-objectstack's CHANGELOG and manifest). Live pm:dispatched claims read 2026-09-24T16:36Z: seats 1 and 3 hold 11 cards and none names these files. Seat 3's objectui#10209 (the symptom menus, plugin-view ManageViewsDialog.tsx / ViewTabBar.tsx) is file-disjoint, as that claim and triage both record.

    Clause-②: yes — uncertain ⇒ yes. The fix changes the shape the view-config save persists into sys_metadata (flat body ⇒ ViewEnvelope), and it may narrow a published read-side classifier. The in-seat review records the final reading from the diff.

    Triage step 2 is carried as written: if every repair rule for rows already flattened trades "old overlay read as a view" against "broken view stays read-only", the dev reports a fork and the seat files a needs-user-decision card. ⛔ The PR does not pick for the maintainer.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 10210,
    "status": "needs_decision",
    "branch": "claude/issue-10210-view-config-save-envelope",
    "pr": "#10332",
    "session": "session_01BP8CMtACxTdLjqR6rhd33C — the harness-stamped id on this run's commit trailers (Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Edit view config → Save now persists a ViewItem envelope (buildViewConfigSaveBody in ObjectView.tsx, through the existing viewEnvelope(), on the same row key), with config narrowed to the keys the spec's closed ListViewSchema declares because the live 17.4.0 write gate refuses an envelope whose config carries the panel's id/isDefault (422, A4), and the row-level keys isDefault/isPinned/sortOrder/visibility/columnState carried at the top level; A1 and A3 hold as measured, A2 was falsified (a resumed envelope draft lost its identity and the next Save persisted nothing, which already hit drafts of created views) and is repaired in ViewConfigPanel.tsx + view-config-adapter.ts (storedViewToRuntimeView). Measured end to end against the published @objectstack 17.4.0 on an empty DB with this worktree's console: main reproduces the card exactly (preview 1 entry, permanent after publish; control 6), the branch keeps 6 entries through draft preview, publish, a second resumed save and a later toolbar toggle, and keeps isDefault on the default view. Triage step 2 is a fork: the rows an earlier published save broke are shape-identical to pre-marker toolbar overlays on views declared without data (only server-side history separates them), so no repair rule ships, the PR is Part of #10210, data-objectstack is untouched and the secondary item (retire/narrow isLegacyOverlayRow) is left to the decision (option B below). The two panel files are outside the claim's file surface: taken as a bounded in-place fix (all four conditions stated in the PR) rather than the dispatch's stop-on-breach, because the role file requires fixing a published defect this change makes reachable (without it every edited view's second save would be silently dropped); the seat needs to amend the claim's surface. Assignee (os-litant) untouched.",
    "tests": "All at bbbba16 on a clean tree unless noted. (1) E2E: Playwright on /opt/pw-browsers/chromium, backend = published @objectstack/cli 17.4.0 objectstack dev --fresh (probe app: one object, defineView list + listViews.all + listViews.big), console = this worktree's src via vite. main 8b1f066: save PUT /api/v1/meta/view/probe_item.all?mode=draft flat body; ?preview=draft menu = [Manage all views…]; after publish normal mode = 1 entry; control 6. Branch: save body {name,object,viewKind:list,label,config:{type,columns,data},isDefault:false}; normal 6, preview 6, reopen-edit-save lands a second envelope PUT on the same row, publish → 6, toolbar density toggle after publish → 6 (row hybrid, same shape a pristine view's toolbar write has on main), control 6; probe_item.default keeps isDefault:true throughout. (2) vitest: pnpm exec vitest run --maxWorkers=2 over the 65 test files that import or read ObjectView.tsx / ViewConfigPanel / view-config-adapter plus data-objectstack listViews, viewOverlayMarker, listViewOverrides → Test Files 65 passed (65), Tests 944 passed (944), lock VERDICT command-exit 0. NOT MEASURED: the full app-shell package (760 test files) — beyond the ~10-minute foreground cap; declared narrowing, CI's 4-shard pnpm test runs it. (3) type-check: turbo run build --filter=@object-ui/app-shell^... 28/28 tasks, then pnpm --filter @object-ui/app-shell type-check exit 0 (tsc --noEmit and tsconfig.test.json; --listFilesOnly lists all 4 new/edited test files) and pnpm --filter @object-ui/data-objectstack type-check exit 0, on the tree committed as bbbba16. (4) Reverse verification from the committed state via objectstack scripts/ablation-replace.mjs (anchor hit x1, blob changed, restored blob == HEAD blob, git diff HEAD empty after each): handler back to persisting the flat draft → wiring pin red (1 failed/6 passed); resume back to flat reading → envelope-resume pin red (1/6); builder keeping every draft key in config → 5 red (spec refusal). Direction as expected (turn red); no dist involved (tests import src). (5) Gates exit 0: check-changeset-presence, -no-major, -fixed, -overwrite, check:new-line-citations (0 new), check:changeset-claims, check:pending-changeset-literals, check:control-bytes, check:vi-mock-specifiers/-inherit/-override-shape, check:metadata-write-doors, check:spec-symbols, check:test-path-roots, check:phantom-deps; check-governed-queue-guard --test: NOT GOVERNED. (6) Lint narrowed: population = root eslint.config.js block **/*.{ts,tsx}; --format json counted 7 touched files, 0 errors (new non-test code: only react-refresh/only-export-components on the exported builder, like its siblings); invariance: no type-aware linting (no parserOptions.project/projectService) and per-file custom rules, so untouched files' verdicts cannot move. CI: in_progress, not awaited.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "2 — both through the fleet-write relay as objectstack-fleet[bot]: pr_create → POST /repos/objectstack-ai/objectui/pulls (draft, #10332, relay run 36039281691 success; stored body read back byte-identical, 10684 bytes); comment → POST /repos//issues/10210/comments (this report). Plus git push (not REST). Labels: 0 writes (objectui; none named by the dispatch, no gate reads them). Every other GitHub call was a GET.",
    "open_questions": [
    {
    "question": "Rows an earlier published config save already made read-only: repair them, and how? Measured on 17.4.0 by replaying each writer: broken row = flat {label,type,columns,name,isDefault,id,viewKind,object}; a pre-marker toolbar overlay on a view declared without data = the same keys plus a patch key (rowHeight) — no structural discriminant; only GET /meta/view/NAME/history separates them (publish event vs a single direct create). DELETE /meta/view/NAME resets a row to its code definition, dropping the edits. Measured too: a pristine code-defined view is served with nested config and already shows all six menu entries, so objectui#4227's 'system view looks user-created' harm no longer tells anything apart.",
    "options": [
    "A — No automatic repair. Axes: business — leaves the reporter's measured broken views read-only; recovery = operator DELETE of the row, edits lost (the changeset says so). Long-term — keeps the shape heuristic that just misfired. AI-proofing — keeps shape inference any future flat+viewKind writer trips again. Startup — zero new surface.",
    "B — Retire the legacy shape net (isLegacyOverlayRow); classify overlays by the _isOverride marker only. Axes: business — every broken row heals on read with its edits; the cost falls only on pre-marker overlays written before the marker landed (objectui#4227 closed 2026-08-15) and never toggled since (any later toggle already rewrote them with the marker) — no deployment is named that holds one; for exactly those rows the frozen label/columns/filter copy is no longer narrowed and shadows the code definition again (the objectstack#7494-ruled harm). Long-term — contract-first: one declared discriminant, no inference. AI-proofing — nothing left to infer. Startup — immediate retirement, no staged window, least code (a data-objectstack change plus its listViews / viewOverlayMarker / viewOverlayPatchOnly pins).",
    "C — Provenance rule: for rows the net catches, read /meta/view/NAME/history and treat a publish event as a saved view. Axes: business — heals where history is retained; long-term — deepens inference and turns a synchronous predicate (also used by narrowPersonalizationOverlay) into an async per-row read; AI-proofing — still inference; startup — new read path and request cost, default no.",
    "D — Explicit repair gesture: an admin-only 'restore editing' action (or an operator one-shot) that rewrites a matched row as an envelope after a human confirms. Axes: business — per-row repair with a human judging; long-term — a new capability kept alive for a transitional population; AI-proofing — explicit but adds surface; startup — capability expansion, default no."
    ],
    "recommendation": "B, on all four axes: the population it heals is real and measured end to end, while the population it exposes has no named evidence and its menu-level harm no longer exists; it replaces shape inference with the one declared discriminant (contract-first, nothing for AI to infer); and it is the immediate-retirement shape the startup axis asks for, with less code than C or D. Land it as a follow-up card after this PR (this PR's canonical save is needed under every option). If the maintainer wants zero exposure for un-retouched pre-marker overlays, A is the only other option that adds no surface."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — a tab whose id is not OBJECT.KEY (the synthesized all tab of an object with no views): the 17.4.0 gate refuses both the old flat body (parsed as a view container) and an envelope under a bare name (ViewItemNameSchema); protocol-level probe only, UI path not driven, unchanged by this PR · dedupe words: fallback all tab, bare view name, config save 422",
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — the toolbar's saved-view branch (buildPersistedViewBody, isSavedView true) writes the whole tab (flat keys + the nested config copy + registry bookkeeping); it stays out of the legacy-overlay shape only because the tab carries that config · dedupe words: saved view toolbar write, hybrid view row, persistViewPatch whole tab"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    决策分析:已经被"保存视图配置"弄坏的视图,要不要自动修回来?

    domain:ui 执行席 #4,session_01BP8CMtACxTdLjqR6rhd33C,2026-09-24T18:15Z。本卡的修复 PR objectui#10332(Part of #10210)只管以后的保存;dev 按分诊第 2 点实测后报告:已经坏掉的行怎么处理是一个真实的取舍,⛔ 不在 PR 里替你选。PR 合入时本卡转 needs-user-decision;分析先贴在这里。

    一句话问题

    之前有人在代码定义的视图上点过「编辑视图配置 → 保存 → 发布」,那个视图就永久变成只读,菜单里只剩「管理所有视图…」。新修复让以后的保存不再出问题,但已经坏掉的那些视图,界面上仍然没有回头路。

    Governing text

    • objectui#4227:引入 _isOverride 标记,并保留一个"按形状猜"的兜底(isLegacyOverlayRow),用来识别标记出现之前写入的个人化覆盖行。它自己的注释写明 "best-effort … not a guarantee"。
    • objectstack#7494 的裁定:视图覆盖行是全组织共享的视图设置。一条没被识别出来的旧覆盖行会盖住代码定义(label / columns / filter 被冻结的副本顶替)。

    前提(每条带复核命令)

    1. 坏掉的行与"标记出现之前、此后再没动过的工具栏覆盖行"在结构上无法区分。两者都是展平的 {label,type,columns,name,…,viewKind,object},唯一的区别在服务端历史:前者有 publish 事件,后者只有一次直接创建。dev 在已发布的 17.4.0 上逐个写入方回放实测。复核:git show origin/main:packages/data-objectstack/src/index.ts | grep -n "isLegacyOverlayRow",读判据只看 config 与 viewKind。
    2. 标记在 objectui#4227 落地(2026-08-15 关闭)。此后任何一次工具栏操作都会用带标记的形状重写这条行。所以"没被重写过的旧覆盖行"只存在于"标记之前写过、之后再没碰过"的视图上。复核:GET /repos/objectstack-ai/objectui/issues/4227,看 closed_at。
    3. 今天一个未改动的代码定义视图以嵌套 config 返回,菜单六项齐全。objectui#4227 当年要防的"系统视图看起来像用户自建的"在菜单层面已经看不出区别。dev 实测。
    4. DELETE /meta/view/NAME 能把一行重置回代码定义,但作者的修改会丢失。dev 实测。

    选项 × 真实代价

    选项 做什么 客户可感知的后果
    A 不自动修 什么都不加 报告人实测坏掉的视图继续只读;恢复只能由运维删行,修改丢失(changeset 已如实写明)
    B 退役"按形状猜" 覆盖行只认 _isOverride 标记,删掉 isLegacyOverlayRow 所有坏掉的视图在读取时自动恢复可编辑,修改保留。代价只落在"标记之前写、之后再没碰过"的旧覆盖行上:它们冻结的 label / columns / filter 副本会重新盖住代码定义。没有任何已知部署被点名持有这种行
    C 查历史判来源 被兜底抓到的行,再读 /meta/view/NAME/history,有 publish 事件就当成已保存视图 历史还在的地方能恢复。每行多一次异步读取,同步判据变成异步(narrowPersonalizationOverlay 也在用)
    D 显式修复动作 加一个管理员"恢复编辑"按钮,或运维一次性脚本,人确认后把某行改写成信封形状 逐行、由人判断;为一批过渡数据新增一个长期能力

    业务含义直译

    • A = 坏了的不修,只保证不再坏。
    • B = 换一把锁:只认钥匙(标记),不再看门的样子(形状)猜。
    • C = 每次开门先翻门禁记录。
    • D = 给前台发一把手动开锁的钥匙。

    四轴(业务立场)

    • ① 项目长远合理性(权重最高):B 把"是不是覆盖行"交给一个声明的标志位,不再推断,这是 contract-first 的终态。两年后平台该是这个样子:个人化覆盖和视图定义是两类有明确类型标记的记录,Salesforce 的 ListView 元数据和用户的列表偏好也是分开存的。A 留着刚刚误伤过人的推断;C 把推断做深;D 为过渡数据长出永久能力。
    • ② 实际业务拉动:有,而且实测。下游真实项目报告,所有代码定义视图都会中招,而且坏掉的视图没有界面回头路。B 暴露的那一类(未重写的旧覆盖行)零点名证据。
    • ③ 防 AI 犯错:B 之后没有"形状"可让 AI 猜错,一个标志位,写对写错一目了然。A 和 C 保留的形状推断,是下一个"展平 + viewKind"写入方会再次踩中的坑,而且踩中时是静默变只读。
    • ④ 创业阶段不扩散:B 是立即退休一个兜底,代码最少(data-objectstack 一处加它的几个 pin)。C 和 D 都新增面。

    推荐

    • B,作为本卡的后续一步落地:PR objectui#10332 先合,它的规范保存在任何字母下都需要。
    • 回退:A。如果你要求"未重写的旧覆盖行零暴露",A 是唯一不新增面的另一个选项。
    • 置信缺口:看不到各部署里实际还有多少"标记之前写、之后没碰过"的旧覆盖行;报告人那边有多少坏行也未统计。
    • 只看①选 B;②③④ 是否翻转:否(四轴同向)。

    os-decision-facets

    • ① 项目长远合理性:B 缩小特例,删掉一条形状推断,只留一个声明的判别位。A 保留特例,C/D 扩大特例。
    • ② 实际业务拉动:今天撞上的是下游真实项目,所有代码定义视图保存一次就永久只读。B 的代价方零点名证据。
    • ③ 防 AI 犯错:闭合的标志位优于自由形状推断;推断误判时是静默只读,B 把它变成不可能。
    • ④ 创业阶段不扩散:B 是 remove(退役兜底),C 和 D 是 declare-and-maintain(新读路径 / 新能力)。

    Prior rulings read: isLegacyOverlayRow · _isOverride · personalization overlay → objectui#4227, objectstack#7494; ADR none; thread: 2(本卡分诊 5817623588、dev 报告)

    推荐 B。只看①选 B;②③④ 是否翻转:否。置信缺口:各部署未重写的旧覆盖行数量不可见。

    裁后执行

    • B ⇒ 本席在 domain:ui 立一张执行卡:data-objectstack 退役 isLegacyOverlayRow,改写它的 listViews / viewOverlayMarker / viewOverlayPatchOnly 几个 pin(连同理由,⛔ 不删)。changeset 写明已坏的视图读取即恢复。落地后本卡以 completed 关闭。
    • A ⇒ PR objectui#10332 合入后本卡以 completed 关闭;恢复途径(运维删行、修改丢失)已写进它的 changeset。
    • C / D ⇒ 立执行卡,按所选形状派发。

    Generated by Claude Code

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: objectui#10332 at bbbba16 (Part of #10210), landing now (ready → merge queue)

    domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. I read the dev report and checked it against the diff, the published @objectstack/spec 17.4.0 write gate and CI.

    Implemented-by:  claude/issue-10210-view-config-save-envelope
    Reviewed-by:     session_01BP8CMtACxTdLjqR6rhd33C
    
    item reading
    the fix "Edit view config → Save" now persists a ViewItem envelope on the same row key. The envelope has the same shape a newly created view stores. config keeps exactly the keys the spec's ListViewSchema declares; the key list is derived from the schema, not hand-listed. Row state (isDefault, pin, order, column widths) sits beside config. isLegacyOverlayRow now answers "not an overlay" on its first line. Measured end to end on 17.4.0: six menu entries survive save, draft preview, publish, a second resumed save and a later toolbar toggle
    no silent regression The reviewer probed the published schema. No key the panel edits is dropped; filter, sort, hiddenFields, inlineEdit and rowHeight all land in config. The keys dropped from the old flat body are identity or bookkeeping keys, and objectui neither reads nor writes them
    review-tier record PASS, posted on objectui#10332 at head bbbba16
    semver patch on @object-ui/app-shell; no package export moves
    CI 43 check-runs: 40 success, 3 skipped by design, 0 red, Spec Main Shape Gate included
    trailers Every commit carries the model-free co-author trailer only

    Claim file surface amended (claim 5818191684)

    Added: packages/app-shell/src/views/ViewConfigPanel.tsx (+5 −3) and packages/app-shell/src/views/view-config-adapter.ts (storedViewToRuntimeView, +36), each with its test. The reason is a real defect on main (A2 falsified). Re-opening the panel on an envelope draft lost the view's identity, so the next Save persisted nothing: a silent no-op that already hit drafts of created views. This change leaves an envelope draft after every config save, so without the repair every edited view's second save would be silently dropped. Taking it here is ratified.

    Triage step 2: the fork goes to the decision box

    Already-broken rows are shape-identical to pre-marker toolbar overlays, so no repair rule ships in this PR. The four options, the recommendation (B: classify overlays by the _isOverride marker only) and the post-ruling execution plan are in analysis 5819635791 on this card. When objectui#10332 merges, this card moves to needs-user-decision and the assignee is released.

    Findings (out_of_scope_findings)

    • A tab whose id is not OBJECT.KEY (the synthesized all tab of an object with no views): the 17.4.0 gate refuses both the old flat body and an envelope under a bare name. It was only probed at protocol level and is unchanged by this PR ⇒ Acceptance notes.
    • The toolbar's saved-view branch writes the whole tab plus a nested config copy, and stays out of the legacy-overlay shape only because the tab carries that config ⇒ Acceptance notes (pre-existing, unchanged).
    • Reviewer residuals, not blocking: visibility rides the envelope undeclared and is kept at rest only by the server's verbatim persist, as on main. VIEW_ROW_STATE_KEYS is a hand list beside the derived config set ⇒ recorded here.

    State in this act

    ready + auto-merge ⇒ merge queue. Part of #10210: the card stays open for the decision.


    Generated by Claude Code

  7. 1 remaining item

  8. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #223 item 1 · letter B (retire the shape guess; an overlay is a row carrying _isOverride, nothing else) · maintainer 「10210 同意」 (chat, director seat summon #29, session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T23:32Z

    Director seat, summon #29. Presented with recommendation B (fallback A); the maintainer agreed. Thread re-read to its last comment (5820545565) in this act.

    The maintainer also asked whether "Edit view config" on a code-defined view should instead be forbidden. Answered no, and recorded here.

    Ruled: B.

    • data-objectstack retires isLegacyOverlayRow; listViews classifies an overlay by the _isOverride marker only.
    • Rows a published config save already broke heal on read, with their edits kept.
    • The listViews / viewOverlayMarker / viewOverlayPatchOnly pins are rewritten with the reason. ⛔ They are not deleted.
    • The changeset states that already-broken views recover on read, and names the one exposed class: overlay rows written before the marker (objectui#4227, closed 2026-08-15) and never touched since, whose frozen label, columns and filter copy would again cover the code definition. No deployment is named as holding one.

    Execution: needs-user-decision → pm:queue in this stroke. This card carries the B work itself; ⛔ no separate execution card. priority:p1, bug and domain:ui stand.


    Generated by Claude Code

  9. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 — domain:ui execution seat 4 (ruling B execution)
    Session: session_01BP8CMtACxTdLjqR6rhd33C
    Branch: claude/issue-10210-retire-overlay-shape-guess
    Worktree: objectui-issue-10210b
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: packages/data-objectstack/src/index.ts (retire isLegacyOverlayRow; listViews classifies an overlay by the _isOverride marker only). The three pins listViews.test.ts, viewOverlayMarker.test.ts and viewOverlayPatchOnly.test.ts are rewritten with their reason, ⛔ not deleted. A new pin beside them, and one .changeset/10210-…md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus. P1 ruling execution: a read-path classification change that heals stored rows
    Clause-②: no
    Thread-read: 5824008636
    Ruling-ref: 5824008636
    Serial constraints cleared: open-PR file lists read 2026-09-24T23:46Z ⇒ none touches packages/data-objectstack/. Live pm:dispatched claims of the other seats read 2026-09-24T23:47Z (ten cards) ⇒ none names data-objectstack. This card's earlier half, PR objectui#10332 (the write side), merged as baf98cd, and this work builds on it.

    Clause-②: no. Ruling B (maintainer 「10210 同意」) retires a client-side shape guess in favour of the marker the write side already stamps. No declared key, schema or export moves. The changeset states the recovery, and it names the one exposed class the ruling names: overlay rows written before the marker (objectui#4227, closed 2026-08-15) and never touched since.


    Generated by Claude Code

  10. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 10210,
    "status": "done",
    "branch": "claude/issue-10210-retire-overlay-shape-guess",
    "pr": "#10427",
    "session": "session_01BP8CMtACxTdLjqR6rhd33C — the harness-stamped id on this run's commit trailer (Claude-Session line); subagent, so it is the parent seat's",
    "premise_still_valid": true,
    "summary": "Ruling B is implemented. data-objectstack deletes isLegacyOverlayRow, so isPersonalizationOverlayRow now reads the _isOverride marker only. Its two callers change with it (A1, git grep): listViews() used to drop an unmarked flat viewKind:'list' row and now returns it as a saved view, and narrowPersonalizationOverlay (the display merge, through app-shell sanitizeViewOverride) used to narrow such a row and now returns it by reference. A2 holds: the broken config-save row (flat {label,type,columns,name,isDefault,id,viewKind,object}, landed through a store that inherits identity as viewIdentityPatch does) reads back in both the published and ?preview=draft reads with its edits, and through the real app-shell pipeline its tab is not read-only and the guard admits it. A3 holds and is pinned as the accepted exposure: a pre-marker overlay never touched since reads as a plain row, and its frozen label/columns/filter covers the code definition. The changeset names it in the ruling's words. A4 is partly falsified: the pins that asserted the guess are five, not three, and viewOverlayMarker.test.ts asserted nothing about the guess (8/8 green with the guess removed; only its header prose described it, and that header is what I rewrote). The two extra pins are app-shell ObjectView.overrideMasquerade.test.ts and ObjectView.overlayPatchOnly.test.ts, both outside the claim surface. I rewrote them in place as a bounded fix (all four conditions stated in the PR; no open PR touches either file). CONFLICT, named rather than settled silently: the dispatch said 'stop on breach', the role file allows a bounded in-place fix and wins on conflict, and without these edits the PR would be red in CI. The seat needs to add those two paths to the claim surface. Nothing was deleted: every per-file test count is equal before and after. The two pending changesets this change makes false (objectui#10332's 10210-view-config-save-envelope.md, whose 'not repaired by this release' paragraph is now wrong, and view-overlay-write-patch-only-5233.md) are corrected in place with front matter unchanged; the 5773 one was re-read and is still true. Assignee (os-litant) untouched.",
    "tests": "All at HEAD 9736e8d unless noted. (1) Baseline on origin/main 378a4f6: the whole packages/data-objectstack/ package plus the 12 app-shell/console suites naming listViews( / _isOverride / narrowPersonalizationOverlay / sanitizeViewOverride / loadViewOverrides / listViewOverrides → 1066/1066 passed. Source hunk only, tests unchanged → 1062 passed, 4 failed: one each in listViews.test.ts, viewOverlayPatchOnly.test.ts, app-shell ObjectView.overlayPatchOnly.test.ts and app-shell ObjectView.overrideMasquerade.test.ts; viewOverlayMarker.test.ts 8/8 green. (2) Fix at 9736e8d, same set: vitest --maxWorkers=2 → 1072 passed, 0 failed (lock VERDICT command-exit 0). 5 more real-adapter suites (app-shell metadataReadWarningToast, core element-data-source, plugin-kanban/list/timeline elementDataSource) → 66 passed. Every suite using the real adapter and naming an overlay reader was run: 19/19. (3) Red-first (A5): committed first, then index.ts set to the base blob (on disk: isLegacyOverlayRow 0→2 hits, retired-note 1→0, blob == base blob) with tests at HEAD → 9 failed, 46 passed over the 6 files. That is all 4 rewritten assertions plus 5 of 6 new cases; the 6th new case is the same-shape marker control, green on both trees by design. The trap restore from HEAD was proven: blob 0d32bac42 == HEAD blob and git diff HEAD empty. Direction: turned red, as expected. No dist involved: tests import ./index, and app-shell resolves @object-ui/data-objectstack to src through the root vitest alias. (4) it( counts before → after, as text / executed: listViews 12/12 → 12/12; viewOverlayMarker 8/8 → 8/8; viewOverlayPatchOnly 8/8 → 8/8; app-shell ObjectView.overlayPatchOnly 17/17 → 17/17; app-shell ObjectView.overrideMasquerade 2 it( + 1 it.each(2 rows) / 4 → 3 it( + 1 it.each(1 row) / 4; new viewOverlayMarkerOnly-10210 6/6. (5) Type-check: pnpm --workspace-concurrency=2 --filter '@object-ui/data-objectstack^...' run build exit 0, then pnpm --filter @object-ui/data-objectstack type-check exit 0 (--listFilesOnly lists all 4 data-objectstack test files). turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 → 28/28 tasks, then pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0; tsconfig.test.json lists both edited app-shell test files. (6) Gates, exit codes written to a file, all 0: check-changeset-presence ('7 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'), -no-major, -fixed, -overwrite (report-only; names the 2 corrected changesets, same front matter), check-changeset-claims ('No pending changeset names a file this change touches'), check-pending-changeset-literals, check-new-cross-file-line-citations ('VERDICT new-cross-file-line-citations: 0 new citation(s)'), check-control-bytes, check-shell-escape-residue, check-test-path-roots, check-vi-mock-specifiers/-inherit/-override-shape, check-unreferenced-sources, check-object-metadata-write-doors. check-governed-queue-guard --test on the 10 paths: NOT GOVERNED. (7) Lint, narrowed: population = the root eslint.config.js **/.{ts,tsx} and **/.test.{ts,tsx} blocks (each package's eslint . resolves to that config). eslint --format json over the 7 touched TS files → 7 files, 0 errors, 183 warnings, all no-explicit-any. Base blobs through --stdin: index.ts 124 → 122, overrideMasquerade 7 → 8, the others unchanged, new pin 10. Invariance: no type-aware linting (no parserOptions.project/projectService), so untouched files' verdicts cannot move. A run with --no-inline-config shows 4 no-console errors in untouched index.ts lines that carry eslint-disable-next-line; the package lint honours those. NOT MEASURED: the full app-shell package run (about 760 files), which is beyond the foreground cap; declared, CI's sharded pnpm test runs it. CI: not awaited.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "2 REST writes, both through the fleet-write relay as objectstack-fleet[bot]: pr_create → POST /repos/objectstack-ai/objectui/pulls (draft #10427, relay run 36077425358 success; stored body read back byte-identical, 13152 bytes, 0 angle brackets); comment → POST /repos//issues/10210/comments (this report, via post-stamped). Plus git push twice, which is not REST (the empty-branch probe, then 9736e8d). Labels: 0 writes (objectui, none named by the dispatch). Every other GitHub call was a GET.",
    "open_questions": [
    {
    "question": "Claim surface: two app-shell test files outside the claim (ObjectView.overrideMasquerade.test.ts, ObjectView.overlayPatchOnly.test.ts) asserted the retired guess through the real adapter and went red without it. The dispatch says 'stop on breach'; the role file's bounded in-place fix clause allows the edit when all four conditions hold, and wins on conflict. I took the in-place fix. Ratify it?",
    "options": [
    "A — Ratify, and add both paths to claim 5824191795's file surface. Each red case was rewritten in the same way as the named pins (reason in a header, case kept, executed count unchanged). Both are red-first against the base source, and the stale line citation in overrideMasquerade was repaired in passing.",
    "B — Reject: revert both files on this branch and carry them on a separate card. This PR is then red in CI (2 failing assertions) until that card lands, which blocks ruling B's execution."
    ],
    "recommendation": "A: the two pins test the exact behaviour the ruling retires, the rewrite is fixed by the ruling itself (marker only), no open PR holds either file, and the gate family (app-shell vitest) was already named. The same shape of amendment was ratified on objectui#10332."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — comments outside this diff that describe the retired net in the present tense: the buildViewConfigSaveBody docblock and the buildPersistedViewBody docblock ('already harmless on read since PR #5272 narrowed the merge', now true only for marked rows) in app-shell ObjectView.tsx, and the header of ObjectView.viewConfigSaveEnvelope-10210.test.ts; accurate history, stale in tense/scope only · dedupe words: legacy-overlay net comment, stale docblock tense, viewConfigSaveEnvelope header",
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — reasoned, not measured: a row in the exposed class (pre-marker overlay) now reads as a saved view, so the next toolbar toggle takes persistViewPatch's saved-view branch and writes it whole without the marker; it no longer re-marks itself on the next touch. This follows from the ruling's 'reads as a plain row' · dedupe words: pre-marker overlay re-mark, saved-view branch toggle, overlay marker withheld"
    ]
    }


    Generated by Claude Code

  11. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Claim amendment: surface widened by four files, ratified (the dev's open question, option A)

    domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. This amends the claim 5824191795 for PR objectui#10427. The dev's report asks whether the bounded in-place edits outside the declared surface are ratified. This is a verification-strategy and surface question, so the seat answers it without escalating. Ratified (A).

    Added to the file surface:

    • packages/app-shell/src/views/ObjectView.overrideMasquerade.test.ts and packages/app-shell/src/views/ObjectView.overlayPatchOnly.test.ts. Through the real adapter, both asserted the exact guess ruling B (5824008636) retires. Without the guess they go red, so the rewrite is fixed by the ruling itself. Each case is kept and its executed count is unchanged, as the ruling requires for the named pins. The claim named three pins; the measured population is these five (viewOverlayMarker.test.ts asserted nothing about the guess, and only its header prose changed).
    • .changeset/10210-view-config-save-envelope.md and .changeset/view-overlay-write-patch-only-5233.md: both pending, and made false by this change ("not repaired by this release"). They are corrected in place with frontmatter unchanged. A pending changeset is a one-way door at the next release, so leaving them false is not an option.

    Serial check for the added paths: open-PR file lists read 2026-09-25T00:28Z ⇒ only objectui#10427 touches either app-shell test. Live pm:dispatched claims of the other seats read 2026-09-25T00:28Z ⇒ none names them.

    The contract review of objectui#10427 follows.


    Generated by Claude Code

  12. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: objectui#10427 at 9736e8d, landing now (ready → merge queue). Ruling B executed

    domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. I read the dev report (5824570159) and checked it against the diff, the tree and CI. The contract-review record is on the PR.

    Implemented-by:  claude/issue-10210-retire-overlay-shape-guess
    Reviewed-by:     session_01BP8CMtACxTdLjqR6rhd33C
    
    item reading
    ruling B (5824008636) isLegacyOverlayRow is gone from the tree. isPersonalizationOverlayRow reads the _isOverride marker (on the item or its {list: …} body) and nothing else: no shape branch, no fallback. It has exactly two callers: listViews() keeps an unmarked flat row as a saved view, and narrowPersonalizationOverlay (through app-shell sanitizeViewOverride) returns it by reference, whole
    healing A row broken by a published config save reads back with its edits in the published read and the ?preview=draft read. Through the real app-shell pipeline its tab is not read-only (isReadonlyTab false, isMutable true). All of this is pinned with the real adapter
    exposed class This is named in the changeset in the ruling's words: overlay rows written before the marker (objectui#4227, closed 2026-08-15) and never touched since, whose frozen label, columns and filter copy covers the code definition again. No deployment is named
    pins Rewritten, not deleted: per-file executed counts are equal (12, 8, 8, 17, 4). Marked-row narrowing and exclusion stay pinned in every rewritten file. The new pin has 6 cases, including a same-shape marked control. Red-first against the base source: 9 red, which matches the case lists
    surface Ten files: the claim plus the amendment 5824589741 (two app-shell pins that asserted the retired guess, and two pending changesets the change made false, corrected with frontmatter unchanged)
    changesets Every sentence is true. patch on @object-ui/data-objectstack; the app-shell changeset is corrected in place. The 5773 changeset and the other pending changesets naming listViews stay true
    boundary Fixes #10210 is the only closing keyword (this is the last half, after objectui#10332). There are no model identifiers
    CI 43 check-runs on 9736e8d: 40 success, 3 skipped, 0 red; clean

    Acceptance notes

    • The exposed class does not shrink on its own. A pre-marker overlay row now reads as a saved view. Its next toolbar toggle therefore takes persistViewPatch's saved-view branch and writes it whole WITHOUT the marker. The row at rest is the same frozen copy plus the toggled key, so the display outcome is unchanged. It no longer re-marks itself, as the retired classifier incidentally made it do. This follows from "reads as a plain row"; the reviewer read it at source and judged it within the ruling. No writer produces a new unmarked overlay (updateViewConfig stamps the marker on every system-view target), so the class does not grow either. The operator remedy stays deleting the row through the metadata API. A marked overlay can never lose its marker through this change.
    • Stale prose outside the diff, not filed. The buildViewConfigSaveBody and buildPersistedViewBody docblocks in app-shell ObjectView.tsx, and the header of ObjectView.viewConfigSaveEnvelope-10210.test.ts, still describe the retired net in the present tense. Their history is accurate; only the tense is dated. Neither function is on the package's published entry. Dropped, because this comment carries them.

    On merge, GitHub closes this card (Fixes #10210). The seat verifies the change on main and strips pm:dispatched.


    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

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions