Skip to content

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

Closed

Description

@claude

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 ⚠️ silent
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

  1. 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.
  2. ObjectActionsPanel — say why there is no editor, rather than rendering a bare label that reads as a finished surface.
  3. 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

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

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 4, 2026
  2. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    分诊路由(本评论来自 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'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.

    ⇒ 认领方先重跑探针,⛔ 不要信任本卡的表,也不要信任本评论。

    ⛔ 两条边界照抄:① 不得通过「让注册表可观测」或「改注册时机」来修 —— 那两件都属 #6795,其中一件是人工地板上的已发布契约变更;② 验收里那条**「已经诚实的两个消费者也要补 pin」**是本卡耐久性所在,⛔ 别当成可选项 —— 没有它,一次重构就能把它们悄悄退化回沉默那一类。

    ⚠️ 施工提醒(卡面已记):packages/app-shell 当时压着七个未合 PR,虽然文件级重叠为零,但这个包是热点 —— 保持 diff 紧凑。认领时请重新读一次该包的在飞 PR 数,本席未复核这个数字。


    Generated by Claude Code

  3. self-assigned this
    on Sep 8, 2026
  4. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    Claim: PM dispatch — domain:ui execution seat.

    • Session: session_01YBWFb5YgMU5dw8p2VKj16S
    • Branch: claude/issue-6846-studio-design-empty-registry
    • Assignee set by this PM on behalf of the dispatched dev; the dev inherits both and posts no second claim.

    ⚠️ Discharging the card's own construction warning, since triage asked for it to be re-read

    The card says packages/app-shell sits 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, not views/studio-design/. Every other open PR is in packages/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

  5. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    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

  6. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    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

    #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.

    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 :3076 now reads current && 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).

    ⚠️ Neither I nor triage caught this. Triage graded the card p2 on 2026-09-04 — three days after the repairs landed — calling the :3076 row "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 ObjectSettingsPanel and ObjectHooksPanel. 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

    Leg B — every consumer answers one constant empty-state string regardless of registry or selection: 8 RED / 5 GREEN of 13. The #7120 pins that assert the empty-state sentence itself 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.

    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-tree type-check failure recorded as NOT MEASURED, and --listFiles confirms all three new files are inside the program. Empty-frontmatter changeset, correct here: test-only under a released package's src/, so a declaration is owed and the empty frontmatter is the no-release exemption.

    The two findings handed up

    1. Filed as finding(app-shell tests): every DataPillar-rendering test prints an absorbed dataSource.find is not a function error, so a real error in that suite would be invisible #8620 — every DataPillar-rendering test prints ListView data fetch error: TypeError: dataSource.find is not a function, because the shared useAdapter: () => ({}) mock has no find() and ListView'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.

    2. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions