test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass - #7133

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set
Sep 1, 2026
Merged

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass#7133
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7117

exclusion-reason-truthfulness.test.ts asks the runtime ComponentRegistry whether a PALETTE_EXCLUSIONS reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package — which has registered page blocks since #6757 and #7091 — nor @object-ui/plugin-detail.

All measurements below were taken on this branch; the union of gates was run at 778fb78b4 (the final commit).

Leg 0 — the blindness, reproduced before any fix

Predicted GREEN (the guard is blind), observed GREEN. On 44ea62d29, PALETTE_EXCLUSIONS['app:launcher'] set to 'no renderer ZZMUTZZ' — a string that DOES match the guard's CLAIMS_NO_RENDERER regex — while views/app-launcher-renderer.tsx registers a renderer for it:

ANCHOR_BEFORE=1 INJECTED_BEFORE=0 blob 186b4a2de89fcc150474b5861ee1e2d56bdc50a3
ANCHOR_AFTER=0 INJECTED_AFTER=1 blob 1734fcd9888fd864b379f4ab0b9e6b2d71fe95bd
=== MUTATION CONFIRMED ON DISK ===
Test Files 1 passed (1)
Tests 4 passed (4)
VITEST_EXIT=0

Restore proven by state, not by an exit code: blob_restored=186b4a2d… (back to the HEAD blob), git diff HEAD 0 bytes, git diff --cached 0 bytes, injected-marker count back to 0.

The at-risk population, re-derived

Measured with a throwaway probe that snapshots ComponentRegistry.getConfig(key) after each import step, rather than argued from source. All eight PALETTE_EXCLUSIONS keys:

ledger keyrenderer?registered byvisible to the OLD import set
app:launcheryes, real (ns=app)app-shell views/app-launcher-renderer.tsx, eager at module loadno — blind
global:notificationsyes, real (ns=global)app-shell views/global-notifications-renderer.tsx, eagerno — blind
record:chatteryes, real (ns=record)@object-ui/plugin-detail, eagerno — blind
element:record_pickeryes (ns=element)@object-ui/componentsyes
element:text_inputyes (ns=element)@object-ui/componentsyes
user:profileno real renderer; placeholder scaffold only under opt-in registerPlaceholders() (ns=protocol-placeholder)n/a
ai:chat_windownone anywheren/a
element:formnone anywheren/a

Three corrections to what the card assumed, all measured:

  1. It is three keys, not "five shell singletons".PALETTE_EXCLUSIONS carries exactly three shell-singleton entries (app:launcher, global:notifications, user:profile); nav:menu and global:search are not excluded at all, so they never enter this guard's loop. The blind set is app:launcher, global:notifications and — outside app-shell — record:chatter.
  2. app:launcher does not need an opt-in probe. It is opt-in-only in @object-ui/components, but since feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) #7091 app-shell registers the real renderer eagerly at module load. The probe finds it with no registerPlaceholders() call: app:launcher REG ns=app label=App Launcher.
  3. No live false claim exists. The only reasons matching CLAIMS_NO_RENDERER today are ai:chat_window and element:form, and a repo-wide grep for a registration of either (control: namespace: 'element', which returns many hits) returns zero. The defect was latent, as filed.

registerPlaceholders() is deliberately NOT called: it would make the file answer "has a renderer" for types that have only the dashed "Component Placeholder" scaffold (user:profile is one, measured). This repo's own language says the scaffold is not a renderer — views/app-launcher-renderer.tsx describes the state before it existed as "nothing rendered it, so a page that authored it drew a dashed box", with placeholders registered the whole time.

Mechanism chosen, and the cost that chose it

Marginal import cost, same harness and same baseline (after @object-ui/components + plugin-chatbot + plugin-form are loaded):

mechanismmarginal costfile duration end to end
the four app-shell renderer leaves553ms (251 + 164 + 128 + 10)9.4s
the package barrel ../../../../index.js6105ms15.1s

The barrel would track the package automatically, but it drags the console, marketplace, cloud and diagnostics graphs into a pure-logic gate in the cheap unit project — whose entire design point (vitest.config.mts) is not paying for graphs it does not touch. So: leaves, plus a derived guard that buys the barrel's one real advantage for 0ms.

the import set covers every page block app-shell registers reads the views/*-renderer.tsx leaf list from the directory, derives each leaf's registered key from its own ComponentRegistry.register(...) call, and requires every one to resolve. A seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted — deriving it is the point. It carries three controls of its own, because a derivation that finds nothing would pass vacuously and that is this file's whole subject: the leaf list must be non-empty, every leaf's key must be extractable, and the derived list must still contain app:launcher and global:notifications.

The barrel remains viable and cycle-free — it loaded and registered correctly in the probe — so this is a cost decision, not a feasibility one.

Positive probes — one per import, each shown hitting

Required by the finding's own text: "it needs one positive probe per new import or it guards vacuously."

importproberesult
@object-ui/components (pre-existing)element:texthits
@object-ui/plugin-chatbot (pre-existing)chatbothits
@object-ui/plugin-form (pre-existing)object-formhits
@object-ui/plugin-detail (new)record:chatterhits
views/app-launcher-renderer.js (new)app:launcher, derivedhits
views/global-notifications-renderer.js (new)global:notifications, derivedhits
views/global-search-renderer.js (new)global:search, derivedhits
views/nav-menu-renderer.js (new)nav:menu, derivedhits
views/record-approvals-renderer.js (new)record:approvals, derivedhits
views/record-attachments-renderer.js (new)record:attachments, derivedhits

All ten hit — the suite is green at 5/5, and every derived probe is an assertion inside the coverage test, so a miss is a red rather than a silent skip. Ablation A3 below proves that is true and not merely stated.

Both pre-existing anti-vacuity guards are preserved verbatim and neither is weakened: the registry under test is actually populated (guards a vacuous green) keeps its three probes and gains a fourth, and the ledger still contains a no-renderer claim to check (guards a vacuous loop) is untouched.

Post-fix ablations — all three predicted RED, all three observed RED

Run against the committed fix, so each restore is git checkout HEAD -- (absolute path) against a HEAD that already contains the implementation. Every leg proved its mutation on disk (anchor count moved, marker count moved, blob hash moved) and its restore by state (blob back to the HEAD blob, git diff HEAD and git diff --cached both 0 bytes).

A1 — the card's own false claim, the one that passed 4/4 in leg 0:

AssertionError: PALETTE_EXCLUSIONS['app:launcher'] says "no renderer ZZMUTZZ", but a renderer IS
registered for it. ... : expected [Function AppLauncherRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A2 — the same false claim on record:chatter, proving the plugin-detail import is not decorative:

AssertionError: PALETTE_EXCLUSIONS['record:chatter'] says "no renderer ZZMUTBB", but a renderer IS
registered for it. ... : expected [Function RecordChatterRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A3 — one renderer-leaf import deleted, proving the derived coverage guard bites:

AssertionError: views/record-attachments-renderer.tsx registers 'record:attachments' and this file
does not import it, so 'record:attachments' reads as UNREGISTERED here. Add
`import '../../../record-attachments-renderer.js';` to the side-effect imports above. ...
: expected undefined to be truthy
Tests 1 failed | 4 passed (5)

Each leg reports Tests 1 failed | 4 passed (5), so the suite really executed rather than collapsing to "no tests" behind a plausible exit 1.

Scope note — one bounded in-place addition, named

@object-ui/plugin-detail is outside the card's title ("excludes app-shell"), and it is included deliberately: record:chatter is a current ledger key, blind in the same loop, in the same file, for the same reason, and measured the same way (A2). The correct shape was already pinned by the two sibling suites in this same directory — palette-discussion-alias.test.tsx:50 and canvas-display-meta.test.tsx:44 both carry import '@object-ui/plugin-detail'; for exactly this question. Closing the app-shell half while leaving a known, measured blind key in the loop this PR certifies would reproduce the defect the card is about.

Gates

gateexitverdictevidence
vitest run on the guard file0greenTest Files 1 passed (1) / Tests 5 passed (5)
vitest run --project unit (whole project)0greenTest Files 801 passed (801) / Tests 12489 passed | 9 skipped (12498)
vitest run packages/app-shell/src/views/metadata-admin/previews/0greenTest Files 45 passed (45) / Tests 521 passed (521)
turbo run type-check --filter=@object-ui/app-shell0greenTasks: 30 successful, 30 total; the package script is tsc --noEmit && tsc -p tsconfig.test.json
eslint on the changed file0greenno output
check-changeset-presence0green"declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate"
check-changeset-no-major0green"No changeset declares a major bump."
check-control-bytes0green"OK (scanned 5899 tracked text file(s); skipped 85 binary)"

The whole-unit-project run is not incidental: the unit project runs with isolate: false, so the added side-effect registrations are visible to every other unit file sharing a worker. 801/801 green is the measurement that they leak into nothing.

Two notes on reading the evidence honestly:

  • The typecheck is a real measurement, not an assumed one. A first attempt reported Cannot find module '@object-ui/*' across files this PR never touches — an unbuilt workspace closure, which is NOT MEASURED rather than red. Routing through turbo builds the closure first, and --listFiles confirmed the edited file is actually in the tsconfig.test.json program (6 hits) rather than excluded like it is from tsconfig.json.
  • Every exit code above was captured by redirect-then-read, never through a pipe.

Not changed

No production code. The ledger text, the palette decisions and CLAIMS_NO_RENDERER are all untouched — this PR changes only what the guard can see. The changeset declares an empty frontmatter: nothing is released.


Generated by Claude Code

…rt set so a false "no renderer" cannot pass
`exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry`
whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its
coverage is bounded by its own side-effect import set. That set never grew to
include this package, which has registered page blocks since #6757 and #7091,
nor `@object-ui/plugin-detail`, which registers `record:chatter`.
Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to
'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER
regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a
renderer for it.
Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and
add a derived guard: the leaf list is read from the directory and every
registered key must resolve, so a seventh renderer leaf that is not imported
here reds this file instead of silently shrinking its coverage. A
hand-maintained list is what drifted; deriving it is the point.
The package barrel would track registrations automatically too, but costs
6105ms to load against 553ms for the leaves (same harness, same baseline) —
this is a pure-logic gate in the cheap `unit` project.
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)3152.3 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-Cten_jDQ.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)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.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 armed — 5 checks in_progress (4 test shards, Type Check). 25 completed, 0 failure. I arm on green.

⭐ I did your lookup, and it flips your open question

You asked the seat to check user:profile against objectstack#12183's phase list rather than guess, and recommended C for exactly that reason. Done, and the answer is not A:

objectstack#12183 is scoped, in its own title and body, to four members — nav:menu, global:search, global:notifications, app:launcher — and its sub_issues_summary reads 3 of 3 completed, 100%. user:profile appears nowhere in it. ⇒ It is not a later phase of that decomposition; it was never in scope, and the asymmetry is unplanned.

⭐ Filed as #7135, carrying your measurement and your framing, plus the constraint you would not have had: #12183's own Ask allows either a renderer or making it explicit in the spec and validate that these are not author-placeable — so a claimant must establish which answer is wanted before building either, possibly cross-repo.

This is the right hand-off shape. You measured what you could reach, named the one fact that decided between two actions, and refused to file blind on the other side of it. That is better than either guessing or dropping it.

⭐⭐ My at-risk population was wrong twice over, and you re-derived rather than accepted it

My order said "the five shell singletons". Measured: PALETTE_EXCLUSIONS holds 8 keys, of which exactly 3 are shell singletons. nav:menu and global:search are not excluded at all and never enter the guard's loop — so two of my five were never at risk.

And the blind set is 3, one of which is record:chatter — neither a shell singleton nor in app-shell at all, but in @object-ui/plugin-detail. ⇒ A fix scoped to "the shell singletons in app-shell", which is what my order described, would have left a third of the blind set uncovered while looking complete.

⭐ You caught that my other warning would have caused a false positive in the opposite direction

I warned that app:launcher registers only via opt-in PROTOCOL_COMPONENTS, so a probe that does not opt in would report it unregistered. Half true — of @object-ui/components. But since #7091, app-shell registers the real renderer eagerly, so the probe finds it with no opt-in.

⚠️ And opting in, as I implied you might need to, would have been actively wrong: registerPlaceholders() registers the dashed scaffold for ~120 protocol types, which would make the guard answer "has a renderer" for user:profile — whose only registration is that scaffold. My warning pointed at a real hazard and prescribed the move that creates a different one. Declining it, with the measurement, was correct.

The mechanism choice is a measurement, and the honest part is what it does not claim

Leaves 553ms vs barrel 6105ms — 11×, same baseline. And you say plainly that the barrel is viable and cycle-free, so this is a cost decision, not a feasibility one. ⭐ That distinction matters: "we couldn't" and "we chose not to" age very differently for the next reader.

The derived coverage guard is better than what I asked for. My fence was "one positive probe per added import, or it guards vacuously." You met that — seven probes, all hitting — and then closed the failure mode one level up: the guard reads the leaf list from the directory and derives each key from the leaf's own register() call, so a seventh renderer leaf reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted here, so restating one would have rebuilt the defect in the fix. Ablation A3 proves it: delete one leaf import and the guard names the file, the key, and the import line to add.

Rejecting the static manifest is right for the same reason, and well argued: a manifest is a second declaration of what the live registry already answers, and it would drift from the registrations the way the import set drifted. The guard's value is asking the same question the reason string answers, of the live registry.

The ablations, and one detail worth naming

Leg 0 reproduced the blindness before anything changed (predicted GREEN, observed GREEN — 'no renderer ZZMUTZZ' passing 4/4 while a renderer is registered). Post-fix, all three legs RED with specific named assertions, and each reporting Tests 1 failed | 4 passed (5) so the suite demonstrably executed rather than collapsing to "no tests" behind a plausible exit 1.

⚠️ That last check is not decoration — a sibling agent hit exactly that trap today (a JSX comment injected in attribute position, syntax error, Tests no tests, exit 1 that looked like the predicted red). Asserting the passing count alongside the failing one is what distinguishes them.

Both pre-existing anti-vacuity guards preserved verbatim, plus three new controls on the derived guard so it cannot pass empty. isolate:false leak risk addressed by running the whole unit project (801 files, 12489 tests green) rather than just the changed file — the right scope for a change that writes into a shared singleton.

#7134 — filed at the right level, and it is the same class again

The unit project's isolate:false is justified by a comment claiming "no ComponentRegistry or DOM state to leak across files" — and this guard now writes ten registrations into that shared singleton, while a packages/fields test asserts ComponentRegistry.get(...) is toBeUndefined(), an absence assertion a shared registry can silently satisfy.

⭐ That is the sixth instance this session of a comment certifying a property the code no longer has — and the most dangerous shape of it, because the thing it protects is a test's ability to fail. Correctly labelled latent, not live (801/801 green, measured), so it is prevention.


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.

finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} 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

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass - #7133

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set
Sep 1, 2026
Merged

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass#7133
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7117

exclusion-reason-truthfulness.test.ts asks the runtime ComponentRegistry whether a PALETTE_EXCLUSIONS reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package — which has registered page blocks since #6757 and #7091 — nor @object-ui/plugin-detail.

All measurements below were taken on this branch; the union of gates was run at 778fb78b4 (the final commit).

Leg 0 — the blindness, reproduced before any fix

Predicted GREEN (the guard is blind), observed GREEN. On 44ea62d29, PALETTE_EXCLUSIONS['app:launcher'] set to 'no renderer ZZMUTZZ' — a string that DOES match the guard's CLAIMS_NO_RENDERER regex — while views/app-launcher-renderer.tsx registers a renderer for it:

ANCHOR_BEFORE=1 INJECTED_BEFORE=0 blob 186b4a2de89fcc150474b5861ee1e2d56bdc50a3
ANCHOR_AFTER=0 INJECTED_AFTER=1 blob 1734fcd9888fd864b379f4ab0b9e6b2d71fe95bd
=== MUTATION CONFIRMED ON DISK ===
Test Files 1 passed (1)
Tests 4 passed (4)
VITEST_EXIT=0

Restore proven by state, not by an exit code: blob_restored=186b4a2d… (back to the HEAD blob), git diff HEAD 0 bytes, git diff --cached 0 bytes, injected-marker count back to 0.

The at-risk population, re-derived

Measured with a throwaway probe that snapshots ComponentRegistry.getConfig(key) after each import step, rather than argued from source. All eight PALETTE_EXCLUSIONS keys:

ledger keyrenderer?registered byvisible to the OLD import set
app:launcheryes, real (ns=app)app-shell views/app-launcher-renderer.tsx, eager at module loadno — blind
global:notificationsyes, real (ns=global)app-shell views/global-notifications-renderer.tsx, eagerno — blind
record:chatteryes, real (ns=record)@object-ui/plugin-detail, eagerno — blind
element:record_pickeryes (ns=element)@object-ui/componentsyes
element:text_inputyes (ns=element)@object-ui/componentsyes
user:profileno real renderer; placeholder scaffold only under opt-in registerPlaceholders() (ns=protocol-placeholder)n/a
ai:chat_windownone anywheren/a
element:formnone anywheren/a

Three corrections to what the card assumed, all measured:

  1. It is three keys, not "five shell singletons".PALETTE_EXCLUSIONS carries exactly three shell-singleton entries (app:launcher, global:notifications, user:profile); nav:menu and global:search are not excluded at all, so they never enter this guard's loop. The blind set is app:launcher, global:notifications and — outside app-shell — record:chatter.
  2. app:launcher does not need an opt-in probe. It is opt-in-only in @object-ui/components, but since feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) #7091 app-shell registers the real renderer eagerly at module load. The probe finds it with no registerPlaceholders() call: app:launcher REG ns=app label=App Launcher.
  3. No live false claim exists. The only reasons matching CLAIMS_NO_RENDERER today are ai:chat_window and element:form, and a repo-wide grep for a registration of either (control: namespace: 'element', which returns many hits) returns zero. The defect was latent, as filed.

registerPlaceholders() is deliberately NOT called: it would make the file answer "has a renderer" for types that have only the dashed "Component Placeholder" scaffold (user:profile is one, measured). This repo's own language says the scaffold is not a renderer — views/app-launcher-renderer.tsx describes the state before it existed as "nothing rendered it, so a page that authored it drew a dashed box", with placeholders registered the whole time.

Mechanism chosen, and the cost that chose it

Marginal import cost, same harness and same baseline (after @object-ui/components + plugin-chatbot + plugin-form are loaded):

mechanismmarginal costfile duration end to end
the four app-shell renderer leaves553ms (251 + 164 + 128 + 10)9.4s
the package barrel ../../../../index.js6105ms15.1s

The barrel would track the package automatically, but it drags the console, marketplace, cloud and diagnostics graphs into a pure-logic gate in the cheap unit project — whose entire design point (vitest.config.mts) is not paying for graphs it does not touch. So: leaves, plus a derived guard that buys the barrel's one real advantage for 0ms.

the import set covers every page block app-shell registers reads the views/*-renderer.tsx leaf list from the directory, derives each leaf's registered key from its own ComponentRegistry.register(...) call, and requires every one to resolve. A seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted — deriving it is the point. It carries three controls of its own, because a derivation that finds nothing would pass vacuously and that is this file's whole subject: the leaf list must be non-empty, every leaf's key must be extractable, and the derived list must still contain app:launcher and global:notifications.

The barrel remains viable and cycle-free — it loaded and registered correctly in the probe — so this is a cost decision, not a feasibility one.

Positive probes — one per import, each shown hitting

Required by the finding's own text: "it needs one positive probe per new import or it guards vacuously."

importproberesult
@object-ui/components (pre-existing)element:texthits
@object-ui/plugin-chatbot (pre-existing)chatbothits
@object-ui/plugin-form (pre-existing)object-formhits
@object-ui/plugin-detail (new)record:chatterhits
views/app-launcher-renderer.js (new)app:launcher, derivedhits
views/global-notifications-renderer.js (new)global:notifications, derivedhits
views/global-search-renderer.js (new)global:search, derivedhits
views/nav-menu-renderer.js (new)nav:menu, derivedhits
views/record-approvals-renderer.js (new)record:approvals, derivedhits
views/record-attachments-renderer.js (new)record:attachments, derivedhits

All ten hit — the suite is green at 5/5, and every derived probe is an assertion inside the coverage test, so a miss is a red rather than a silent skip. Ablation A3 below proves that is true and not merely stated.

Both pre-existing anti-vacuity guards are preserved verbatim and neither is weakened: the registry under test is actually populated (guards a vacuous green) keeps its three probes and gains a fourth, and the ledger still contains a no-renderer claim to check (guards a vacuous loop) is untouched.

Post-fix ablations — all three predicted RED, all three observed RED

Run against the committed fix, so each restore is git checkout HEAD -- (absolute path) against a HEAD that already contains the implementation. Every leg proved its mutation on disk (anchor count moved, marker count moved, blob hash moved) and its restore by state (blob back to the HEAD blob, git diff HEAD and git diff --cached both 0 bytes).

A1 — the card's own false claim, the one that passed 4/4 in leg 0:

AssertionError: PALETTE_EXCLUSIONS['app:launcher'] says "no renderer ZZMUTZZ", but a renderer IS
registered for it. ... : expected [Function AppLauncherRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A2 — the same false claim on record:chatter, proving the plugin-detail import is not decorative:

AssertionError: PALETTE_EXCLUSIONS['record:chatter'] says "no renderer ZZMUTBB", but a renderer IS
registered for it. ... : expected [Function RecordChatterRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A3 — one renderer-leaf import deleted, proving the derived coverage guard bites:

AssertionError: views/record-attachments-renderer.tsx registers 'record:attachments' and this file
does not import it, so 'record:attachments' reads as UNREGISTERED here. Add
`import '../../../record-attachments-renderer.js';` to the side-effect imports above. ...
: expected undefined to be truthy
Tests 1 failed | 4 passed (5)

Each leg reports Tests 1 failed | 4 passed (5), so the suite really executed rather than collapsing to "no tests" behind a plausible exit 1.

Scope note — one bounded in-place addition, named

@object-ui/plugin-detail is outside the card's title ("excludes app-shell"), and it is included deliberately: record:chatter is a current ledger key, blind in the same loop, in the same file, for the same reason, and measured the same way (A2). The correct shape was already pinned by the two sibling suites in this same directory — palette-discussion-alias.test.tsx:50 and canvas-display-meta.test.tsx:44 both carry import '@object-ui/plugin-detail'; for exactly this question. Closing the app-shell half while leaving a known, measured blind key in the loop this PR certifies would reproduce the defect the card is about.

Gates

gateexitverdictevidence
vitest run on the guard file0greenTest Files 1 passed (1) / Tests 5 passed (5)
vitest run --project unit (whole project)0greenTest Files 801 passed (801) / Tests 12489 passed | 9 skipped (12498)
vitest run packages/app-shell/src/views/metadata-admin/previews/0greenTest Files 45 passed (45) / Tests 521 passed (521)
turbo run type-check --filter=@object-ui/app-shell0greenTasks: 30 successful, 30 total; the package script is tsc --noEmit && tsc -p tsconfig.test.json
eslint on the changed file0greenno output
check-changeset-presence0green"declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate"
check-changeset-no-major0green"No changeset declares a major bump."
check-control-bytes0green"OK (scanned 5899 tracked text file(s); skipped 85 binary)"

The whole-unit-project run is not incidental: the unit project runs with isolate: false, so the added side-effect registrations are visible to every other unit file sharing a worker. 801/801 green is the measurement that they leak into nothing.

Two notes on reading the evidence honestly:

  • The typecheck is a real measurement, not an assumed one. A first attempt reported Cannot find module '@object-ui/*' across files this PR never touches — an unbuilt workspace closure, which is NOT MEASURED rather than red. Routing through turbo builds the closure first, and --listFiles confirmed the edited file is actually in the tsconfig.test.json program (6 hits) rather than excluded like it is from tsconfig.json.
  • Every exit code above was captured by redirect-then-read, never through a pipe.

Not changed

No production code. The ledger text, the palette decisions and CLAIMS_NO_RENDERER are all untouched — this PR changes only what the guard can see. The changeset declares an empty frontmatter: nothing is released.


Generated by Claude Code

…rt set so a false "no renderer" cannot pass
`exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry`
whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its
coverage is bounded by its own side-effect import set. That set never grew to
include this package, which has registered page blocks since #6757 and #7091,
nor `@object-ui/plugin-detail`, which registers `record:chatter`.
Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to
'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER
regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a
renderer for it.
Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and
add a derived guard: the leaf list is read from the directory and every
registered key must resolve, so a seventh renderer leaf that is not imported
here reds this file instead of silently shrinking its coverage. A
hand-maintained list is what drifted; deriving it is the point.
The package barrel would track registrations automatically too, but costs
6105ms to load against 553ms for the leaves (same harness, same baseline) —
this is a pure-logic gate in the cheap `unit` project.
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)3152.3 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-Cten_jDQ.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)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.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 armed — 5 checks in_progress (4 test shards, Type Check). 25 completed, 0 failure. I arm on green.

⭐ I did your lookup, and it flips your open question

You asked the seat to check user:profile against objectstack#12183's phase list rather than guess, and recommended C for exactly that reason. Done, and the answer is not A:

objectstack#12183 is scoped, in its own title and body, to four members — nav:menu, global:search, global:notifications, app:launcher — and its sub_issues_summary reads 3 of 3 completed, 100%. user:profile appears nowhere in it. ⇒ It is not a later phase of that decomposition; it was never in scope, and the asymmetry is unplanned.

⭐ Filed as #7135, carrying your measurement and your framing, plus the constraint you would not have had: #12183's own Ask allows either a renderer or making it explicit in the spec and validate that these are not author-placeable — so a claimant must establish which answer is wanted before building either, possibly cross-repo.

This is the right hand-off shape. You measured what you could reach, named the one fact that decided between two actions, and refused to file blind on the other side of it. That is better than either guessing or dropping it.

⭐⭐ My at-risk population was wrong twice over, and you re-derived rather than accepted it

My order said "the five shell singletons". Measured: PALETTE_EXCLUSIONS holds 8 keys, of which exactly 3 are shell singletons. nav:menu and global:search are not excluded at all and never enter the guard's loop — so two of my five were never at risk.

And the blind set is 3, one of which is record:chatter — neither a shell singleton nor in app-shell at all, but in @object-ui/plugin-detail. ⇒ A fix scoped to "the shell singletons in app-shell", which is what my order described, would have left a third of the blind set uncovered while looking complete.

⭐ You caught that my other warning would have caused a false positive in the opposite direction

I warned that app:launcher registers only via opt-in PROTOCOL_COMPONENTS, so a probe that does not opt in would report it unregistered. Half true — of @object-ui/components. But since #7091, app-shell registers the real renderer eagerly, so the probe finds it with no opt-in.

⚠️ And opting in, as I implied you might need to, would have been actively wrong: registerPlaceholders() registers the dashed scaffold for ~120 protocol types, which would make the guard answer "has a renderer" for user:profile — whose only registration is that scaffold. My warning pointed at a real hazard and prescribed the move that creates a different one. Declining it, with the measurement, was correct.

The mechanism choice is a measurement, and the honest part is what it does not claim

Leaves 553ms vs barrel 6105ms — 11×, same baseline. And you say plainly that the barrel is viable and cycle-free, so this is a cost decision, not a feasibility one. ⭐ That distinction matters: "we couldn't" and "we chose not to" age very differently for the next reader.

The derived coverage guard is better than what I asked for. My fence was "one positive probe per added import, or it guards vacuously." You met that — seven probes, all hitting — and then closed the failure mode one level up: the guard reads the leaf list from the directory and derives each key from the leaf's own register() call, so a seventh renderer leaf reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted here, so restating one would have rebuilt the defect in the fix. Ablation A3 proves it: delete one leaf import and the guard names the file, the key, and the import line to add.

Rejecting the static manifest is right for the same reason, and well argued: a manifest is a second declaration of what the live registry already answers, and it would drift from the registrations the way the import set drifted. The guard's value is asking the same question the reason string answers, of the live registry.

The ablations, and one detail worth naming

Leg 0 reproduced the blindness before anything changed (predicted GREEN, observed GREEN — 'no renderer ZZMUTZZ' passing 4/4 while a renderer is registered). Post-fix, all three legs RED with specific named assertions, and each reporting Tests 1 failed | 4 passed (5) so the suite demonstrably executed rather than collapsing to "no tests" behind a plausible exit 1.

⚠️ That last check is not decoration — a sibling agent hit exactly that trap today (a JSX comment injected in attribute position, syntax error, Tests no tests, exit 1 that looked like the predicted red). Asserting the passing count alongside the failing one is what distinguishes them.

Both pre-existing anti-vacuity guards preserved verbatim, plus three new controls on the derived guard so it cannot pass empty. isolate:false leak risk addressed by running the whole unit project (801 files, 12489 tests green) rather than just the changed file — the right scope for a change that writes into a shared singleton.

#7134 — filed at the right level, and it is the same class again

The unit project's isolate:false is justified by a comment claiming "no ComponentRegistry or DOM state to leak across files" — and this guard now writes ten registrations into that shared singleton, while a packages/fields test asserts ComponentRegistry.get(...) is toBeUndefined(), an absence assertion a shared registry can silently satisfy.

⭐ That is the sixth instance this session of a comment certifying a property the code no longer has — and the most dangerous shape of it, because the thing it protects is a test's ability to fail. Correctly labelled latent, not live (801/801 green, measured), so it is prevention.


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.

finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green

2 participants

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

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass - #7133

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set
Sep 1, 2026
Merged

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass#7133
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7117

exclusion-reason-truthfulness.test.ts asks the runtime ComponentRegistry whether a PALETTE_EXCLUSIONS reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package — which has registered page blocks since #6757 and #7091 — nor @object-ui/plugin-detail.

All measurements below were taken on this branch; the union of gates was run at 778fb78b4 (the final commit).

Leg 0 — the blindness, reproduced before any fix

Predicted GREEN (the guard is blind), observed GREEN. On 44ea62d29, PALETTE_EXCLUSIONS['app:launcher'] set to 'no renderer ZZMUTZZ' — a string that DOES match the guard's CLAIMS_NO_RENDERER regex — while views/app-launcher-renderer.tsx registers a renderer for it:

ANCHOR_BEFORE=1 INJECTED_BEFORE=0 blob 186b4a2de89fcc150474b5861ee1e2d56bdc50a3
ANCHOR_AFTER=0 INJECTED_AFTER=1 blob 1734fcd9888fd864b379f4ab0b9e6b2d71fe95bd
=== MUTATION CONFIRMED ON DISK ===
Test Files 1 passed (1)
Tests 4 passed (4)
VITEST_EXIT=0

Restore proven by state, not by an exit code: blob_restored=186b4a2d… (back to the HEAD blob), git diff HEAD 0 bytes, git diff --cached 0 bytes, injected-marker count back to 0.

The at-risk population, re-derived

Measured with a throwaway probe that snapshots ComponentRegistry.getConfig(key) after each import step, rather than argued from source. All eight PALETTE_EXCLUSIONS keys:

ledger keyrenderer?registered byvisible to the OLD import set
app:launcheryes, real (ns=app)app-shell views/app-launcher-renderer.tsx, eager at module loadno — blind
global:notificationsyes, real (ns=global)app-shell views/global-notifications-renderer.tsx, eagerno — blind
record:chatteryes, real (ns=record)@object-ui/plugin-detail, eagerno — blind
element:record_pickeryes (ns=element)@object-ui/componentsyes
element:text_inputyes (ns=element)@object-ui/componentsyes
user:profileno real renderer; placeholder scaffold only under opt-in registerPlaceholders() (ns=protocol-placeholder)n/a
ai:chat_windownone anywheren/a
element:formnone anywheren/a

Three corrections to what the card assumed, all measured:

  1. It is three keys, not "five shell singletons".PALETTE_EXCLUSIONS carries exactly three shell-singleton entries (app:launcher, global:notifications, user:profile); nav:menu and global:search are not excluded at all, so they never enter this guard's loop. The blind set is app:launcher, global:notifications and — outside app-shell — record:chatter.
  2. app:launcher does not need an opt-in probe. It is opt-in-only in @object-ui/components, but since feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) #7091 app-shell registers the real renderer eagerly at module load. The probe finds it with no registerPlaceholders() call: app:launcher REG ns=app label=App Launcher.
  3. No live false claim exists. The only reasons matching CLAIMS_NO_RENDERER today are ai:chat_window and element:form, and a repo-wide grep for a registration of either (control: namespace: 'element', which returns many hits) returns zero. The defect was latent, as filed.

registerPlaceholders() is deliberately NOT called: it would make the file answer "has a renderer" for types that have only the dashed "Component Placeholder" scaffold (user:profile is one, measured). This repo's own language says the scaffold is not a renderer — views/app-launcher-renderer.tsx describes the state before it existed as "nothing rendered it, so a page that authored it drew a dashed box", with placeholders registered the whole time.

Mechanism chosen, and the cost that chose it

Marginal import cost, same harness and same baseline (after @object-ui/components + plugin-chatbot + plugin-form are loaded):

mechanismmarginal costfile duration end to end
the four app-shell renderer leaves553ms (251 + 164 + 128 + 10)9.4s
the package barrel ../../../../index.js6105ms15.1s

The barrel would track the package automatically, but it drags the console, marketplace, cloud and diagnostics graphs into a pure-logic gate in the cheap unit project — whose entire design point (vitest.config.mts) is not paying for graphs it does not touch. So: leaves, plus a derived guard that buys the barrel's one real advantage for 0ms.

the import set covers every page block app-shell registers reads the views/*-renderer.tsx leaf list from the directory, derives each leaf's registered key from its own ComponentRegistry.register(...) call, and requires every one to resolve. A seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted — deriving it is the point. It carries three controls of its own, because a derivation that finds nothing would pass vacuously and that is this file's whole subject: the leaf list must be non-empty, every leaf's key must be extractable, and the derived list must still contain app:launcher and global:notifications.

The barrel remains viable and cycle-free — it loaded and registered correctly in the probe — so this is a cost decision, not a feasibility one.

Positive probes — one per import, each shown hitting

Required by the finding's own text: "it needs one positive probe per new import or it guards vacuously."

importproberesult
@object-ui/components (pre-existing)element:texthits
@object-ui/plugin-chatbot (pre-existing)chatbothits
@object-ui/plugin-form (pre-existing)object-formhits
@object-ui/plugin-detail (new)record:chatterhits
views/app-launcher-renderer.js (new)app:launcher, derivedhits
views/global-notifications-renderer.js (new)global:notifications, derivedhits
views/global-search-renderer.js (new)global:search, derivedhits
views/nav-menu-renderer.js (new)nav:menu, derivedhits
views/record-approvals-renderer.js (new)record:approvals, derivedhits
views/record-attachments-renderer.js (new)record:attachments, derivedhits

All ten hit — the suite is green at 5/5, and every derived probe is an assertion inside the coverage test, so a miss is a red rather than a silent skip. Ablation A3 below proves that is true and not merely stated.

Both pre-existing anti-vacuity guards are preserved verbatim and neither is weakened: the registry under test is actually populated (guards a vacuous green) keeps its three probes and gains a fourth, and the ledger still contains a no-renderer claim to check (guards a vacuous loop) is untouched.

Post-fix ablations — all three predicted RED, all three observed RED

Run against the committed fix, so each restore is git checkout HEAD -- (absolute path) against a HEAD that already contains the implementation. Every leg proved its mutation on disk (anchor count moved, marker count moved, blob hash moved) and its restore by state (blob back to the HEAD blob, git diff HEAD and git diff --cached both 0 bytes).

A1 — the card's own false claim, the one that passed 4/4 in leg 0:

AssertionError: PALETTE_EXCLUSIONS['app:launcher'] says "no renderer ZZMUTZZ", but a renderer IS
registered for it. ... : expected [Function AppLauncherRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A2 — the same false claim on record:chatter, proving the plugin-detail import is not decorative:

AssertionError: PALETTE_EXCLUSIONS['record:chatter'] says "no renderer ZZMUTBB", but a renderer IS
registered for it. ... : expected [Function RecordChatterRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A3 — one renderer-leaf import deleted, proving the derived coverage guard bites:

AssertionError: views/record-attachments-renderer.tsx registers 'record:attachments' and this file
does not import it, so 'record:attachments' reads as UNREGISTERED here. Add
`import '../../../record-attachments-renderer.js';` to the side-effect imports above. ...
: expected undefined to be truthy
Tests 1 failed | 4 passed (5)

Each leg reports Tests 1 failed | 4 passed (5), so the suite really executed rather than collapsing to "no tests" behind a plausible exit 1.

Scope note — one bounded in-place addition, named

@object-ui/plugin-detail is outside the card's title ("excludes app-shell"), and it is included deliberately: record:chatter is a current ledger key, blind in the same loop, in the same file, for the same reason, and measured the same way (A2). The correct shape was already pinned by the two sibling suites in this same directory — palette-discussion-alias.test.tsx:50 and canvas-display-meta.test.tsx:44 both carry import '@object-ui/plugin-detail'; for exactly this question. Closing the app-shell half while leaving a known, measured blind key in the loop this PR certifies would reproduce the defect the card is about.

Gates

gateexitverdictevidence
vitest run on the guard file0greenTest Files 1 passed (1) / Tests 5 passed (5)
vitest run --project unit (whole project)0greenTest Files 801 passed (801) / Tests 12489 passed | 9 skipped (12498)
vitest run packages/app-shell/src/views/metadata-admin/previews/0greenTest Files 45 passed (45) / Tests 521 passed (521)
turbo run type-check --filter=@object-ui/app-shell0greenTasks: 30 successful, 30 total; the package script is tsc --noEmit && tsc -p tsconfig.test.json
eslint on the changed file0greenno output
check-changeset-presence0green"declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate"
check-changeset-no-major0green"No changeset declares a major bump."
check-control-bytes0green"OK (scanned 5899 tracked text file(s); skipped 85 binary)"

The whole-unit-project run is not incidental: the unit project runs with isolate: false, so the added side-effect registrations are visible to every other unit file sharing a worker. 801/801 green is the measurement that they leak into nothing.

Two notes on reading the evidence honestly:

  • The typecheck is a real measurement, not an assumed one. A first attempt reported Cannot find module '@object-ui/*' across files this PR never touches — an unbuilt workspace closure, which is NOT MEASURED rather than red. Routing through turbo builds the closure first, and --listFiles confirmed the edited file is actually in the tsconfig.test.json program (6 hits) rather than excluded like it is from tsconfig.json.
  • Every exit code above was captured by redirect-then-read, never through a pipe.

Not changed

No production code. The ledger text, the palette decisions and CLAIMS_NO_RENDERER are all untouched — this PR changes only what the guard can see. The changeset declares an empty frontmatter: nothing is released.


Generated by Claude Code

…rt set so a false "no renderer" cannot pass
`exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry`
whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its
coverage is bounded by its own side-effect import set. That set never grew to
include this package, which has registered page blocks since #6757 and #7091,
nor `@object-ui/plugin-detail`, which registers `record:chatter`.
Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to
'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER
regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a
renderer for it.
Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and
add a derived guard: the leaf list is read from the directory and every
registered key must resolve, so a seventh renderer leaf that is not imported
here reds this file instead of silently shrinking its coverage. A
hand-maintained list is what drifted; deriving it is the point.
The package barrel would track registrations automatically too, but costs
6105ms to load against 553ms for the leaves (same harness, same baseline) —
this is a pure-logic gate in the cheap `unit` project.
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)3152.3 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-Cten_jDQ.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)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.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 armed — 5 checks in_progress (4 test shards, Type Check). 25 completed, 0 failure. I arm on green.

⭐ I did your lookup, and it flips your open question

You asked the seat to check user:profile against objectstack#12183's phase list rather than guess, and recommended C for exactly that reason. Done, and the answer is not A:

objectstack#12183 is scoped, in its own title and body, to four members — nav:menu, global:search, global:notifications, app:launcher — and its sub_issues_summary reads 3 of 3 completed, 100%. user:profile appears nowhere in it. ⇒ It is not a later phase of that decomposition; it was never in scope, and the asymmetry is unplanned.

⭐ Filed as #7135, carrying your measurement and your framing, plus the constraint you would not have had: #12183's own Ask allows either a renderer or making it explicit in the spec and validate that these are not author-placeable — so a claimant must establish which answer is wanted before building either, possibly cross-repo.

This is the right hand-off shape. You measured what you could reach, named the one fact that decided between two actions, and refused to file blind on the other side of it. That is better than either guessing or dropping it.

⭐⭐ My at-risk population was wrong twice over, and you re-derived rather than accepted it

My order said "the five shell singletons". Measured: PALETTE_EXCLUSIONS holds 8 keys, of which exactly 3 are shell singletons. nav:menu and global:search are not excluded at all and never enter the guard's loop — so two of my five were never at risk.

And the blind set is 3, one of which is record:chatter — neither a shell singleton nor in app-shell at all, but in @object-ui/plugin-detail. ⇒ A fix scoped to "the shell singletons in app-shell", which is what my order described, would have left a third of the blind set uncovered while looking complete.

⭐ You caught that my other warning would have caused a false positive in the opposite direction

I warned that app:launcher registers only via opt-in PROTOCOL_COMPONENTS, so a probe that does not opt in would report it unregistered. Half true — of @object-ui/components. But since #7091, app-shell registers the real renderer eagerly, so the probe finds it with no opt-in.

⚠️ And opting in, as I implied you might need to, would have been actively wrong: registerPlaceholders() registers the dashed scaffold for ~120 protocol types, which would make the guard answer "has a renderer" for user:profile — whose only registration is that scaffold. My warning pointed at a real hazard and prescribed the move that creates a different one. Declining it, with the measurement, was correct.

The mechanism choice is a measurement, and the honest part is what it does not claim

Leaves 553ms vs barrel 6105ms — 11×, same baseline. And you say plainly that the barrel is viable and cycle-free, so this is a cost decision, not a feasibility one. ⭐ That distinction matters: "we couldn't" and "we chose not to" age very differently for the next reader.

The derived coverage guard is better than what I asked for. My fence was "one positive probe per added import, or it guards vacuously." You met that — seven probes, all hitting — and then closed the failure mode one level up: the guard reads the leaf list from the directory and derives each key from the leaf's own register() call, so a seventh renderer leaf reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted here, so restating one would have rebuilt the defect in the fix. Ablation A3 proves it: delete one leaf import and the guard names the file, the key, and the import line to add.

Rejecting the static manifest is right for the same reason, and well argued: a manifest is a second declaration of what the live registry already answers, and it would drift from the registrations the way the import set drifted. The guard's value is asking the same question the reason string answers, of the live registry.

The ablations, and one detail worth naming

Leg 0 reproduced the blindness before anything changed (predicted GREEN, observed GREEN — 'no renderer ZZMUTZZ' passing 4/4 while a renderer is registered). Post-fix, all three legs RED with specific named assertions, and each reporting Tests 1 failed | 4 passed (5) so the suite demonstrably executed rather than collapsing to "no tests" behind a plausible exit 1.

⚠️ That last check is not decoration — a sibling agent hit exactly that trap today (a JSX comment injected in attribute position, syntax error, Tests no tests, exit 1 that looked like the predicted red). Asserting the passing count alongside the failing one is what distinguishes them.

Both pre-existing anti-vacuity guards preserved verbatim, plus three new controls on the derived guard so it cannot pass empty. isolate:false leak risk addressed by running the whole unit project (801 files, 12489 tests green) rather than just the changed file — the right scope for a change that writes into a shared singleton.

#7134 — filed at the right level, and it is the same class again

The unit project's isolate:false is justified by a comment claiming "no ComponentRegistry or DOM state to leak across files" — and this guard now writes ten registrations into that shared singleton, while a packages/fields test asserts ComponentRegistry.get(...) is toBeUndefined(), an absence assertion a shared registry can silently satisfy.

⭐ That is the sixth instance this session of a comment certifying a property the code no longer has — and the most dangerous shape of it, because the thing it protects is a test's ability to fail. Correctly labelled latent, not live (801/801 green, measured), so it is prevention.


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.

finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green

2 participants

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

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass - #7133

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set
Sep 1, 2026
Merged

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass#7133
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7117

exclusion-reason-truthfulness.test.ts asks the runtime ComponentRegistry whether a PALETTE_EXCLUSIONS reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package — which has registered page blocks since #6757 and #7091 — nor @object-ui/plugin-detail.

All measurements below were taken on this branch; the union of gates was run at 778fb78b4 (the final commit).

Leg 0 — the blindness, reproduced before any fix

Predicted GREEN (the guard is blind), observed GREEN. On 44ea62d29, PALETTE_EXCLUSIONS['app:launcher'] set to 'no renderer ZZMUTZZ' — a string that DOES match the guard's CLAIMS_NO_RENDERER regex — while views/app-launcher-renderer.tsx registers a renderer for it:

ANCHOR_BEFORE=1 INJECTED_BEFORE=0 blob 186b4a2de89fcc150474b5861ee1e2d56bdc50a3
ANCHOR_AFTER=0 INJECTED_AFTER=1 blob 1734fcd9888fd864b379f4ab0b9e6b2d71fe95bd
=== MUTATION CONFIRMED ON DISK ===
Test Files 1 passed (1)
Tests 4 passed (4)
VITEST_EXIT=0

Restore proven by state, not by an exit code: blob_restored=186b4a2d… (back to the HEAD blob), git diff HEAD 0 bytes, git diff --cached 0 bytes, injected-marker count back to 0.

The at-risk population, re-derived

Measured with a throwaway probe that snapshots ComponentRegistry.getConfig(key) after each import step, rather than argued from source. All eight PALETTE_EXCLUSIONS keys:

ledger keyrenderer?registered byvisible to the OLD import set
app:launcheryes, real (ns=app)app-shell views/app-launcher-renderer.tsx, eager at module loadno — blind
global:notificationsyes, real (ns=global)app-shell views/global-notifications-renderer.tsx, eagerno — blind
record:chatteryes, real (ns=record)@object-ui/plugin-detail, eagerno — blind
element:record_pickeryes (ns=element)@object-ui/componentsyes
element:text_inputyes (ns=element)@object-ui/componentsyes
user:profileno real renderer; placeholder scaffold only under opt-in registerPlaceholders() (ns=protocol-placeholder)n/a
ai:chat_windownone anywheren/a
element:formnone anywheren/a

Three corrections to what the card assumed, all measured:

  1. It is three keys, not "five shell singletons".PALETTE_EXCLUSIONS carries exactly three shell-singleton entries (app:launcher, global:notifications, user:profile); nav:menu and global:search are not excluded at all, so they never enter this guard's loop. The blind set is app:launcher, global:notifications and — outside app-shell — record:chatter.
  2. app:launcher does not need an opt-in probe. It is opt-in-only in @object-ui/components, but since feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) #7091 app-shell registers the real renderer eagerly at module load. The probe finds it with no registerPlaceholders() call: app:launcher REG ns=app label=App Launcher.
  3. No live false claim exists. The only reasons matching CLAIMS_NO_RENDERER today are ai:chat_window and element:form, and a repo-wide grep for a registration of either (control: namespace: 'element', which returns many hits) returns zero. The defect was latent, as filed.

registerPlaceholders() is deliberately NOT called: it would make the file answer "has a renderer" for types that have only the dashed "Component Placeholder" scaffold (user:profile is one, measured). This repo's own language says the scaffold is not a renderer — views/app-launcher-renderer.tsx describes the state before it existed as "nothing rendered it, so a page that authored it drew a dashed box", with placeholders registered the whole time.

Mechanism chosen, and the cost that chose it

Marginal import cost, same harness and same baseline (after @object-ui/components + plugin-chatbot + plugin-form are loaded):

mechanismmarginal costfile duration end to end
the four app-shell renderer leaves553ms (251 + 164 + 128 + 10)9.4s
the package barrel ../../../../index.js6105ms15.1s

The barrel would track the package automatically, but it drags the console, marketplace, cloud and diagnostics graphs into a pure-logic gate in the cheap unit project — whose entire design point (vitest.config.mts) is not paying for graphs it does not touch. So: leaves, plus a derived guard that buys the barrel's one real advantage for 0ms.

the import set covers every page block app-shell registers reads the views/*-renderer.tsx leaf list from the directory, derives each leaf's registered key from its own ComponentRegistry.register(...) call, and requires every one to resolve. A seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted — deriving it is the point. It carries three controls of its own, because a derivation that finds nothing would pass vacuously and that is this file's whole subject: the leaf list must be non-empty, every leaf's key must be extractable, and the derived list must still contain app:launcher and global:notifications.

The barrel remains viable and cycle-free — it loaded and registered correctly in the probe — so this is a cost decision, not a feasibility one.

Positive probes — one per import, each shown hitting

Required by the finding's own text: "it needs one positive probe per new import or it guards vacuously."

importproberesult
@object-ui/components (pre-existing)element:texthits
@object-ui/plugin-chatbot (pre-existing)chatbothits
@object-ui/plugin-form (pre-existing)object-formhits
@object-ui/plugin-detail (new)record:chatterhits
views/app-launcher-renderer.js (new)app:launcher, derivedhits
views/global-notifications-renderer.js (new)global:notifications, derivedhits
views/global-search-renderer.js (new)global:search, derivedhits
views/nav-menu-renderer.js (new)nav:menu, derivedhits
views/record-approvals-renderer.js (new)record:approvals, derivedhits
views/record-attachments-renderer.js (new)record:attachments, derivedhits

All ten hit — the suite is green at 5/5, and every derived probe is an assertion inside the coverage test, so a miss is a red rather than a silent skip. Ablation A3 below proves that is true and not merely stated.

Both pre-existing anti-vacuity guards are preserved verbatim and neither is weakened: the registry under test is actually populated (guards a vacuous green) keeps its three probes and gains a fourth, and the ledger still contains a no-renderer claim to check (guards a vacuous loop) is untouched.

Post-fix ablations — all three predicted RED, all three observed RED

Run against the committed fix, so each restore is git checkout HEAD -- (absolute path) against a HEAD that already contains the implementation. Every leg proved its mutation on disk (anchor count moved, marker count moved, blob hash moved) and its restore by state (blob back to the HEAD blob, git diff HEAD and git diff --cached both 0 bytes).

A1 — the card's own false claim, the one that passed 4/4 in leg 0:

AssertionError: PALETTE_EXCLUSIONS['app:launcher'] says "no renderer ZZMUTZZ", but a renderer IS
registered for it. ... : expected [Function AppLauncherRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A2 — the same false claim on record:chatter, proving the plugin-detail import is not decorative:

AssertionError: PALETTE_EXCLUSIONS['record:chatter'] says "no renderer ZZMUTBB", but a renderer IS
registered for it. ... : expected [Function RecordChatterRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A3 — one renderer-leaf import deleted, proving the derived coverage guard bites:

AssertionError: views/record-attachments-renderer.tsx registers 'record:attachments' and this file
does not import it, so 'record:attachments' reads as UNREGISTERED here. Add
`import '../../../record-attachments-renderer.js';` to the side-effect imports above. ...
: expected undefined to be truthy
Tests 1 failed | 4 passed (5)

Each leg reports Tests 1 failed | 4 passed (5), so the suite really executed rather than collapsing to "no tests" behind a plausible exit 1.

Scope note — one bounded in-place addition, named

@object-ui/plugin-detail is outside the card's title ("excludes app-shell"), and it is included deliberately: record:chatter is a current ledger key, blind in the same loop, in the same file, for the same reason, and measured the same way (A2). The correct shape was already pinned by the two sibling suites in this same directory — palette-discussion-alias.test.tsx:50 and canvas-display-meta.test.tsx:44 both carry import '@object-ui/plugin-detail'; for exactly this question. Closing the app-shell half while leaving a known, measured blind key in the loop this PR certifies would reproduce the defect the card is about.

Gates

gateexitverdictevidence
vitest run on the guard file0greenTest Files 1 passed (1) / Tests 5 passed (5)
vitest run --project unit (whole project)0greenTest Files 801 passed (801) / Tests 12489 passed | 9 skipped (12498)
vitest run packages/app-shell/src/views/metadata-admin/previews/0greenTest Files 45 passed (45) / Tests 521 passed (521)
turbo run type-check --filter=@object-ui/app-shell0greenTasks: 30 successful, 30 total; the package script is tsc --noEmit && tsc -p tsconfig.test.json
eslint on the changed file0greenno output
check-changeset-presence0green"declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate"
check-changeset-no-major0green"No changeset declares a major bump."
check-control-bytes0green"OK (scanned 5899 tracked text file(s); skipped 85 binary)"

The whole-unit-project run is not incidental: the unit project runs with isolate: false, so the added side-effect registrations are visible to every other unit file sharing a worker. 801/801 green is the measurement that they leak into nothing.

Two notes on reading the evidence honestly:

  • The typecheck is a real measurement, not an assumed one. A first attempt reported Cannot find module '@object-ui/*' across files this PR never touches — an unbuilt workspace closure, which is NOT MEASURED rather than red. Routing through turbo builds the closure first, and --listFiles confirmed the edited file is actually in the tsconfig.test.json program (6 hits) rather than excluded like it is from tsconfig.json.
  • Every exit code above was captured by redirect-then-read, never through a pipe.

Not changed

No production code. The ledger text, the palette decisions and CLAIMS_NO_RENDERER are all untouched — this PR changes only what the guard can see. The changeset declares an empty frontmatter: nothing is released.


Generated by Claude Code

…rt set so a false "no renderer" cannot pass
`exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry`
whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its
coverage is bounded by its own side-effect import set. That set never grew to
include this package, which has registered page blocks since #6757 and #7091,
nor `@object-ui/plugin-detail`, which registers `record:chatter`.
Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to
'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER
regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a
renderer for it.
Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and
add a derived guard: the leaf list is read from the directory and every
registered key must resolve, so a seventh renderer leaf that is not imported
here reds this file instead of silently shrinking its coverage. A
hand-maintained list is what drifted; deriving it is the point.
The package barrel would track registrations automatically too, but costs
6105ms to load against 553ms for the leaves (same harness, same baseline) —
this is a pure-logic gate in the cheap `unit` project.
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)3152.3 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-Cten_jDQ.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)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.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 armed — 5 checks in_progress (4 test shards, Type Check). 25 completed, 0 failure. I arm on green.

⭐ I did your lookup, and it flips your open question

You asked the seat to check user:profile against objectstack#12183's phase list rather than guess, and recommended C for exactly that reason. Done, and the answer is not A:

objectstack#12183 is scoped, in its own title and body, to four members — nav:menu, global:search, global:notifications, app:launcher — and its sub_issues_summary reads 3 of 3 completed, 100%. user:profile appears nowhere in it. ⇒ It is not a later phase of that decomposition; it was never in scope, and the asymmetry is unplanned.

⭐ Filed as #7135, carrying your measurement and your framing, plus the constraint you would not have had: #12183's own Ask allows either a renderer or making it explicit in the spec and validate that these are not author-placeable — so a claimant must establish which answer is wanted before building either, possibly cross-repo.

This is the right hand-off shape. You measured what you could reach, named the one fact that decided between two actions, and refused to file blind on the other side of it. That is better than either guessing or dropping it.

⭐⭐ My at-risk population was wrong twice over, and you re-derived rather than accepted it

My order said "the five shell singletons". Measured: PALETTE_EXCLUSIONS holds 8 keys, of which exactly 3 are shell singletons. nav:menu and global:search are not excluded at all and never enter the guard's loop — so two of my five were never at risk.

And the blind set is 3, one of which is record:chatter — neither a shell singleton nor in app-shell at all, but in @object-ui/plugin-detail. ⇒ A fix scoped to "the shell singletons in app-shell", which is what my order described, would have left a third of the blind set uncovered while looking complete.

⭐ You caught that my other warning would have caused a false positive in the opposite direction

I warned that app:launcher registers only via opt-in PROTOCOL_COMPONENTS, so a probe that does not opt in would report it unregistered. Half true — of @object-ui/components. But since #7091, app-shell registers the real renderer eagerly, so the probe finds it with no opt-in.

⚠️ And opting in, as I implied you might need to, would have been actively wrong: registerPlaceholders() registers the dashed scaffold for ~120 protocol types, which would make the guard answer "has a renderer" for user:profile — whose only registration is that scaffold. My warning pointed at a real hazard and prescribed the move that creates a different one. Declining it, with the measurement, was correct.

The mechanism choice is a measurement, and the honest part is what it does not claim

Leaves 553ms vs barrel 6105ms — 11×, same baseline. And you say plainly that the barrel is viable and cycle-free, so this is a cost decision, not a feasibility one. ⭐ That distinction matters: "we couldn't" and "we chose not to" age very differently for the next reader.

The derived coverage guard is better than what I asked for. My fence was "one positive probe per added import, or it guards vacuously." You met that — seven probes, all hitting — and then closed the failure mode one level up: the guard reads the leaf list from the directory and derives each key from the leaf's own register() call, so a seventh renderer leaf reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted here, so restating one would have rebuilt the defect in the fix. Ablation A3 proves it: delete one leaf import and the guard names the file, the key, and the import line to add.

Rejecting the static manifest is right for the same reason, and well argued: a manifest is a second declaration of what the live registry already answers, and it would drift from the registrations the way the import set drifted. The guard's value is asking the same question the reason string answers, of the live registry.

The ablations, and one detail worth naming

Leg 0 reproduced the blindness before anything changed (predicted GREEN, observed GREEN — 'no renderer ZZMUTZZ' passing 4/4 while a renderer is registered). Post-fix, all three legs RED with specific named assertions, and each reporting Tests 1 failed | 4 passed (5) so the suite demonstrably executed rather than collapsing to "no tests" behind a plausible exit 1.

⚠️ That last check is not decoration — a sibling agent hit exactly that trap today (a JSX comment injected in attribute position, syntax error, Tests no tests, exit 1 that looked like the predicted red). Asserting the passing count alongside the failing one is what distinguishes them.

Both pre-existing anti-vacuity guards preserved verbatim, plus three new controls on the derived guard so it cannot pass empty. isolate:false leak risk addressed by running the whole unit project (801 files, 12489 tests green) rather than just the changed file — the right scope for a change that writes into a shared singleton.

#7134 — filed at the right level, and it is the same class again

The unit project's isolate:false is justified by a comment claiming "no ComponentRegistry or DOM state to leak across files" — and this guard now writes ten registrations into that shared singleton, while a packages/fields test asserts ComponentRegistry.get(...) is toBeUndefined(), an absence assertion a shared registry can silently satisfy.

⭐ That is the sixth instance this session of a comment certifying a property the code no longer has — and the most dangerous shape of it, because the thing it protects is a test's ability to fail. Correctly labelled latent, not live (801/801 green, measured), so it is prevention.


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.

finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green

2 participants

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

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass - #7133

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set
Sep 1, 2026
Merged

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass#7133
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7117

exclusion-reason-truthfulness.test.ts asks the runtime ComponentRegistry whether a PALETTE_EXCLUSIONS reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package — which has registered page blocks since #6757 and #7091 — nor @object-ui/plugin-detail.

All measurements below were taken on this branch; the union of gates was run at 778fb78b4 (the final commit).

Leg 0 — the blindness, reproduced before any fix

Predicted GREEN (the guard is blind), observed GREEN. On 44ea62d29, PALETTE_EXCLUSIONS['app:launcher'] set to 'no renderer ZZMUTZZ' — a string that DOES match the guard's CLAIMS_NO_RENDERER regex — while views/app-launcher-renderer.tsx registers a renderer for it:

ANCHOR_BEFORE=1 INJECTED_BEFORE=0 blob 186b4a2de89fcc150474b5861ee1e2d56bdc50a3
ANCHOR_AFTER=0 INJECTED_AFTER=1 blob 1734fcd9888fd864b379f4ab0b9e6b2d71fe95bd
=== MUTATION CONFIRMED ON DISK ===
Test Files 1 passed (1)
Tests 4 passed (4)
VITEST_EXIT=0

Restore proven by state, not by an exit code: blob_restored=186b4a2d… (back to the HEAD blob), git diff HEAD 0 bytes, git diff --cached 0 bytes, injected-marker count back to 0.

The at-risk population, re-derived

Measured with a throwaway probe that snapshots ComponentRegistry.getConfig(key) after each import step, rather than argued from source. All eight PALETTE_EXCLUSIONS keys:

ledger keyrenderer?registered byvisible to the OLD import set
app:launcheryes, real (ns=app)app-shell views/app-launcher-renderer.tsx, eager at module loadno — blind
global:notificationsyes, real (ns=global)app-shell views/global-notifications-renderer.tsx, eagerno — blind
record:chatteryes, real (ns=record)@object-ui/plugin-detail, eagerno — blind
element:record_pickeryes (ns=element)@object-ui/componentsyes
element:text_inputyes (ns=element)@object-ui/componentsyes
user:profileno real renderer; placeholder scaffold only under opt-in registerPlaceholders() (ns=protocol-placeholder)n/a
ai:chat_windownone anywheren/a
element:formnone anywheren/a

Three corrections to what the card assumed, all measured:

  1. It is three keys, not "five shell singletons".PALETTE_EXCLUSIONS carries exactly three shell-singleton entries (app:launcher, global:notifications, user:profile); nav:menu and global:search are not excluded at all, so they never enter this guard's loop. The blind set is app:launcher, global:notifications and — outside app-shell — record:chatter.
  2. app:launcher does not need an opt-in probe. It is opt-in-only in @object-ui/components, but since feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) #7091 app-shell registers the real renderer eagerly at module load. The probe finds it with no registerPlaceholders() call: app:launcher REG ns=app label=App Launcher.
  3. No live false claim exists. The only reasons matching CLAIMS_NO_RENDERER today are ai:chat_window and element:form, and a repo-wide grep for a registration of either (control: namespace: 'element', which returns many hits) returns zero. The defect was latent, as filed.

registerPlaceholders() is deliberately NOT called: it would make the file answer "has a renderer" for types that have only the dashed "Component Placeholder" scaffold (user:profile is one, measured). This repo's own language says the scaffold is not a renderer — views/app-launcher-renderer.tsx describes the state before it existed as "nothing rendered it, so a page that authored it drew a dashed box", with placeholders registered the whole time.

Mechanism chosen, and the cost that chose it

Marginal import cost, same harness and same baseline (after @object-ui/components + plugin-chatbot + plugin-form are loaded):

mechanismmarginal costfile duration end to end
the four app-shell renderer leaves553ms (251 + 164 + 128 + 10)9.4s
the package barrel ../../../../index.js6105ms15.1s

The barrel would track the package automatically, but it drags the console, marketplace, cloud and diagnostics graphs into a pure-logic gate in the cheap unit project — whose entire design point (vitest.config.mts) is not paying for graphs it does not touch. So: leaves, plus a derived guard that buys the barrel's one real advantage for 0ms.

the import set covers every page block app-shell registers reads the views/*-renderer.tsx leaf list from the directory, derives each leaf's registered key from its own ComponentRegistry.register(...) call, and requires every one to resolve. A seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted — deriving it is the point. It carries three controls of its own, because a derivation that finds nothing would pass vacuously and that is this file's whole subject: the leaf list must be non-empty, every leaf's key must be extractable, and the derived list must still contain app:launcher and global:notifications.

The barrel remains viable and cycle-free — it loaded and registered correctly in the probe — so this is a cost decision, not a feasibility one.

Positive probes — one per import, each shown hitting

Required by the finding's own text: "it needs one positive probe per new import or it guards vacuously."

importproberesult
@object-ui/components (pre-existing)element:texthits
@object-ui/plugin-chatbot (pre-existing)chatbothits
@object-ui/plugin-form (pre-existing)object-formhits
@object-ui/plugin-detail (new)record:chatterhits
views/app-launcher-renderer.js (new)app:launcher, derivedhits
views/global-notifications-renderer.js (new)global:notifications, derivedhits
views/global-search-renderer.js (new)global:search, derivedhits
views/nav-menu-renderer.js (new)nav:menu, derivedhits
views/record-approvals-renderer.js (new)record:approvals, derivedhits
views/record-attachments-renderer.js (new)record:attachments, derivedhits

All ten hit — the suite is green at 5/5, and every derived probe is an assertion inside the coverage test, so a miss is a red rather than a silent skip. Ablation A3 below proves that is true and not merely stated.

Both pre-existing anti-vacuity guards are preserved verbatim and neither is weakened: the registry under test is actually populated (guards a vacuous green) keeps its three probes and gains a fourth, and the ledger still contains a no-renderer claim to check (guards a vacuous loop) is untouched.

Post-fix ablations — all three predicted RED, all three observed RED

Run against the committed fix, so each restore is git checkout HEAD -- (absolute path) against a HEAD that already contains the implementation. Every leg proved its mutation on disk (anchor count moved, marker count moved, blob hash moved) and its restore by state (blob back to the HEAD blob, git diff HEAD and git diff --cached both 0 bytes).

A1 — the card's own false claim, the one that passed 4/4 in leg 0:

AssertionError: PALETTE_EXCLUSIONS['app:launcher'] says "no renderer ZZMUTZZ", but a renderer IS
registered for it. ... : expected [Function AppLauncherRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A2 — the same false claim on record:chatter, proving the plugin-detail import is not decorative:

AssertionError: PALETTE_EXCLUSIONS['record:chatter'] says "no renderer ZZMUTBB", but a renderer IS
registered for it. ... : expected [Function RecordChatterRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A3 — one renderer-leaf import deleted, proving the derived coverage guard bites:

AssertionError: views/record-attachments-renderer.tsx registers 'record:attachments' and this file
does not import it, so 'record:attachments' reads as UNREGISTERED here. Add
`import '../../../record-attachments-renderer.js';` to the side-effect imports above. ...
: expected undefined to be truthy
Tests 1 failed | 4 passed (5)

Each leg reports Tests 1 failed | 4 passed (5), so the suite really executed rather than collapsing to "no tests" behind a plausible exit 1.

Scope note — one bounded in-place addition, named

@object-ui/plugin-detail is outside the card's title ("excludes app-shell"), and it is included deliberately: record:chatter is a current ledger key, blind in the same loop, in the same file, for the same reason, and measured the same way (A2). The correct shape was already pinned by the two sibling suites in this same directory — palette-discussion-alias.test.tsx:50 and canvas-display-meta.test.tsx:44 both carry import '@object-ui/plugin-detail'; for exactly this question. Closing the app-shell half while leaving a known, measured blind key in the loop this PR certifies would reproduce the defect the card is about.

Gates

gateexitverdictevidence
vitest run on the guard file0greenTest Files 1 passed (1) / Tests 5 passed (5)
vitest run --project unit (whole project)0greenTest Files 801 passed (801) / Tests 12489 passed | 9 skipped (12498)
vitest run packages/app-shell/src/views/metadata-admin/previews/0greenTest Files 45 passed (45) / Tests 521 passed (521)
turbo run type-check --filter=@object-ui/app-shell0greenTasks: 30 successful, 30 total; the package script is tsc --noEmit && tsc -p tsconfig.test.json
eslint on the changed file0greenno output
check-changeset-presence0green"declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate"
check-changeset-no-major0green"No changeset declares a major bump."
check-control-bytes0green"OK (scanned 5899 tracked text file(s); skipped 85 binary)"

The whole-unit-project run is not incidental: the unit project runs with isolate: false, so the added side-effect registrations are visible to every other unit file sharing a worker. 801/801 green is the measurement that they leak into nothing.

Two notes on reading the evidence honestly:

  • The typecheck is a real measurement, not an assumed one. A first attempt reported Cannot find module '@object-ui/*' across files this PR never touches — an unbuilt workspace closure, which is NOT MEASURED rather than red. Routing through turbo builds the closure first, and --listFiles confirmed the edited file is actually in the tsconfig.test.json program (6 hits) rather than excluded like it is from tsconfig.json.
  • Every exit code above was captured by redirect-then-read, never through a pipe.

Not changed

No production code. The ledger text, the palette decisions and CLAIMS_NO_RENDERER are all untouched — this PR changes only what the guard can see. The changeset declares an empty frontmatter: nothing is released.


Generated by Claude Code

…rt set so a false "no renderer" cannot pass
`exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry`
whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its
coverage is bounded by its own side-effect import set. That set never grew to
include this package, which has registered page blocks since #6757 and #7091,
nor `@object-ui/plugin-detail`, which registers `record:chatter`.
Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to
'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER
regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a
renderer for it.
Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and
add a derived guard: the leaf list is read from the directory and every
registered key must resolve, so a seventh renderer leaf that is not imported
here reds this file instead of silently shrinking its coverage. A
hand-maintained list is what drifted; deriving it is the point.
The package barrel would track registrations automatically too, but costs
6105ms to load against 553ms for the leaves (same harness, same baseline) —
this is a pure-logic gate in the cheap `unit` project.
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)3152.3 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-Cten_jDQ.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)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.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 armed — 5 checks in_progress (4 test shards, Type Check). 25 completed, 0 failure. I arm on green.

⭐ I did your lookup, and it flips your open question

You asked the seat to check user:profile against objectstack#12183's phase list rather than guess, and recommended C for exactly that reason. Done, and the answer is not A:

objectstack#12183 is scoped, in its own title and body, to four members — nav:menu, global:search, global:notifications, app:launcher — and its sub_issues_summary reads 3 of 3 completed, 100%. user:profile appears nowhere in it. ⇒ It is not a later phase of that decomposition; it was never in scope, and the asymmetry is unplanned.

⭐ Filed as #7135, carrying your measurement and your framing, plus the constraint you would not have had: #12183's own Ask allows either a renderer or making it explicit in the spec and validate that these are not author-placeable — so a claimant must establish which answer is wanted before building either, possibly cross-repo.

This is the right hand-off shape. You measured what you could reach, named the one fact that decided between two actions, and refused to file blind on the other side of it. That is better than either guessing or dropping it.

⭐⭐ My at-risk population was wrong twice over, and you re-derived rather than accepted it

My order said "the five shell singletons". Measured: PALETTE_EXCLUSIONS holds 8 keys, of which exactly 3 are shell singletons. nav:menu and global:search are not excluded at all and never enter the guard's loop — so two of my five were never at risk.

And the blind set is 3, one of which is record:chatter — neither a shell singleton nor in app-shell at all, but in @object-ui/plugin-detail. ⇒ A fix scoped to "the shell singletons in app-shell", which is what my order described, would have left a third of the blind set uncovered while looking complete.

⭐ You caught that my other warning would have caused a false positive in the opposite direction

I warned that app:launcher registers only via opt-in PROTOCOL_COMPONENTS, so a probe that does not opt in would report it unregistered. Half true — of @object-ui/components. But since #7091, app-shell registers the real renderer eagerly, so the probe finds it with no opt-in.

⚠️ And opting in, as I implied you might need to, would have been actively wrong: registerPlaceholders() registers the dashed scaffold for ~120 protocol types, which would make the guard answer "has a renderer" for user:profile — whose only registration is that scaffold. My warning pointed at a real hazard and prescribed the move that creates a different one. Declining it, with the measurement, was correct.

The mechanism choice is a measurement, and the honest part is what it does not claim

Leaves 553ms vs barrel 6105ms — 11×, same baseline. And you say plainly that the barrel is viable and cycle-free, so this is a cost decision, not a feasibility one. ⭐ That distinction matters: "we couldn't" and "we chose not to" age very differently for the next reader.

The derived coverage guard is better than what I asked for. My fence was "one positive probe per added import, or it guards vacuously." You met that — seven probes, all hitting — and then closed the failure mode one level up: the guard reads the leaf list from the directory and derives each key from the leaf's own register() call, so a seventh renderer leaf reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted here, so restating one would have rebuilt the defect in the fix. Ablation A3 proves it: delete one leaf import and the guard names the file, the key, and the import line to add.

Rejecting the static manifest is right for the same reason, and well argued: a manifest is a second declaration of what the live registry already answers, and it would drift from the registrations the way the import set drifted. The guard's value is asking the same question the reason string answers, of the live registry.

The ablations, and one detail worth naming

Leg 0 reproduced the blindness before anything changed (predicted GREEN, observed GREEN — 'no renderer ZZMUTZZ' passing 4/4 while a renderer is registered). Post-fix, all three legs RED with specific named assertions, and each reporting Tests 1 failed | 4 passed (5) so the suite demonstrably executed rather than collapsing to "no tests" behind a plausible exit 1.

⚠️ That last check is not decoration — a sibling agent hit exactly that trap today (a JSX comment injected in attribute position, syntax error, Tests no tests, exit 1 that looked like the predicted red). Asserting the passing count alongside the failing one is what distinguishes them.

Both pre-existing anti-vacuity guards preserved verbatim, plus three new controls on the derived guard so it cannot pass empty. isolate:false leak risk addressed by running the whole unit project (801 files, 12489 tests green) rather than just the changed file — the right scope for a change that writes into a shared singleton.

#7134 — filed at the right level, and it is the same class again

The unit project's isolate:false is justified by a comment claiming "no ComponentRegistry or DOM state to leak across files" — and this guard now writes ten registrations into that shared singleton, while a packages/fields test asserts ComponentRegistry.get(...) is toBeUndefined(), an absence assertion a shared registry can silently satisfy.

⭐ That is the sixth instance this session of a comment certifying a property the code no longer has — and the most dangerous shape of it, because the thing it protects is a test's ability to fail. Correctly labelled latent, not live (801/801 green, measured), so it is prevention.


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.

finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green

2 participants

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

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass - #7133

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set
Sep 1, 2026
Merged

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass#7133
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7117

exclusion-reason-truthfulness.test.ts asks the runtime ComponentRegistry whether a PALETTE_EXCLUSIONS reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package — which has registered page blocks since #6757 and #7091 — nor @object-ui/plugin-detail.

All measurements below were taken on this branch; the union of gates was run at 778fb78b4 (the final commit).

Leg 0 — the blindness, reproduced before any fix

Predicted GREEN (the guard is blind), observed GREEN. On 44ea62d29, PALETTE_EXCLUSIONS['app:launcher'] set to 'no renderer ZZMUTZZ' — a string that DOES match the guard's CLAIMS_NO_RENDERER regex — while views/app-launcher-renderer.tsx registers a renderer for it:

ANCHOR_BEFORE=1 INJECTED_BEFORE=0 blob 186b4a2de89fcc150474b5861ee1e2d56bdc50a3
ANCHOR_AFTER=0 INJECTED_AFTER=1 blob 1734fcd9888fd864b379f4ab0b9e6b2d71fe95bd
=== MUTATION CONFIRMED ON DISK ===
Test Files 1 passed (1)
Tests 4 passed (4)
VITEST_EXIT=0

Restore proven by state, not by an exit code: blob_restored=186b4a2d… (back to the HEAD blob), git diff HEAD 0 bytes, git diff --cached 0 bytes, injected-marker count back to 0.

The at-risk population, re-derived

Measured with a throwaway probe that snapshots ComponentRegistry.getConfig(key) after each import step, rather than argued from source. All eight PALETTE_EXCLUSIONS keys:

ledger keyrenderer?registered byvisible to the OLD import set
app:launcheryes, real (ns=app)app-shell views/app-launcher-renderer.tsx, eager at module loadno — blind
global:notificationsyes, real (ns=global)app-shell views/global-notifications-renderer.tsx, eagerno — blind
record:chatteryes, real (ns=record)@object-ui/plugin-detail, eagerno — blind
element:record_pickeryes (ns=element)@object-ui/componentsyes
element:text_inputyes (ns=element)@object-ui/componentsyes
user:profileno real renderer; placeholder scaffold only under opt-in registerPlaceholders() (ns=protocol-placeholder)n/a
ai:chat_windownone anywheren/a
element:formnone anywheren/a

Three corrections to what the card assumed, all measured:

  1. It is three keys, not "five shell singletons".PALETTE_EXCLUSIONS carries exactly three shell-singleton entries (app:launcher, global:notifications, user:profile); nav:menu and global:search are not excluded at all, so they never enter this guard's loop. The blind set is app:launcher, global:notifications and — outside app-shell — record:chatter.
  2. app:launcher does not need an opt-in probe. It is opt-in-only in @object-ui/components, but since feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) #7091 app-shell registers the real renderer eagerly at module load. The probe finds it with no registerPlaceholders() call: app:launcher REG ns=app label=App Launcher.
  3. No live false claim exists. The only reasons matching CLAIMS_NO_RENDERER today are ai:chat_window and element:form, and a repo-wide grep for a registration of either (control: namespace: 'element', which returns many hits) returns zero. The defect was latent, as filed.

registerPlaceholders() is deliberately NOT called: it would make the file answer "has a renderer" for types that have only the dashed "Component Placeholder" scaffold (user:profile is one, measured). This repo's own language says the scaffold is not a renderer — views/app-launcher-renderer.tsx describes the state before it existed as "nothing rendered it, so a page that authored it drew a dashed box", with placeholders registered the whole time.

Mechanism chosen, and the cost that chose it

Marginal import cost, same harness and same baseline (after @object-ui/components + plugin-chatbot + plugin-form are loaded):

mechanismmarginal costfile duration end to end
the four app-shell renderer leaves553ms (251 + 164 + 128 + 10)9.4s
the package barrel ../../../../index.js6105ms15.1s

The barrel would track the package automatically, but it drags the console, marketplace, cloud and diagnostics graphs into a pure-logic gate in the cheap unit project — whose entire design point (vitest.config.mts) is not paying for graphs it does not touch. So: leaves, plus a derived guard that buys the barrel's one real advantage for 0ms.

the import set covers every page block app-shell registers reads the views/*-renderer.tsx leaf list from the directory, derives each leaf's registered key from its own ComponentRegistry.register(...) call, and requires every one to resolve. A seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted — deriving it is the point. It carries three controls of its own, because a derivation that finds nothing would pass vacuously and that is this file's whole subject: the leaf list must be non-empty, every leaf's key must be extractable, and the derived list must still contain app:launcher and global:notifications.

The barrel remains viable and cycle-free — it loaded and registered correctly in the probe — so this is a cost decision, not a feasibility one.

Positive probes — one per import, each shown hitting

Required by the finding's own text: "it needs one positive probe per new import or it guards vacuously."

importproberesult
@object-ui/components (pre-existing)element:texthits
@object-ui/plugin-chatbot (pre-existing)chatbothits
@object-ui/plugin-form (pre-existing)object-formhits
@object-ui/plugin-detail (new)record:chatterhits
views/app-launcher-renderer.js (new)app:launcher, derivedhits
views/global-notifications-renderer.js (new)global:notifications, derivedhits
views/global-search-renderer.js (new)global:search, derivedhits
views/nav-menu-renderer.js (new)nav:menu, derivedhits
views/record-approvals-renderer.js (new)record:approvals, derivedhits
views/record-attachments-renderer.js (new)record:attachments, derivedhits

All ten hit — the suite is green at 5/5, and every derived probe is an assertion inside the coverage test, so a miss is a red rather than a silent skip. Ablation A3 below proves that is true and not merely stated.

Both pre-existing anti-vacuity guards are preserved verbatim and neither is weakened: the registry under test is actually populated (guards a vacuous green) keeps its three probes and gains a fourth, and the ledger still contains a no-renderer claim to check (guards a vacuous loop) is untouched.

Post-fix ablations — all three predicted RED, all three observed RED

Run against the committed fix, so each restore is git checkout HEAD -- (absolute path) against a HEAD that already contains the implementation. Every leg proved its mutation on disk (anchor count moved, marker count moved, blob hash moved) and its restore by state (blob back to the HEAD blob, git diff HEAD and git diff --cached both 0 bytes).

A1 — the card's own false claim, the one that passed 4/4 in leg 0:

AssertionError: PALETTE_EXCLUSIONS['app:launcher'] says "no renderer ZZMUTZZ", but a renderer IS
registered for it. ... : expected [Function AppLauncherRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A2 — the same false claim on record:chatter, proving the plugin-detail import is not decorative:

AssertionError: PALETTE_EXCLUSIONS['record:chatter'] says "no renderer ZZMUTBB", but a renderer IS
registered for it. ... : expected [Function RecordChatterRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A3 — one renderer-leaf import deleted, proving the derived coverage guard bites:

AssertionError: views/record-attachments-renderer.tsx registers 'record:attachments' and this file
does not import it, so 'record:attachments' reads as UNREGISTERED here. Add
`import '../../../record-attachments-renderer.js';` to the side-effect imports above. ...
: expected undefined to be truthy
Tests 1 failed | 4 passed (5)

Each leg reports Tests 1 failed | 4 passed (5), so the suite really executed rather than collapsing to "no tests" behind a plausible exit 1.

Scope note — one bounded in-place addition, named

@object-ui/plugin-detail is outside the card's title ("excludes app-shell"), and it is included deliberately: record:chatter is a current ledger key, blind in the same loop, in the same file, for the same reason, and measured the same way (A2). The correct shape was already pinned by the two sibling suites in this same directory — palette-discussion-alias.test.tsx:50 and canvas-display-meta.test.tsx:44 both carry import '@object-ui/plugin-detail'; for exactly this question. Closing the app-shell half while leaving a known, measured blind key in the loop this PR certifies would reproduce the defect the card is about.

Gates

gateexitverdictevidence
vitest run on the guard file0greenTest Files 1 passed (1) / Tests 5 passed (5)
vitest run --project unit (whole project)0greenTest Files 801 passed (801) / Tests 12489 passed | 9 skipped (12498)
vitest run packages/app-shell/src/views/metadata-admin/previews/0greenTest Files 45 passed (45) / Tests 521 passed (521)
turbo run type-check --filter=@object-ui/app-shell0greenTasks: 30 successful, 30 total; the package script is tsc --noEmit && tsc -p tsconfig.test.json
eslint on the changed file0greenno output
check-changeset-presence0green"declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate"
check-changeset-no-major0green"No changeset declares a major bump."
check-control-bytes0green"OK (scanned 5899 tracked text file(s); skipped 85 binary)"

The whole-unit-project run is not incidental: the unit project runs with isolate: false, so the added side-effect registrations are visible to every other unit file sharing a worker. 801/801 green is the measurement that they leak into nothing.

Two notes on reading the evidence honestly:

  • The typecheck is a real measurement, not an assumed one. A first attempt reported Cannot find module '@object-ui/*' across files this PR never touches — an unbuilt workspace closure, which is NOT MEASURED rather than red. Routing through turbo builds the closure first, and --listFiles confirmed the edited file is actually in the tsconfig.test.json program (6 hits) rather than excluded like it is from tsconfig.json.
  • Every exit code above was captured by redirect-then-read, never through a pipe.

Not changed

No production code. The ledger text, the palette decisions and CLAIMS_NO_RENDERER are all untouched — this PR changes only what the guard can see. The changeset declares an empty frontmatter: nothing is released.


Generated by Claude Code

…rt set so a false "no renderer" cannot pass
`exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry`
whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its
coverage is bounded by its own side-effect import set. That set never grew to
include this package, which has registered page blocks since #6757 and #7091,
nor `@object-ui/plugin-detail`, which registers `record:chatter`.
Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to
'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER
regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a
renderer for it.
Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and
add a derived guard: the leaf list is read from the directory and every
registered key must resolve, so a seventh renderer leaf that is not imported
here reds this file instead of silently shrinking its coverage. A
hand-maintained list is what drifted; deriving it is the point.
The package barrel would track registrations automatically too, but costs
6105ms to load against 553ms for the leaves (same harness, same baseline) —
this is a pure-logic gate in the cheap `unit` project.
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)3152.3 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-Cten_jDQ.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)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.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 armed — 5 checks in_progress (4 test shards, Type Check). 25 completed, 0 failure. I arm on green.

⭐ I did your lookup, and it flips your open question

You asked the seat to check user:profile against objectstack#12183's phase list rather than guess, and recommended C for exactly that reason. Done, and the answer is not A:

objectstack#12183 is scoped, in its own title and body, to four members — nav:menu, global:search, global:notifications, app:launcher — and its sub_issues_summary reads 3 of 3 completed, 100%. user:profile appears nowhere in it. ⇒ It is not a later phase of that decomposition; it was never in scope, and the asymmetry is unplanned.

⭐ Filed as #7135, carrying your measurement and your framing, plus the constraint you would not have had: #12183's own Ask allows either a renderer or making it explicit in the spec and validate that these are not author-placeable — so a claimant must establish which answer is wanted before building either, possibly cross-repo.

This is the right hand-off shape. You measured what you could reach, named the one fact that decided between two actions, and refused to file blind on the other side of it. That is better than either guessing or dropping it.

⭐⭐ My at-risk population was wrong twice over, and you re-derived rather than accepted it

My order said "the five shell singletons". Measured: PALETTE_EXCLUSIONS holds 8 keys, of which exactly 3 are shell singletons. nav:menu and global:search are not excluded at all and never enter the guard's loop — so two of my five were never at risk.

And the blind set is 3, one of which is record:chatter — neither a shell singleton nor in app-shell at all, but in @object-ui/plugin-detail. ⇒ A fix scoped to "the shell singletons in app-shell", which is what my order described, would have left a third of the blind set uncovered while looking complete.

⭐ You caught that my other warning would have caused a false positive in the opposite direction

I warned that app:launcher registers only via opt-in PROTOCOL_COMPONENTS, so a probe that does not opt in would report it unregistered. Half true — of @object-ui/components. But since #7091, app-shell registers the real renderer eagerly, so the probe finds it with no opt-in.

⚠️ And opting in, as I implied you might need to, would have been actively wrong: registerPlaceholders() registers the dashed scaffold for ~120 protocol types, which would make the guard answer "has a renderer" for user:profile — whose only registration is that scaffold. My warning pointed at a real hazard and prescribed the move that creates a different one. Declining it, with the measurement, was correct.

The mechanism choice is a measurement, and the honest part is what it does not claim

Leaves 553ms vs barrel 6105ms — 11×, same baseline. And you say plainly that the barrel is viable and cycle-free, so this is a cost decision, not a feasibility one. ⭐ That distinction matters: "we couldn't" and "we chose not to" age very differently for the next reader.

The derived coverage guard is better than what I asked for. My fence was "one positive probe per added import, or it guards vacuously." You met that — seven probes, all hitting — and then closed the failure mode one level up: the guard reads the leaf list from the directory and derives each key from the leaf's own register() call, so a seventh renderer leaf reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted here, so restating one would have rebuilt the defect in the fix. Ablation A3 proves it: delete one leaf import and the guard names the file, the key, and the import line to add.

Rejecting the static manifest is right for the same reason, and well argued: a manifest is a second declaration of what the live registry already answers, and it would drift from the registrations the way the import set drifted. The guard's value is asking the same question the reason string answers, of the live registry.

The ablations, and one detail worth naming

Leg 0 reproduced the blindness before anything changed (predicted GREEN, observed GREEN — 'no renderer ZZMUTZZ' passing 4/4 while a renderer is registered). Post-fix, all three legs RED with specific named assertions, and each reporting Tests 1 failed | 4 passed (5) so the suite demonstrably executed rather than collapsing to "no tests" behind a plausible exit 1.

⚠️ That last check is not decoration — a sibling agent hit exactly that trap today (a JSX comment injected in attribute position, syntax error, Tests no tests, exit 1 that looked like the predicted red). Asserting the passing count alongside the failing one is what distinguishes them.

Both pre-existing anti-vacuity guards preserved verbatim, plus three new controls on the derived guard so it cannot pass empty. isolate:false leak risk addressed by running the whole unit project (801 files, 12489 tests green) rather than just the changed file — the right scope for a change that writes into a shared singleton.

#7134 — filed at the right level, and it is the same class again

The unit project's isolate:false is justified by a comment claiming "no ComponentRegistry or DOM state to leak across files" — and this guard now writes ten registrations into that shared singleton, while a packages/fields test asserts ComponentRegistry.get(...) is toBeUndefined(), an absence assertion a shared registry can silently satisfy.

⭐ That is the sixth instance this session of a comment certifying a property the code no longer has — and the most dangerous shape of it, because the thing it protects is a test's ability to fail. Correctly labelled latent, not live (801/801 green, measured), so it is prevention.


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.

finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green

2 participants

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

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass - #7133

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set
Sep 1, 2026
Merged

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass#7133
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7117

exclusion-reason-truthfulness.test.ts asks the runtime ComponentRegistry whether a PALETTE_EXCLUSIONS reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package — which has registered page blocks since #6757 and #7091 — nor @object-ui/plugin-detail.

All measurements below were taken on this branch; the union of gates was run at 778fb78b4 (the final commit).

Leg 0 — the blindness, reproduced before any fix

Predicted GREEN (the guard is blind), observed GREEN. On 44ea62d29, PALETTE_EXCLUSIONS['app:launcher'] set to 'no renderer ZZMUTZZ' — a string that DOES match the guard's CLAIMS_NO_RENDERER regex — while views/app-launcher-renderer.tsx registers a renderer for it:

ANCHOR_BEFORE=1 INJECTED_BEFORE=0 blob 186b4a2de89fcc150474b5861ee1e2d56bdc50a3
ANCHOR_AFTER=0 INJECTED_AFTER=1 blob 1734fcd9888fd864b379f4ab0b9e6b2d71fe95bd
=== MUTATION CONFIRMED ON DISK ===
Test Files 1 passed (1)
Tests 4 passed (4)
VITEST_EXIT=0

Restore proven by state, not by an exit code: blob_restored=186b4a2d… (back to the HEAD blob), git diff HEAD 0 bytes, git diff --cached 0 bytes, injected-marker count back to 0.

The at-risk population, re-derived

Measured with a throwaway probe that snapshots ComponentRegistry.getConfig(key) after each import step, rather than argued from source. All eight PALETTE_EXCLUSIONS keys:

ledger keyrenderer?registered byvisible to the OLD import set
app:launcheryes, real (ns=app)app-shell views/app-launcher-renderer.tsx, eager at module loadno — blind
global:notificationsyes, real (ns=global)app-shell views/global-notifications-renderer.tsx, eagerno — blind
record:chatteryes, real (ns=record)@object-ui/plugin-detail, eagerno — blind
element:record_pickeryes (ns=element)@object-ui/componentsyes
element:text_inputyes (ns=element)@object-ui/componentsyes
user:profileno real renderer; placeholder scaffold only under opt-in registerPlaceholders() (ns=protocol-placeholder)n/a
ai:chat_windownone anywheren/a
element:formnone anywheren/a

Three corrections to what the card assumed, all measured:

  1. It is three keys, not "five shell singletons".PALETTE_EXCLUSIONS carries exactly three shell-singleton entries (app:launcher, global:notifications, user:profile); nav:menu and global:search are not excluded at all, so they never enter this guard's loop. The blind set is app:launcher, global:notifications and — outside app-shell — record:chatter.
  2. app:launcher does not need an opt-in probe. It is opt-in-only in @object-ui/components, but since feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) #7091 app-shell registers the real renderer eagerly at module load. The probe finds it with no registerPlaceholders() call: app:launcher REG ns=app label=App Launcher.
  3. No live false claim exists. The only reasons matching CLAIMS_NO_RENDERER today are ai:chat_window and element:form, and a repo-wide grep for a registration of either (control: namespace: 'element', which returns many hits) returns zero. The defect was latent, as filed.

registerPlaceholders() is deliberately NOT called: it would make the file answer "has a renderer" for types that have only the dashed "Component Placeholder" scaffold (user:profile is one, measured). This repo's own language says the scaffold is not a renderer — views/app-launcher-renderer.tsx describes the state before it existed as "nothing rendered it, so a page that authored it drew a dashed box", with placeholders registered the whole time.

Mechanism chosen, and the cost that chose it

Marginal import cost, same harness and same baseline (after @object-ui/components + plugin-chatbot + plugin-form are loaded):

mechanismmarginal costfile duration end to end
the four app-shell renderer leaves553ms (251 + 164 + 128 + 10)9.4s
the package barrel ../../../../index.js6105ms15.1s

The barrel would track the package automatically, but it drags the console, marketplace, cloud and diagnostics graphs into a pure-logic gate in the cheap unit project — whose entire design point (vitest.config.mts) is not paying for graphs it does not touch. So: leaves, plus a derived guard that buys the barrel's one real advantage for 0ms.

the import set covers every page block app-shell registers reads the views/*-renderer.tsx leaf list from the directory, derives each leaf's registered key from its own ComponentRegistry.register(...) call, and requires every one to resolve. A seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted — deriving it is the point. It carries three controls of its own, because a derivation that finds nothing would pass vacuously and that is this file's whole subject: the leaf list must be non-empty, every leaf's key must be extractable, and the derived list must still contain app:launcher and global:notifications.

The barrel remains viable and cycle-free — it loaded and registered correctly in the probe — so this is a cost decision, not a feasibility one.

Positive probes — one per import, each shown hitting

Required by the finding's own text: "it needs one positive probe per new import or it guards vacuously."

importproberesult
@object-ui/components (pre-existing)element:texthits
@object-ui/plugin-chatbot (pre-existing)chatbothits
@object-ui/plugin-form (pre-existing)object-formhits
@object-ui/plugin-detail (new)record:chatterhits
views/app-launcher-renderer.js (new)app:launcher, derivedhits
views/global-notifications-renderer.js (new)global:notifications, derivedhits
views/global-search-renderer.js (new)global:search, derivedhits
views/nav-menu-renderer.js (new)nav:menu, derivedhits
views/record-approvals-renderer.js (new)record:approvals, derivedhits
views/record-attachments-renderer.js (new)record:attachments, derivedhits

All ten hit — the suite is green at 5/5, and every derived probe is an assertion inside the coverage test, so a miss is a red rather than a silent skip. Ablation A3 below proves that is true and not merely stated.

Both pre-existing anti-vacuity guards are preserved verbatim and neither is weakened: the registry under test is actually populated (guards a vacuous green) keeps its three probes and gains a fourth, and the ledger still contains a no-renderer claim to check (guards a vacuous loop) is untouched.

Post-fix ablations — all three predicted RED, all three observed RED

Run against the committed fix, so each restore is git checkout HEAD -- (absolute path) against a HEAD that already contains the implementation. Every leg proved its mutation on disk (anchor count moved, marker count moved, blob hash moved) and its restore by state (blob back to the HEAD blob, git diff HEAD and git diff --cached both 0 bytes).

A1 — the card's own false claim, the one that passed 4/4 in leg 0:

AssertionError: PALETTE_EXCLUSIONS['app:launcher'] says "no renderer ZZMUTZZ", but a renderer IS
registered for it. ... : expected [Function AppLauncherRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A2 — the same false claim on record:chatter, proving the plugin-detail import is not decorative:

AssertionError: PALETTE_EXCLUSIONS['record:chatter'] says "no renderer ZZMUTBB", but a renderer IS
registered for it. ... : expected [Function RecordChatterRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A3 — one renderer-leaf import deleted, proving the derived coverage guard bites:

AssertionError: views/record-attachments-renderer.tsx registers 'record:attachments' and this file
does not import it, so 'record:attachments' reads as UNREGISTERED here. Add
`import '../../../record-attachments-renderer.js';` to the side-effect imports above. ...
: expected undefined to be truthy
Tests 1 failed | 4 passed (5)

Each leg reports Tests 1 failed | 4 passed (5), so the suite really executed rather than collapsing to "no tests" behind a plausible exit 1.

Scope note — one bounded in-place addition, named

@object-ui/plugin-detail is outside the card's title ("excludes app-shell"), and it is included deliberately: record:chatter is a current ledger key, blind in the same loop, in the same file, for the same reason, and measured the same way (A2). The correct shape was already pinned by the two sibling suites in this same directory — palette-discussion-alias.test.tsx:50 and canvas-display-meta.test.tsx:44 both carry import '@object-ui/plugin-detail'; for exactly this question. Closing the app-shell half while leaving a known, measured blind key in the loop this PR certifies would reproduce the defect the card is about.

Gates

gateexitverdictevidence
vitest run on the guard file0greenTest Files 1 passed (1) / Tests 5 passed (5)
vitest run --project unit (whole project)0greenTest Files 801 passed (801) / Tests 12489 passed | 9 skipped (12498)
vitest run packages/app-shell/src/views/metadata-admin/previews/0greenTest Files 45 passed (45) / Tests 521 passed (521)
turbo run type-check --filter=@object-ui/app-shell0greenTasks: 30 successful, 30 total; the package script is tsc --noEmit && tsc -p tsconfig.test.json
eslint on the changed file0greenno output
check-changeset-presence0green"declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate"
check-changeset-no-major0green"No changeset declares a major bump."
check-control-bytes0green"OK (scanned 5899 tracked text file(s); skipped 85 binary)"

The whole-unit-project run is not incidental: the unit project runs with isolate: false, so the added side-effect registrations are visible to every other unit file sharing a worker. 801/801 green is the measurement that they leak into nothing.

Two notes on reading the evidence honestly:

  • The typecheck is a real measurement, not an assumed one. A first attempt reported Cannot find module '@object-ui/*' across files this PR never touches — an unbuilt workspace closure, which is NOT MEASURED rather than red. Routing through turbo builds the closure first, and --listFiles confirmed the edited file is actually in the tsconfig.test.json program (6 hits) rather than excluded like it is from tsconfig.json.
  • Every exit code above was captured by redirect-then-read, never through a pipe.

Not changed

No production code. The ledger text, the palette decisions and CLAIMS_NO_RENDERER are all untouched — this PR changes only what the guard can see. The changeset declares an empty frontmatter: nothing is released.


Generated by Claude Code

…rt set so a false "no renderer" cannot pass
`exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry`
whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its
coverage is bounded by its own side-effect import set. That set never grew to
include this package, which has registered page blocks since #6757 and #7091,
nor `@object-ui/plugin-detail`, which registers `record:chatter`.
Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to
'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER
regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a
renderer for it.
Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and
add a derived guard: the leaf list is read from the directory and every
registered key must resolve, so a seventh renderer leaf that is not imported
here reds this file instead of silently shrinking its coverage. A
hand-maintained list is what drifted; deriving it is the point.
The package barrel would track registrations automatically too, but costs
6105ms to load against 553ms for the leaves (same harness, same baseline) —
this is a pure-logic gate in the cheap `unit` project.
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)3152.3 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-Cten_jDQ.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)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.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 armed — 5 checks in_progress (4 test shards, Type Check). 25 completed, 0 failure. I arm on green.

⭐ I did your lookup, and it flips your open question

You asked the seat to check user:profile against objectstack#12183's phase list rather than guess, and recommended C for exactly that reason. Done, and the answer is not A:

objectstack#12183 is scoped, in its own title and body, to four members — nav:menu, global:search, global:notifications, app:launcher — and its sub_issues_summary reads 3 of 3 completed, 100%. user:profile appears nowhere in it. ⇒ It is not a later phase of that decomposition; it was never in scope, and the asymmetry is unplanned.

⭐ Filed as #7135, carrying your measurement and your framing, plus the constraint you would not have had: #12183's own Ask allows either a renderer or making it explicit in the spec and validate that these are not author-placeable — so a claimant must establish which answer is wanted before building either, possibly cross-repo.

This is the right hand-off shape. You measured what you could reach, named the one fact that decided between two actions, and refused to file blind on the other side of it. That is better than either guessing or dropping it.

⭐⭐ My at-risk population was wrong twice over, and you re-derived rather than accepted it

My order said "the five shell singletons". Measured: PALETTE_EXCLUSIONS holds 8 keys, of which exactly 3 are shell singletons. nav:menu and global:search are not excluded at all and never enter the guard's loop — so two of my five were never at risk.

And the blind set is 3, one of which is record:chatter — neither a shell singleton nor in app-shell at all, but in @object-ui/plugin-detail. ⇒ A fix scoped to "the shell singletons in app-shell", which is what my order described, would have left a third of the blind set uncovered while looking complete.

⭐ You caught that my other warning would have caused a false positive in the opposite direction

I warned that app:launcher registers only via opt-in PROTOCOL_COMPONENTS, so a probe that does not opt in would report it unregistered. Half true — of @object-ui/components. But since #7091, app-shell registers the real renderer eagerly, so the probe finds it with no opt-in.

⚠️ And opting in, as I implied you might need to, would have been actively wrong: registerPlaceholders() registers the dashed scaffold for ~120 protocol types, which would make the guard answer "has a renderer" for user:profile — whose only registration is that scaffold. My warning pointed at a real hazard and prescribed the move that creates a different one. Declining it, with the measurement, was correct.

The mechanism choice is a measurement, and the honest part is what it does not claim

Leaves 553ms vs barrel 6105ms — 11×, same baseline. And you say plainly that the barrel is viable and cycle-free, so this is a cost decision, not a feasibility one. ⭐ That distinction matters: "we couldn't" and "we chose not to" age very differently for the next reader.

The derived coverage guard is better than what I asked for. My fence was "one positive probe per added import, or it guards vacuously." You met that — seven probes, all hitting — and then closed the failure mode one level up: the guard reads the leaf list from the directory and derives each key from the leaf's own register() call, so a seventh renderer leaf reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted here, so restating one would have rebuilt the defect in the fix. Ablation A3 proves it: delete one leaf import and the guard names the file, the key, and the import line to add.

Rejecting the static manifest is right for the same reason, and well argued: a manifest is a second declaration of what the live registry already answers, and it would drift from the registrations the way the import set drifted. The guard's value is asking the same question the reason string answers, of the live registry.

The ablations, and one detail worth naming

Leg 0 reproduced the blindness before anything changed (predicted GREEN, observed GREEN — 'no renderer ZZMUTZZ' passing 4/4 while a renderer is registered). Post-fix, all three legs RED with specific named assertions, and each reporting Tests 1 failed | 4 passed (5) so the suite demonstrably executed rather than collapsing to "no tests" behind a plausible exit 1.

⚠️ That last check is not decoration — a sibling agent hit exactly that trap today (a JSX comment injected in attribute position, syntax error, Tests no tests, exit 1 that looked like the predicted red). Asserting the passing count alongside the failing one is what distinguishes them.

Both pre-existing anti-vacuity guards preserved verbatim, plus three new controls on the derived guard so it cannot pass empty. isolate:false leak risk addressed by running the whole unit project (801 files, 12489 tests green) rather than just the changed file — the right scope for a change that writes into a shared singleton.

#7134 — filed at the right level, and it is the same class again

The unit project's isolate:false is justified by a comment claiming "no ComponentRegistry or DOM state to leak across files" — and this guard now writes ten registrations into that shared singleton, while a packages/fields test asserts ComponentRegistry.get(...) is toBeUndefined(), an absence assertion a shared registry can silently satisfy.

⭐ That is the sixth instance this session of a comment certifying a property the code no longer has — and the most dangerous shape of it, because the thing it protects is a test's ability to fail. Correctly labelled latent, not live (801/801 green, measured), so it is prevention.


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.

finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green

2 participants

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

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass - #7133

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set
Sep 1, 2026
Merged

test(app-shell): widen the exclusion-reason truthfulness guard's import set so a false "no renderer" cannot pass#7133
os-warren merged 1 commit into
mainfrom
claude/issue-7117-truthfulness-guard-import-set

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7117

exclusion-reason-truthfulness.test.ts asks the runtime ComponentRegistry whether a PALETTE_EXCLUSIONS reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package — which has registered page blocks since #6757 and #7091 — nor @object-ui/plugin-detail.

All measurements below were taken on this branch; the union of gates was run at 778fb78b4 (the final commit).

Leg 0 — the blindness, reproduced before any fix

Predicted GREEN (the guard is blind), observed GREEN. On 44ea62d29, PALETTE_EXCLUSIONS['app:launcher'] set to 'no renderer ZZMUTZZ' — a string that DOES match the guard's CLAIMS_NO_RENDERER regex — while views/app-launcher-renderer.tsx registers a renderer for it:

ANCHOR_BEFORE=1 INJECTED_BEFORE=0 blob 186b4a2de89fcc150474b5861ee1e2d56bdc50a3
ANCHOR_AFTER=0 INJECTED_AFTER=1 blob 1734fcd9888fd864b379f4ab0b9e6b2d71fe95bd
=== MUTATION CONFIRMED ON DISK ===
Test Files 1 passed (1)
Tests 4 passed (4)
VITEST_EXIT=0

Restore proven by state, not by an exit code: blob_restored=186b4a2d… (back to the HEAD blob), git diff HEAD 0 bytes, git diff --cached 0 bytes, injected-marker count back to 0.

The at-risk population, re-derived

Measured with a throwaway probe that snapshots ComponentRegistry.getConfig(key) after each import step, rather than argued from source. All eight PALETTE_EXCLUSIONS keys:

ledger keyrenderer?registered byvisible to the OLD import set
app:launcheryes, real (ns=app)app-shell views/app-launcher-renderer.tsx, eager at module loadno — blind
global:notificationsyes, real (ns=global)app-shell views/global-notifications-renderer.tsx, eagerno — blind
record:chatteryes, real (ns=record)@object-ui/plugin-detail, eagerno — blind
element:record_pickeryes (ns=element)@object-ui/componentsyes
element:text_inputyes (ns=element)@object-ui/componentsyes
user:profileno real renderer; placeholder scaffold only under opt-in registerPlaceholders() (ns=protocol-placeholder)n/a
ai:chat_windownone anywheren/a
element:formnone anywheren/a

Three corrections to what the card assumed, all measured:

  1. It is three keys, not "five shell singletons".PALETTE_EXCLUSIONS carries exactly three shell-singleton entries (app:launcher, global:notifications, user:profile); nav:menu and global:search are not excluded at all, so they never enter this guard's loop. The blind set is app:launcher, global:notifications and — outside app-shell — record:chatter.
  2. app:launcher does not need an opt-in probe. It is opt-in-only in @object-ui/components, but since feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) #7091 app-shell registers the real renderer eagerly at module load. The probe finds it with no registerPlaceholders() call: app:launcher REG ns=app label=App Launcher.
  3. No live false claim exists. The only reasons matching CLAIMS_NO_RENDERER today are ai:chat_window and element:form, and a repo-wide grep for a registration of either (control: namespace: 'element', which returns many hits) returns zero. The defect was latent, as filed.

registerPlaceholders() is deliberately NOT called: it would make the file answer "has a renderer" for types that have only the dashed "Component Placeholder" scaffold (user:profile is one, measured). This repo's own language says the scaffold is not a renderer — views/app-launcher-renderer.tsx describes the state before it existed as "nothing rendered it, so a page that authored it drew a dashed box", with placeholders registered the whole time.

Mechanism chosen, and the cost that chose it

Marginal import cost, same harness and same baseline (after @object-ui/components + plugin-chatbot + plugin-form are loaded):

mechanismmarginal costfile duration end to end
the four app-shell renderer leaves553ms (251 + 164 + 128 + 10)9.4s
the package barrel ../../../../index.js6105ms15.1s

The barrel would track the package automatically, but it drags the console, marketplace, cloud and diagnostics graphs into a pure-logic gate in the cheap unit project — whose entire design point (vitest.config.mts) is not paying for graphs it does not touch. So: leaves, plus a derived guard that buys the barrel's one real advantage for 0ms.

the import set covers every page block app-shell registers reads the views/*-renderer.tsx leaf list from the directory, derives each leaf's registered key from its own ComponentRegistry.register(...) call, and requires every one to resolve. A seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted — deriving it is the point. It carries three controls of its own, because a derivation that finds nothing would pass vacuously and that is this file's whole subject: the leaf list must be non-empty, every leaf's key must be extractable, and the derived list must still contain app:launcher and global:notifications.

The barrel remains viable and cycle-free — it loaded and registered correctly in the probe — so this is a cost decision, not a feasibility one.

Positive probes — one per import, each shown hitting

Required by the finding's own text: "it needs one positive probe per new import or it guards vacuously."

importproberesult
@object-ui/components (pre-existing)element:texthits
@object-ui/plugin-chatbot (pre-existing)chatbothits
@object-ui/plugin-form (pre-existing)object-formhits
@object-ui/plugin-detail (new)record:chatterhits
views/app-launcher-renderer.js (new)app:launcher, derivedhits
views/global-notifications-renderer.js (new)global:notifications, derivedhits
views/global-search-renderer.js (new)global:search, derivedhits
views/nav-menu-renderer.js (new)nav:menu, derivedhits
views/record-approvals-renderer.js (new)record:approvals, derivedhits
views/record-attachments-renderer.js (new)record:attachments, derivedhits

All ten hit — the suite is green at 5/5, and every derived probe is an assertion inside the coverage test, so a miss is a red rather than a silent skip. Ablation A3 below proves that is true and not merely stated.

Both pre-existing anti-vacuity guards are preserved verbatim and neither is weakened: the registry under test is actually populated (guards a vacuous green) keeps its three probes and gains a fourth, and the ledger still contains a no-renderer claim to check (guards a vacuous loop) is untouched.

Post-fix ablations — all three predicted RED, all three observed RED

Run against the committed fix, so each restore is git checkout HEAD -- (absolute path) against a HEAD that already contains the implementation. Every leg proved its mutation on disk (anchor count moved, marker count moved, blob hash moved) and its restore by state (blob back to the HEAD blob, git diff HEAD and git diff --cached both 0 bytes).

A1 — the card's own false claim, the one that passed 4/4 in leg 0:

AssertionError: PALETTE_EXCLUSIONS['app:launcher'] says "no renderer ZZMUTZZ", but a renderer IS
registered for it. ... : expected [Function AppLauncherRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A2 — the same false claim on record:chatter, proving the plugin-detail import is not decorative:

AssertionError: PALETTE_EXCLUSIONS['record:chatter'] says "no renderer ZZMUTBB", but a renderer IS
registered for it. ... : expected [Function RecordChatterRenderer] to be falsy
Tests 1 failed | 4 passed (5)

A3 — one renderer-leaf import deleted, proving the derived coverage guard bites:

AssertionError: views/record-attachments-renderer.tsx registers 'record:attachments' and this file
does not import it, so 'record:attachments' reads as UNREGISTERED here. Add
`import '../../../record-attachments-renderer.js';` to the side-effect imports above. ...
: expected undefined to be truthy
Tests 1 failed | 4 passed (5)

Each leg reports Tests 1 failed | 4 passed (5), so the suite really executed rather than collapsing to "no tests" behind a plausible exit 1.

Scope note — one bounded in-place addition, named

@object-ui/plugin-detail is outside the card's title ("excludes app-shell"), and it is included deliberately: record:chatter is a current ledger key, blind in the same loop, in the same file, for the same reason, and measured the same way (A2). The correct shape was already pinned by the two sibling suites in this same directory — palette-discussion-alias.test.tsx:50 and canvas-display-meta.test.tsx:44 both carry import '@object-ui/plugin-detail'; for exactly this question. Closing the app-shell half while leaving a known, measured blind key in the loop this PR certifies would reproduce the defect the card is about.

Gates

gateexitverdictevidence
vitest run on the guard file0greenTest Files 1 passed (1) / Tests 5 passed (5)
vitest run --project unit (whole project)0greenTest Files 801 passed (801) / Tests 12489 passed | 9 skipped (12498)
vitest run packages/app-shell/src/views/metadata-admin/previews/0greenTest Files 45 passed (45) / Tests 521 passed (521)
turbo run type-check --filter=@object-ui/app-shell0greenTasks: 30 successful, 30 total; the package script is tsc --noEmit && tsc -p tsconfig.test.json
eslint on the changed file0greenno output
check-changeset-presence0green"declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate"
check-changeset-no-major0green"No changeset declares a major bump."
check-control-bytes0green"OK (scanned 5899 tracked text file(s); skipped 85 binary)"

The whole-unit-project run is not incidental: the unit project runs with isolate: false, so the added side-effect registrations are visible to every other unit file sharing a worker. 801/801 green is the measurement that they leak into nothing.

Two notes on reading the evidence honestly:

  • The typecheck is a real measurement, not an assumed one. A first attempt reported Cannot find module '@object-ui/*' across files this PR never touches — an unbuilt workspace closure, which is NOT MEASURED rather than red. Routing through turbo builds the closure first, and --listFiles confirmed the edited file is actually in the tsconfig.test.json program (6 hits) rather than excluded like it is from tsconfig.json.
  • Every exit code above was captured by redirect-then-read, never through a pipe.

Not changed

No production code. The ledger text, the palette decisions and CLAIMS_NO_RENDERER are all untouched — this PR changes only what the guard can see. The changeset declares an empty frontmatter: nothing is released.


Generated by Claude Code

…rt set so a false "no renderer" cannot pass
`exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry`
whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its
coverage is bounded by its own side-effect import set. That set never grew to
include this package, which has registered page blocks since #6757 and #7091,
nor `@object-ui/plugin-detail`, which registers `record:chatter`.
Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to
'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER
regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a
renderer for it.
Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and
add a derived guard: the leaf list is read from the directory and every
registered key must resolve, so a seventh renderer leaf that is not imported
here reds this file instead of silently shrinking its coverage. A
hand-maintained list is what drifted; deriving it is the point.
The package barrel would track registrations automatically too, but costs
6105ms to load against 553ms for the leaves (same harness, same baseline) —
this is a pure-logic gate in the cheap `unit` project.
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)3152.3 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-Cten_jDQ.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)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.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 armed — 5 checks in_progress (4 test shards, Type Check). 25 completed, 0 failure. I arm on green.

⭐ I did your lookup, and it flips your open question

You asked the seat to check user:profile against objectstack#12183's phase list rather than guess, and recommended C for exactly that reason. Done, and the answer is not A:

objectstack#12183 is scoped, in its own title and body, to four members — nav:menu, global:search, global:notifications, app:launcher — and its sub_issues_summary reads 3 of 3 completed, 100%. user:profile appears nowhere in it. ⇒ It is not a later phase of that decomposition; it was never in scope, and the asymmetry is unplanned.

⭐ Filed as #7135, carrying your measurement and your framing, plus the constraint you would not have had: #12183's own Ask allows either a renderer or making it explicit in the spec and validate that these are not author-placeable — so a claimant must establish which answer is wanted before building either, possibly cross-repo.

This is the right hand-off shape. You measured what you could reach, named the one fact that decided between two actions, and refused to file blind on the other side of it. That is better than either guessing or dropping it.

⭐⭐ My at-risk population was wrong twice over, and you re-derived rather than accepted it

My order said "the five shell singletons". Measured: PALETTE_EXCLUSIONS holds 8 keys, of which exactly 3 are shell singletons. nav:menu and global:search are not excluded at all and never enter the guard's loop — so two of my five were never at risk.

And the blind set is 3, one of which is record:chatter — neither a shell singleton nor in app-shell at all, but in @object-ui/plugin-detail. ⇒ A fix scoped to "the shell singletons in app-shell", which is what my order described, would have left a third of the blind set uncovered while looking complete.

⭐ You caught that my other warning would have caused a false positive in the opposite direction

I warned that app:launcher registers only via opt-in PROTOCOL_COMPONENTS, so a probe that does not opt in would report it unregistered. Half true — of @object-ui/components. But since #7091, app-shell registers the real renderer eagerly, so the probe finds it with no opt-in.

⚠️ And opting in, as I implied you might need to, would have been actively wrong: registerPlaceholders() registers the dashed scaffold for ~120 protocol types, which would make the guard answer "has a renderer" for user:profile — whose only registration is that scaffold. My warning pointed at a real hazard and prescribed the move that creates a different one. Declining it, with the measurement, was correct.

The mechanism choice is a measurement, and the honest part is what it does not claim

Leaves 553ms vs barrel 6105ms — 11×, same baseline. And you say plainly that the barrel is viable and cycle-free, so this is a cost decision, not a feasibility one. ⭐ That distinction matters: "we couldn't" and "we chose not to" age very differently for the next reader.

The derived coverage guard is better than what I asked for. My fence was "one positive probe per added import, or it guards vacuously." You met that — seven probes, all hitting — and then closed the failure mode one level up: the guard reads the leaf list from the directory and derives each key from the leaf's own register() call, so a seventh renderer leaf reds this file instead of silently shrinking its coverage. A hand-maintained list is exactly what drifted here, so restating one would have rebuilt the defect in the fix. Ablation A3 proves it: delete one leaf import and the guard names the file, the key, and the import line to add.

Rejecting the static manifest is right for the same reason, and well argued: a manifest is a second declaration of what the live registry already answers, and it would drift from the registrations the way the import set drifted. The guard's value is asking the same question the reason string answers, of the live registry.

The ablations, and one detail worth naming

Leg 0 reproduced the blindness before anything changed (predicted GREEN, observed GREEN — 'no renderer ZZMUTZZ' passing 4/4 while a renderer is registered). Post-fix, all three legs RED with specific named assertions, and each reporting Tests 1 failed | 4 passed (5) so the suite demonstrably executed rather than collapsing to "no tests" behind a plausible exit 1.

⚠️ That last check is not decoration — a sibling agent hit exactly that trap today (a JSX comment injected in attribute position, syntax error, Tests no tests, exit 1 that looked like the predicted red). Asserting the passing count alongside the failing one is what distinguishes them.

Both pre-existing anti-vacuity guards preserved verbatim, plus three new controls on the derived guard so it cannot pass empty. isolate:false leak risk addressed by running the whole unit project (801 files, 12489 tests green) rather than just the changed file — the right scope for a change that writes into a shared singleton.

#7134 — filed at the right level, and it is the same class again

The unit project's isolate:false is justified by a comment claiming "no ComponentRegistry or DOM state to leak across files" — and this guard now writes ten registrations into that shared singleton, while a packages/fields test asserts ComponentRegistry.get(...) is toBeUndefined(), an absence assertion a shared registry can silently satisfy.

⭐ That is the sixth instance this session of a comment certifying a property the code no longer has — and the most dangerous shape of it, because the thing it protects is a test's ability to fail. Correctly labelled latent, not live (801/801 green, measured), so it is prevention.


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.

finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green

2 participants

@os-warren@claude