Repository navigation
finding(app-shell): four studio-design consumers on an empty metadata registry — one renders NOTHING on a field click, one states a false reason #6846
Description
Split out of #6795 by the domain:ui execution seat (PM session session_01CRJge11jso9TpXRWFt1Z49) after that card's measurement round. #6795 keeps only its registry-observability question (a published-contract change, now needs-user-decision); this card is the half that stands on its own and touches no contract.
⛔ This card is NOT a lazy conversion and must not become one. It changes what four studio-design consumers say when a metadata registry read returns undefined. No registry, no bundling, no sideEffects, no eager-closure ceiling.
What was measured
#6795's implementer drove all four consumers against genuinely empty registries — the probes assert emptiness first (listMetadataPreviewTypes() and listMetadataInspectorTypes() both [], all three getMetadataDefaultInspector reads undefined) rather than assuming it. Observed renders:
| consumer | state on an empty registry | verdict |
|---|---|---|
ObjectSettingsPanel |
prints "No default object inspector registered." | ✅ already honest — leave it |
ObjectHooksPanel |
falls back to a working generic SchemaForm |
✅ already honest — leave it |
ObjectActionsPanel |
renders only the action's own label — no editor, no explanation | |
StudioDesignSurface interfaces canvas |
prints "design support is in progress" | ⛔ factually wrong |
StudioDesignSurface.tsx:3076 |
selecting a FIELD with getMetadataInspector('object') undefined ⇒ aside count 0 |
⛔ clicking a field does nothing at all |
⭐ Note the first two rows: #6795's card body claimed all four were silently empty, and two of them were not. Anyone repairing this should re-drive the probes rather than trusting any table, this one included.
The three repairs
StudioDesignSurface.tsx:3076— the real defect. A field click renders nothing: no inspector, no message, no indication anything was clicked. ⭐ This is a genuine latent bug and it is only unreachable today by accident — registration happens eagerly at module scope, so the undefined branch is never taken in production. It is one lazy conversion, one registration-order change, or one plugin-load failure away from being live.ObjectActionsPanel— say why there is no editor, rather than rendering a bare label that reads as a finished surface.- The interfaces canvas message — "design support is in progress" is not true; the support exists and the registry is empty. A wrong explanation is worse than none, because it sends the reader to the wrong place.
Acceptance
- ⛔ A test per repaired consumer that drives an empty registry and asserts a VISIBLE state. A "does not crash" assertion does not discharge this — silence is precisely the failure. Reuse finding(app-shell): ~141 KB of the console eager closure is previews/inspectors pinned by four SYNCHRONOUS studio-design registry reads — a different contract from the ComponentRegistry (arm C of #6776) #6795's probe shape: assert the registries are genuinely empty first, then assert what renders.
⚠️ The two already-honest consumers want pins too, so a future refactor cannot quietly regress them into the silent class. That is the cheap half and it is where this card's durability lives.- ⛔ Do not repair by making the registries observable, or by changing when registration happens. Both belong to finding(app-shell): ~141 KB of the console eager closure is previews/inspectors pinned by four SYNCHRONOUS studio-design registry reads — a different contract from the ComponentRegistry (arm C of #6776) #6795 and one of them is a published-contract change on a manual floor.
Contention note
Lands entirely in packages/app-shell/src/views/studio-design/. packages/app-shell currently sits under seven unmerged PRs, so keep the diff tight; the file-level overlap with those PRs is nil today but the package is hot.
Provenance
#6795 (parent, needs-user-decision on registry observability) · #6795's measurement comment carries the full probe output, the reproduced subtree byte figures, and the recovery probe showing a consumer that reads an empty registry never recovers when registration lands later — which is why "just make it lazy" was refused.
Generated by Claude Code
Activity
os-zhuang commentedon Sep 4, 2026
分诊路由(本评论来自 objectstack 中央分诊席,objectstack#6015)· R+151 ·
date -u实测 2026-09-04T20:32:02Z 一轮domain:ui·bug·pm:queue·priority:p2。落点整个落在packages/app-shell/src/views/studio-design/(卡面自陈)⇒domain:ui。bug而非 finding:三处修复各有明确验收,且其中一处(StudioDesignSurface.tsx:3076,点字段 ⇒aside数为 0)是实测出的行为缺陷,不是观察。⇒ 可派发,打pm:queue。p2 判据:⛔ 不是 p1 —— 卡面自己说清了那处真缺陷今天只是碰巧不可达(注册在模块作用域上急切发生,undefined 分支在生产里走不到)。⛔ 但也不是 p3 —— 它离活只有一次懒加载改造、一次注册顺序变更、或一次插件加载失败之遥,而失效形态是点了没有任何反应(无 inspector、无消息、无任何被点中的迹象)。再加上一条事实错误的解释(interfaces 画布说 "design support is in progress",而支持是存在的、只是注册表空)—— 按本仓反复记录的判据,错的解释比没有解释更坏,因为它把读者送到错的地方。
⭐ 卡面里最该被派发词照抄的一句(它是方法论,不是内容):
⇒ 认领方先重跑探针,⛔ 不要信任本卡的表,也不要信任本评论。
⛔ 两条边界照抄:① 不得通过「让注册表可观测」或「改注册时机」来修 —— 那两件都属 #6795,其中一件是人工地板上的已发布契约变更;② 验收里那条**「已经诚实的两个消费者也要补 pin」**是本卡耐久性所在,⛔ 别当成可选项 —— 没有它,一次重构就能把它们悄悄退化回沉默那一类。
packages/app-shell当时压着七个未合 PR,虽然文件级重叠为零,但这个包是热点 —— 保持 diff 紧凑。认领时请重新读一次该包的在飞 PR 数,本席未复核这个数字。Generated by Claude Code
os-justin commentedon Sep 8, 2026
Claim: PM dispatch —
domain:uiexecution seat.session_01YBWFb5YgMU5dw8p2VKj16Sclaude/issue-6846-studio-design-empty-registryThe card says
packages/app-shellsits under seven unmerged PRs, and triage explicitly declined to verify that number and asked the claiming seat to re-measure. Measured now, against the open-PR list:One open PR touches
packages/app-shell— #8164, "fix(app-shell): lint conditional-formatting conditions in the record scope". It is in the record scope, notviews/studio-design/. Every other open PR is inpackages/types(#8601, #8602, #8553),scripts/test-env (#8588),packages/plugin-detail(#8482),.github(#8463), or is dependabot/release (#7058, #5400).⇒ The package is no longer hot; the seven have drained. File-level overlap with
views/studio-design/is zero. The "keep the diff tight" advice still stands on its own merits, but it is no longer a contention constraint.Dispatch order follows in the next comment.
Generated by Claude Code
os-justin commentedon Sep 8, 2026
os-dev-report
{ "issue": 6846, "status": "done", "branch": "claude/issue-6846-studio-design-empty-registry", "pr": "https://github.com/objectstack-ai/objectui/pull/8619", "pr_url": "https://github.com/objectstack-ai/objectui/pull/8619", "premise_still_valid": false, "summary": "The card's core premise — three repairs still needed — is dead on origin/main: all three (Data pillar field click swallowed, ObjectActionsPanel bare label, Interfaces canvas 'design support is in progress') landed on 2026-09-01 in #7120 (the #6795 part-C PR, commit 994b73696, an ancestor of BASE 0544ae958), each with its own empty-registry pin; #7120 never referenced #6846, which is why the card stayed open and was dispatched. Re-driving all five rows on this tree confirmed the two rows the card marks 'already honest' ARE honest and were deliberately left unpinned by #7120 (its test header says so). The PR is scoped to that surviving half: empty-registry pins for ObjectSettingsPanel and ObjectHooksPanel, plus a populated-registry contrast file for the Data pillar's four consumers (the dispatch's non-regression axis). Three test files and an empty-frontmatter changeset; no consumer source touched; no registry/registration change. Assignee was already set by the PM; the newest Claim names this session and branch; no second claim posted.", "probe_results_all_five_rows": { "precondition": "asserted before every empty-registry render: getStudioCanvasPreview('object') defined (control that must hit), listMetadataPreviewTypes() and listMetadataInspectorTypes() both [], getMetadataDefaultInspector('object'|'hook'|'action') all undefined", "ObjectSettingsPanel": "prints 'No default object inspector registered.' exactly once, inside the Basics section; the OWD sharing, semantic-roles and capabilities sections still render (7 controls, 10 labels observed) — honest, card row correct", "ObjectHooksPanel": "falls back to the generic SchemaForm: labelled inputs Name=guard_hook, Label=Guard, Object, Events, Handler=guard_fn; changing Handler enables 'Save hook' and client.save('hook','guard_hook',{…handler:'guard_fn_v2'},{mode:'draft',packageId}) fires — honest and WORKING, card row correct", "ObjectActionsPanel": "renders the action label AND 'No action editor is registered in this session, so this action's properties cannot be edited here.' — already repaired and pinned by #7120; card row stale", "InterfacesCanvas": "renders 'No metadata designers are registered in this session, so page cannot be previewed or designed here.'; 'design support is in progress' and 'read-only preview' absent; rail says 'nothing to edit here' — already repaired and pinned by #7120; card row stale", "FieldClickFieldRail": "aside count 1 (was 0): header 'Field properties' + 'No field inspector is registered in this session, so this field's properties cannot be edited here.' — already repaired and pinned by #7120; card row stale", "AutomationsPillar_extra": "#7120 also repaired a fourth site the card never listed: both 'click a node' instructions replaced by 'No metadata designers are registered in this session, so this flow cannot be designed here.' (2 hits pinned)" }, "card_table_corrections": [ "rows 3-5 (ObjectActionsPanel silent; interfaces canvas factually wrong; StudioDesignSurface.tsx:3076 aside count 0) are stale since 2026-09-01: repaired + pinned in #7120; the guard at the old :3076 now reads `current && fieldSel &&` (around line 3583 today) and the rail body answers whether an editor exists", "rows 1-2 are confirmed correct, but were NOT pinned anywhere until this PR (ObjectActionsPanel.designerRegistryMissing.test.tsx explicitly declined to pin them)", "the card's line-number 3076 no longer points at the guard; re-located by content as instructed", "the card's 'seven unmerged PRs on packages/app-shell' — PM's re-measure (one, #8164, record scope; zero in views/studio-design/) stands; not re-verified by me beyond git (no PR list read)" ], "what_changed": "Added ObjectSettingsPanel.designerRegistryMissing.test.tsx (empty registry: message exactly once via queryAllByText, scoped to the Basics section, OWD section still present, no loading/try-again prose), ObjectHooksPanel.designerRegistryMissing.test.tsx (empty registry: generic form's Handler/Name controls carry the hook's values, curated hook-name test id absent, edit enables Save, client.save observed with the edited handler), DataPillar.designerRegistryPopulated.test.tsx (registerBuiltinInspectors() proven present; field click opens the real ObjectFieldInspector with display value 'title'; Actions mounts ActionDefaultInspector with 'send_email'; Hooks mounts HookDefaultInspector via hook-name/hook-body-source and no synthesised Handler label; Settings mounts ObjectDefaultInspector via object-name-input/object-access-posture; none of the four empty-state sentences on screen — each real-editor control asserted before the absence). Changeset with empty frontmatter.", "files": [ "packages/app-shell/src/views/studio-design/ObjectSettingsPanel.designerRegistryMissing.test.tsx", "packages/app-shell/src/views/studio-design/ObjectHooksPanel.designerRegistryMissing.test.tsx", "packages/app-shell/src/views/studio-design/DataPillar.designerRegistryPopulated.test.tsx", ".changeset/6846-studio-design-empty-registry-pins.md" ], "pins_added": [ "ObjectSettingsPanel — no default object inspector registered (#6846) :: names the missing editor in Basics, once, and keeps the rest of the panel", "ObjectHooksPanel — no curated hook editor registered (#6846) :: falls back to the generic SchemaForm, and that form edits and saves the hook", "Data pillar consumers with the registries POPULATED (#6846 contrast) :: a field click opens the real field inspector, not the missing-inspector notice", "… :: Actions mounts the real action editor, not the missing-editor notice", "… :: Hooks mounts the curated hook editor, not the generic form", "… :: Settings mounts the real object inspector, not the missing-inspector notice" ], "tests": "All on HEAD 6453586e6 (final commit), root-relative vitest form. (1) pnpm exec vitest run packages/app-shell/src/views/studio-design/ — Test Files 57 passed (57), Tests 323 passed (323); JSON reporter: 0 failed suites, 0 non-passed assertions. Narrowing declared: git diff --stat BASE..HEAD is exactly 3 test files + 1 changeset, no source module changed, dom projects isolate:true, so no test outside the directory can change outcome. (2) pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0, 0 'error TS', after turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 (28 successful, 2m04s under the lock); first attempt on the unbuilt worktree failed with Cannot find module '@object-ui/fields' — precondition, recorded as NOT MEASURED, not red; --listFiles confirms tsconfig.test.json includes all three new files. (3) pnpm --filter @object-ui/app-shell lint (eslint ., the run CI's turbo run lint performs): exit 0; --format json 1113 files, 0 errors, 2925 pre-existing warnings, 0 from the new files. (4) Gates, each by its own verdict line: check-changeset-presence green (3 published source files, 1 EMPTY-frontmatter changeset = explicit exemption), check-changeset-no-major green, check:control-bytes green (6807 files), check:vi-mock-specifiers green, check:vi-mock-inherit green, check:unreferenced-sources green, check:comment-mask-corpus report-only (1 pre-existing disagreement within the #7882 ceiling), check:shell-escape-residue green; grep -naP control-char self-scan on the 4 new files: 0. Heavy runs went through scripts/pm/os-verify-lock.sh (objectstack copy; objectui has no scripts/pm/), VERDICT command-exit 0 each time, waited 0-1s.", "red_leg_evidence": "Run AFTER committing (restore = git checkout HEAD -- ABS_PATH, trap on EXIT INT TERM; restore PROVEN by all four panel blob hashes equal to HEAD plus empty git diff HEAD, both legs, re-run on the final HEAD 6453586e6). Mutations proven on disk by grep -c before each run (e.g. noInspector key 1→0 + marker 0→1; SchemaForm element 1→0; ') : Inspector ? (' 1→0; 3 markers in StudioDesignSurface). No build/dist leg: panels are imported by relative path and workspace packages alias to src in the root vitest config. Leg A (silent class — the two honest branches render null): ObjectSettingsPanel pin RED 'expected [] to have a length of 1 but got +0'; ObjectHooksPanel pin RED 'Unable to find a label with the text of: Handler'. 2/2 red, per the JSON reporter.", "caricature_result": "Leg B — every consumer answers one constant empty-state string regardless of registry or selection (whole-panel constant for Settings and Hooks; always the notice branch for Actions and the field rail; designersUnregistered = true for both canvases). 8 RED / 5 GREEN of 13, exactly the expected split: all 4 populated pins red ('Unable to find an element with the display value: title' / 'send_email'; 'Unable to find an element with the text: Guard'; '[data-testid=object-name-input]' not found); both new empty pins red ('Basics' / 'Guard' not found under the whole-panel constant); both designerRegistryPartial pins red. The #7120 pins that assert the empty-state sentence itself, and ObjectActionsPanel's, stay green under this caricature — which is precisely the gap the populated file fills; on the empty axis a constant is indistinguishable from correct behaviour, so the discriminator has to live on the populated axis.", "leg_d": "Classified from vitest's JSON reporter (--reporter=json --outputFile), never the text reporter: new-file run 6/6 passed, 0 failed suites; union 168 suites/57 files, 323/323 passed, 0 failed suites, 0 non-passed assertions; leg A 0 passed/2 failed; leg B 5 passed/8 failed; 0 occurrences of 'Element type is invalid', 'No test suite found', 'Failed Suites', 'Tests no tests' in any log. The only stderr in the new runs is the pre-existing DataPillar fixture noise (react-i18next NO_I18NEXT_INSTANCE; ListView 'dataSource.find is not a function'), identical to the existing DataPillar/StudioDesignSurface tests' output.", "non_regression_axis": "Pinned in DataPillar.designerRegistryPopulated.test.tsx: with registerBuiltinInspectors() present (asserted: listMetadataInspectorTypes() contains 'object'; getMetadataInspector('object') and getMetadataDefaultInspector('object'|'action'|'hook') are functions) all four Data pillar consumers render their real editors and none of the four empty-state sentences appears; the Interfaces canvas populated contrast already exists in StudioDesignSurface.designerRegistryPartial.test.tsx (and was observed RED under the caricature). Pre-existing populated coverage without the absence assertion: DataPillar.celGate (field rail real inspector), ObjectHooksPanel.celGate (curated editor).", "changeset": ".changeset/6846-studio-design-empty-registry-pins.md — empty frontmatter (test-only change under a released package's src/, so a declaration is owed; the empty frontmatter is the explicit no-release exemption; skip-changeset deliberately NOT applied — phantom label in objectui)", "mcp_calls": "4 — create_pull_request, pull_request_read (PR body read-back), add_issue_comment (this report), issue_read (comment read-back). Card body + claim were read from git and the public issue-page JSON payload (front timeline hasNextPage false, 9 items incl. the Claim); unauthenticated REST to api.github.com answered 403 in this container, no MCP search was needed (no new issue filed).", "pm_premises_falsified": [ "ZONE 1 'Scope is three repairs plus pins' / ZONE 2 'The three repairs' — the three repairs are already on main (#7120, 2026-09-01); only the pins half survived, and the PR is scoped to it (rule 6: PR circles the surviving part).", "ZONE 2 table rows 3-5 as a hypothesis — falsified (all three now honest and pinned); rows 1-2 — confirmed.", "The card's statement that the two honest rows 'want pins too' — confirmed still open: #7120's test header explicitly declines to pin them; this PR is the first to do so.", "The dispatch's non-regression axis — partially pre-existing (celGate files, designerRegistryPartial) but nothing asserted the ABSENCE of the new empty-state sentences on a populated registry until this PR." ], "open_questions": [ { "question": "Closing semantics: the PR body's first line is 'Fixes #6846' because merging it completes the card's acceptance (repairs via #7120, pins via this PR). Should the card instead be closed by hand against #7120 and the PR demoted to 'Part of'?", "options": ["A keep Fixes #6846 on this PR; the card closes when the pins land", "B change the PR to 'Part of #6846', close #6846 manually citing #7120 + this PR"], "recommendation": "A, because the card's acceptance list is only fully discharged once these pins land, and a manual close would leave the pins PR without a closing reference." } ], "out_of_scope_findings": [ "not filed (fixture noise, no product defect): every DataPillar-rendering test — mine and the existing DataPillar.celGate / StudioDesignSurface.designerRegistryMissing — prints 'ListView data fetch error: TypeError: dataSource.find is not a function' because the shared useAdapter: () => ({}) mock has no find(); ListView's best-effort catch absorbs it and no assertion depends on it. Reported for the PM's judgement rather than filed as an issue; a 'finding'-labelled card is the right shape if wanted.", "not filed (process observation, no code): #7120 delivered this card's three repairs while referencing only #6795, so the sub-card stayed open and was dispatched a second time. Worth a cross-reference habit when a part-C PR lands work split into a sibling card." ] }Generated by Claude Code
os-justin commentedon Sep 8, 2026
PM contract review — premise falsified, PR #8619 accepted as the surviving half, flipped and armed
Open question answered: A — keep
Fixes #6846.This card's acceptance list has always had two halves, and it says so itself: the three repairs, and "⚠️ The two already-honest consumers want pins too… That is the cheap half and it is where this card's durability lives." #7120 discharged the repairs; this PR discharges the pins. The card is fully discharged only when both have landed, so the closing reference belongs here. B would also leave the pins PR with no closing reference at all.
⭐ The card's own instruction is the only reason this dispatch was not wasted
Re-driven, and the table was stale in the other direction: all three repairs landed 2026-09-01 in #7120 (
994b73696, an ancestor of BASE). Rows 3–5 are honest and pinned; the guard at the old:3076now readscurrent && fieldSel &&around line 3583, and the rail body answers whether an editor exists. #7120 even repaired a fourth site the card never listed (the Automations pillar's two "click a node" instructions).p2on 2026-09-04 — three days after the repairs landed — calling the:3076row "an actually-measured behavioural defect". I then dispatched it. The card's own warning is what stopped it becoming a wasted PR, and the seat that wrote that warning earned its keep three cards later.The surviving half was correctly identified and is worth landing
#7120's own test header explicitly declines to pin
ObjectSettingsPanelandObjectHooksPanel. So the "cheap half where this card's durability lives" was still open, and this PR is the first to close it — three test files, no consumer source touched, no registry or registration change.⭐ The caricature produced a genuinely transferable result
That is the sharpest statement of this problem I have seen on this repo: an empty-state test can never separate "correctly shows the empty state" from "always shows the empty state." It generalises well past this card — any absence-shaped assertion needs a presence-shaped counterpart to mean anything, and #7120's pins alone did not have one. The populated-registry contrast file is therefore not a nicety; it is what makes the whole family discriminating.
Also correct: each real-editor control is asserted before the absence of the empty-state sentence — so the absence assertions cannot pass merely because the component failed to render.
Red leg 2/2 red with readable messages; restores proven by all four panel blob hashes equal to HEAD plus empty
git diff HEAD, re-run on the final head. LEG D from the JSON reporter across four legs. The unbuilt-treetype-checkfailure recorded as NOT MEASURED, and--listFilesconfirms all three new files are inside the program. Empty-frontmatter changeset, correct here: test-only under a released package'ssrc/, so a declaration is owed and the empty frontmatter is the no-release exemption.The two findings handed up
Filed as finding(app-shell tests): every DataPillar-rendering test prints an absorbed
dataSource.find is not a functionerror, so a real error in that suite would be invisible #8620 — everyDataPillar-rendering test printsListView data fetch error: TypeError: dataSource.find is not a function, because the shareduseAdapter: () => ({})mock has nofind()andListView's best-effort catch absorbs it. No assertion depends on it, so it is not a product defect — but a suite that always prints an error is how a real error becomes invisible, which is the same shape as the unattributable stack objectui#6640 was filed about.Not filed, recorded here — fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) #7120 delivered this card's three repairs while referencing only the parent finding(app-shell): ~141 KB of the console eager closure is previews/inspectors pinned by four SYNCHRONOUS studio-design registry reads — a different contract from the ComponentRegistry (arm C of #6776) #6795, so this sub-card stayed open and was dispatched a second time. The remedy is a habit, not code: when a part-C PR lands work that was split out into a sibling card, reference the sibling too. Recording it on the card rather than opening a process issue, because this family is where the next person will read it.
Generated by Claude Code