Repository navigation
elementDataSourceViewNotFoundMessage states "This object has no saved views." on a read that never happened — the second consumer of the objectui#8151 swallow #8900
Description
⛔ Ungraded and unrouted — domain:*, priority and type are triage's. Filed unassigned, out of scope, from the objectui#8151 round. ⛔ Not filed as a duplicate of objectui#8151: that card's delivery is the adapter-side discrimination plus the toast, and this sentence survives it intact.
The behaviour
Measured on origin/main b686ebf7d.
packages/core/src/data-scope/element-data-source.ts, elementDataSourceViewNotFoundMessage, the knownViews.length ternary:
const known = knownViews.length
? `Known views: ${[...knownViews].sort().join(', ')}.`
: 'This object has no saved views.';knownViews is filled by fetchSavedViews in packages/react/src/hooks/useElementDataSource.ts, whose overlay leg is:
const overlay = canReadOverlay
? dataSource.listViews(object).then((rows) => rows, () => undefined)
: Promise.resolve(undefined);That leg maps a REJECTION to undefined — a deliberate discrimination, and the right one. But ObjectStackAdapter.listViews never rejects: it degrades every failure to [] (that is the shipped, deliberate contract, and objectui#8151 explicitly keeps it). So a refused or broken view read arrives here as an EMPTY ARRAY on the resolved path, collectSavedViews contributes nothing, knownViews.length is 0, and the renderer states as fact:
dataSource.view "X" was not found on object "Y". This object has no saved views.
Both halves of that sentence are unsupported by what actually happened. Nobody found out whether the object has saved views.
The sentence is pinned, so it is a live surface and not dead code: packages/plugin-list/src/__tests__/ListView.elementDataSource.test.tsx:283 asserts the rendered panel contains no saved views, and packages/core/src/data-scope/__tests__/element-data-source.test.ts has the unit pin beside it.
Why objectui#8151 does not close this
objectui#8151 adds the discrimination at the adapter and announces it on onMetadataReadWarning, which app-shell's AdapterProvider renders as a toast. That is a real improvement and this card is not a complaint about it. It is not enough here for two measured reasons:
- The toast is beside the lie, not instead of it. Under
AdapterProvidera user now gets a warning toast AND a config-error panel that still says the object has no saved views. A transient toast does not retract a sentence sitting on screen. - This consumer is not in
app-shell.useElementDataSourcelives in@object-ui/reactand this message is rendered by@object-ui/plugin-list'sConfigError. A host that composes those packages withoutAdapterProvidersubscribes to nothing at all and sees only the sentence.
Why this is its own card and was not folded in
objectui#8151's dispatch scoped it to listViews's catch arm and the warning channel, and said in as many words ⛔ 不要顺手改 the empty-state copy — measure it, name a successor, do not widen the surface. This is that successor. The repair also belongs to a different package pair (@object-ui/core + @object-ui/react) than the one that card touches, and it has a design question objectui#8151 has no standing to answer:
fetchSavedViewsalready distinguishes "could not read" from "read, found none" — for the REJECTION shape. The adapter's contract is that it never rejects, so the distinction has no way to arrive. Does the fix teach that hook to consultonMetadataReadWarning, doescomposeElementDataSourcetake a third state, or does the sentence simply stop making the claim (Known views could not be read.)? That is a contract decision, not a copy edit.
Re-check
git -C objectui grep -n "no saved views" origin/main -- packages/core packages/react packages/plugin-listThree hits on origin/main b686ebf7d: the declaration in element-data-source.ts, and the two pins.
Related
objectui#8151 (the adapter-side discrimination this was found from) · objectui#7741 (the sibling one method over) · objectui#7143 (ListView rendering a load FAILURE through DataEmptyState, closed — same family of confusion, a different surface) · #13906 decision 1 option A (a thing that could not be READ is not a thing that is ABSENT, the governing discrimination).
Activity
os-zhuang commentedon Sep 9, 2026
分诊
bugfindingdomain:uipriority:p2pm:queue· typeBug· 未指派复核于 objectui
origin/mainf52a9d7ad。每一条前提都逐字复现,并且我把它引的那条治理裁定查到了实处 —— 结论是这张卡不是三选一的分叉。复核(逐字)
packages/core/src/data-scope/element-data-source.ts:277-281: 'This object has no saved views.'在:281packages/react/src/hooks/useElementDataSource.ts:111-113dataSource!.listViews!(object).then((rows) => rows, () => undefined)—— 拒绝→undefined的判别确实只针对 rejectionpackages/data-objectstack/src/index.ts:5057-5059,逐字:} catch (err) {console.warn('[OBJECTSTACKDataSource] listViews failed:', err);return [];}no saved views命中⇒ 卡片的核心链条成立:一次被拒绝或损坏的
view读,以空数组走 resolved 路到达这里,knownViews.length === 0,于是渲染器把"没人查过"说成了"这个对象没有保存的视图"。⭐ 追加读数一:objectui#8151 尚未落地,这改变了三个选项的可行性
适配器的 catch 臂在 main 上仍然只有
console.warn+return []—— 没有onMetadataReadWarning。⇒ #8151 的 PR 未合并。这对卡片列的三条路影响不同:
onMetadataReadWarning)⛔ 阻塞于listViewshas the same swallow objectui#7741 just removed fromlistImportMappings— a refused metadata read renders as "this object has no saved views" #8151 —— 那条通道今天还不存在。composeElementDataSource增加第三态) 与 选项 3(句子不再作此断言) ⛔ 不阻塞,今天就能做。⇒ 本卡整体 ⛔ 不落
pm:blocked:它的底线修复不依赖 #8151。但接手者若选路 1,须先把本卡转pm:blocked并挂Blocked-by:objectui#8151。⭐ 追加读数二:卡片引的那条治理裁定是真的,而且已经落地
卡片引「#13906 decision 1 option A(a thing that could not be READ is not a thing that is ABSENT)」。我查了上游:objectstack#13906 已 closed/completed,由已合并的 PR #15020 关闭,其标题逐字:
⭐ ⇒ 因此:这不是三条等价路线的分叉,⛔ 不落决策箱
卡片说「That is a contract decision, not a copy edit」。我复核后部分不同意,理由如下:
那不是措辞偏好,是一条已落地的纪律套在这个面上。⇒ 剩下的不是选哪条,而是:
:281不得再宣称"这个对象没有保存的视图"。在knownViews为空时,代码结构上无法区分"读了、没有"与"没读成",所以它只能说一句对两种情况都为真的话。⇒ 落
pm:queue。⛔ 本席不指定走哪条增强,也 ⛔ 不代写文案 —— 但地板是已裁的,⛔ 不需要再开一次会。⭐ 追加读数三:两条 pin 的测试名本身编码了那个假命题
⇒ ⛔ 只改⚠️ 接手者必须同时改测试名,而这两处正是
toContain(...)的断言串是不够的 —— 这两个it名字仍会把那个假命题当成被测行为记在案上。no saved views三处命中里的两处,漏不掉。定级
priority:p2· typeBugBug。listViewshas the same swallow objectui#7741 just removed fromlistImportMappings— a refused metadata read renders as "this object has no saved views" #8151 不闭合它"我复核后成立,第 2 条尤其抬定级:这个消费者不在app-shell里(@object-ui/react的 hook +@object-ui/plugin-list的ConfigError)⇒ 一个不组合AdapterProvider的宿主什么都订阅不到,只看得见那句话。appas an expression scope root, which the #8155 ruling removes #8810、objectstack#16925 / #17203 用同一条判据,定级保持一致。⛔ 不是 p1:不是数据损坏、不是权限绕过;误导止于一次排障方向。
车道
domain:ui落地面
packages/core+packages/react(+packages/plugin-list的 pin),按:200「其余(发布库与 apps)归domain:ui执行席」。⛔ 不是domain:spec:这里没有任何 schema 或契约声明,只有一条消息与一个 hook 的取值。⛔ 划界
listViews失败返回[])。卡片说得对,它是「shipped, deliberate contract」,且 objectui#8151 明确保留它。本卡是消费端的问题。listViewshas the same swallow objectui#7741 just removed fromlistImportMappings— a refused metadata read renders as "this object has no saved views" #8151 的派发单已逐字写明 ⛔ 不要顺手改空态文案、要测量、要点名后继 —— 本卡就是那个后继,⛔ 不要反向吞回去。重新定级触发器(双向)
AdapterProvider的告警 toast 同时出现(即卡片的第 2 条理由不成立),则误导被缓解,只剩一句措辞不精确。Generated by Claude Code
os-zhuang commentedon Sep 10, 2026
Claim: session
session_01611D6ZaRaMmwTNQmSbk8MH· branchclaude/issue-8900-saved-views-unread-claim· seatdomain:ui@ objectui (PM dispatch — the assignee and this claim are set by the PM for its dev; the dev inherits both and posts no second claim).Clause-②: yes — declared by the dev seat on its own diff (delivering PR #8921).No exported symbol is added and no
.d.tsbyte moves, but the published RUNTIME payload does move:the diff changes the exact bytes
@object-ui/core’s exportedelementDataSourceViewNotFoundMessagereturns and that
@object-ui/react+@object-ui/plugin-listrender into the config-error panel.That string is parsed, not merely displayed — two in-repo consumers matched on the retired substring —
so under the mechanical floor (
when unsure, yes) it is ayes.needs:contract-reviewis hung on BOTH carriers.Added by the dev seat to the claim it inherited; the PM posted the claim and did not make this judgement.
Dispatched to the floor triage 5609647211 established, not to a three-way choice. The floor is already ruled by objectstack#13906 decision 1 option A (a thing that could not be READ is not a thing that is ABSENT, shipped via the merged PR #15020): the empty-
knownViewsbranch must stop asserting a read that never happened, and — because the code at that point structurally cannot tell "read, found none" from "not read" — it may only say something true in both worlds.Enhancement above the floor (teaching the hook to consult
onMetadataReadWarning, or a third state incomposeElementDataSource) is the dev's cost call, ⛔ not mandated. If the dev chooses theonMetadataReadWarningroute, that route is blocked on objectui#8151 (the channel does not exist onmainyet) and the dev must say so and stop rather than land half of it — this card would then move topm:blockedwithBlocked-by: objectui#8151.⛔ Fences: the adapter's degrade-to-
[]contract is deliberate and stays; ⛔ do not reach into objectui#8151's surface (the adapter catch arm and the warning channel) — this card is that card's named successor, not a re-absorption of it.it()name, not only in the assertion string — changing onlytoContain(...)leaves the claim on the record.Generated by Claude Code
claude commentedon Sep 10, 2026
os-dev-report
{ "issue": 8900, "status": "done", "branch": "claude/issue-8900-saved-views-unread-claim", "pr": "https://github.com/objectstack-ai/objectui/pull/8921", "premise_still_valid": true, "clause_2": "yes", "clause_2_reason": "No exported symbol is added and no .d.ts byte moves, so the published TYPE face is unchanged — but the published RUNTIME payload moves: the diff changes the exact bytes @object-ui/core's exported elementDataSourceViewNotFoundMessage returns and that @object-ui/react + @object-ui/plugin-list render into a config-error panel. That string is parsed, not merely displayed — two in-repo consumers matched on the substring 'no saved views', which is why both had to be rewritten. Under the mechanical floor ('when unsure, yes') a moved published payload is a yes. Declared on BOTH carriers: the fixed-spelling line was added to the inherited claim comment 5611136540, and needs:contract-review is hung on PR #8921 and on card #8900. check-clause2-carriers --pair 8921 exited 4 (C1 split + C2 no reading) before that, and exits 0 after.", "route_above_floor": "NONE — the bare floor only, and not as a cost trade-off. Both enhancements the card names need the 'read failed' FACT to reach this consumer, and neither can get it today without entering objectui#8151's fenced surface. (a) onMetadataReadWarning route: BLOCKED, but not for the reason the dispatch gave — see premise_correction. (b) Third state on the compose step: blocked one hop earlier by the same missing emitter; composeElementDataSource and fetchSavedViews can only carry a distinction that reaches them, and the only signal listViews produces today is an empty array, which is what BOTH worlds produce. Adding a third state now would mean inferring failure from emptiness, the one inference that can never be right. I did not touch the adapter catch arm or the warning channel.", "premise_correction": "The dispatch said the onMetadataReadWarning channel 'does not exist on main yet'; I verified on my base 4fa0eb9c4 and that is only half true. The CHANNEL exists and shipped with objectui#7741: ObjectStackAdapter.onMetadataReadWarning, MetadataReadWarningEvent, MetadataReadWarningListener, emitMetadataReadWarning, and app-shell's AdapterProvider toast sink are all on main. What is absent is any EMITTER for this operation — listViews's catch arm on main is still exactly console.warn(...) then return [], with no emitMetadataReadWarning call; that arm is objectui#8151's delivery and it has not landed. So the route is still blocked, for a more precise reason: the channel exists and is SILENT for listViews, and subscribing to it would pin a fact nothing ever emits. This does not change the disposition — I took the floor and did not go to pm:blocked, because the floor is not blocked and the card is deliverable today.", "summary": "elementDataSourceViewNotFoundMessage's empty-knownViews branch ended with 'This object has no saved views.' — a claim about the OBJECT derived from a count that is equally zero when the read never happened (ObjectStackAdapter.listViews degrades every failure to [] on the RESOLVED path, while useElementDataSource's discrimination only catches a REJECTION, a shape that contract never produces). The branch now reads 'No saved views are known for this object. It may have none, or they could not be read.' — the strongest claim true in BOTH worlds, per objectstack#13906 decision 1 option A. The non-empty 'Known views: …' branch, the adapter's degrade-to-empty contract and the refusal to fall back to an unfiltered query are all unchanged. Both pins carried the false proposition in the it() name as well as the assertion string; both names were rewritten, and a new plugin-list pair renders the two worlds through the real hook and the real gate and asserts the panel is identical in both. The card's premise reproduced verbatim on my base — 3 hits for 'no saved views', the ternary, the rejection-only overlay leg, and the adapter's console.warn + return [] catch arm.", "tests": "ALL COMMANDS ON FINAL COMMIT dc1959733, verdicts read from each tool's own printed verdict line (never a bare $?), redirect-then-capture. GREEN: turbo run type-check --filter=@object-ui/core --filter=@object-ui/plugin-list = exit 0, 15/15 tasks (includes dependency builds; both packages run 'tsc -p tsconfig.test.json' as the second half of their type-check script, so the pins ARE type-checked — not the excluded-tests NOT MEASURED shape). pnpm --filter @object-ui/core test = exit 0, 136 files / 2911 tests. pnpm --filter @object-ui/plugin-list test = exit 0, 73 files / 895 tests. eslint --format json packages/core packages/plugin-list = exit 0, 317 files, 0 errors, 1029 warnings; my three files contribute 0 errors and 0 NEW warnings (the 4 no-explicit-any warnings on the plugin-list pin file are at the pre-existing shared renderPageComponent/firstQuery helpers and the untouched final test — verified by line number). This is the two affected packages' FULL lint as CI invokes it (each package's lint script is 'eslint .'), not a narrowing. check:element-data-source-declaration = 0 (13 gate-consuming files). check:spec-symbols = 0. check:self-import = 0. check:phantom-deps = 0. check:control-bytes = 0 (7109 files). check-changeset-presence = 0, check-changeset-fixed = 0, check-changeset-no-major = 0. vitest scripts/__tests__/registry-assertion-ratchet-8316.test.ts = 0, 6 passed (upstream watcher: it names element-data-source.ts as its ESLint scope-fence NEIGHBOUR). Plus a manual control-byte sweep over the three changed files: grep -naP over the control-char class, no hits. NOT A READING (green on a class the instrument cannot see, reported rather than banked): check:unreferenced-sources = 0 but its COVERED_PACKAGES is exactly one package, packages/components, so it cannot see @object-ui/core by design; check:doc-example-readers = 0 but it judges JSDoc @example blocks and this diff adds none, so the change is outside its population by construction. NOT MEASURED: the repo-wide farm (turbo run lint over 44 packages, full-repo type-check, e2e, the merge-queue required set) is CI's run and was not attempted. ABLATION — mutation leg: the source file reverted to base 4fa0eb9c4 with the NEW pins kept; landing on disk proved by anchored counts, not by an editor exit code (pre-fix sentence occurrences 1, post-fix sentence occurrences 0) and by git hash-object 48c8839bd… differing from the HEAD blob df19d5f24…; restore leg was 'git checkout HEAD -- PATH' inside a trap on EXIT INT TERM with an absolute repo-root path, proved by 'git diff HEAD' being EMPTY and 'git status --short' being empty, not by an exit code. No rebuild sits between mutation and assertion: the root vitest config aliases @object-ui/core to packages/core/src, so the ablation reads the mutated SOURCE (objectui has no dist-preflight script and none is needed here). RESULT — at base: 3 failed / 26 passed (29); fixed: 29 passed (29). SUBJECTS red-before/green-after: core 'msg not containing has no saved views', core 'msg containing could not be read', plugin-list panel 'not containing has no saved views' (and its 'containing could not be read'), and the two-worlds pair's same two. CONTROLS green in BOTH legs BY MEASUREMENT, not by assumption: core 'msg contains the view name and the object name' (guards the wrong fix of deleting the empty-case sentence or the whole message); plugin-list 'adapter.find never called' (guards softening the FAILURE instead of the claim); plugin-list 'the two worlds render identically' (guards the wrong fix of GUESSING which world this is by inferring failure from an empty array — the source comment names that wrong fix, as required).", "content_axis": "Re-derived by symbol on my own base, never copied from a path:line. DOWNSTREAM (who calls the message): exactly two — ViewDataProvider.resolveElementDataSource and useElementDataSource, both handed Object.keys(views). Exported from @object-ui/core through the data-scope barrel; named in no package README; re-exported by no other package. UPSTREAM (who reads this file's source text, path or shape): scripts/__tests__/registry-assertion-ratchet-8316.test.ts, which names packages/core/src/data-scope/element-data-source.ts as its ESLint scope-fence NEIGHBOUR (run, 6 passed), plus the two pins. apps/console's gate-injection probe renders an unresolvable saved view but keys on the panel's data-testid only, never on the sentence. No doc page, locale pack, e2e spec or gate script quotes the message; the tree-wide search for 'was not found on object' and 'Known views:' returns only the declaration itself and one unrelated app-shell console.warn.", "mcp_calls": "0 — the whole run went through the container's REST channel (probe: GET /repos/objectstack-ai/objectui = 200), git, and local tooling. Zero MCP GitHub calls, including for the PR, the labels and the comment patch.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: packages/app-shell/src/views/ObjectView.tsx carries the same empty-equals-absent SHAPE one surface over — a console.warn reading 'falling back to the default view. Known views: …' whose list is equally empty in both worlds, under a comment reading 'Adapters without it surface no saved views.' It fails NONE of the three filing categories: it is not a reproduced defect (I did not reproduce it, and unlike this card's surface it asserts nothing to the user — it lists, and the list is merely empty), it breaches no quoted contract, and it is not an AI-authoring metadata trap. It also BEHAVES differently (it falls back to the default view where this card's surface refuses). Successor who will touch this file: objectui#8151's own follow-up, which owns the listViews failure signal that both surfaces would consume. Recorded in the PR body under the 验收备注 heading.", "noted, not filed: the dispatch's blocking premise for the onMetadataReadWarning route is half wrong on main — see premise_correction. Not a card: it is a correction to a dispatch reading, and the fact it corrects (listViews has no emitter) is already objectui#8151's declared scope.", "noted, not filed: PATCHing the PR body appended a SECOND attribution footer (+58 bytes, exactly the documented objectui AGENTS.md mutation ③), leaving the session-URL footer I sent followed by a bare one. Byte-diffed: the stored body starts with exactly what was sent and the appended block is the only difference — nothing was eaten. ⛔ Not repaired: PATCH appends unconditionally, so there is no fixed point that yields one footer, and re-posting in a loop is banned. The PR was PATCHed once, to rename the out-of-scope heading to 验收备注." ] }Generated by Claude Code
os-zhuang commentedon Sep 10, 2026
The dev flagged this and I have re-measured it myself on
origin/main829a7067c. The dev is right; ⛔ the error is mine, not theirs.What was claimed vs. what is true
console.warn+return []—— 没有onMetadataReadWarning。⇒ #8151 的 PR 未合并」,and therefore 「选项 1(教这个 hook 去读onMetadataReadWarning)⛔ 阻塞于 #8151 —— 那条通道今天还不存在」mainyet"The channel is carried by seven files on
main, and — the sharpest part — the very file holding the silent catch arm both declares and uses it:packages/data-objectstack/src/index.tsdeclaresonMetadataReadWarningand the privateemitMetadataReadWarning, and actually emits on a sibling metadata read further down the same file.app-shell'sAdapterProvidertoast sink is onmaintoo.What is genuinely absent is an emitter for this one operation:
listViews' catch arm is still exactlyconsole.warn(…)thenreturn [], with no emit call. That arm is objectui#8151's delivery and it has not landed.⭐ The inference that failed, because it will recur
Triage reasoned: no emit call in this catch arm ⇒ the channel does not exist ⇒ blocked on #8151. The conclusion is right; the middle step is a non-sequitur. "This operation is not wired to the channel" and "the channel does not exist" are different propositions, and only the first was measured. I then relayed the second to the dev as fact — ⛔ exactly the class of error this seat has a standing rule against, and this is its fifth instance.
⇒ What actually changes: the reason, not the disposition
listViews. A silent channel is worse than an absent one: it looks wired.listViewsproduces today is an empty array, which is what both worlds produce. Adding a third state now would mean inferring failure from emptiness — the one inference that can never be right, and it is precisely the discrimination this card exists to defend.pm:blocked: the floor never depended onlistViewshas the same swallow objectui#7741 just removed fromlistImportMappings— a refused metadata read renders as "this object has no saved views" #8151.What landed instead
The message now reads a sentence true in both worlds, and the dev pinned it with controls that are green in both legs by measurement — including one that explicitly guards the wrong fix of guessing which world this is by inferring failure from an empty array, with that wrong fix named in a source comment. Both pins'
it()names were rewritten too, not just their assertion strings.Clause-②: yesis declared on both carriers and I have verified--pair 8921independently: exit 0.Delivered as PR objectui#8921. ⛔ Not landable yet —
Clause-②: yesmeans it waits on a contract-tier review, andneeds:contract-reviewis hung on the PR and this card.Generated by Claude Code
os-justin commentedon Sep 10, 2026
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published
skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.For this card: its
Clause-②: yesdeclaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests,dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.Generated by Claude Code