fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) - #7120

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states
Sep 1, 2026
Merged

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C)#7120
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #6795 — part C only, exactly as ruled (维护者, 2026-08-30, 第 5 场总监席决裁批 #4, verbatim「同意」): repair the misleading/silent consumer states now, defer part A (registry observability → lazy conversion) to the maintainer's sequencing surface.

⛔ Part A is untouched. No lazy conversion, no subscribe/notify, no useSyncExternalStore, no change to MAX_EAGER_CLOSURE_GZIP_BYTES, the #6683sideEffects array, or preview-gallery.tsx. No registry module is in this diff.

The mechanism that constrains what these messages may say

The three registries are plain Maps filled by a module-scope side effect, and every studio-design consumer reads them during render with no subscription. The card measured that a consumer which reads an empty registry never recovers when registration lands later:

fallback before registration: true | still fallback after registration: true | late inspector rendered: false

So every message here states a fact and never promises recovery — no "loading…", no "try again", no spinner. That would swap one false statement for another. Making recovery real is part A.

Suspense cannot repair any of this: these are synchronous reads returning undefined, and Suspense catches a thrown promise. Triage fenced this explicitly; it is mechanical, not stylistic.

Sites — actual locations on this base (03380aa14)

The card's line numbers came from 26896c689 and were stale. Re-located:

sitecard saidactual location nowstatus
⭐ Data pillar field railStudioDesignSurface.tsx:3076:3076 (unchanged), read at :2564fixed
Interfaces canvas + railnot line-numberedcanvas :1646, rail :1786, reads at :1391/:1397/:1402fixed
ObjectActionsPanel:101:101 (unchanged), render at :266fixed
Automations pillarnot on the card at allreads at :3245/:3246, chip :3520, rail :3574fixed (fourth site)
ObjectSettingsPanel:75 "silently empty":75⛔ untouched — already correct
ObjectHooksPanel:88 "silently empty":88⛔ untouched — already correct

Fourth-site sweep — result, with its control

Swept every registry read in views/studio-design/. The set is closed at six read sites (three in StudioDesignSurface, one each in the three panels); nothing else in the directory reads a metadata registry during render.

Found a fourth site the card and the ruling both missed: the Automations pillar. With the registries unpopulated it printed two false instructions at once — the canvas header chip "Visual orchestration · click a node to configure" and the rail "Click a node on the canvas, and its configuration appears here." — while the canvas below was a raw JSON dump with no nodes to click. Same class as the Interfaces rail, so it is repaired here.

Control for the zero:getStudioCanvasPreview('object') — registered at module scope inside the file the read comes from, so it must hit. Measured function in every probe while listMetadataPreviewTypes() and listMetadataInspectorTypes() were both []. Without that control the zeros would be indistinguishable from a failed import.

Deliberately NOT repaired, and why:studio-canvas-preview.tsx reads its own registry but self-registers object in the same module, so it cannot be empty in this way — not in the class.

Measured before / after (registries asserted empty first, every time)

sitebeforeafter
Data pillar field railaside count 0 — clicking a field did literally nothing, while the designer above it kept saying "click a field to edit its properties"aside count 1; rail opens with "Field properties" and names the missing inspector
Interfaces canvas"page shows a read-only preview for now; design support is in progress.""No metadata designers are registered in this session, so page cannot be previewed or designed here."
Interfaces rail"Click a block on the canvas, and edit its properties right here." (no canvas)"No metadata designers are registered in this session, so there is nothing to edit here."
Automations chip + rail"click a node to configure" / "Click a node on the canvas…" (canvas was a JSON dump)both replaced with "No metadata designers are registered in this session, so this flow cannot be designed here."
ObjectActionsPanel"Send Email" alone — reads as "this action has no properties""Send Email" plus "No action editor is registered in this session, so this action's properties cannot be edited here."

The Interfaces canvas message is split into the two genuinely different causes, because Preview === undefined has two of them and the retired sentence asserted one cause in the other's words. listMetadataPreviewTypes() tells them apart — a read of the same already-imported registry module, inventing no state:

  • registry empty wholesale → engine.studio.if.designersMissing
  • registry populated, this type has no designer → engine.studio.if.noDesigner

The retired engine.studio.if.readonlyPreview was false twice over: this branch renders no preview at all, and page design support exists.

Pins

Three new files, 7 tests. Every empty-registry test asserts its own precondition (both list*Types() empty, the get* reads undefined) with the control above — a zero that is not asserted is not a reading.

  • StudioDesignSurface.designerRegistryMissing.test.tsx — sites 1, 2, 4. Never registers a designer.
  • StudioDesignSurface.designerRegistryPartial.test.tsx — the contrast: registry populated, this type has none. Separate file because these Maps are module state shared by a whole file, so splitting is what lets each file assert its own precondition instead of depending on test order.
  • ObjectActionsPanel.designerRegistryMissing.test.tsx — site 3.

Ablation

Predicted directions recorded before running. Mutation: revert the three repaired source files to HEAD~1, keeping the pins at HEAD. i18n.ts is included on purpose — reverting only the .tsx files would leave the reverted call site asking for a key the pack no longer defines, so the "must not contain the old text" half of each assertion would pass vacuously.

Proven on disk, not by an editor exit code — blob hashes and marker counts both:

fileHEAD blobmutated blobmarkers
StudioDesignSurface.tsxd5abcd2d77ca4ff3designersUnregistered 5→0, fieldInspectorMissing 1→0, auto.designersMissing 2→0
ObjectActionsPanel.tsx37fd92abf58da0feactions.editorMissing 1→0
i18n.tsb04271c7604b7487if.designersMissing 2→0; readonlyPreview 1→2 (old string restored)

Pre-flight asserted each target pristine first, and an empty hash was treated as FAILURE, not as "nothing to compare". Restore leg used git checkout HEAD -- (never a bare git checkout --, which reads from the polluted index), with absolute paths in an EXIT/INT/TERM trap.

Predicted vs observed — 6 red, 1 green, exactly as predicted:

testpredictedobserved
site 1, field railREDRED
site 2, canvasREDRED
site 2, railREDRED
site 4, automationsREDRED
site 3, actions panelREDRED
contrast: "no designer for this type"REDRED
contrast: "click a block" survives with a populated registryGREEN, predictedGREEN

That last green is reported because it is informative, not because it is a pass: it pins the must-not-change half, so it is expected to survive the ablation and tells me which layer it does not cover.

Restore proven by state:git diff HEAD = 0 lines, git diff --cached = 0 lines (the index too — path-scoped checkout stages what it writes), and all three blob hashes back to their HEAD values. No number in this PR was taken on a mutated tree.

Gates — all run on the final commit 9ad81e0f1

Exit codes captured before any pipe (cmd > out 2>&1; EXIT=$?), and each verdict quoted from the gate's own printed line.

gateexitverdict
vitest run packages/app-shell/src/views/studio-design/041 files / 218 tests passed — the 3 new pins plus every pre-existing studio-design suite
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json
@object-ui/app-shell lint (whole package, unnarrowed)02852 problems (0 errors, 2852 warnings)
check:i18n-keys0"Every in-scope call-site key resolves against the en pack (2842 keys)…"
check:i18n-drift0"No en value changed in this range."
check:i18n-dead-keys0report-only; none of the six new keys appears (control workspace.createFailed hit twice)
check:control-bytes0"OK (scanned 5898 tracked text file(s))"
check:eager-closure0"Console eager closure is 3151.9 KB gzipped across 48 of 517 chunks (budget: 3191.4 KB, headroom: 39.5 KB)" — ceiling untouched, sensitivity green at 0.44x
check:side-effects-array0"names exactly the 14 module(s) that register at load time"
check:sdui-registration-pins0"All 16 registration(s) … present in the built console (517 chunks weighed)"
check:readme-exports0"OK (43 tracked README(s) … 0 unbuilt)"
check:entry-guard · check:designer-field-key-parity · check:self-import · check:esm-specifiers0green
lint:coverage · type-check:coverage0"46/46 packages linted, 0 with outstanding errors"; "41/41 packages compile their tests"
check-changeset-presence · -no-major · -fixed · -overwrite0"3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"

check:readme-exports and check:sdui-registration-pins are the two known to need a full build; both ran afterpnpm turbo run build --filter='./packages/*' (39/39) and apps/console build, so they are real greens, not NOT MEASURED.

Two measurements worth naming precisely:

  • Lint is not a claim, it is a comparison. The 20 warnings on StudioDesignSurface.tsx are pre-existing: linting the HEAD~1 version of the same file under the same config gives the same 20 warnings / 0 errors. My diff adds zero findings. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not itself contain.
  • The new test files really are typechecked.tsc -p tsconfig.test.json --listFiles lists all three (control: DataPillar.celGate.test.tsx also listed; 4506 files in the program). The package tsconfig.json excludes **/*.test.tsx, so a green tsc --noEmit alone would have said nothing about them.

Not re-measured: the eager closure on the base tree — that needs a second full build. What is measured is that the gate is green with MAX_EAGER_CLOSURE_GZIP_BYTES untouched by this diff, which is what part C owes. (This diff can only add bytes, so the headroom-sensitivity gauge error the dispatch warned about — a byte removal — is not reachable from here.)

One thing deliberately left

The Interfaces "Design" button is still offered when there is nothing designable. Repairing it means gating the Design/Run toggle on isEditable, which also reaches the StudioCanvas (object) leaves — where the registry is populated and the misleading state has a different cause. That is outside the registry-emptiness class this card is about, so it is filed rather than fixed; see the report comment on #6795.


Generated by Claude Code

…stries are empty (#6795 part C)
The three metadata designer registries are plain Maps filled by a module-scope
side effect, read during render with no subscription. A consumer that reads an
empty one gets undefined and NEVER recovers (measured: "still fallback after
registration: true | late inspector rendered: false"). Four studio-design
consumer states lied about that; one was silent.
- Data pillar field rail: the guard `fieldSel && (fieldSel.kind === 'group' ||
inspector)` dropped the entire aside when a FIELD was selected with no
inspector registered — measured aside count 0, i.e. clicking a field did
literally nothing while the designer above it said "click a field to edit its
properties". A selection now always opens its rail; the rail body answers
whether an editor exists.
- Interfaces canvas: "{type} shows a read-only preview for now; design support
is in progress" was false twice over — this branch renders no preview at all,
and page design support exists. Split via listMetadataPreviewTypes() into the
two genuinely different causes.
- Interfaces rail: stops telling the author to click a canvas that is absent.
- Automations pillar (a fourth site, found by sweeping past the three the
ruling named): the canvas chip and the rail both said "click a node" while
the canvas was a raw JSON dump.
- ObjectActionsPanel: rendered only the action's own label, reading as "this
action has no properties"; the label now carries the reason.
No message promises recovery — no "loading…", no "try again", no spinner. That
would replace one false statement with another; making recovery real is part A,
which the ruling deferred to the maintainer's sequencing surface.
ObjectSettingsPanel and ObjectHooksPanel are untouched: the measurement found
both already correct, contradicting the card body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.9 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-3ZHtTMdx.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

Landing not yet armed — 5 checks in_progress at 9ad81e0f1 (Type Check, all four test shards). 25 completed: 22 success, 3 skipped, 0 failure. in_progress is not a pass. Conflict pre-check runs against current main before I flip ready.

I verified the scope boundary myself, because "part A untouched" is the load-bearing claim

Part A is the published-contract half and is the maintainer's sequencing. If it leaked in, the PR would be unlandable under the exhausted Fable 5 quota — so this is checked, not accepted:

probe, diff vs merge-base 03380aa14expectedmeasured
files changed77
ObjectSettingsPanel.tsx / ObjectHooksPanel.tsx — the two ruled NON-defects00 / 0
preview-registry.ts / inspector-registry.ts / default-inspector-registry.ts00 / 0 / 0
metadata-admin/index.ts, preview-gallery.tsx00 / 0
MAX_EAGER_CLOSURE_GZIP_BYTES, sideEffects, useSyncExternalStore, subscribe anywhere in the diff00 / 0 / 0 / 0
⚠️StudioDesignSurface.tsxcontrol, must be present11
⚠️designersMissing in the diff — control, must hit>010
sibling agents' held files00 / 0

Both controls fire, so the zeros are absences rather than a broken query. Part A is intact and both ruled non-defects are byte-for-byte untouched.

⭐⭐ A fourth site, in the same class, that neither the card nor the ruling names

The Automations (flow) pillar printed two false instructions simultaneously — the chip "click a node to configure" and the rail "Click a node on the canvas…" — beside a raw JSON dump with no nodes to click.

⭐ This is the second time on this card that sweeping past the enumerated list found the worst instance: site 1 (:3076) was itself absent from the original filing, discovered only because the prior measurement swept beyond the card's four consumers. A card's site list is a starting population, never a closed one — and this run closed it properly, at six read sites across views/studio-design, with a control (getStudioCanvasPreview('object'), which self-registers at module scope inside the file the read comes from, so it must resolve if the module graph loaded at all).

⭐ The fifth candidate you declined is as important as the fourth you fixed

The Automations canvas fallback — a raw JSON dump — is degraded but states nothing false, so you left it. That is exactly the judgment the prior measurement made about ObjectHooksPanel's SchemaForm fallback, and exactly the judgment the ruling enforced by naming two non-defects. ⇒ The class is "says something untrue," not "looks bad." Widening it to the second would have been scope creep wearing the costume of thoroughness.

⭐ The ablation anticipated a vacuous red — which is rarer than anticipating a vacuous green

Including i18n.ts in the revert on purpose, because reverting only the .tsx files would leave the call site requesting a key the pack no longer defines, making each "must not contain the old text" assertion pass for the wrong reason. That is a red that would have looked like proof and been worth nothing.

And the prediction was mixed and specific — 6 red, 1 green, with the green named in advance (contrast test 2 pins the must-not-change half, so it is expected to survive) — and it landed exactly. A mixed prediction that comes true is much stronger evidence than an all-red one.

Restore proven by state including git diff --cached, which catches the failure mode a path-scoped git checkout creates by staging what it writes. Correct, and not a step most runs take.

⭐ You falsified the ruling's own account of the defect

The ruling flagged "design support is in progress" as the false half of the retired string. You measured that the other half is false too: that branch renders no preview at all, so "shows a read-only preview" is untrue even with a populated registry. Both halves are gone. ⭐ A ruling is binding on what to do, not on the accuracy of its incidental description — reporting that distinction rather than quietly fixing more than described is the right handling.

Assumptions falsified

  • "Line numbers are stale — locate them." Partly wrong, in the harmless direction: two of three were exactly where the card said. Reported as measured, and each located before it was edited — which is the part that matters, since the same reading would have caught it had they moved.
  • "Pins may be infeasible without part A's observability." Falsified, with the cleanest possible reason: each pin renders against a registry it has just proven empty — a static precondition, not an event. Only a recovery pin would need change notification, and that behaviour does not exist yet, so there is nothing to pin. That distinction is the whole answer, and it is now on record for whoever takes part A.

The adjacent edit is declared, and I accept it

metadata-admin/i18n.ts sits outside views/studio-design. It is mechanically forced — studio-design has no message catalogue of its own, so repairing any string requires keys there — it is neither sibling agent's held file, and it was declared rather than absorbed silently. check:i18n-keys, check:i18n-drift and the dead-key control all green; the retired key is gone from both packs.

check:eager-closure — the narrowing is stated and sound

Green at 39.5 KB headroom, ceiling not re-pinned. The base tree was not re-measured (a second full build), with the reason given: this diff can only add bytes, and the headroom-sensitivity gauge error is triggered by byte removal. That is a correct piece of reasoning about which direction the gate can fail in, and it is stated as a narrowing rather than passed off as a measurement.

#7121, and the self-correction

#7121 is filed at the right confidence level — measured with a populated registry, therefore a different cause from this card's, and deliberately not fixed because repairing it means gating the Design/Run toggle across every registerStudioCanvasPreview leaf. I will triage it; ⛔ it is not part A and it is not this card.

Also noted: the mcp_calls count was corrected upward against the copy posted on the card, with the reason (the read-back calls postdated it). Small, and exactly the right instinct.


⚠️#6795 stays OPEN after this lands. The PR is Part of, not Fixes — part A (registry observability, 118.7 KB gzip) remains on the maintainer's sequencing surface, behind the packages/app-shell PR backlog.

Re-checking CI; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) - #7120

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states
Sep 1, 2026
Merged

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C)#7120
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #6795 — part C only, exactly as ruled (维护者, 2026-08-30, 第 5 场总监席决裁批 #4, verbatim「同意」): repair the misleading/silent consumer states now, defer part A (registry observability → lazy conversion) to the maintainer's sequencing surface.

⛔ Part A is untouched. No lazy conversion, no subscribe/notify, no useSyncExternalStore, no change to MAX_EAGER_CLOSURE_GZIP_BYTES, the #6683sideEffects array, or preview-gallery.tsx. No registry module is in this diff.

The mechanism that constrains what these messages may say

The three registries are plain Maps filled by a module-scope side effect, and every studio-design consumer reads them during render with no subscription. The card measured that a consumer which reads an empty registry never recovers when registration lands later:

fallback before registration: true | still fallback after registration: true | late inspector rendered: false

So every message here states a fact and never promises recovery — no "loading…", no "try again", no spinner. That would swap one false statement for another. Making recovery real is part A.

Suspense cannot repair any of this: these are synchronous reads returning undefined, and Suspense catches a thrown promise. Triage fenced this explicitly; it is mechanical, not stylistic.

Sites — actual locations on this base (03380aa14)

The card's line numbers came from 26896c689 and were stale. Re-located:

sitecard saidactual location nowstatus
⭐ Data pillar field railStudioDesignSurface.tsx:3076:3076 (unchanged), read at :2564fixed
Interfaces canvas + railnot line-numberedcanvas :1646, rail :1786, reads at :1391/:1397/:1402fixed
ObjectActionsPanel:101:101 (unchanged), render at :266fixed
Automations pillarnot on the card at allreads at :3245/:3246, chip :3520, rail :3574fixed (fourth site)
ObjectSettingsPanel:75 "silently empty":75⛔ untouched — already correct
ObjectHooksPanel:88 "silently empty":88⛔ untouched — already correct

Fourth-site sweep — result, with its control

Swept every registry read in views/studio-design/. The set is closed at six read sites (three in StudioDesignSurface, one each in the three panels); nothing else in the directory reads a metadata registry during render.

Found a fourth site the card and the ruling both missed: the Automations pillar. With the registries unpopulated it printed two false instructions at once — the canvas header chip "Visual orchestration · click a node to configure" and the rail "Click a node on the canvas, and its configuration appears here." — while the canvas below was a raw JSON dump with no nodes to click. Same class as the Interfaces rail, so it is repaired here.

Control for the zero:getStudioCanvasPreview('object') — registered at module scope inside the file the read comes from, so it must hit. Measured function in every probe while listMetadataPreviewTypes() and listMetadataInspectorTypes() were both []. Without that control the zeros would be indistinguishable from a failed import.

Deliberately NOT repaired, and why:studio-canvas-preview.tsx reads its own registry but self-registers object in the same module, so it cannot be empty in this way — not in the class.

Measured before / after (registries asserted empty first, every time)

sitebeforeafter
Data pillar field railaside count 0 — clicking a field did literally nothing, while the designer above it kept saying "click a field to edit its properties"aside count 1; rail opens with "Field properties" and names the missing inspector
Interfaces canvas"page shows a read-only preview for now; design support is in progress.""No metadata designers are registered in this session, so page cannot be previewed or designed here."
Interfaces rail"Click a block on the canvas, and edit its properties right here." (no canvas)"No metadata designers are registered in this session, so there is nothing to edit here."
Automations chip + rail"click a node to configure" / "Click a node on the canvas…" (canvas was a JSON dump)both replaced with "No metadata designers are registered in this session, so this flow cannot be designed here."
ObjectActionsPanel"Send Email" alone — reads as "this action has no properties""Send Email" plus "No action editor is registered in this session, so this action's properties cannot be edited here."

The Interfaces canvas message is split into the two genuinely different causes, because Preview === undefined has two of them and the retired sentence asserted one cause in the other's words. listMetadataPreviewTypes() tells them apart — a read of the same already-imported registry module, inventing no state:

  • registry empty wholesale → engine.studio.if.designersMissing
  • registry populated, this type has no designer → engine.studio.if.noDesigner

The retired engine.studio.if.readonlyPreview was false twice over: this branch renders no preview at all, and page design support exists.

Pins

Three new files, 7 tests. Every empty-registry test asserts its own precondition (both list*Types() empty, the get* reads undefined) with the control above — a zero that is not asserted is not a reading.

  • StudioDesignSurface.designerRegistryMissing.test.tsx — sites 1, 2, 4. Never registers a designer.
  • StudioDesignSurface.designerRegistryPartial.test.tsx — the contrast: registry populated, this type has none. Separate file because these Maps are module state shared by a whole file, so splitting is what lets each file assert its own precondition instead of depending on test order.
  • ObjectActionsPanel.designerRegistryMissing.test.tsx — site 3.

Ablation

Predicted directions recorded before running. Mutation: revert the three repaired source files to HEAD~1, keeping the pins at HEAD. i18n.ts is included on purpose — reverting only the .tsx files would leave the reverted call site asking for a key the pack no longer defines, so the "must not contain the old text" half of each assertion would pass vacuously.

Proven on disk, not by an editor exit code — blob hashes and marker counts both:

fileHEAD blobmutated blobmarkers
StudioDesignSurface.tsxd5abcd2d77ca4ff3designersUnregistered 5→0, fieldInspectorMissing 1→0, auto.designersMissing 2→0
ObjectActionsPanel.tsx37fd92abf58da0feactions.editorMissing 1→0
i18n.tsb04271c7604b7487if.designersMissing 2→0; readonlyPreview 1→2 (old string restored)

Pre-flight asserted each target pristine first, and an empty hash was treated as FAILURE, not as "nothing to compare". Restore leg used git checkout HEAD -- (never a bare git checkout --, which reads from the polluted index), with absolute paths in an EXIT/INT/TERM trap.

Predicted vs observed — 6 red, 1 green, exactly as predicted:

testpredictedobserved
site 1, field railREDRED
site 2, canvasREDRED
site 2, railREDRED
site 4, automationsREDRED
site 3, actions panelREDRED
contrast: "no designer for this type"REDRED
contrast: "click a block" survives with a populated registryGREEN, predictedGREEN

That last green is reported because it is informative, not because it is a pass: it pins the must-not-change half, so it is expected to survive the ablation and tells me which layer it does not cover.

Restore proven by state:git diff HEAD = 0 lines, git diff --cached = 0 lines (the index too — path-scoped checkout stages what it writes), and all three blob hashes back to their HEAD values. No number in this PR was taken on a mutated tree.

Gates — all run on the final commit 9ad81e0f1

Exit codes captured before any pipe (cmd > out 2>&1; EXIT=$?), and each verdict quoted from the gate's own printed line.

gateexitverdict
vitest run packages/app-shell/src/views/studio-design/041 files / 218 tests passed — the 3 new pins plus every pre-existing studio-design suite
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json
@object-ui/app-shell lint (whole package, unnarrowed)02852 problems (0 errors, 2852 warnings)
check:i18n-keys0"Every in-scope call-site key resolves against the en pack (2842 keys)…"
check:i18n-drift0"No en value changed in this range."
check:i18n-dead-keys0report-only; none of the six new keys appears (control workspace.createFailed hit twice)
check:control-bytes0"OK (scanned 5898 tracked text file(s))"
check:eager-closure0"Console eager closure is 3151.9 KB gzipped across 48 of 517 chunks (budget: 3191.4 KB, headroom: 39.5 KB)" — ceiling untouched, sensitivity green at 0.44x
check:side-effects-array0"names exactly the 14 module(s) that register at load time"
check:sdui-registration-pins0"All 16 registration(s) … present in the built console (517 chunks weighed)"
check:readme-exports0"OK (43 tracked README(s) … 0 unbuilt)"
check:entry-guard · check:designer-field-key-parity · check:self-import · check:esm-specifiers0green
lint:coverage · type-check:coverage0"46/46 packages linted, 0 with outstanding errors"; "41/41 packages compile their tests"
check-changeset-presence · -no-major · -fixed · -overwrite0"3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"

check:readme-exports and check:sdui-registration-pins are the two known to need a full build; both ran afterpnpm turbo run build --filter='./packages/*' (39/39) and apps/console build, so they are real greens, not NOT MEASURED.

Two measurements worth naming precisely:

  • Lint is not a claim, it is a comparison. The 20 warnings on StudioDesignSurface.tsx are pre-existing: linting the HEAD~1 version of the same file under the same config gives the same 20 warnings / 0 errors. My diff adds zero findings. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not itself contain.
  • The new test files really are typechecked.tsc -p tsconfig.test.json --listFiles lists all three (control: DataPillar.celGate.test.tsx also listed; 4506 files in the program). The package tsconfig.json excludes **/*.test.tsx, so a green tsc --noEmit alone would have said nothing about them.

Not re-measured: the eager closure on the base tree — that needs a second full build. What is measured is that the gate is green with MAX_EAGER_CLOSURE_GZIP_BYTES untouched by this diff, which is what part C owes. (This diff can only add bytes, so the headroom-sensitivity gauge error the dispatch warned about — a byte removal — is not reachable from here.)

One thing deliberately left

The Interfaces "Design" button is still offered when there is nothing designable. Repairing it means gating the Design/Run toggle on isEditable, which also reaches the StudioCanvas (object) leaves — where the registry is populated and the misleading state has a different cause. That is outside the registry-emptiness class this card is about, so it is filed rather than fixed; see the report comment on #6795.


Generated by Claude Code

…stries are empty (#6795 part C)
The three metadata designer registries are plain Maps filled by a module-scope
side effect, read during render with no subscription. A consumer that reads an
empty one gets undefined and NEVER recovers (measured: "still fallback after
registration: true | late inspector rendered: false"). Four studio-design
consumer states lied about that; one was silent.
- Data pillar field rail: the guard `fieldSel && (fieldSel.kind === 'group' ||
inspector)` dropped the entire aside when a FIELD was selected with no
inspector registered — measured aside count 0, i.e. clicking a field did
literally nothing while the designer above it said "click a field to edit its
properties". A selection now always opens its rail; the rail body answers
whether an editor exists.
- Interfaces canvas: "{type} shows a read-only preview for now; design support
is in progress" was false twice over — this branch renders no preview at all,
and page design support exists. Split via listMetadataPreviewTypes() into the
two genuinely different causes.
- Interfaces rail: stops telling the author to click a canvas that is absent.
- Automations pillar (a fourth site, found by sweeping past the three the
ruling named): the canvas chip and the rail both said "click a node" while
the canvas was a raw JSON dump.
- ObjectActionsPanel: rendered only the action's own label, reading as "this
action has no properties"; the label now carries the reason.
No message promises recovery — no "loading…", no "try again", no spinner. That
would replace one false statement with another; making recovery real is part A,
which the ruling deferred to the maintainer's sequencing surface.
ObjectSettingsPanel and ObjectHooksPanel are untouched: the measurement found
both already correct, contradicting the card body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.9 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-3ZHtTMdx.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

Landing not yet armed — 5 checks in_progress at 9ad81e0f1 (Type Check, all four test shards). 25 completed: 22 success, 3 skipped, 0 failure. in_progress is not a pass. Conflict pre-check runs against current main before I flip ready.

I verified the scope boundary myself, because "part A untouched" is the load-bearing claim

Part A is the published-contract half and is the maintainer's sequencing. If it leaked in, the PR would be unlandable under the exhausted Fable 5 quota — so this is checked, not accepted:

probe, diff vs merge-base 03380aa14expectedmeasured
files changed77
ObjectSettingsPanel.tsx / ObjectHooksPanel.tsx — the two ruled NON-defects00 / 0
preview-registry.ts / inspector-registry.ts / default-inspector-registry.ts00 / 0 / 0
metadata-admin/index.ts, preview-gallery.tsx00 / 0
MAX_EAGER_CLOSURE_GZIP_BYTES, sideEffects, useSyncExternalStore, subscribe anywhere in the diff00 / 0 / 0 / 0
⚠️StudioDesignSurface.tsxcontrol, must be present11
⚠️designersMissing in the diff — control, must hit>010
sibling agents' held files00 / 0

Both controls fire, so the zeros are absences rather than a broken query. Part A is intact and both ruled non-defects are byte-for-byte untouched.

⭐⭐ A fourth site, in the same class, that neither the card nor the ruling names

The Automations (flow) pillar printed two false instructions simultaneously — the chip "click a node to configure" and the rail "Click a node on the canvas…" — beside a raw JSON dump with no nodes to click.

⭐ This is the second time on this card that sweeping past the enumerated list found the worst instance: site 1 (:3076) was itself absent from the original filing, discovered only because the prior measurement swept beyond the card's four consumers. A card's site list is a starting population, never a closed one — and this run closed it properly, at six read sites across views/studio-design, with a control (getStudioCanvasPreview('object'), which self-registers at module scope inside the file the read comes from, so it must resolve if the module graph loaded at all).

⭐ The fifth candidate you declined is as important as the fourth you fixed

The Automations canvas fallback — a raw JSON dump — is degraded but states nothing false, so you left it. That is exactly the judgment the prior measurement made about ObjectHooksPanel's SchemaForm fallback, and exactly the judgment the ruling enforced by naming two non-defects. ⇒ The class is "says something untrue," not "looks bad." Widening it to the second would have been scope creep wearing the costume of thoroughness.

⭐ The ablation anticipated a vacuous red — which is rarer than anticipating a vacuous green

Including i18n.ts in the revert on purpose, because reverting only the .tsx files would leave the call site requesting a key the pack no longer defines, making each "must not contain the old text" assertion pass for the wrong reason. That is a red that would have looked like proof and been worth nothing.

And the prediction was mixed and specific — 6 red, 1 green, with the green named in advance (contrast test 2 pins the must-not-change half, so it is expected to survive) — and it landed exactly. A mixed prediction that comes true is much stronger evidence than an all-red one.

Restore proven by state including git diff --cached, which catches the failure mode a path-scoped git checkout creates by staging what it writes. Correct, and not a step most runs take.

⭐ You falsified the ruling's own account of the defect

The ruling flagged "design support is in progress" as the false half of the retired string. You measured that the other half is false too: that branch renders no preview at all, so "shows a read-only preview" is untrue even with a populated registry. Both halves are gone. ⭐ A ruling is binding on what to do, not on the accuracy of its incidental description — reporting that distinction rather than quietly fixing more than described is the right handling.

Assumptions falsified

  • "Line numbers are stale — locate them." Partly wrong, in the harmless direction: two of three were exactly where the card said. Reported as measured, and each located before it was edited — which is the part that matters, since the same reading would have caught it had they moved.
  • "Pins may be infeasible without part A's observability." Falsified, with the cleanest possible reason: each pin renders against a registry it has just proven empty — a static precondition, not an event. Only a recovery pin would need change notification, and that behaviour does not exist yet, so there is nothing to pin. That distinction is the whole answer, and it is now on record for whoever takes part A.

The adjacent edit is declared, and I accept it

metadata-admin/i18n.ts sits outside views/studio-design. It is mechanically forced — studio-design has no message catalogue of its own, so repairing any string requires keys there — it is neither sibling agent's held file, and it was declared rather than absorbed silently. check:i18n-keys, check:i18n-drift and the dead-key control all green; the retired key is gone from both packs.

check:eager-closure — the narrowing is stated and sound

Green at 39.5 KB headroom, ceiling not re-pinned. The base tree was not re-measured (a second full build), with the reason given: this diff can only add bytes, and the headroom-sensitivity gauge error is triggered by byte removal. That is a correct piece of reasoning about which direction the gate can fail in, and it is stated as a narrowing rather than passed off as a measurement.

#7121, and the self-correction

#7121 is filed at the right confidence level — measured with a populated registry, therefore a different cause from this card's, and deliberately not fixed because repairing it means gating the Design/Run toggle across every registerStudioCanvasPreview leaf. I will triage it; ⛔ it is not part A and it is not this card.

Also noted: the mcp_calls count was corrected upward against the copy posted on the card, with the reason (the read-back calls postdated it). Small, and exactly the right instinct.


⚠️#6795 stays OPEN after this lands. The PR is Part of, not Fixes — part A (registry observability, 118.7 KB gzip) remains on the maintainer's sequencing surface, behind the packages/app-shell PR backlog.

Re-checking CI; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) - #7120

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states
Sep 1, 2026
Merged

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C)#7120
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #6795 — part C only, exactly as ruled (维护者, 2026-08-30, 第 5 场总监席决裁批 #4, verbatim「同意」): repair the misleading/silent consumer states now, defer part A (registry observability → lazy conversion) to the maintainer's sequencing surface.

⛔ Part A is untouched. No lazy conversion, no subscribe/notify, no useSyncExternalStore, no change to MAX_EAGER_CLOSURE_GZIP_BYTES, the #6683sideEffects array, or preview-gallery.tsx. No registry module is in this diff.

The mechanism that constrains what these messages may say

The three registries are plain Maps filled by a module-scope side effect, and every studio-design consumer reads them during render with no subscription. The card measured that a consumer which reads an empty registry never recovers when registration lands later:

fallback before registration: true | still fallback after registration: true | late inspector rendered: false

So every message here states a fact and never promises recovery — no "loading…", no "try again", no spinner. That would swap one false statement for another. Making recovery real is part A.

Suspense cannot repair any of this: these are synchronous reads returning undefined, and Suspense catches a thrown promise. Triage fenced this explicitly; it is mechanical, not stylistic.

Sites — actual locations on this base (03380aa14)

The card's line numbers came from 26896c689 and were stale. Re-located:

sitecard saidactual location nowstatus
⭐ Data pillar field railStudioDesignSurface.tsx:3076:3076 (unchanged), read at :2564fixed
Interfaces canvas + railnot line-numberedcanvas :1646, rail :1786, reads at :1391/:1397/:1402fixed
ObjectActionsPanel:101:101 (unchanged), render at :266fixed
Automations pillarnot on the card at allreads at :3245/:3246, chip :3520, rail :3574fixed (fourth site)
ObjectSettingsPanel:75 "silently empty":75⛔ untouched — already correct
ObjectHooksPanel:88 "silently empty":88⛔ untouched — already correct

Fourth-site sweep — result, with its control

Swept every registry read in views/studio-design/. The set is closed at six read sites (three in StudioDesignSurface, one each in the three panels); nothing else in the directory reads a metadata registry during render.

Found a fourth site the card and the ruling both missed: the Automations pillar. With the registries unpopulated it printed two false instructions at once — the canvas header chip "Visual orchestration · click a node to configure" and the rail "Click a node on the canvas, and its configuration appears here." — while the canvas below was a raw JSON dump with no nodes to click. Same class as the Interfaces rail, so it is repaired here.

Control for the zero:getStudioCanvasPreview('object') — registered at module scope inside the file the read comes from, so it must hit. Measured function in every probe while listMetadataPreviewTypes() and listMetadataInspectorTypes() were both []. Without that control the zeros would be indistinguishable from a failed import.

Deliberately NOT repaired, and why:studio-canvas-preview.tsx reads its own registry but self-registers object in the same module, so it cannot be empty in this way — not in the class.

Measured before / after (registries asserted empty first, every time)

sitebeforeafter
Data pillar field railaside count 0 — clicking a field did literally nothing, while the designer above it kept saying "click a field to edit its properties"aside count 1; rail opens with "Field properties" and names the missing inspector
Interfaces canvas"page shows a read-only preview for now; design support is in progress.""No metadata designers are registered in this session, so page cannot be previewed or designed here."
Interfaces rail"Click a block on the canvas, and edit its properties right here." (no canvas)"No metadata designers are registered in this session, so there is nothing to edit here."
Automations chip + rail"click a node to configure" / "Click a node on the canvas…" (canvas was a JSON dump)both replaced with "No metadata designers are registered in this session, so this flow cannot be designed here."
ObjectActionsPanel"Send Email" alone — reads as "this action has no properties""Send Email" plus "No action editor is registered in this session, so this action's properties cannot be edited here."

The Interfaces canvas message is split into the two genuinely different causes, because Preview === undefined has two of them and the retired sentence asserted one cause in the other's words. listMetadataPreviewTypes() tells them apart — a read of the same already-imported registry module, inventing no state:

  • registry empty wholesale → engine.studio.if.designersMissing
  • registry populated, this type has no designer → engine.studio.if.noDesigner

The retired engine.studio.if.readonlyPreview was false twice over: this branch renders no preview at all, and page design support exists.

Pins

Three new files, 7 tests. Every empty-registry test asserts its own precondition (both list*Types() empty, the get* reads undefined) with the control above — a zero that is not asserted is not a reading.

  • StudioDesignSurface.designerRegistryMissing.test.tsx — sites 1, 2, 4. Never registers a designer.
  • StudioDesignSurface.designerRegistryPartial.test.tsx — the contrast: registry populated, this type has none. Separate file because these Maps are module state shared by a whole file, so splitting is what lets each file assert its own precondition instead of depending on test order.
  • ObjectActionsPanel.designerRegistryMissing.test.tsx — site 3.

Ablation

Predicted directions recorded before running. Mutation: revert the three repaired source files to HEAD~1, keeping the pins at HEAD. i18n.ts is included on purpose — reverting only the .tsx files would leave the reverted call site asking for a key the pack no longer defines, so the "must not contain the old text" half of each assertion would pass vacuously.

Proven on disk, not by an editor exit code — blob hashes and marker counts both:

fileHEAD blobmutated blobmarkers
StudioDesignSurface.tsxd5abcd2d77ca4ff3designersUnregistered 5→0, fieldInspectorMissing 1→0, auto.designersMissing 2→0
ObjectActionsPanel.tsx37fd92abf58da0feactions.editorMissing 1→0
i18n.tsb04271c7604b7487if.designersMissing 2→0; readonlyPreview 1→2 (old string restored)

Pre-flight asserted each target pristine first, and an empty hash was treated as FAILURE, not as "nothing to compare". Restore leg used git checkout HEAD -- (never a bare git checkout --, which reads from the polluted index), with absolute paths in an EXIT/INT/TERM trap.

Predicted vs observed — 6 red, 1 green, exactly as predicted:

testpredictedobserved
site 1, field railREDRED
site 2, canvasREDRED
site 2, railREDRED
site 4, automationsREDRED
site 3, actions panelREDRED
contrast: "no designer for this type"REDRED
contrast: "click a block" survives with a populated registryGREEN, predictedGREEN

That last green is reported because it is informative, not because it is a pass: it pins the must-not-change half, so it is expected to survive the ablation and tells me which layer it does not cover.

Restore proven by state:git diff HEAD = 0 lines, git diff --cached = 0 lines (the index too — path-scoped checkout stages what it writes), and all three blob hashes back to their HEAD values. No number in this PR was taken on a mutated tree.

Gates — all run on the final commit 9ad81e0f1

Exit codes captured before any pipe (cmd > out 2>&1; EXIT=$?), and each verdict quoted from the gate's own printed line.

gateexitverdict
vitest run packages/app-shell/src/views/studio-design/041 files / 218 tests passed — the 3 new pins plus every pre-existing studio-design suite
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json
@object-ui/app-shell lint (whole package, unnarrowed)02852 problems (0 errors, 2852 warnings)
check:i18n-keys0"Every in-scope call-site key resolves against the en pack (2842 keys)…"
check:i18n-drift0"No en value changed in this range."
check:i18n-dead-keys0report-only; none of the six new keys appears (control workspace.createFailed hit twice)
check:control-bytes0"OK (scanned 5898 tracked text file(s))"
check:eager-closure0"Console eager closure is 3151.9 KB gzipped across 48 of 517 chunks (budget: 3191.4 KB, headroom: 39.5 KB)" — ceiling untouched, sensitivity green at 0.44x
check:side-effects-array0"names exactly the 14 module(s) that register at load time"
check:sdui-registration-pins0"All 16 registration(s) … present in the built console (517 chunks weighed)"
check:readme-exports0"OK (43 tracked README(s) … 0 unbuilt)"
check:entry-guard · check:designer-field-key-parity · check:self-import · check:esm-specifiers0green
lint:coverage · type-check:coverage0"46/46 packages linted, 0 with outstanding errors"; "41/41 packages compile their tests"
check-changeset-presence · -no-major · -fixed · -overwrite0"3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"

check:readme-exports and check:sdui-registration-pins are the two known to need a full build; both ran afterpnpm turbo run build --filter='./packages/*' (39/39) and apps/console build, so they are real greens, not NOT MEASURED.

Two measurements worth naming precisely:

  • Lint is not a claim, it is a comparison. The 20 warnings on StudioDesignSurface.tsx are pre-existing: linting the HEAD~1 version of the same file under the same config gives the same 20 warnings / 0 errors. My diff adds zero findings. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not itself contain.
  • The new test files really are typechecked.tsc -p tsconfig.test.json --listFiles lists all three (control: DataPillar.celGate.test.tsx also listed; 4506 files in the program). The package tsconfig.json excludes **/*.test.tsx, so a green tsc --noEmit alone would have said nothing about them.

Not re-measured: the eager closure on the base tree — that needs a second full build. What is measured is that the gate is green with MAX_EAGER_CLOSURE_GZIP_BYTES untouched by this diff, which is what part C owes. (This diff can only add bytes, so the headroom-sensitivity gauge error the dispatch warned about — a byte removal — is not reachable from here.)

One thing deliberately left

The Interfaces "Design" button is still offered when there is nothing designable. Repairing it means gating the Design/Run toggle on isEditable, which also reaches the StudioCanvas (object) leaves — where the registry is populated and the misleading state has a different cause. That is outside the registry-emptiness class this card is about, so it is filed rather than fixed; see the report comment on #6795.


Generated by Claude Code

…stries are empty (#6795 part C)
The three metadata designer registries are plain Maps filled by a module-scope
side effect, read during render with no subscription. A consumer that reads an
empty one gets undefined and NEVER recovers (measured: "still fallback after
registration: true | late inspector rendered: false"). Four studio-design
consumer states lied about that; one was silent.
- Data pillar field rail: the guard `fieldSel && (fieldSel.kind === 'group' ||
inspector)` dropped the entire aside when a FIELD was selected with no
inspector registered — measured aside count 0, i.e. clicking a field did
literally nothing while the designer above it said "click a field to edit its
properties". A selection now always opens its rail; the rail body answers
whether an editor exists.
- Interfaces canvas: "{type} shows a read-only preview for now; design support
is in progress" was false twice over — this branch renders no preview at all,
and page design support exists. Split via listMetadataPreviewTypes() into the
two genuinely different causes.
- Interfaces rail: stops telling the author to click a canvas that is absent.
- Automations pillar (a fourth site, found by sweeping past the three the
ruling named): the canvas chip and the rail both said "click a node" while
the canvas was a raw JSON dump.
- ObjectActionsPanel: rendered only the action's own label, reading as "this
action has no properties"; the label now carries the reason.
No message promises recovery — no "loading…", no "try again", no spinner. That
would replace one false statement with another; making recovery real is part A,
which the ruling deferred to the maintainer's sequencing surface.
ObjectSettingsPanel and ObjectHooksPanel are untouched: the measurement found
both already correct, contradicting the card body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.9 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-3ZHtTMdx.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

Landing not yet armed — 5 checks in_progress at 9ad81e0f1 (Type Check, all four test shards). 25 completed: 22 success, 3 skipped, 0 failure. in_progress is not a pass. Conflict pre-check runs against current main before I flip ready.

I verified the scope boundary myself, because "part A untouched" is the load-bearing claim

Part A is the published-contract half and is the maintainer's sequencing. If it leaked in, the PR would be unlandable under the exhausted Fable 5 quota — so this is checked, not accepted:

probe, diff vs merge-base 03380aa14expectedmeasured
files changed77
ObjectSettingsPanel.tsx / ObjectHooksPanel.tsx — the two ruled NON-defects00 / 0
preview-registry.ts / inspector-registry.ts / default-inspector-registry.ts00 / 0 / 0
metadata-admin/index.ts, preview-gallery.tsx00 / 0
MAX_EAGER_CLOSURE_GZIP_BYTES, sideEffects, useSyncExternalStore, subscribe anywhere in the diff00 / 0 / 0 / 0
⚠️StudioDesignSurface.tsxcontrol, must be present11
⚠️designersMissing in the diff — control, must hit>010
sibling agents' held files00 / 0

Both controls fire, so the zeros are absences rather than a broken query. Part A is intact and both ruled non-defects are byte-for-byte untouched.

⭐⭐ A fourth site, in the same class, that neither the card nor the ruling names

The Automations (flow) pillar printed two false instructions simultaneously — the chip "click a node to configure" and the rail "Click a node on the canvas…" — beside a raw JSON dump with no nodes to click.

⭐ This is the second time on this card that sweeping past the enumerated list found the worst instance: site 1 (:3076) was itself absent from the original filing, discovered only because the prior measurement swept beyond the card's four consumers. A card's site list is a starting population, never a closed one — and this run closed it properly, at six read sites across views/studio-design, with a control (getStudioCanvasPreview('object'), which self-registers at module scope inside the file the read comes from, so it must resolve if the module graph loaded at all).

⭐ The fifth candidate you declined is as important as the fourth you fixed

The Automations canvas fallback — a raw JSON dump — is degraded but states nothing false, so you left it. That is exactly the judgment the prior measurement made about ObjectHooksPanel's SchemaForm fallback, and exactly the judgment the ruling enforced by naming two non-defects. ⇒ The class is "says something untrue," not "looks bad." Widening it to the second would have been scope creep wearing the costume of thoroughness.

⭐ The ablation anticipated a vacuous red — which is rarer than anticipating a vacuous green

Including i18n.ts in the revert on purpose, because reverting only the .tsx files would leave the call site requesting a key the pack no longer defines, making each "must not contain the old text" assertion pass for the wrong reason. That is a red that would have looked like proof and been worth nothing.

And the prediction was mixed and specific — 6 red, 1 green, with the green named in advance (contrast test 2 pins the must-not-change half, so it is expected to survive) — and it landed exactly. A mixed prediction that comes true is much stronger evidence than an all-red one.

Restore proven by state including git diff --cached, which catches the failure mode a path-scoped git checkout creates by staging what it writes. Correct, and not a step most runs take.

⭐ You falsified the ruling's own account of the defect

The ruling flagged "design support is in progress" as the false half of the retired string. You measured that the other half is false too: that branch renders no preview at all, so "shows a read-only preview" is untrue even with a populated registry. Both halves are gone. ⭐ A ruling is binding on what to do, not on the accuracy of its incidental description — reporting that distinction rather than quietly fixing more than described is the right handling.

Assumptions falsified

  • "Line numbers are stale — locate them." Partly wrong, in the harmless direction: two of three were exactly where the card said. Reported as measured, and each located before it was edited — which is the part that matters, since the same reading would have caught it had they moved.
  • "Pins may be infeasible without part A's observability." Falsified, with the cleanest possible reason: each pin renders against a registry it has just proven empty — a static precondition, not an event. Only a recovery pin would need change notification, and that behaviour does not exist yet, so there is nothing to pin. That distinction is the whole answer, and it is now on record for whoever takes part A.

The adjacent edit is declared, and I accept it

metadata-admin/i18n.ts sits outside views/studio-design. It is mechanically forced — studio-design has no message catalogue of its own, so repairing any string requires keys there — it is neither sibling agent's held file, and it was declared rather than absorbed silently. check:i18n-keys, check:i18n-drift and the dead-key control all green; the retired key is gone from both packs.

check:eager-closure — the narrowing is stated and sound

Green at 39.5 KB headroom, ceiling not re-pinned. The base tree was not re-measured (a second full build), with the reason given: this diff can only add bytes, and the headroom-sensitivity gauge error is triggered by byte removal. That is a correct piece of reasoning about which direction the gate can fail in, and it is stated as a narrowing rather than passed off as a measurement.

#7121, and the self-correction

#7121 is filed at the right confidence level — measured with a populated registry, therefore a different cause from this card's, and deliberately not fixed because repairing it means gating the Design/Run toggle across every registerStudioCanvasPreview leaf. I will triage it; ⛔ it is not part A and it is not this card.

Also noted: the mcp_calls count was corrected upward against the copy posted on the card, with the reason (the read-back calls postdated it). Small, and exactly the right instinct.


⚠️#6795 stays OPEN after this lands. The PR is Part of, not Fixes — part A (registry observability, 118.7 KB gzip) remains on the maintainer's sequencing surface, behind the packages/app-shell PR backlog.

Re-checking CI; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) - #7120

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states
Sep 1, 2026
Merged

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C)#7120
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #6795 — part C only, exactly as ruled (维护者, 2026-08-30, 第 5 场总监席决裁批 #4, verbatim「同意」): repair the misleading/silent consumer states now, defer part A (registry observability → lazy conversion) to the maintainer's sequencing surface.

⛔ Part A is untouched. No lazy conversion, no subscribe/notify, no useSyncExternalStore, no change to MAX_EAGER_CLOSURE_GZIP_BYTES, the #6683sideEffects array, or preview-gallery.tsx. No registry module is in this diff.

The mechanism that constrains what these messages may say

The three registries are plain Maps filled by a module-scope side effect, and every studio-design consumer reads them during render with no subscription. The card measured that a consumer which reads an empty registry never recovers when registration lands later:

fallback before registration: true | still fallback after registration: true | late inspector rendered: false

So every message here states a fact and never promises recovery — no "loading…", no "try again", no spinner. That would swap one false statement for another. Making recovery real is part A.

Suspense cannot repair any of this: these are synchronous reads returning undefined, and Suspense catches a thrown promise. Triage fenced this explicitly; it is mechanical, not stylistic.

Sites — actual locations on this base (03380aa14)

The card's line numbers came from 26896c689 and were stale. Re-located:

sitecard saidactual location nowstatus
⭐ Data pillar field railStudioDesignSurface.tsx:3076:3076 (unchanged), read at :2564fixed
Interfaces canvas + railnot line-numberedcanvas :1646, rail :1786, reads at :1391/:1397/:1402fixed
ObjectActionsPanel:101:101 (unchanged), render at :266fixed
Automations pillarnot on the card at allreads at :3245/:3246, chip :3520, rail :3574fixed (fourth site)
ObjectSettingsPanel:75 "silently empty":75⛔ untouched — already correct
ObjectHooksPanel:88 "silently empty":88⛔ untouched — already correct

Fourth-site sweep — result, with its control

Swept every registry read in views/studio-design/. The set is closed at six read sites (three in StudioDesignSurface, one each in the three panels); nothing else in the directory reads a metadata registry during render.

Found a fourth site the card and the ruling both missed: the Automations pillar. With the registries unpopulated it printed two false instructions at once — the canvas header chip "Visual orchestration · click a node to configure" and the rail "Click a node on the canvas, and its configuration appears here." — while the canvas below was a raw JSON dump with no nodes to click. Same class as the Interfaces rail, so it is repaired here.

Control for the zero:getStudioCanvasPreview('object') — registered at module scope inside the file the read comes from, so it must hit. Measured function in every probe while listMetadataPreviewTypes() and listMetadataInspectorTypes() were both []. Without that control the zeros would be indistinguishable from a failed import.

Deliberately NOT repaired, and why:studio-canvas-preview.tsx reads its own registry but self-registers object in the same module, so it cannot be empty in this way — not in the class.

Measured before / after (registries asserted empty first, every time)

sitebeforeafter
Data pillar field railaside count 0 — clicking a field did literally nothing, while the designer above it kept saying "click a field to edit its properties"aside count 1; rail opens with "Field properties" and names the missing inspector
Interfaces canvas"page shows a read-only preview for now; design support is in progress.""No metadata designers are registered in this session, so page cannot be previewed or designed here."
Interfaces rail"Click a block on the canvas, and edit its properties right here." (no canvas)"No metadata designers are registered in this session, so there is nothing to edit here."
Automations chip + rail"click a node to configure" / "Click a node on the canvas…" (canvas was a JSON dump)both replaced with "No metadata designers are registered in this session, so this flow cannot be designed here."
ObjectActionsPanel"Send Email" alone — reads as "this action has no properties""Send Email" plus "No action editor is registered in this session, so this action's properties cannot be edited here."

The Interfaces canvas message is split into the two genuinely different causes, because Preview === undefined has two of them and the retired sentence asserted one cause in the other's words. listMetadataPreviewTypes() tells them apart — a read of the same already-imported registry module, inventing no state:

  • registry empty wholesale → engine.studio.if.designersMissing
  • registry populated, this type has no designer → engine.studio.if.noDesigner

The retired engine.studio.if.readonlyPreview was false twice over: this branch renders no preview at all, and page design support exists.

Pins

Three new files, 7 tests. Every empty-registry test asserts its own precondition (both list*Types() empty, the get* reads undefined) with the control above — a zero that is not asserted is not a reading.

  • StudioDesignSurface.designerRegistryMissing.test.tsx — sites 1, 2, 4. Never registers a designer.
  • StudioDesignSurface.designerRegistryPartial.test.tsx — the contrast: registry populated, this type has none. Separate file because these Maps are module state shared by a whole file, so splitting is what lets each file assert its own precondition instead of depending on test order.
  • ObjectActionsPanel.designerRegistryMissing.test.tsx — site 3.

Ablation

Predicted directions recorded before running. Mutation: revert the three repaired source files to HEAD~1, keeping the pins at HEAD. i18n.ts is included on purpose — reverting only the .tsx files would leave the reverted call site asking for a key the pack no longer defines, so the "must not contain the old text" half of each assertion would pass vacuously.

Proven on disk, not by an editor exit code — blob hashes and marker counts both:

fileHEAD blobmutated blobmarkers
StudioDesignSurface.tsxd5abcd2d77ca4ff3designersUnregistered 5→0, fieldInspectorMissing 1→0, auto.designersMissing 2→0
ObjectActionsPanel.tsx37fd92abf58da0feactions.editorMissing 1→0
i18n.tsb04271c7604b7487if.designersMissing 2→0; readonlyPreview 1→2 (old string restored)

Pre-flight asserted each target pristine first, and an empty hash was treated as FAILURE, not as "nothing to compare". Restore leg used git checkout HEAD -- (never a bare git checkout --, which reads from the polluted index), with absolute paths in an EXIT/INT/TERM trap.

Predicted vs observed — 6 red, 1 green, exactly as predicted:

testpredictedobserved
site 1, field railREDRED
site 2, canvasREDRED
site 2, railREDRED
site 4, automationsREDRED
site 3, actions panelREDRED
contrast: "no designer for this type"REDRED
contrast: "click a block" survives with a populated registryGREEN, predictedGREEN

That last green is reported because it is informative, not because it is a pass: it pins the must-not-change half, so it is expected to survive the ablation and tells me which layer it does not cover.

Restore proven by state:git diff HEAD = 0 lines, git diff --cached = 0 lines (the index too — path-scoped checkout stages what it writes), and all three blob hashes back to their HEAD values. No number in this PR was taken on a mutated tree.

Gates — all run on the final commit 9ad81e0f1

Exit codes captured before any pipe (cmd > out 2>&1; EXIT=$?), and each verdict quoted from the gate's own printed line.

gateexitverdict
vitest run packages/app-shell/src/views/studio-design/041 files / 218 tests passed — the 3 new pins plus every pre-existing studio-design suite
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json
@object-ui/app-shell lint (whole package, unnarrowed)02852 problems (0 errors, 2852 warnings)
check:i18n-keys0"Every in-scope call-site key resolves against the en pack (2842 keys)…"
check:i18n-drift0"No en value changed in this range."
check:i18n-dead-keys0report-only; none of the six new keys appears (control workspace.createFailed hit twice)
check:control-bytes0"OK (scanned 5898 tracked text file(s))"
check:eager-closure0"Console eager closure is 3151.9 KB gzipped across 48 of 517 chunks (budget: 3191.4 KB, headroom: 39.5 KB)" — ceiling untouched, sensitivity green at 0.44x
check:side-effects-array0"names exactly the 14 module(s) that register at load time"
check:sdui-registration-pins0"All 16 registration(s) … present in the built console (517 chunks weighed)"
check:readme-exports0"OK (43 tracked README(s) … 0 unbuilt)"
check:entry-guard · check:designer-field-key-parity · check:self-import · check:esm-specifiers0green
lint:coverage · type-check:coverage0"46/46 packages linted, 0 with outstanding errors"; "41/41 packages compile their tests"
check-changeset-presence · -no-major · -fixed · -overwrite0"3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"

check:readme-exports and check:sdui-registration-pins are the two known to need a full build; both ran afterpnpm turbo run build --filter='./packages/*' (39/39) and apps/console build, so they are real greens, not NOT MEASURED.

Two measurements worth naming precisely:

  • Lint is not a claim, it is a comparison. The 20 warnings on StudioDesignSurface.tsx are pre-existing: linting the HEAD~1 version of the same file under the same config gives the same 20 warnings / 0 errors. My diff adds zero findings. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not itself contain.
  • The new test files really are typechecked.tsc -p tsconfig.test.json --listFiles lists all three (control: DataPillar.celGate.test.tsx also listed; 4506 files in the program). The package tsconfig.json excludes **/*.test.tsx, so a green tsc --noEmit alone would have said nothing about them.

Not re-measured: the eager closure on the base tree — that needs a second full build. What is measured is that the gate is green with MAX_EAGER_CLOSURE_GZIP_BYTES untouched by this diff, which is what part C owes. (This diff can only add bytes, so the headroom-sensitivity gauge error the dispatch warned about — a byte removal — is not reachable from here.)

One thing deliberately left

The Interfaces "Design" button is still offered when there is nothing designable. Repairing it means gating the Design/Run toggle on isEditable, which also reaches the StudioCanvas (object) leaves — where the registry is populated and the misleading state has a different cause. That is outside the registry-emptiness class this card is about, so it is filed rather than fixed; see the report comment on #6795.


Generated by Claude Code

…stries are empty (#6795 part C)
The three metadata designer registries are plain Maps filled by a module-scope
side effect, read during render with no subscription. A consumer that reads an
empty one gets undefined and NEVER recovers (measured: "still fallback after
registration: true | late inspector rendered: false"). Four studio-design
consumer states lied about that; one was silent.
- Data pillar field rail: the guard `fieldSel && (fieldSel.kind === 'group' ||
inspector)` dropped the entire aside when a FIELD was selected with no
inspector registered — measured aside count 0, i.e. clicking a field did
literally nothing while the designer above it said "click a field to edit its
properties". A selection now always opens its rail; the rail body answers
whether an editor exists.
- Interfaces canvas: "{type} shows a read-only preview for now; design support
is in progress" was false twice over — this branch renders no preview at all,
and page design support exists. Split via listMetadataPreviewTypes() into the
two genuinely different causes.
- Interfaces rail: stops telling the author to click a canvas that is absent.
- Automations pillar (a fourth site, found by sweeping past the three the
ruling named): the canvas chip and the rail both said "click a node" while
the canvas was a raw JSON dump.
- ObjectActionsPanel: rendered only the action's own label, reading as "this
action has no properties"; the label now carries the reason.
No message promises recovery — no "loading…", no "try again", no spinner. That
would replace one false statement with another; making recovery real is part A,
which the ruling deferred to the maintainer's sequencing surface.
ObjectSettingsPanel and ObjectHooksPanel are untouched: the measurement found
both already correct, contradicting the card body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.9 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-3ZHtTMdx.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

Landing not yet armed — 5 checks in_progress at 9ad81e0f1 (Type Check, all four test shards). 25 completed: 22 success, 3 skipped, 0 failure. in_progress is not a pass. Conflict pre-check runs against current main before I flip ready.

I verified the scope boundary myself, because "part A untouched" is the load-bearing claim

Part A is the published-contract half and is the maintainer's sequencing. If it leaked in, the PR would be unlandable under the exhausted Fable 5 quota — so this is checked, not accepted:

probe, diff vs merge-base 03380aa14expectedmeasured
files changed77
ObjectSettingsPanel.tsx / ObjectHooksPanel.tsx — the two ruled NON-defects00 / 0
preview-registry.ts / inspector-registry.ts / default-inspector-registry.ts00 / 0 / 0
metadata-admin/index.ts, preview-gallery.tsx00 / 0
MAX_EAGER_CLOSURE_GZIP_BYTES, sideEffects, useSyncExternalStore, subscribe anywhere in the diff00 / 0 / 0 / 0
⚠️StudioDesignSurface.tsxcontrol, must be present11
⚠️designersMissing in the diff — control, must hit>010
sibling agents' held files00 / 0

Both controls fire, so the zeros are absences rather than a broken query. Part A is intact and both ruled non-defects are byte-for-byte untouched.

⭐⭐ A fourth site, in the same class, that neither the card nor the ruling names

The Automations (flow) pillar printed two false instructions simultaneously — the chip "click a node to configure" and the rail "Click a node on the canvas…" — beside a raw JSON dump with no nodes to click.

⭐ This is the second time on this card that sweeping past the enumerated list found the worst instance: site 1 (:3076) was itself absent from the original filing, discovered only because the prior measurement swept beyond the card's four consumers. A card's site list is a starting population, never a closed one — and this run closed it properly, at six read sites across views/studio-design, with a control (getStudioCanvasPreview('object'), which self-registers at module scope inside the file the read comes from, so it must resolve if the module graph loaded at all).

⭐ The fifth candidate you declined is as important as the fourth you fixed

The Automations canvas fallback — a raw JSON dump — is degraded but states nothing false, so you left it. That is exactly the judgment the prior measurement made about ObjectHooksPanel's SchemaForm fallback, and exactly the judgment the ruling enforced by naming two non-defects. ⇒ The class is "says something untrue," not "looks bad." Widening it to the second would have been scope creep wearing the costume of thoroughness.

⭐ The ablation anticipated a vacuous red — which is rarer than anticipating a vacuous green

Including i18n.ts in the revert on purpose, because reverting only the .tsx files would leave the call site requesting a key the pack no longer defines, making each "must not contain the old text" assertion pass for the wrong reason. That is a red that would have looked like proof and been worth nothing.

And the prediction was mixed and specific — 6 red, 1 green, with the green named in advance (contrast test 2 pins the must-not-change half, so it is expected to survive) — and it landed exactly. A mixed prediction that comes true is much stronger evidence than an all-red one.

Restore proven by state including git diff --cached, which catches the failure mode a path-scoped git checkout creates by staging what it writes. Correct, and not a step most runs take.

⭐ You falsified the ruling's own account of the defect

The ruling flagged "design support is in progress" as the false half of the retired string. You measured that the other half is false too: that branch renders no preview at all, so "shows a read-only preview" is untrue even with a populated registry. Both halves are gone. ⭐ A ruling is binding on what to do, not on the accuracy of its incidental description — reporting that distinction rather than quietly fixing more than described is the right handling.

Assumptions falsified

  • "Line numbers are stale — locate them." Partly wrong, in the harmless direction: two of three were exactly where the card said. Reported as measured, and each located before it was edited — which is the part that matters, since the same reading would have caught it had they moved.
  • "Pins may be infeasible without part A's observability." Falsified, with the cleanest possible reason: each pin renders against a registry it has just proven empty — a static precondition, not an event. Only a recovery pin would need change notification, and that behaviour does not exist yet, so there is nothing to pin. That distinction is the whole answer, and it is now on record for whoever takes part A.

The adjacent edit is declared, and I accept it

metadata-admin/i18n.ts sits outside views/studio-design. It is mechanically forced — studio-design has no message catalogue of its own, so repairing any string requires keys there — it is neither sibling agent's held file, and it was declared rather than absorbed silently. check:i18n-keys, check:i18n-drift and the dead-key control all green; the retired key is gone from both packs.

check:eager-closure — the narrowing is stated and sound

Green at 39.5 KB headroom, ceiling not re-pinned. The base tree was not re-measured (a second full build), with the reason given: this diff can only add bytes, and the headroom-sensitivity gauge error is triggered by byte removal. That is a correct piece of reasoning about which direction the gate can fail in, and it is stated as a narrowing rather than passed off as a measurement.

#7121, and the self-correction

#7121 is filed at the right confidence level — measured with a populated registry, therefore a different cause from this card's, and deliberately not fixed because repairing it means gating the Design/Run toggle across every registerStudioCanvasPreview leaf. I will triage it; ⛔ it is not part A and it is not this card.

Also noted: the mcp_calls count was corrected upward against the copy posted on the card, with the reason (the read-back calls postdated it). Small, and exactly the right instinct.


⚠️#6795 stays OPEN after this lands. The PR is Part of, not Fixes — part A (registry observability, 118.7 KB gzip) remains on the maintainer's sequencing surface, behind the packages/app-shell PR backlog.

Re-checking CI; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) - #7120

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states
Sep 1, 2026
Merged

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C)#7120
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #6795 — part C only, exactly as ruled (维护者, 2026-08-30, 第 5 场总监席决裁批 #4, verbatim「同意」): repair the misleading/silent consumer states now, defer part A (registry observability → lazy conversion) to the maintainer's sequencing surface.

⛔ Part A is untouched. No lazy conversion, no subscribe/notify, no useSyncExternalStore, no change to MAX_EAGER_CLOSURE_GZIP_BYTES, the #6683sideEffects array, or preview-gallery.tsx. No registry module is in this diff.

The mechanism that constrains what these messages may say

The three registries are plain Maps filled by a module-scope side effect, and every studio-design consumer reads them during render with no subscription. The card measured that a consumer which reads an empty registry never recovers when registration lands later:

fallback before registration: true | still fallback after registration: true | late inspector rendered: false

So every message here states a fact and never promises recovery — no "loading…", no "try again", no spinner. That would swap one false statement for another. Making recovery real is part A.

Suspense cannot repair any of this: these are synchronous reads returning undefined, and Suspense catches a thrown promise. Triage fenced this explicitly; it is mechanical, not stylistic.

Sites — actual locations on this base (03380aa14)

The card's line numbers came from 26896c689 and were stale. Re-located:

sitecard saidactual location nowstatus
⭐ Data pillar field railStudioDesignSurface.tsx:3076:3076 (unchanged), read at :2564fixed
Interfaces canvas + railnot line-numberedcanvas :1646, rail :1786, reads at :1391/:1397/:1402fixed
ObjectActionsPanel:101:101 (unchanged), render at :266fixed
Automations pillarnot on the card at allreads at :3245/:3246, chip :3520, rail :3574fixed (fourth site)
ObjectSettingsPanel:75 "silently empty":75⛔ untouched — already correct
ObjectHooksPanel:88 "silently empty":88⛔ untouched — already correct

Fourth-site sweep — result, with its control

Swept every registry read in views/studio-design/. The set is closed at six read sites (three in StudioDesignSurface, one each in the three panels); nothing else in the directory reads a metadata registry during render.

Found a fourth site the card and the ruling both missed: the Automations pillar. With the registries unpopulated it printed two false instructions at once — the canvas header chip "Visual orchestration · click a node to configure" and the rail "Click a node on the canvas, and its configuration appears here." — while the canvas below was a raw JSON dump with no nodes to click. Same class as the Interfaces rail, so it is repaired here.

Control for the zero:getStudioCanvasPreview('object') — registered at module scope inside the file the read comes from, so it must hit. Measured function in every probe while listMetadataPreviewTypes() and listMetadataInspectorTypes() were both []. Without that control the zeros would be indistinguishable from a failed import.

Deliberately NOT repaired, and why:studio-canvas-preview.tsx reads its own registry but self-registers object in the same module, so it cannot be empty in this way — not in the class.

Measured before / after (registries asserted empty first, every time)

sitebeforeafter
Data pillar field railaside count 0 — clicking a field did literally nothing, while the designer above it kept saying "click a field to edit its properties"aside count 1; rail opens with "Field properties" and names the missing inspector
Interfaces canvas"page shows a read-only preview for now; design support is in progress.""No metadata designers are registered in this session, so page cannot be previewed or designed here."
Interfaces rail"Click a block on the canvas, and edit its properties right here." (no canvas)"No metadata designers are registered in this session, so there is nothing to edit here."
Automations chip + rail"click a node to configure" / "Click a node on the canvas…" (canvas was a JSON dump)both replaced with "No metadata designers are registered in this session, so this flow cannot be designed here."
ObjectActionsPanel"Send Email" alone — reads as "this action has no properties""Send Email" plus "No action editor is registered in this session, so this action's properties cannot be edited here."

The Interfaces canvas message is split into the two genuinely different causes, because Preview === undefined has two of them and the retired sentence asserted one cause in the other's words. listMetadataPreviewTypes() tells them apart — a read of the same already-imported registry module, inventing no state:

  • registry empty wholesale → engine.studio.if.designersMissing
  • registry populated, this type has no designer → engine.studio.if.noDesigner

The retired engine.studio.if.readonlyPreview was false twice over: this branch renders no preview at all, and page design support exists.

Pins

Three new files, 7 tests. Every empty-registry test asserts its own precondition (both list*Types() empty, the get* reads undefined) with the control above — a zero that is not asserted is not a reading.

  • StudioDesignSurface.designerRegistryMissing.test.tsx — sites 1, 2, 4. Never registers a designer.
  • StudioDesignSurface.designerRegistryPartial.test.tsx — the contrast: registry populated, this type has none. Separate file because these Maps are module state shared by a whole file, so splitting is what lets each file assert its own precondition instead of depending on test order.
  • ObjectActionsPanel.designerRegistryMissing.test.tsx — site 3.

Ablation

Predicted directions recorded before running. Mutation: revert the three repaired source files to HEAD~1, keeping the pins at HEAD. i18n.ts is included on purpose — reverting only the .tsx files would leave the reverted call site asking for a key the pack no longer defines, so the "must not contain the old text" half of each assertion would pass vacuously.

Proven on disk, not by an editor exit code — blob hashes and marker counts both:

fileHEAD blobmutated blobmarkers
StudioDesignSurface.tsxd5abcd2d77ca4ff3designersUnregistered 5→0, fieldInspectorMissing 1→0, auto.designersMissing 2→0
ObjectActionsPanel.tsx37fd92abf58da0feactions.editorMissing 1→0
i18n.tsb04271c7604b7487if.designersMissing 2→0; readonlyPreview 1→2 (old string restored)

Pre-flight asserted each target pristine first, and an empty hash was treated as FAILURE, not as "nothing to compare". Restore leg used git checkout HEAD -- (never a bare git checkout --, which reads from the polluted index), with absolute paths in an EXIT/INT/TERM trap.

Predicted vs observed — 6 red, 1 green, exactly as predicted:

testpredictedobserved
site 1, field railREDRED
site 2, canvasREDRED
site 2, railREDRED
site 4, automationsREDRED
site 3, actions panelREDRED
contrast: "no designer for this type"REDRED
contrast: "click a block" survives with a populated registryGREEN, predictedGREEN

That last green is reported because it is informative, not because it is a pass: it pins the must-not-change half, so it is expected to survive the ablation and tells me which layer it does not cover.

Restore proven by state:git diff HEAD = 0 lines, git diff --cached = 0 lines (the index too — path-scoped checkout stages what it writes), and all three blob hashes back to their HEAD values. No number in this PR was taken on a mutated tree.

Gates — all run on the final commit 9ad81e0f1

Exit codes captured before any pipe (cmd > out 2>&1; EXIT=$?), and each verdict quoted from the gate's own printed line.

gateexitverdict
vitest run packages/app-shell/src/views/studio-design/041 files / 218 tests passed — the 3 new pins plus every pre-existing studio-design suite
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json
@object-ui/app-shell lint (whole package, unnarrowed)02852 problems (0 errors, 2852 warnings)
check:i18n-keys0"Every in-scope call-site key resolves against the en pack (2842 keys)…"
check:i18n-drift0"No en value changed in this range."
check:i18n-dead-keys0report-only; none of the six new keys appears (control workspace.createFailed hit twice)
check:control-bytes0"OK (scanned 5898 tracked text file(s))"
check:eager-closure0"Console eager closure is 3151.9 KB gzipped across 48 of 517 chunks (budget: 3191.4 KB, headroom: 39.5 KB)" — ceiling untouched, sensitivity green at 0.44x
check:side-effects-array0"names exactly the 14 module(s) that register at load time"
check:sdui-registration-pins0"All 16 registration(s) … present in the built console (517 chunks weighed)"
check:readme-exports0"OK (43 tracked README(s) … 0 unbuilt)"
check:entry-guard · check:designer-field-key-parity · check:self-import · check:esm-specifiers0green
lint:coverage · type-check:coverage0"46/46 packages linted, 0 with outstanding errors"; "41/41 packages compile their tests"
check-changeset-presence · -no-major · -fixed · -overwrite0"3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"

check:readme-exports and check:sdui-registration-pins are the two known to need a full build; both ran afterpnpm turbo run build --filter='./packages/*' (39/39) and apps/console build, so they are real greens, not NOT MEASURED.

Two measurements worth naming precisely:

  • Lint is not a claim, it is a comparison. The 20 warnings on StudioDesignSurface.tsx are pre-existing: linting the HEAD~1 version of the same file under the same config gives the same 20 warnings / 0 errors. My diff adds zero findings. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not itself contain.
  • The new test files really are typechecked.tsc -p tsconfig.test.json --listFiles lists all three (control: DataPillar.celGate.test.tsx also listed; 4506 files in the program). The package tsconfig.json excludes **/*.test.tsx, so a green tsc --noEmit alone would have said nothing about them.

Not re-measured: the eager closure on the base tree — that needs a second full build. What is measured is that the gate is green with MAX_EAGER_CLOSURE_GZIP_BYTES untouched by this diff, which is what part C owes. (This diff can only add bytes, so the headroom-sensitivity gauge error the dispatch warned about — a byte removal — is not reachable from here.)

One thing deliberately left

The Interfaces "Design" button is still offered when there is nothing designable. Repairing it means gating the Design/Run toggle on isEditable, which also reaches the StudioCanvas (object) leaves — where the registry is populated and the misleading state has a different cause. That is outside the registry-emptiness class this card is about, so it is filed rather than fixed; see the report comment on #6795.


Generated by Claude Code

…stries are empty (#6795 part C)
The three metadata designer registries are plain Maps filled by a module-scope
side effect, read during render with no subscription. A consumer that reads an
empty one gets undefined and NEVER recovers (measured: "still fallback after
registration: true | late inspector rendered: false"). Four studio-design
consumer states lied about that; one was silent.
- Data pillar field rail: the guard `fieldSel && (fieldSel.kind === 'group' ||
inspector)` dropped the entire aside when a FIELD was selected with no
inspector registered — measured aside count 0, i.e. clicking a field did
literally nothing while the designer above it said "click a field to edit its
properties". A selection now always opens its rail; the rail body answers
whether an editor exists.
- Interfaces canvas: "{type} shows a read-only preview for now; design support
is in progress" was false twice over — this branch renders no preview at all,
and page design support exists. Split via listMetadataPreviewTypes() into the
two genuinely different causes.
- Interfaces rail: stops telling the author to click a canvas that is absent.
- Automations pillar (a fourth site, found by sweeping past the three the
ruling named): the canvas chip and the rail both said "click a node" while
the canvas was a raw JSON dump.
- ObjectActionsPanel: rendered only the action's own label, reading as "this
action has no properties"; the label now carries the reason.
No message promises recovery — no "loading…", no "try again", no spinner. That
would replace one false statement with another; making recovery real is part A,
which the ruling deferred to the maintainer's sequencing surface.
ObjectSettingsPanel and ObjectHooksPanel are untouched: the measurement found
both already correct, contradicting the card body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.9 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-3ZHtTMdx.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

Landing not yet armed — 5 checks in_progress at 9ad81e0f1 (Type Check, all four test shards). 25 completed: 22 success, 3 skipped, 0 failure. in_progress is not a pass. Conflict pre-check runs against current main before I flip ready.

I verified the scope boundary myself, because "part A untouched" is the load-bearing claim

Part A is the published-contract half and is the maintainer's sequencing. If it leaked in, the PR would be unlandable under the exhausted Fable 5 quota — so this is checked, not accepted:

probe, diff vs merge-base 03380aa14expectedmeasured
files changed77
ObjectSettingsPanel.tsx / ObjectHooksPanel.tsx — the two ruled NON-defects00 / 0
preview-registry.ts / inspector-registry.ts / default-inspector-registry.ts00 / 0 / 0
metadata-admin/index.ts, preview-gallery.tsx00 / 0
MAX_EAGER_CLOSURE_GZIP_BYTES, sideEffects, useSyncExternalStore, subscribe anywhere in the diff00 / 0 / 0 / 0
⚠️StudioDesignSurface.tsxcontrol, must be present11
⚠️designersMissing in the diff — control, must hit>010
sibling agents' held files00 / 0

Both controls fire, so the zeros are absences rather than a broken query. Part A is intact and both ruled non-defects are byte-for-byte untouched.

⭐⭐ A fourth site, in the same class, that neither the card nor the ruling names

The Automations (flow) pillar printed two false instructions simultaneously — the chip "click a node to configure" and the rail "Click a node on the canvas…" — beside a raw JSON dump with no nodes to click.

⭐ This is the second time on this card that sweeping past the enumerated list found the worst instance: site 1 (:3076) was itself absent from the original filing, discovered only because the prior measurement swept beyond the card's four consumers. A card's site list is a starting population, never a closed one — and this run closed it properly, at six read sites across views/studio-design, with a control (getStudioCanvasPreview('object'), which self-registers at module scope inside the file the read comes from, so it must resolve if the module graph loaded at all).

⭐ The fifth candidate you declined is as important as the fourth you fixed

The Automations canvas fallback — a raw JSON dump — is degraded but states nothing false, so you left it. That is exactly the judgment the prior measurement made about ObjectHooksPanel's SchemaForm fallback, and exactly the judgment the ruling enforced by naming two non-defects. ⇒ The class is "says something untrue," not "looks bad." Widening it to the second would have been scope creep wearing the costume of thoroughness.

⭐ The ablation anticipated a vacuous red — which is rarer than anticipating a vacuous green

Including i18n.ts in the revert on purpose, because reverting only the .tsx files would leave the call site requesting a key the pack no longer defines, making each "must not contain the old text" assertion pass for the wrong reason. That is a red that would have looked like proof and been worth nothing.

And the prediction was mixed and specific — 6 red, 1 green, with the green named in advance (contrast test 2 pins the must-not-change half, so it is expected to survive) — and it landed exactly. A mixed prediction that comes true is much stronger evidence than an all-red one.

Restore proven by state including git diff --cached, which catches the failure mode a path-scoped git checkout creates by staging what it writes. Correct, and not a step most runs take.

⭐ You falsified the ruling's own account of the defect

The ruling flagged "design support is in progress" as the false half of the retired string. You measured that the other half is false too: that branch renders no preview at all, so "shows a read-only preview" is untrue even with a populated registry. Both halves are gone. ⭐ A ruling is binding on what to do, not on the accuracy of its incidental description — reporting that distinction rather than quietly fixing more than described is the right handling.

Assumptions falsified

  • "Line numbers are stale — locate them." Partly wrong, in the harmless direction: two of three were exactly where the card said. Reported as measured, and each located before it was edited — which is the part that matters, since the same reading would have caught it had they moved.
  • "Pins may be infeasible without part A's observability." Falsified, with the cleanest possible reason: each pin renders against a registry it has just proven empty — a static precondition, not an event. Only a recovery pin would need change notification, and that behaviour does not exist yet, so there is nothing to pin. That distinction is the whole answer, and it is now on record for whoever takes part A.

The adjacent edit is declared, and I accept it

metadata-admin/i18n.ts sits outside views/studio-design. It is mechanically forced — studio-design has no message catalogue of its own, so repairing any string requires keys there — it is neither sibling agent's held file, and it was declared rather than absorbed silently. check:i18n-keys, check:i18n-drift and the dead-key control all green; the retired key is gone from both packs.

check:eager-closure — the narrowing is stated and sound

Green at 39.5 KB headroom, ceiling not re-pinned. The base tree was not re-measured (a second full build), with the reason given: this diff can only add bytes, and the headroom-sensitivity gauge error is triggered by byte removal. That is a correct piece of reasoning about which direction the gate can fail in, and it is stated as a narrowing rather than passed off as a measurement.

#7121, and the self-correction

#7121 is filed at the right confidence level — measured with a populated registry, therefore a different cause from this card's, and deliberately not fixed because repairing it means gating the Design/Run toggle across every registerStudioCanvasPreview leaf. I will triage it; ⛔ it is not part A and it is not this card.

Also noted: the mcp_calls count was corrected upward against the copy posted on the card, with the reason (the read-back calls postdated it). Small, and exactly the right instinct.


⚠️#6795 stays OPEN after this lands. The PR is Part of, not Fixes — part A (registry observability, 118.7 KB gzip) remains on the maintainer's sequencing surface, behind the packages/app-shell PR backlog.

Re-checking CI; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) - #7120

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states
Sep 1, 2026
Merged

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C)#7120
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #6795 — part C only, exactly as ruled (维护者, 2026-08-30, 第 5 场总监席决裁批 #4, verbatim「同意」): repair the misleading/silent consumer states now, defer part A (registry observability → lazy conversion) to the maintainer's sequencing surface.

⛔ Part A is untouched. No lazy conversion, no subscribe/notify, no useSyncExternalStore, no change to MAX_EAGER_CLOSURE_GZIP_BYTES, the #6683sideEffects array, or preview-gallery.tsx. No registry module is in this diff.

The mechanism that constrains what these messages may say

The three registries are plain Maps filled by a module-scope side effect, and every studio-design consumer reads them during render with no subscription. The card measured that a consumer which reads an empty registry never recovers when registration lands later:

fallback before registration: true | still fallback after registration: true | late inspector rendered: false

So every message here states a fact and never promises recovery — no "loading…", no "try again", no spinner. That would swap one false statement for another. Making recovery real is part A.

Suspense cannot repair any of this: these are synchronous reads returning undefined, and Suspense catches a thrown promise. Triage fenced this explicitly; it is mechanical, not stylistic.

Sites — actual locations on this base (03380aa14)

The card's line numbers came from 26896c689 and were stale. Re-located:

sitecard saidactual location nowstatus
⭐ Data pillar field railStudioDesignSurface.tsx:3076:3076 (unchanged), read at :2564fixed
Interfaces canvas + railnot line-numberedcanvas :1646, rail :1786, reads at :1391/:1397/:1402fixed
ObjectActionsPanel:101:101 (unchanged), render at :266fixed
Automations pillarnot on the card at allreads at :3245/:3246, chip :3520, rail :3574fixed (fourth site)
ObjectSettingsPanel:75 "silently empty":75⛔ untouched — already correct
ObjectHooksPanel:88 "silently empty":88⛔ untouched — already correct

Fourth-site sweep — result, with its control

Swept every registry read in views/studio-design/. The set is closed at six read sites (three in StudioDesignSurface, one each in the three panels); nothing else in the directory reads a metadata registry during render.

Found a fourth site the card and the ruling both missed: the Automations pillar. With the registries unpopulated it printed two false instructions at once — the canvas header chip "Visual orchestration · click a node to configure" and the rail "Click a node on the canvas, and its configuration appears here." — while the canvas below was a raw JSON dump with no nodes to click. Same class as the Interfaces rail, so it is repaired here.

Control for the zero:getStudioCanvasPreview('object') — registered at module scope inside the file the read comes from, so it must hit. Measured function in every probe while listMetadataPreviewTypes() and listMetadataInspectorTypes() were both []. Without that control the zeros would be indistinguishable from a failed import.

Deliberately NOT repaired, and why:studio-canvas-preview.tsx reads its own registry but self-registers object in the same module, so it cannot be empty in this way — not in the class.

Measured before / after (registries asserted empty first, every time)

sitebeforeafter
Data pillar field railaside count 0 — clicking a field did literally nothing, while the designer above it kept saying "click a field to edit its properties"aside count 1; rail opens with "Field properties" and names the missing inspector
Interfaces canvas"page shows a read-only preview for now; design support is in progress.""No metadata designers are registered in this session, so page cannot be previewed or designed here."
Interfaces rail"Click a block on the canvas, and edit its properties right here." (no canvas)"No metadata designers are registered in this session, so there is nothing to edit here."
Automations chip + rail"click a node to configure" / "Click a node on the canvas…" (canvas was a JSON dump)both replaced with "No metadata designers are registered in this session, so this flow cannot be designed here."
ObjectActionsPanel"Send Email" alone — reads as "this action has no properties""Send Email" plus "No action editor is registered in this session, so this action's properties cannot be edited here."

The Interfaces canvas message is split into the two genuinely different causes, because Preview === undefined has two of them and the retired sentence asserted one cause in the other's words. listMetadataPreviewTypes() tells them apart — a read of the same already-imported registry module, inventing no state:

  • registry empty wholesale → engine.studio.if.designersMissing
  • registry populated, this type has no designer → engine.studio.if.noDesigner

The retired engine.studio.if.readonlyPreview was false twice over: this branch renders no preview at all, and page design support exists.

Pins

Three new files, 7 tests. Every empty-registry test asserts its own precondition (both list*Types() empty, the get* reads undefined) with the control above — a zero that is not asserted is not a reading.

  • StudioDesignSurface.designerRegistryMissing.test.tsx — sites 1, 2, 4. Never registers a designer.
  • StudioDesignSurface.designerRegistryPartial.test.tsx — the contrast: registry populated, this type has none. Separate file because these Maps are module state shared by a whole file, so splitting is what lets each file assert its own precondition instead of depending on test order.
  • ObjectActionsPanel.designerRegistryMissing.test.tsx — site 3.

Ablation

Predicted directions recorded before running. Mutation: revert the three repaired source files to HEAD~1, keeping the pins at HEAD. i18n.ts is included on purpose — reverting only the .tsx files would leave the reverted call site asking for a key the pack no longer defines, so the "must not contain the old text" half of each assertion would pass vacuously.

Proven on disk, not by an editor exit code — blob hashes and marker counts both:

fileHEAD blobmutated blobmarkers
StudioDesignSurface.tsxd5abcd2d77ca4ff3designersUnregistered 5→0, fieldInspectorMissing 1→0, auto.designersMissing 2→0
ObjectActionsPanel.tsx37fd92abf58da0feactions.editorMissing 1→0
i18n.tsb04271c7604b7487if.designersMissing 2→0; readonlyPreview 1→2 (old string restored)

Pre-flight asserted each target pristine first, and an empty hash was treated as FAILURE, not as "nothing to compare". Restore leg used git checkout HEAD -- (never a bare git checkout --, which reads from the polluted index), with absolute paths in an EXIT/INT/TERM trap.

Predicted vs observed — 6 red, 1 green, exactly as predicted:

testpredictedobserved
site 1, field railREDRED
site 2, canvasREDRED
site 2, railREDRED
site 4, automationsREDRED
site 3, actions panelREDRED
contrast: "no designer for this type"REDRED
contrast: "click a block" survives with a populated registryGREEN, predictedGREEN

That last green is reported because it is informative, not because it is a pass: it pins the must-not-change half, so it is expected to survive the ablation and tells me which layer it does not cover.

Restore proven by state:git diff HEAD = 0 lines, git diff --cached = 0 lines (the index too — path-scoped checkout stages what it writes), and all three blob hashes back to their HEAD values. No number in this PR was taken on a mutated tree.

Gates — all run on the final commit 9ad81e0f1

Exit codes captured before any pipe (cmd > out 2>&1; EXIT=$?), and each verdict quoted from the gate's own printed line.

gateexitverdict
vitest run packages/app-shell/src/views/studio-design/041 files / 218 tests passed — the 3 new pins plus every pre-existing studio-design suite
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json
@object-ui/app-shell lint (whole package, unnarrowed)02852 problems (0 errors, 2852 warnings)
check:i18n-keys0"Every in-scope call-site key resolves against the en pack (2842 keys)…"
check:i18n-drift0"No en value changed in this range."
check:i18n-dead-keys0report-only; none of the six new keys appears (control workspace.createFailed hit twice)
check:control-bytes0"OK (scanned 5898 tracked text file(s))"
check:eager-closure0"Console eager closure is 3151.9 KB gzipped across 48 of 517 chunks (budget: 3191.4 KB, headroom: 39.5 KB)" — ceiling untouched, sensitivity green at 0.44x
check:side-effects-array0"names exactly the 14 module(s) that register at load time"
check:sdui-registration-pins0"All 16 registration(s) … present in the built console (517 chunks weighed)"
check:readme-exports0"OK (43 tracked README(s) … 0 unbuilt)"
check:entry-guard · check:designer-field-key-parity · check:self-import · check:esm-specifiers0green
lint:coverage · type-check:coverage0"46/46 packages linted, 0 with outstanding errors"; "41/41 packages compile their tests"
check-changeset-presence · -no-major · -fixed · -overwrite0"3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"

check:readme-exports and check:sdui-registration-pins are the two known to need a full build; both ran afterpnpm turbo run build --filter='./packages/*' (39/39) and apps/console build, so they are real greens, not NOT MEASURED.

Two measurements worth naming precisely:

  • Lint is not a claim, it is a comparison. The 20 warnings on StudioDesignSurface.tsx are pre-existing: linting the HEAD~1 version of the same file under the same config gives the same 20 warnings / 0 errors. My diff adds zero findings. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not itself contain.
  • The new test files really are typechecked.tsc -p tsconfig.test.json --listFiles lists all three (control: DataPillar.celGate.test.tsx also listed; 4506 files in the program). The package tsconfig.json excludes **/*.test.tsx, so a green tsc --noEmit alone would have said nothing about them.

Not re-measured: the eager closure on the base tree — that needs a second full build. What is measured is that the gate is green with MAX_EAGER_CLOSURE_GZIP_BYTES untouched by this diff, which is what part C owes. (This diff can only add bytes, so the headroom-sensitivity gauge error the dispatch warned about — a byte removal — is not reachable from here.)

One thing deliberately left

The Interfaces "Design" button is still offered when there is nothing designable. Repairing it means gating the Design/Run toggle on isEditable, which also reaches the StudioCanvas (object) leaves — where the registry is populated and the misleading state has a different cause. That is outside the registry-emptiness class this card is about, so it is filed rather than fixed; see the report comment on #6795.


Generated by Claude Code

…stries are empty (#6795 part C)
The three metadata designer registries are plain Maps filled by a module-scope
side effect, read during render with no subscription. A consumer that reads an
empty one gets undefined and NEVER recovers (measured: "still fallback after
registration: true | late inspector rendered: false"). Four studio-design
consumer states lied about that; one was silent.
- Data pillar field rail: the guard `fieldSel && (fieldSel.kind === 'group' ||
inspector)` dropped the entire aside when a FIELD was selected with no
inspector registered — measured aside count 0, i.e. clicking a field did
literally nothing while the designer above it said "click a field to edit its
properties". A selection now always opens its rail; the rail body answers
whether an editor exists.
- Interfaces canvas: "{type} shows a read-only preview for now; design support
is in progress" was false twice over — this branch renders no preview at all,
and page design support exists. Split via listMetadataPreviewTypes() into the
two genuinely different causes.
- Interfaces rail: stops telling the author to click a canvas that is absent.
- Automations pillar (a fourth site, found by sweeping past the three the
ruling named): the canvas chip and the rail both said "click a node" while
the canvas was a raw JSON dump.
- ObjectActionsPanel: rendered only the action's own label, reading as "this
action has no properties"; the label now carries the reason.
No message promises recovery — no "loading…", no "try again", no spinner. That
would replace one false statement with another; making recovery real is part A,
which the ruling deferred to the maintainer's sequencing surface.
ObjectSettingsPanel and ObjectHooksPanel are untouched: the measurement found
both already correct, contradicting the card body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.9 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-3ZHtTMdx.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

Landing not yet armed — 5 checks in_progress at 9ad81e0f1 (Type Check, all four test shards). 25 completed: 22 success, 3 skipped, 0 failure. in_progress is not a pass. Conflict pre-check runs against current main before I flip ready.

I verified the scope boundary myself, because "part A untouched" is the load-bearing claim

Part A is the published-contract half and is the maintainer's sequencing. If it leaked in, the PR would be unlandable under the exhausted Fable 5 quota — so this is checked, not accepted:

probe, diff vs merge-base 03380aa14expectedmeasured
files changed77
ObjectSettingsPanel.tsx / ObjectHooksPanel.tsx — the two ruled NON-defects00 / 0
preview-registry.ts / inspector-registry.ts / default-inspector-registry.ts00 / 0 / 0
metadata-admin/index.ts, preview-gallery.tsx00 / 0
MAX_EAGER_CLOSURE_GZIP_BYTES, sideEffects, useSyncExternalStore, subscribe anywhere in the diff00 / 0 / 0 / 0
⚠️StudioDesignSurface.tsxcontrol, must be present11
⚠️designersMissing in the diff — control, must hit>010
sibling agents' held files00 / 0

Both controls fire, so the zeros are absences rather than a broken query. Part A is intact and both ruled non-defects are byte-for-byte untouched.

⭐⭐ A fourth site, in the same class, that neither the card nor the ruling names

The Automations (flow) pillar printed two false instructions simultaneously — the chip "click a node to configure" and the rail "Click a node on the canvas…" — beside a raw JSON dump with no nodes to click.

⭐ This is the second time on this card that sweeping past the enumerated list found the worst instance: site 1 (:3076) was itself absent from the original filing, discovered only because the prior measurement swept beyond the card's four consumers. A card's site list is a starting population, never a closed one — and this run closed it properly, at six read sites across views/studio-design, with a control (getStudioCanvasPreview('object'), which self-registers at module scope inside the file the read comes from, so it must resolve if the module graph loaded at all).

⭐ The fifth candidate you declined is as important as the fourth you fixed

The Automations canvas fallback — a raw JSON dump — is degraded but states nothing false, so you left it. That is exactly the judgment the prior measurement made about ObjectHooksPanel's SchemaForm fallback, and exactly the judgment the ruling enforced by naming two non-defects. ⇒ The class is "says something untrue," not "looks bad." Widening it to the second would have been scope creep wearing the costume of thoroughness.

⭐ The ablation anticipated a vacuous red — which is rarer than anticipating a vacuous green

Including i18n.ts in the revert on purpose, because reverting only the .tsx files would leave the call site requesting a key the pack no longer defines, making each "must not contain the old text" assertion pass for the wrong reason. That is a red that would have looked like proof and been worth nothing.

And the prediction was mixed and specific — 6 red, 1 green, with the green named in advance (contrast test 2 pins the must-not-change half, so it is expected to survive) — and it landed exactly. A mixed prediction that comes true is much stronger evidence than an all-red one.

Restore proven by state including git diff --cached, which catches the failure mode a path-scoped git checkout creates by staging what it writes. Correct, and not a step most runs take.

⭐ You falsified the ruling's own account of the defect

The ruling flagged "design support is in progress" as the false half of the retired string. You measured that the other half is false too: that branch renders no preview at all, so "shows a read-only preview" is untrue even with a populated registry. Both halves are gone. ⭐ A ruling is binding on what to do, not on the accuracy of its incidental description — reporting that distinction rather than quietly fixing more than described is the right handling.

Assumptions falsified

  • "Line numbers are stale — locate them." Partly wrong, in the harmless direction: two of three were exactly where the card said. Reported as measured, and each located before it was edited — which is the part that matters, since the same reading would have caught it had they moved.
  • "Pins may be infeasible without part A's observability." Falsified, with the cleanest possible reason: each pin renders against a registry it has just proven empty — a static precondition, not an event. Only a recovery pin would need change notification, and that behaviour does not exist yet, so there is nothing to pin. That distinction is the whole answer, and it is now on record for whoever takes part A.

The adjacent edit is declared, and I accept it

metadata-admin/i18n.ts sits outside views/studio-design. It is mechanically forced — studio-design has no message catalogue of its own, so repairing any string requires keys there — it is neither sibling agent's held file, and it was declared rather than absorbed silently. check:i18n-keys, check:i18n-drift and the dead-key control all green; the retired key is gone from both packs.

check:eager-closure — the narrowing is stated and sound

Green at 39.5 KB headroom, ceiling not re-pinned. The base tree was not re-measured (a second full build), with the reason given: this diff can only add bytes, and the headroom-sensitivity gauge error is triggered by byte removal. That is a correct piece of reasoning about which direction the gate can fail in, and it is stated as a narrowing rather than passed off as a measurement.

#7121, and the self-correction

#7121 is filed at the right confidence level — measured with a populated registry, therefore a different cause from this card's, and deliberately not fixed because repairing it means gating the Design/Run toggle across every registerStudioCanvasPreview leaf. I will triage it; ⛔ it is not part A and it is not this card.

Also noted: the mcp_calls count was corrected upward against the copy posted on the card, with the reason (the read-back calls postdated it). Small, and exactly the right instinct.


⚠️#6795 stays OPEN after this lands. The PR is Part of, not Fixes — part A (registry observability, 118.7 KB gzip) remains on the maintainer's sequencing surface, behind the packages/app-shell PR backlog.

Re-checking CI; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) - #7120

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states
Sep 1, 2026
Merged

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C)#7120
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #6795 — part C only, exactly as ruled (维护者, 2026-08-30, 第 5 场总监席决裁批 #4, verbatim「同意」): repair the misleading/silent consumer states now, defer part A (registry observability → lazy conversion) to the maintainer's sequencing surface.

⛔ Part A is untouched. No lazy conversion, no subscribe/notify, no useSyncExternalStore, no change to MAX_EAGER_CLOSURE_GZIP_BYTES, the #6683sideEffects array, or preview-gallery.tsx. No registry module is in this diff.

The mechanism that constrains what these messages may say

The three registries are plain Maps filled by a module-scope side effect, and every studio-design consumer reads them during render with no subscription. The card measured that a consumer which reads an empty registry never recovers when registration lands later:

fallback before registration: true | still fallback after registration: true | late inspector rendered: false

So every message here states a fact and never promises recovery — no "loading…", no "try again", no spinner. That would swap one false statement for another. Making recovery real is part A.

Suspense cannot repair any of this: these are synchronous reads returning undefined, and Suspense catches a thrown promise. Triage fenced this explicitly; it is mechanical, not stylistic.

Sites — actual locations on this base (03380aa14)

The card's line numbers came from 26896c689 and were stale. Re-located:

sitecard saidactual location nowstatus
⭐ Data pillar field railStudioDesignSurface.tsx:3076:3076 (unchanged), read at :2564fixed
Interfaces canvas + railnot line-numberedcanvas :1646, rail :1786, reads at :1391/:1397/:1402fixed
ObjectActionsPanel:101:101 (unchanged), render at :266fixed
Automations pillarnot on the card at allreads at :3245/:3246, chip :3520, rail :3574fixed (fourth site)
ObjectSettingsPanel:75 "silently empty":75⛔ untouched — already correct
ObjectHooksPanel:88 "silently empty":88⛔ untouched — already correct

Fourth-site sweep — result, with its control

Swept every registry read in views/studio-design/. The set is closed at six read sites (three in StudioDesignSurface, one each in the three panels); nothing else in the directory reads a metadata registry during render.

Found a fourth site the card and the ruling both missed: the Automations pillar. With the registries unpopulated it printed two false instructions at once — the canvas header chip "Visual orchestration · click a node to configure" and the rail "Click a node on the canvas, and its configuration appears here." — while the canvas below was a raw JSON dump with no nodes to click. Same class as the Interfaces rail, so it is repaired here.

Control for the zero:getStudioCanvasPreview('object') — registered at module scope inside the file the read comes from, so it must hit. Measured function in every probe while listMetadataPreviewTypes() and listMetadataInspectorTypes() were both []. Without that control the zeros would be indistinguishable from a failed import.

Deliberately NOT repaired, and why:studio-canvas-preview.tsx reads its own registry but self-registers object in the same module, so it cannot be empty in this way — not in the class.

Measured before / after (registries asserted empty first, every time)

sitebeforeafter
Data pillar field railaside count 0 — clicking a field did literally nothing, while the designer above it kept saying "click a field to edit its properties"aside count 1; rail opens with "Field properties" and names the missing inspector
Interfaces canvas"page shows a read-only preview for now; design support is in progress.""No metadata designers are registered in this session, so page cannot be previewed or designed here."
Interfaces rail"Click a block on the canvas, and edit its properties right here." (no canvas)"No metadata designers are registered in this session, so there is nothing to edit here."
Automations chip + rail"click a node to configure" / "Click a node on the canvas…" (canvas was a JSON dump)both replaced with "No metadata designers are registered in this session, so this flow cannot be designed here."
ObjectActionsPanel"Send Email" alone — reads as "this action has no properties""Send Email" plus "No action editor is registered in this session, so this action's properties cannot be edited here."

The Interfaces canvas message is split into the two genuinely different causes, because Preview === undefined has two of them and the retired sentence asserted one cause in the other's words. listMetadataPreviewTypes() tells them apart — a read of the same already-imported registry module, inventing no state:

  • registry empty wholesale → engine.studio.if.designersMissing
  • registry populated, this type has no designer → engine.studio.if.noDesigner

The retired engine.studio.if.readonlyPreview was false twice over: this branch renders no preview at all, and page design support exists.

Pins

Three new files, 7 tests. Every empty-registry test asserts its own precondition (both list*Types() empty, the get* reads undefined) with the control above — a zero that is not asserted is not a reading.

  • StudioDesignSurface.designerRegistryMissing.test.tsx — sites 1, 2, 4. Never registers a designer.
  • StudioDesignSurface.designerRegistryPartial.test.tsx — the contrast: registry populated, this type has none. Separate file because these Maps are module state shared by a whole file, so splitting is what lets each file assert its own precondition instead of depending on test order.
  • ObjectActionsPanel.designerRegistryMissing.test.tsx — site 3.

Ablation

Predicted directions recorded before running. Mutation: revert the three repaired source files to HEAD~1, keeping the pins at HEAD. i18n.ts is included on purpose — reverting only the .tsx files would leave the reverted call site asking for a key the pack no longer defines, so the "must not contain the old text" half of each assertion would pass vacuously.

Proven on disk, not by an editor exit code — blob hashes and marker counts both:

fileHEAD blobmutated blobmarkers
StudioDesignSurface.tsxd5abcd2d77ca4ff3designersUnregistered 5→0, fieldInspectorMissing 1→0, auto.designersMissing 2→0
ObjectActionsPanel.tsx37fd92abf58da0feactions.editorMissing 1→0
i18n.tsb04271c7604b7487if.designersMissing 2→0; readonlyPreview 1→2 (old string restored)

Pre-flight asserted each target pristine first, and an empty hash was treated as FAILURE, not as "nothing to compare". Restore leg used git checkout HEAD -- (never a bare git checkout --, which reads from the polluted index), with absolute paths in an EXIT/INT/TERM trap.

Predicted vs observed — 6 red, 1 green, exactly as predicted:

testpredictedobserved
site 1, field railREDRED
site 2, canvasREDRED
site 2, railREDRED
site 4, automationsREDRED
site 3, actions panelREDRED
contrast: "no designer for this type"REDRED
contrast: "click a block" survives with a populated registryGREEN, predictedGREEN

That last green is reported because it is informative, not because it is a pass: it pins the must-not-change half, so it is expected to survive the ablation and tells me which layer it does not cover.

Restore proven by state:git diff HEAD = 0 lines, git diff --cached = 0 lines (the index too — path-scoped checkout stages what it writes), and all three blob hashes back to their HEAD values. No number in this PR was taken on a mutated tree.

Gates — all run on the final commit 9ad81e0f1

Exit codes captured before any pipe (cmd > out 2>&1; EXIT=$?), and each verdict quoted from the gate's own printed line.

gateexitverdict
vitest run packages/app-shell/src/views/studio-design/041 files / 218 tests passed — the 3 new pins plus every pre-existing studio-design suite
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json
@object-ui/app-shell lint (whole package, unnarrowed)02852 problems (0 errors, 2852 warnings)
check:i18n-keys0"Every in-scope call-site key resolves against the en pack (2842 keys)…"
check:i18n-drift0"No en value changed in this range."
check:i18n-dead-keys0report-only; none of the six new keys appears (control workspace.createFailed hit twice)
check:control-bytes0"OK (scanned 5898 tracked text file(s))"
check:eager-closure0"Console eager closure is 3151.9 KB gzipped across 48 of 517 chunks (budget: 3191.4 KB, headroom: 39.5 KB)" — ceiling untouched, sensitivity green at 0.44x
check:side-effects-array0"names exactly the 14 module(s) that register at load time"
check:sdui-registration-pins0"All 16 registration(s) … present in the built console (517 chunks weighed)"
check:readme-exports0"OK (43 tracked README(s) … 0 unbuilt)"
check:entry-guard · check:designer-field-key-parity · check:self-import · check:esm-specifiers0green
lint:coverage · type-check:coverage0"46/46 packages linted, 0 with outstanding errors"; "41/41 packages compile their tests"
check-changeset-presence · -no-major · -fixed · -overwrite0"3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"

check:readme-exports and check:sdui-registration-pins are the two known to need a full build; both ran afterpnpm turbo run build --filter='./packages/*' (39/39) and apps/console build, so they are real greens, not NOT MEASURED.

Two measurements worth naming precisely:

  • Lint is not a claim, it is a comparison. The 20 warnings on StudioDesignSurface.tsx are pre-existing: linting the HEAD~1 version of the same file under the same config gives the same 20 warnings / 0 errors. My diff adds zero findings. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not itself contain.
  • The new test files really are typechecked.tsc -p tsconfig.test.json --listFiles lists all three (control: DataPillar.celGate.test.tsx also listed; 4506 files in the program). The package tsconfig.json excludes **/*.test.tsx, so a green tsc --noEmit alone would have said nothing about them.

Not re-measured: the eager closure on the base tree — that needs a second full build. What is measured is that the gate is green with MAX_EAGER_CLOSURE_GZIP_BYTES untouched by this diff, which is what part C owes. (This diff can only add bytes, so the headroom-sensitivity gauge error the dispatch warned about — a byte removal — is not reachable from here.)

One thing deliberately left

The Interfaces "Design" button is still offered when there is nothing designable. Repairing it means gating the Design/Run toggle on isEditable, which also reaches the StudioCanvas (object) leaves — where the registry is populated and the misleading state has a different cause. That is outside the registry-emptiness class this card is about, so it is filed rather than fixed; see the report comment on #6795.


Generated by Claude Code

…stries are empty (#6795 part C)
The three metadata designer registries are plain Maps filled by a module-scope
side effect, read during render with no subscription. A consumer that reads an
empty one gets undefined and NEVER recovers (measured: "still fallback after
registration: true | late inspector rendered: false"). Four studio-design
consumer states lied about that; one was silent.
- Data pillar field rail: the guard `fieldSel && (fieldSel.kind === 'group' ||
inspector)` dropped the entire aside when a FIELD was selected with no
inspector registered — measured aside count 0, i.e. clicking a field did
literally nothing while the designer above it said "click a field to edit its
properties". A selection now always opens its rail; the rail body answers
whether an editor exists.
- Interfaces canvas: "{type} shows a read-only preview for now; design support
is in progress" was false twice over — this branch renders no preview at all,
and page design support exists. Split via listMetadataPreviewTypes() into the
two genuinely different causes.
- Interfaces rail: stops telling the author to click a canvas that is absent.
- Automations pillar (a fourth site, found by sweeping past the three the
ruling named): the canvas chip and the rail both said "click a node" while
the canvas was a raw JSON dump.
- ObjectActionsPanel: rendered only the action's own label, reading as "this
action has no properties"; the label now carries the reason.
No message promises recovery — no "loading…", no "try again", no spinner. That
would replace one false statement with another; making recovery real is part A,
which the ruling deferred to the maintainer's sequencing surface.
ObjectSettingsPanel and ObjectHooksPanel are untouched: the measurement found
both already correct, contradicting the card body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.9 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-3ZHtTMdx.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

Landing not yet armed — 5 checks in_progress at 9ad81e0f1 (Type Check, all four test shards). 25 completed: 22 success, 3 skipped, 0 failure. in_progress is not a pass. Conflict pre-check runs against current main before I flip ready.

I verified the scope boundary myself, because "part A untouched" is the load-bearing claim

Part A is the published-contract half and is the maintainer's sequencing. If it leaked in, the PR would be unlandable under the exhausted Fable 5 quota — so this is checked, not accepted:

probe, diff vs merge-base 03380aa14expectedmeasured
files changed77
ObjectSettingsPanel.tsx / ObjectHooksPanel.tsx — the two ruled NON-defects00 / 0
preview-registry.ts / inspector-registry.ts / default-inspector-registry.ts00 / 0 / 0
metadata-admin/index.ts, preview-gallery.tsx00 / 0
MAX_EAGER_CLOSURE_GZIP_BYTES, sideEffects, useSyncExternalStore, subscribe anywhere in the diff00 / 0 / 0 / 0
⚠️StudioDesignSurface.tsxcontrol, must be present11
⚠️designersMissing in the diff — control, must hit>010
sibling agents' held files00 / 0

Both controls fire, so the zeros are absences rather than a broken query. Part A is intact and both ruled non-defects are byte-for-byte untouched.

⭐⭐ A fourth site, in the same class, that neither the card nor the ruling names

The Automations (flow) pillar printed two false instructions simultaneously — the chip "click a node to configure" and the rail "Click a node on the canvas…" — beside a raw JSON dump with no nodes to click.

⭐ This is the second time on this card that sweeping past the enumerated list found the worst instance: site 1 (:3076) was itself absent from the original filing, discovered only because the prior measurement swept beyond the card's four consumers. A card's site list is a starting population, never a closed one — and this run closed it properly, at six read sites across views/studio-design, with a control (getStudioCanvasPreview('object'), which self-registers at module scope inside the file the read comes from, so it must resolve if the module graph loaded at all).

⭐ The fifth candidate you declined is as important as the fourth you fixed

The Automations canvas fallback — a raw JSON dump — is degraded but states nothing false, so you left it. That is exactly the judgment the prior measurement made about ObjectHooksPanel's SchemaForm fallback, and exactly the judgment the ruling enforced by naming two non-defects. ⇒ The class is "says something untrue," not "looks bad." Widening it to the second would have been scope creep wearing the costume of thoroughness.

⭐ The ablation anticipated a vacuous red — which is rarer than anticipating a vacuous green

Including i18n.ts in the revert on purpose, because reverting only the .tsx files would leave the call site requesting a key the pack no longer defines, making each "must not contain the old text" assertion pass for the wrong reason. That is a red that would have looked like proof and been worth nothing.

And the prediction was mixed and specific — 6 red, 1 green, with the green named in advance (contrast test 2 pins the must-not-change half, so it is expected to survive) — and it landed exactly. A mixed prediction that comes true is much stronger evidence than an all-red one.

Restore proven by state including git diff --cached, which catches the failure mode a path-scoped git checkout creates by staging what it writes. Correct, and not a step most runs take.

⭐ You falsified the ruling's own account of the defect

The ruling flagged "design support is in progress" as the false half of the retired string. You measured that the other half is false too: that branch renders no preview at all, so "shows a read-only preview" is untrue even with a populated registry. Both halves are gone. ⭐ A ruling is binding on what to do, not on the accuracy of its incidental description — reporting that distinction rather than quietly fixing more than described is the right handling.

Assumptions falsified

  • "Line numbers are stale — locate them." Partly wrong, in the harmless direction: two of three were exactly where the card said. Reported as measured, and each located before it was edited — which is the part that matters, since the same reading would have caught it had they moved.
  • "Pins may be infeasible without part A's observability." Falsified, with the cleanest possible reason: each pin renders against a registry it has just proven empty — a static precondition, not an event. Only a recovery pin would need change notification, and that behaviour does not exist yet, so there is nothing to pin. That distinction is the whole answer, and it is now on record for whoever takes part A.

The adjacent edit is declared, and I accept it

metadata-admin/i18n.ts sits outside views/studio-design. It is mechanically forced — studio-design has no message catalogue of its own, so repairing any string requires keys there — it is neither sibling agent's held file, and it was declared rather than absorbed silently. check:i18n-keys, check:i18n-drift and the dead-key control all green; the retired key is gone from both packs.

check:eager-closure — the narrowing is stated and sound

Green at 39.5 KB headroom, ceiling not re-pinned. The base tree was not re-measured (a second full build), with the reason given: this diff can only add bytes, and the headroom-sensitivity gauge error is triggered by byte removal. That is a correct piece of reasoning about which direction the gate can fail in, and it is stated as a narrowing rather than passed off as a measurement.

#7121, and the self-correction

#7121 is filed at the right confidence level — measured with a populated registry, therefore a different cause from this card's, and deliberately not fixed because repairing it means gating the Design/Run toggle across every registerStudioCanvasPreview leaf. I will triage it; ⛔ it is not part A and it is not this card.

Also noted: the mcp_calls count was corrected upward against the copy posted on the card, with the reason (the read-back calls postdated it). Small, and exactly the right instinct.


⚠️#6795 stays OPEN after this lands. The PR is Part of, not Fixes — part A (registry observability, 118.7 KB gzip) remains on the maintainer's sequencing surface, behind the packages/app-shell PR backlog.

Re-checking CI; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C) - #7120

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states
Sep 1, 2026
Merged

fix(app-shell): say what is true when the studio-design designer registries are empty (#6795 part C)#7120
os-warren merged 1 commit into
mainfrom
claude/issue-6795-studio-design-consumer-states

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #6795 — part C only, exactly as ruled (维护者, 2026-08-30, 第 5 场总监席决裁批 #4, verbatim「同意」): repair the misleading/silent consumer states now, defer part A (registry observability → lazy conversion) to the maintainer's sequencing surface.

⛔ Part A is untouched. No lazy conversion, no subscribe/notify, no useSyncExternalStore, no change to MAX_EAGER_CLOSURE_GZIP_BYTES, the #6683sideEffects array, or preview-gallery.tsx. No registry module is in this diff.

The mechanism that constrains what these messages may say

The three registries are plain Maps filled by a module-scope side effect, and every studio-design consumer reads them during render with no subscription. The card measured that a consumer which reads an empty registry never recovers when registration lands later:

fallback before registration: true | still fallback after registration: true | late inspector rendered: false

So every message here states a fact and never promises recovery — no "loading…", no "try again", no spinner. That would swap one false statement for another. Making recovery real is part A.

Suspense cannot repair any of this: these are synchronous reads returning undefined, and Suspense catches a thrown promise. Triage fenced this explicitly; it is mechanical, not stylistic.

Sites — actual locations on this base (03380aa14)

The card's line numbers came from 26896c689 and were stale. Re-located:

sitecard saidactual location nowstatus
⭐ Data pillar field railStudioDesignSurface.tsx:3076:3076 (unchanged), read at :2564fixed
Interfaces canvas + railnot line-numberedcanvas :1646, rail :1786, reads at :1391/:1397/:1402fixed
ObjectActionsPanel:101:101 (unchanged), render at :266fixed
Automations pillarnot on the card at allreads at :3245/:3246, chip :3520, rail :3574fixed (fourth site)
ObjectSettingsPanel:75 "silently empty":75⛔ untouched — already correct
ObjectHooksPanel:88 "silently empty":88⛔ untouched — already correct

Fourth-site sweep — result, with its control

Swept every registry read in views/studio-design/. The set is closed at six read sites (three in StudioDesignSurface, one each in the three panels); nothing else in the directory reads a metadata registry during render.

Found a fourth site the card and the ruling both missed: the Automations pillar. With the registries unpopulated it printed two false instructions at once — the canvas header chip "Visual orchestration · click a node to configure" and the rail "Click a node on the canvas, and its configuration appears here." — while the canvas below was a raw JSON dump with no nodes to click. Same class as the Interfaces rail, so it is repaired here.

Control for the zero:getStudioCanvasPreview('object') — registered at module scope inside the file the read comes from, so it must hit. Measured function in every probe while listMetadataPreviewTypes() and listMetadataInspectorTypes() were both []. Without that control the zeros would be indistinguishable from a failed import.

Deliberately NOT repaired, and why:studio-canvas-preview.tsx reads its own registry but self-registers object in the same module, so it cannot be empty in this way — not in the class.

Measured before / after (registries asserted empty first, every time)

sitebeforeafter
Data pillar field railaside count 0 — clicking a field did literally nothing, while the designer above it kept saying "click a field to edit its properties"aside count 1; rail opens with "Field properties" and names the missing inspector
Interfaces canvas"page shows a read-only preview for now; design support is in progress.""No metadata designers are registered in this session, so page cannot be previewed or designed here."
Interfaces rail"Click a block on the canvas, and edit its properties right here." (no canvas)"No metadata designers are registered in this session, so there is nothing to edit here."
Automations chip + rail"click a node to configure" / "Click a node on the canvas…" (canvas was a JSON dump)both replaced with "No metadata designers are registered in this session, so this flow cannot be designed here."
ObjectActionsPanel"Send Email" alone — reads as "this action has no properties""Send Email" plus "No action editor is registered in this session, so this action's properties cannot be edited here."

The Interfaces canvas message is split into the two genuinely different causes, because Preview === undefined has two of them and the retired sentence asserted one cause in the other's words. listMetadataPreviewTypes() tells them apart — a read of the same already-imported registry module, inventing no state:

  • registry empty wholesale → engine.studio.if.designersMissing
  • registry populated, this type has no designer → engine.studio.if.noDesigner

The retired engine.studio.if.readonlyPreview was false twice over: this branch renders no preview at all, and page design support exists.

Pins

Three new files, 7 tests. Every empty-registry test asserts its own precondition (both list*Types() empty, the get* reads undefined) with the control above — a zero that is not asserted is not a reading.

  • StudioDesignSurface.designerRegistryMissing.test.tsx — sites 1, 2, 4. Never registers a designer.
  • StudioDesignSurface.designerRegistryPartial.test.tsx — the contrast: registry populated, this type has none. Separate file because these Maps are module state shared by a whole file, so splitting is what lets each file assert its own precondition instead of depending on test order.
  • ObjectActionsPanel.designerRegistryMissing.test.tsx — site 3.

Ablation

Predicted directions recorded before running. Mutation: revert the three repaired source files to HEAD~1, keeping the pins at HEAD. i18n.ts is included on purpose — reverting only the .tsx files would leave the reverted call site asking for a key the pack no longer defines, so the "must not contain the old text" half of each assertion would pass vacuously.

Proven on disk, not by an editor exit code — blob hashes and marker counts both:

fileHEAD blobmutated blobmarkers
StudioDesignSurface.tsxd5abcd2d77ca4ff3designersUnregistered 5→0, fieldInspectorMissing 1→0, auto.designersMissing 2→0
ObjectActionsPanel.tsx37fd92abf58da0feactions.editorMissing 1→0
i18n.tsb04271c7604b7487if.designersMissing 2→0; readonlyPreview 1→2 (old string restored)

Pre-flight asserted each target pristine first, and an empty hash was treated as FAILURE, not as "nothing to compare". Restore leg used git checkout HEAD -- (never a bare git checkout --, which reads from the polluted index), with absolute paths in an EXIT/INT/TERM trap.

Predicted vs observed — 6 red, 1 green, exactly as predicted:

testpredictedobserved
site 1, field railREDRED
site 2, canvasREDRED
site 2, railREDRED
site 4, automationsREDRED
site 3, actions panelREDRED
contrast: "no designer for this type"REDRED
contrast: "click a block" survives with a populated registryGREEN, predictedGREEN

That last green is reported because it is informative, not because it is a pass: it pins the must-not-change half, so it is expected to survive the ablation and tells me which layer it does not cover.

Restore proven by state:git diff HEAD = 0 lines, git diff --cached = 0 lines (the index too — path-scoped checkout stages what it writes), and all three blob hashes back to their HEAD values. No number in this PR was taken on a mutated tree.

Gates — all run on the final commit 9ad81e0f1

Exit codes captured before any pipe (cmd > out 2>&1; EXIT=$?), and each verdict quoted from the gate's own printed line.

gateexitverdict
vitest run packages/app-shell/src/views/studio-design/041 files / 218 tests passed — the 3 new pins plus every pre-existing studio-design suite
@object-ui/app-shell type-check0tsc --noEmit && tsc -p tsconfig.test.json
@object-ui/app-shell lint (whole package, unnarrowed)02852 problems (0 errors, 2852 warnings)
check:i18n-keys0"Every in-scope call-site key resolves against the en pack (2842 keys)…"
check:i18n-drift0"No en value changed in this range."
check:i18n-dead-keys0report-only; none of the six new keys appears (control workspace.createFailed hit twice)
check:control-bytes0"OK (scanned 5898 tracked text file(s))"
check:eager-closure0"Console eager closure is 3151.9 KB gzipped across 48 of 517 chunks (budget: 3191.4 KB, headroom: 39.5 KB)" — ceiling untouched, sensitivity green at 0.44x
check:side-effects-array0"names exactly the 14 module(s) that register at load time"
check:sdui-registration-pins0"All 16 registration(s) … present in the built console (517 chunks weighed)"
check:readme-exports0"OK (43 tracked README(s) … 0 unbuilt)"
check:entry-guard · check:designer-field-key-parity · check:self-import · check:esm-specifiers0green
lint:coverage · type-check:coverage0"46/46 packages linted, 0 with outstanding errors"; "41/41 packages compile their tests"
check-changeset-presence · -no-major · -fixed · -overwrite0"3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"

check:readme-exports and check:sdui-registration-pins are the two known to need a full build; both ran afterpnpm turbo run build --filter='./packages/*' (39/39) and apps/console build, so they are real greens, not NOT MEASURED.

Two measurements worth naming precisely:

  • Lint is not a claim, it is a comparison. The 20 warnings on StudioDesignSurface.tsx are pre-existing: linting the HEAD~1 version of the same file under the same config gives the same 20 warnings / 0 errors. My diff adds zero findings. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not itself contain.
  • The new test files really are typechecked.tsc -p tsconfig.test.json --listFiles lists all three (control: DataPillar.celGate.test.tsx also listed; 4506 files in the program). The package tsconfig.json excludes **/*.test.tsx, so a green tsc --noEmit alone would have said nothing about them.

Not re-measured: the eager closure on the base tree — that needs a second full build. What is measured is that the gate is green with MAX_EAGER_CLOSURE_GZIP_BYTES untouched by this diff, which is what part C owes. (This diff can only add bytes, so the headroom-sensitivity gauge error the dispatch warned about — a byte removal — is not reachable from here.)

One thing deliberately left

The Interfaces "Design" button is still offered when there is nothing designable. Repairing it means gating the Design/Run toggle on isEditable, which also reaches the StudioCanvas (object) leaves — where the registry is populated and the misleading state has a different cause. That is outside the registry-emptiness class this card is about, so it is filed rather than fixed; see the report comment on #6795.


Generated by Claude Code

…stries are empty (#6795 part C)
The three metadata designer registries are plain Maps filled by a module-scope
side effect, read during render with no subscription. A consumer that reads an
empty one gets undefined and NEVER recovers (measured: "still fallback after
registration: true | late inspector rendered: false"). Four studio-design
consumer states lied about that; one was silent.
- Data pillar field rail: the guard `fieldSel && (fieldSel.kind === 'group' ||
inspector)` dropped the entire aside when a FIELD was selected with no
inspector registered — measured aside count 0, i.e. clicking a field did
literally nothing while the designer above it said "click a field to edit its
properties". A selection now always opens its rail; the rail body answers
whether an editor exists.
- Interfaces canvas: "{type} shows a read-only preview for now; design support
is in progress" was false twice over — this branch renders no preview at all,
and page design support exists. Split via listMetadataPreviewTypes() into the
two genuinely different causes.
- Interfaces rail: stops telling the author to click a canvas that is absent.
- Automations pillar (a fourth site, found by sweeping past the three the
ruling named): the canvas chip and the rail both said "click a node" while
the canvas was a raw JSON dump.
- ObjectActionsPanel: rendered only the action's own label, reading as "this
action has no properties"; the label now carries the reason.
No message promises recovery — no "loading…", no "try again", no spinner. That
would replace one false statement with another; making recovery real is part A,
which the ruling deferred to the maintainer's sequencing surface.
ObjectSettingsPanel and ObjectHooksPanel are untouched: the measurement found
both already correct, contradicting the card body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.9 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-3ZHtTMdx.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.94KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

Landing not yet armed — 5 checks in_progress at 9ad81e0f1 (Type Check, all four test shards). 25 completed: 22 success, 3 skipped, 0 failure. in_progress is not a pass. Conflict pre-check runs against current main before I flip ready.

I verified the scope boundary myself, because "part A untouched" is the load-bearing claim

Part A is the published-contract half and is the maintainer's sequencing. If it leaked in, the PR would be unlandable under the exhausted Fable 5 quota — so this is checked, not accepted:

probe, diff vs merge-base 03380aa14expectedmeasured
files changed77
ObjectSettingsPanel.tsx / ObjectHooksPanel.tsx — the two ruled NON-defects00 / 0
preview-registry.ts / inspector-registry.ts / default-inspector-registry.ts00 / 0 / 0
metadata-admin/index.ts, preview-gallery.tsx00 / 0
MAX_EAGER_CLOSURE_GZIP_BYTES, sideEffects, useSyncExternalStore, subscribe anywhere in the diff00 / 0 / 0 / 0
⚠️StudioDesignSurface.tsxcontrol, must be present11
⚠️designersMissing in the diff — control, must hit>010
sibling agents' held files00 / 0

Both controls fire, so the zeros are absences rather than a broken query. Part A is intact and both ruled non-defects are byte-for-byte untouched.

⭐⭐ A fourth site, in the same class, that neither the card nor the ruling names

The Automations (flow) pillar printed two false instructions simultaneously — the chip "click a node to configure" and the rail "Click a node on the canvas…" — beside a raw JSON dump with no nodes to click.

⭐ This is the second time on this card that sweeping past the enumerated list found the worst instance: site 1 (:3076) was itself absent from the original filing, discovered only because the prior measurement swept beyond the card's four consumers. A card's site list is a starting population, never a closed one — and this run closed it properly, at six read sites across views/studio-design, with a control (getStudioCanvasPreview('object'), which self-registers at module scope inside the file the read comes from, so it must resolve if the module graph loaded at all).

⭐ The fifth candidate you declined is as important as the fourth you fixed

The Automations canvas fallback — a raw JSON dump — is degraded but states nothing false, so you left it. That is exactly the judgment the prior measurement made about ObjectHooksPanel's SchemaForm fallback, and exactly the judgment the ruling enforced by naming two non-defects. ⇒ The class is "says something untrue," not "looks bad." Widening it to the second would have been scope creep wearing the costume of thoroughness.

⭐ The ablation anticipated a vacuous red — which is rarer than anticipating a vacuous green

Including i18n.ts in the revert on purpose, because reverting only the .tsx files would leave the call site requesting a key the pack no longer defines, making each "must not contain the old text" assertion pass for the wrong reason. That is a red that would have looked like proof and been worth nothing.

And the prediction was mixed and specific — 6 red, 1 green, with the green named in advance (contrast test 2 pins the must-not-change half, so it is expected to survive) — and it landed exactly. A mixed prediction that comes true is much stronger evidence than an all-red one.

Restore proven by state including git diff --cached, which catches the failure mode a path-scoped git checkout creates by staging what it writes. Correct, and not a step most runs take.

⭐ You falsified the ruling's own account of the defect

The ruling flagged "design support is in progress" as the false half of the retired string. You measured that the other half is false too: that branch renders no preview at all, so "shows a read-only preview" is untrue even with a populated registry. Both halves are gone. ⭐ A ruling is binding on what to do, not on the accuracy of its incidental description — reporting that distinction rather than quietly fixing more than described is the right handling.

Assumptions falsified

  • "Line numbers are stale — locate them." Partly wrong, in the harmless direction: two of three were exactly where the card said. Reported as measured, and each located before it was edited — which is the part that matters, since the same reading would have caught it had they moved.
  • "Pins may be infeasible without part A's observability." Falsified, with the cleanest possible reason: each pin renders against a registry it has just proven empty — a static precondition, not an event. Only a recovery pin would need change notification, and that behaviour does not exist yet, so there is nothing to pin. That distinction is the whole answer, and it is now on record for whoever takes part A.

The adjacent edit is declared, and I accept it

metadata-admin/i18n.ts sits outside views/studio-design. It is mechanically forced — studio-design has no message catalogue of its own, so repairing any string requires keys there — it is neither sibling agent's held file, and it was declared rather than absorbed silently. check:i18n-keys, check:i18n-drift and the dead-key control all green; the retired key is gone from both packs.

check:eager-closure — the narrowing is stated and sound

Green at 39.5 KB headroom, ceiling not re-pinned. The base tree was not re-measured (a second full build), with the reason given: this diff can only add bytes, and the headroom-sensitivity gauge error is triggered by byte removal. That is a correct piece of reasoning about which direction the gate can fail in, and it is stated as a narrowing rather than passed off as a measurement.

#7121, and the self-correction

#7121 is filed at the right confidence level — measured with a populated registry, therefore a different cause from this card's, and deliberately not fixed because repairing it means gating the Design/Run toggle across every registerStudioCanvasPreview leaf. I will triage it; ⛔ it is not part A and it is not this card.

Also noted: the mcp_calls count was corrected upward against the copy posted on the card, with the reason (the read-back calls postdated it). Small, and exactly the right instinct.


⚠️#6795 stays OPEN after this lands. The PR is Part of, not Fixes — part A (registry observability, 118.7 KB gzip) remains on the maintainer's sequencing surface, behind the packages/app-shell PR backlog.

Re-checking CI; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude