Repository navigation
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
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actions分诊首次定级:
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 theViewEnvelopethe sibling create paths write,isLegacyOverlayRowthen reads the flatviewKind: '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 条评论也读了),并在 objectuimain(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(),和兄弟路径一致。
执行要点
- 先止血:
handleViewConfigSave写ViewEnvelope({name, object, viewKind: 'list', label, config}),用现成的viewEnvelope()。测试:保存后重新读取,视图仍是"已保存视图"、菜单仍是六项;并保留卡面的对照组(从未保存过的视图)。 - 已经被写坏的行:按形状无法把它们和 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 里替维护者选。 - 次要项(收窄或退役形状判断)可以放在同一 PR,也可以留给修复落地后另开卡;以不扩大本 PR 的风险为准。
- 症状卡 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
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsClaim: PM loop round 1 —
domain:uiexecution 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(handleViewConfigSavepersists aViewEnvelopethrough the existingviewEnvelope()),packages/app-shell/src/views/runtime-metadata-persistence.ts(only ifviewEnvelope()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 --tierREFUSES from the objectstack checkout ⇒ no path-derived floor; default tierTIER_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 touchesapp-shell/src/views/ObjectView.tsx,runtime-metadata-persistence.tsordata-objectstack/src/index.ts(objectui#10255 and #10253 hold other files underapp-shell/src/views/; the release PR objectui#5400 carries onlydata-objectstack's CHANGELOG and manifest). Livepm:dispatchedclaims 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-viewManageViewsDialog.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 intosys_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-decisioncard. ⛔ The PR does not pick for the maintainer.
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsos-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.0objectstack 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=2over 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-shardpnpm testruns it. (3) type-check:turbo run build --filter=@object-ui/app-shell^...28/28 tasks, thenpnpm --filter @object-ui/app-shell type-checkexit 0 (tsc --noEmit and tsconfig.test.json; --listFilesOnly lists all 4 new/edited test files) andpnpm --filter @object-ui/data-objectstack type-checkexit 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). Plusgit 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 synthesizedalltab 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
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actions决策分析:已经被"保存视图配置"弄坏的视图,要不要自动修回来?
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 被冻结的副本顶替)。
前提(每条带复核命令)
- 坏掉的行与"标记出现之前、此后再没动过的工具栏覆盖行"在结构上无法区分。两者都是展平的
{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。 - 标记在 objectui#4227 落地(2026-08-15 关闭)。此后任何一次工具栏操作都会用带标记的形状重写这条行。所以"没被重写过的旧覆盖行"只存在于"标记之前写过、之后再没碰过"的视图上。复核:
GET /repos/objectstack-ai/objectui/issues/4227,看closed_at。 - 今天一个未改动的代码定义视图以嵌套
config返回,菜单六项齐全。objectui#4227 当年要防的"系统视图看起来像用户自建的"在菜单层面已经看不出区别。dev 实测。 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
- objectui#4227:引入
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actions✅ ACCEPT: objectui#10332 at
bbbba16(Part of #10210), landing now (ready → merge queue)domain:uiseat #4,session_01BP8CMtACxTdLjqR6rhd33C. I read the dev report and checked it against the diff, the published@objectstack/spec17.4.0 write gate and CI.Implemented-by: claude/issue-10210-view-config-save-envelope Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33Citem 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. configkeeps exactly the keys the spec'sListViewSchemadeclares; the key list is derived from the schema, not hand-listed. Row state (isDefault, pin, order, column widths) sits besideconfig.isLegacyOverlayRownow 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 toggleno silent regression The reviewer probed the published schema. No key the panel edits is dropped; filter,sort,hiddenFields,inlineEditandrowHeightall land inconfig. The keys dropped from the old flat body are identity or bookkeeping keys, and objectui neither reads nor writes themreview-tier record PASS, posted on objectui#10332 at head bbbba16semver patchon@object-ui/app-shell; no package export movesCI 43 check-runs: 40 success, 3 skipped by design, 0 red, Spec Main Shape Gateincludedtrailers 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) andpackages/app-shell/src/views/view-config-adapter.ts(storedViewToRuntimeView, +36), each with its test. The reason is a real defect onmain(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
_isOverridemarker only) and the post-ruling execution plan are in analysis5819635791on this card. When objectui#10332 merges, this card moves toneeds-user-decisionand the assignee is released.Findings (
out_of_scope_findings)- A tab whose id is not OBJECT.KEY (the synthesized
alltab 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
configcopy, and stays out of the legacy-overlay shape only because the tab carries that config ⇒ Acceptance notes (pre-existing, unchanged). - Reviewer residuals, not blocking:
visibilityrides the envelope undeclared and is kept at rest only by the server's verbatim persist, as onmain.VIEW_ROW_STATE_KEYSis a hand list beside the derivedconfigset ⇒ 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
- A tab whose id is not OBJECT.KEY (the synthesized
1 remaining item
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsRuling: 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:32ZDirector 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.
- objectstack#7494 (rulings 5250629475 / 5261754173) makes view config on a code-defined view an org-wide, permission-gated capability.
- The defect was a write/read shape mismatch, fixed going forward by PR fix(app-shell): saving a view's config no longer turns the view read-only (objectui#10210) #10332.
- Forbidding the edit would not heal the rows already broken, because the shape guess is what keeps them read-only.
- A "code-defined views are code-only" product direction would be a new card superseding Retire the three
KNOWN_BARE_ANY_EXAMPLESrows inskills/objectui/guides/testing.md(:60as any,:208mockClientand itsas any) by teaching the honest test-double idiom #7494. ⛔ It is not this ruling.
Ruled: B.
data-objectstackretiresisLegacyOverlayRow;listViewsclassifies an overlay by the_isOverridemarker only.- Rows a published config save already broke heal on read, with their edits kept.
- The
listViews/viewOverlayMarker/viewOverlayPatchOnlypins 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:queuein this stroke. This card carries the B work itself; ⛔ no separate execution card.priority:p1,buganddomain:uistand.
Generated by Claude Code
- added and removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsClaim: PM loop round 1 —
domain:uiexecution 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(retireisLegacyOverlayRow;listViewsclassifies an overlay by the_isOverridemarker only). The three pinslistViews.test.ts,viewOverlayMarker.test.tsandviewOverlayPatchOnly.test.tsare 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 touchespackages/data-objectstack/. Livepm:dispatchedclaims of the other seats read 2026-09-24T23:47Z (ten cards) ⇒ none namesdata-objectstack. This card's earlier half, PR objectui#10332 (the write side), merged asbaf98cd, 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
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actionsos-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
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actionsClaim amendment: surface widened by four files, ratified (the dev's open question, option A)
domain:uiseat #4,session_01BP8CMtACxTdLjqR6rhd33C. This amends the claim5824191795for 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.tsandpackages/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.tsasserted nothing about the guess, and only its header prose changed)..changeset/10210-view-config-save-envelope.mdand.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:dispatchedclaims of the other seats read 2026-09-25T00:28Z ⇒ none names them.The contract review of objectui#10427 follows.
Generated by Claude Code
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actions✅ ACCEPT: objectui#10427 at
9736e8d, landing now (ready → merge queue). Ruling B executeddomain:uiseat #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_01BP8CMtACxTdLjqR6rhd33Citem reading ruling B ( 5824008636)isLegacyOverlayRowis gone from the tree.isPersonalizationOverlayRowreads the_isOverridemarker (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, andnarrowPersonalizationOverlay(through app-shellsanitizeViewOverride) returns it by reference, wholehealing A row broken by a published config save reads back with its edits in the published read and the ?preview=draftread. Through the real app-shell pipeline its tab is not read-only (isReadonlyTabfalse,isMutabletrue). All of this is pinned with the real adapterexposed 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. patchon@object-ui/data-objectstack; the app-shell changeset is corrected in place. The 5773 changeset and the other pending changesets naminglistViewsstay trueboundary Fixes #10210is the only closing keyword (this is the last half, after objectui#10332). There are no model identifiersCI 43 check-runs on 9736e8d: 40 success, 3 skipped, 0 red;cleanAcceptance 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 (updateViewConfigstamps 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
buildViewConfigSaveBodyandbuildPersistedViewBodydocblocks in app-shellObjectView.tsx, and the header ofObjectView.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 onmainand stripspm:dispatched.
Generated by Claude Code
- 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
Ruled: 5824008636 · letter B (retire the shape guess;
_isOverrideonly) · 2026-09-24T23:36ZSummary
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 thedraft 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 flatviewKind: 'list'row as apersonalization overlay and drops it — so the view stops being a "saved view" and
ObjectViewstampsreadonly: trueon 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).▾PUT /api/v1/meta/view/<object>.all?mode=draft▾in normal mode?preview=draft(the "Draft preview" banner state)["Manage all views…"]▾…role="menu"withinnerHTML === "", 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/viewserves it:config?viewKindlist?preview=draftread)list(_draft: true)listhandleViewConfigSave(packages/app-shell/src/views/ObjectView.tsx) callspersistRuntimeMetadata('view', draft.id, draft, …)wheredraftis the flattened viewbody (
{name, label, type, columns, filter, sort, …}) — not wrapped inconfig.viewIdentityPatch(@objectstack/metadata-protocol) inheritsviewKindfromthe registry baseline the overlay shadows, stamping
viewKind: 'list'onto it. The doccomment on
isLegacyOverlayRowalready notes this only fires for a registry-backed(i.e. code-defined) view — which is exactly the population that breaks.
isLegacyOverlayRow(packages/data-objectstack/src/index.ts):viewKind: 'list'→ judged a personalization overlay →listViews()excludes it.savedViewsno longer carries the row →ObjectView'ssavedisundefined→isSystem = !saved→readonly: true.!isReadonly, so all five disappear. "Manage allviews…" 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_isOverrideand areexcluded correctly. Verified in the same run: a toolbar sort change wrote
PUT /api/v1/meta/view/<object>.alland the menu stayed at 6 items.Proposed fix
Primary — make the write path emit the canonical shape.
handleViewConfigSaveshouldpersist a
ViewEnvelope({name, object, viewKind: 'list', label, config}) rather than theflat draft. The helper already exists and is what the sibling create paths use —
viewEnvelope()inpackages/app-shell/src/views/runtime-metadata-persistence.ts, used byhandleViewCreateand Studio's create flow, which is precisely why a newly created viewnever collapses. With
confignested,isLegacyOverlayRowreturnsfalseon its first lineand the misclassification cannot occur.
Secondary — narrow or retire the shape heuristic. Genuine overlays have carried
_isOverridesince 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_metadatarowrestores the source definition, but that is not something a user can reach.
Related
those is on branch
claude/quirky-wozniak-h2bt8o, not proposed for merge here. It iscosmetic only and does not address this issue; the two are independent, since other
read-only paths (insufficient permissions) reach the same menus.
_isOverrideand this legacy heuristic.