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
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 |
handleViewConfigSave(packages/app-shell/src/views/ObjectView.tsx) calls
persistRuntimeMetadata('view', draft.id, draft, …)wheredraftis the flattened view
body ({name, label, type, columns, filter, sort, …}) — not wrapped inconfig.- Server-side
viewIdentityPatch(@objectstack/metadata-protocol) inheritsviewKindfrom
the registry baseline the overlay shadows, stampingviewKind: 'list'onto it. The doc
comment onisLegacyOverlayRowalready 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):The saved row is flat +if (spec && spec.config && typeof spec.config === 'object') return false; const viewKind = item?.viewKind ?? spec?.viewKind; return viewKind === 'list';
viewKind: 'list'→ judged a personalization overlay →
listViews()excludes it.savedViewsno longer carries the row →ObjectView'ssavedisundefined→
isSystem = !saved→readonly: true.- 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 branchclaude/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
_isOverrideand this legacy heuristic. - objectui#7205 — the adjacent complaint that view overlays get written with no save gesture.
Activity
objectstack-fleet commentedon Sep 24, 2026
分诊首次定级:
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⇒ 只读。与卡面一致。定级说明
viewEnvelope(),和兄弟路径一致。执行要点
handleViewConfigSave写ViewEnvelope({name, object, viewKind: 'list', label, config}),用现成的viewEnvelope()。测试:保存后重新读取,视图仍是"已保存视图"、菜单仍是六项;并保留卡面的对照组(从未保存过的视图)。listViews()as a saved view #4227 之前的个人化覆盖层区分开。PR 里先测量、再提出规则(例如:展平、无_isOverride、带_draft或名称与注册表视图相同……);needs-user-decision卡,⛔ 不要在 PR 里替维护者选。…menu in Manage views opens EMPTY (a 180×10px strip), and its tab menu opens on a leading separator #10209(菜单组件)文件不相交,可以同批、分开做。Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Claim: PM loop round 1 —
domain:uiexecution seat 4Session:
session_01BP8CMtACxTdLjqR6rhd33CBranch:
claude/issue-10210-view-config-save-envelopeWorktree:
objectui-issue-10210Domain:
domain:uiSeat:
domain:ui#4File 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 measurementClause-②: 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.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 commentedon Sep 24, 2026
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=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). 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
alltab 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 commentedon Sep 24, 2026
决策分析:已经被"保存视图配置"弄坏的视图,要不要自动修回来?
domain:ui执行席 #4,session_01BP8CMtACxTdLjqR6rhd33C,2026-09-24T18:15Z。本卡的修复 PR objectui#10332(Part of #10210)只管以后的保存;dev 按分诊第 2 点实测后报告:已经坏掉的行怎么处理是一个真实的取舍,⛔ 不在 PR 里替你选。PR 合入时本卡转needs-user-decision;分析先贴在这里。一句话问题
之前有人在代码定义的视图上点过「编辑视图配置 → 保存 → 发布」,那个视图就永久变成只读,菜单里只剩「管理所有视图…」。新修复让以后的保存不再出问题,但已经坏掉的那些视图,界面上仍然没有回头路。
Governing text
_isOverride标记,并保留一个"按形状猜"的兜底(isLegacyOverlayRow),用来识别标记出现之前写入的个人化覆盖行。它自己的注释写明 "best-effort … not a guarantee"。前提(每条带复核命令)
{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。GET /repos/objectstack-ai/objectui/issues/4227,看closed_at。config返回,菜单六项齐全。objectui#4227 当年要防的"系统视图看起来像用户自建的"在菜单层面已经看不出区别。dev 实测。DELETE /meta/view/NAME能把一行重置回代码定义,但作者的修改会丢失。dev 实测。选项 × 真实代价
_isOverride标记,删掉isLegacyOverlayRow/meta/view/NAME/history,有 publish 事件就当成已保存视图narrowPersonalizationOverlay也在用)业务含义直译
四轴(业务立场)
ListView元数据和用户的列表偏好也是分开存的。A 留着刚刚误伤过人的推断;C 把推断做深;D 为过渡数据长出永久能力。data-objectstack一处加它的几个 pin)。C 和 D 都新增面。推荐
os-decision-facets
Prior rulings read:
isLegacyOverlayRow·_isOverride·personalization overlay→ objectui#4227, objectstack#7494; ADR none; thread: 2(本卡分诊5817623588、dev 报告)推荐 B。只看①选 B;②③④ 是否翻转:否。置信缺口:各部署未重写的旧覆盖行数量不可见。
裁后执行
domain:ui立一张执行卡:data-objectstack退役isLegacyOverlayRow,改写它的listViews/viewOverlayMarker/viewOverlayPatchOnly几个 pin(连同理由,⛔ 不删)。changeset 写明已坏的视图读取即恢复。落地后本卡以 completed 关闭。Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
✅ 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.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 togglefilter,sort,hiddenFields,inlineEditandrowHeightall land inconfig. The keys dropped from the old flat body are identity or bookkeeping keys, and objectui neither reads nor writes thembbbba16patchon@object-ui/app-shell; no package export movesSpec Main Shape GateincludedClaim 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)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.configcopy, and stays out of the legacy-overlay shape only because the tab carries that config ⇒ Acceptance notes (pre-existing, unchanged).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
1 remaining item
objectstack-fleet commentedon Sep 24, 2026
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: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.
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.listViews/viewOverlayMarker/viewOverlayPatchOnlypins are rewritten with the reason. ⛔ They are not deleted.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
objectstack-fleet commentedon Sep 24, 2026
Claim: PM loop round 1 —
domain:uiexecution seat 4 (ruling B execution)Session:
session_01BP8CMtACxTdLjqR6rhd33CBranch:
claude/issue-10210-retire-overlay-shape-guessWorktree:
objectui-issue-10210bDomain:
domain:uiSeat:
domain:ui#4File 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 rowsClause-②: no
Thread-read: 5824008636
Ruling-ref: 5824008636
Serial constraints cleared: open-PR file lists read 2026-09-24T23:46Z ⇒ none touches
packages/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 commentedon Sep 25, 2026
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
objectstack-fleet commentedon Sep 25, 2026
Claim 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 commentedon Sep 25, 2026
✅ 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.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, whole?preview=draftread. Through the real app-shell pipeline its tab is not read-only (isReadonlyTabfalse,isMutabletrue). All of this is pinned with the real adapter5824589741(two app-shell pins that asserted the retired guess, and two pending changesets the change made false, corrected with frontmatter unchanged)patchon@object-ui/data-objectstack; the app-shell changeset is corrected in place. The 5773 changeset and the other pending changesets naminglistViewsstay trueFixes #10210is the only closing keyword (this is the last half, after objectui#10332). There are no model identifiers9736e8d: 40 success, 3 skipped, 0 red;cleanAcceptance notes
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.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
fix(app-shell): saving a view's config no longer turns the view read-…
fix(data-objectstack): classify a view overlay by its `_isOverride` m…