test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors - #7118

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin
Sep 1, 2026
Merged

test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors#7118
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6969

One pin over the advisory chain's middle link, asserting the CONNECTION rather than either end of it: a server-sent advisory travels the real hook, the real client factory and the real MetadataClient, and arrives at the toast — for the save door and for the publish door.

Leg 0 — the card's precondition, re-measured on current main

The card required the claimant to re-measure that the gap is still real. It is.

Seam location — assumption falsified. The card and the dispatch both name packages/app-shell/src/hooks/useMetadata.ts. That file does not exist. The real seam is packages/app-shell/src/views/metadata-admin/useMetadata.ts — the line numbers 130-135 were right, the directory was not. packages/app-shell/src/hooks/useMetadataService.ts is a different thing (it mints a MetadataService from the adapter) and carries no advisory wiring.

Ablation — predicted direction stated before running: every named suite stays GREEN, and that green IS the finding.

Cut, at the seam the card names:

- () => createConsoleMetadataClient({ previewDrafts, environmentId, onSaveAdvisory }),
+ () => createConsoleMetadataClient({ previewDrafts, environmentId }),
value
predictedall named suites stay green
observedall named suites stayed green — 226 files / 2369 tests, zero reds
blob before4bbebbf78439ca088ba313a7a708d8a007535a32 (identical to the HEAD blob)
blob after7d2ade859f42f27f76723fea95c3d4810243b7f1
mutation provenanchor count for the deleted text 1 to 0, for the injected text 0 to 1, and the blob hash moved
restore provenby STATE: blob back to 4bbebbf78...andgit diff HEAD 0 bytes

Green suites through the cut (reported because a suite that survives an ablation tells you which layer it does NOT cover):

  • metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, onSaveAdvisory.test.ts — producer end, green
  • saveAdvisoryToast.test.ts — renderer end, green
  • MetadataService.saveAdvisories.test.ts, metadata-client-auth.ratchet.test.ts — green
  • all 220 suites in views/metadata-admin/ — green, 2299 passed / 1 skipped, byte-identical to the baseline

Red suites through the cut: none.

Why nothing could see it, measured: 51 suites vi.mock('./useMetadata') — legitimately, they test pages, not plumbing — and, before this PR, exactly 0 imported the real useMetadataClient. Control: 76 suites name useMetadataClient at all, so the zero is a reading, not a dead query. The seam was mocked away everywhere it appeared.

The pin

packages/app-shell/src/views/metadata-admin/useMetadataClient.advisorySink.test.tsx — 3 cases:

  1. SAVE doorsave() through the hook's client; the toast title matches /^Saved\b/ and carries the finding's message and rule.
  2. PUBLISH doorpublishDraft() through the same client (the method the console's own usePublishAllDrafts calls through this hook); title matches /^Published\b/.
  3. CONTROL — a clean 200 with no advisories says nothing at either door, and fetch is asserted to have been called twice, so the silence is about the empty advisory list and not about a chain that never ran.

The door verb is the load-bearing assertion: it is set by the producer and read by the renderer, so it only reads correctly if the discriminator survived the whole seam.

Real: the hook, its useCallback sink, the i18n t, the PreviewModeContext read, createConsoleMetadataClient, the authenticated fetch wrapper, the MetadataClient, its response parsing, readSaveAdvisories, emitSaveAdvisories. Stubbed, only these two: sonner (the terminal sink — the hook imports it as a module binding, so it cannot be handed over the way the renderer suite hands over its own) and globalThis.fetch (the server).

Scope — deliberately not a bigger test at either end. The warning tier, the per-finding formatting and the empty-list drop stay the renderer suite's; withEnvironment clone survival and response-shape filtering stay the producer suites' (both already pin them, both doors).

Pin ablation — it fails when the middle is cut, and passes when it is intact

Both halves of the middle link were cut, separately. Predicted before running: the two door cases go RED and the CONTROL case stays GREEN, because a severed chain also satisfies "the sink was not called" — which is exactly why the control alone could never have caught this.

legfileblob before to afterobserved
AuseMetadata.ts (the hand-off)4bbebbf78 to 7d2ade8592 failed / 1 passed — as predicted
BmetadataClientFactory.ts (the pass-through into the constructor)e0f04f9a4 to fddd1d5552 failed / 1 passed — as predicted

Both legs: mutation proven on disk by anchor counts and blob-hash movement; restore proven by state — blob back to the HEAD blob and git diff HEAD 0 bytes.

⚠️ Leg B was run twice and the first run is reported as VOID, not as a result: the verification used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them), so the guard refused the reading. The mutation had in fact landed; the check was broken. Re-run with a single-line anchor, which is the row above.

Gates

Run at final HEAD b5a989f0c. Verdict lines quoted from the gates themselves, never a bare exit code.

gateverdict
union regressionTest Files 227 passed (227) · Tests 2372 passed | 1 skipped (2373)
check-changeset-presenceEvery 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-majorNo changeset declares a major bump.
check-vi-mock-specifiersOK (4074 tracked source file(s), 2349 test-named; 516 carry a mock; ...)
check-vi-mock-inheritOK (... 114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked) ...)
check-control-bytesOK (scanned 5893 tracked text file(s); skipped 85 binary).
check-shell-escape-residueOK (4/4 root(s) resolved ...)
check-lint-coveragelint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coveragetest type-check coverage: 41/41 packages compile their tests, 0 declared debt
eslint (plain form)exit 0, no output
pnpm --filter @object-ui/app-shell run type-checkexit 0; output echoed tsc --noEmit && tsc -p tsconfig.test.json, so it was not a zero-match

The typecheck actually covers the new file, proven rather than assumed: tsc -p tsconfig.test.json --listFiles names it (1 hit) out of 4504 program files, with a control that must hit — the sibling providers/saveAdvisoryToast.test.ts, also 1 hit. That config sets paths: {}, so it resolves through built .d.ts; the 29-package dependency closure was built first, otherwise the result would have been NOT MEASURED rather than green.

The changeset carries an EMPTY frontmatter — the presence gate's own named exemption for a test-only change, quoted above. No skip-changeset label was applied: in this repo that label is a phantom (the object exists from a historical mislabel, but nothing reads it, and scripts/__tests__/ci-cd-pipeline-doc.test.ts fails if anything under .github/ or scripts/ ever wires it in).

Out of scope, filed separately

#7116 — the SIBLING advisory wiring in AdapterProvider.tsx (the ObjectStackAdapter channel, built by #4237) has the same unpinned middle link. Measured by grep with a control, not by ablation, and said so on the card. Not touched here.

Notes for review


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 02:06
…s the toast, both doors
The advisory chain's middle link was pinned by nothing: the producer suites
assert the event is emitted into a sink they build themselves, and the renderer
suite asserts the message is built over an event it writes by hand. Cutting the
hand-off in useMetadata.ts silenced both doors with 226 files / 2369 tests green.
Mounts the real hook, the real client factory and the real MetadataClient, and
asserts a server-sent advisory reaches the toast for save and for publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Empty frontmatter — the presence gate's explicit exemption for a test-only
change. Nothing published moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.4 KB3191.4 KB
Main entry chunk (gzip)142.3 KB350 KB
Entry fileindex-LPKhNev8.js
StatusPASS

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

Landing NOT yet armed. 8 checks are in_progress at b5a989f0c (4 test shards, Type Check, Doc Snippet Type Check, Lint, README Export Check). in_progress is not a pass and I will not flip ready until every check is green. Conflict pre-check is already done and clean — head is behind main by exactly 1 commit (#7109), merge-tree returns 0 conflicts (tree 1e3181ee3), and the merge queue rebuilds each entry on current main, so behind is not an action item here.

⭐ The seam path was wrong, and it was wrong in MY order

I relayed the card's packages/app-shell/src/hooks/useMetadata.ts without checking it. Verified independently just now:

proberesult
packages/app-shell/src/hooks/useMetadata.tsdoes not exist
packages/app-shell/src/hooks/useMetadataService.ts⚠️control✅ exists — so hooks/ is real and the probe is live
packages/app-shell/src/views/metadata-admin/useMetadata.ts✅ exists; onSaveAdvisory at 130 / 135 / 136

So the card's line numbers were exact and its directory was wrong — a combination that reads as authoritative and is the hardest kind to catch by skimming. My order told you the line numbers might have moved and to find the real location; it should also have told you the path itself was unverified. You checked the thing I asserted rather than the thing I flagged, which is the correct instinct.

⭐ The middle link is TWO hand-offs — the falsification that mattered most

My assumption said one seam. Confirmed independently: useMetadata.ts:135 passes onSaveAdvisory into the factory, and metadataClientFactory.ts:89 passes it into the MetadataClient constructor (...(onSaveAdvisory ? { onSaveAdvisory } : {})).

Ablating both is what makes the pin trustworthy. A pin proven against only one half would leave the other half cuttable with the pin still green — i.e. it would reproduce this card's own defect one level down. Both legs red (2 failed / 1 passed, as predicted) is the difference between a pin and a decoration.

⭐ The VOID leg is the best thing in this report

Leg B's first run used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them) — and your guard refused to render a verdict on it. The mutation had landed; the check was broken.

Reporting that as VOID rather than as a result is exactly right, and it is the third instance today on this lane of the same class: a broken instrument that still prints something plausible (rg -ril silently rewriting its own output on #7015; my own landing sweep printing "queue drained" from an echo while every git call errored). ⇒ Going into the seat's standing lessons as a named class.

The zero that carries the whole finding is properly controlled

"Before this PR, exactly 0 suites imported the real useMetadataClient" is paired, in the same sweep, with 76 suites naming it at all and 51 that vi.mock it away. So the zero is a reading, and it also explains itself: the seam was mocked out everywhere it appeared — which is why 226 files and 2369 tests could stay green through the cut.

That green-through-ablation is the finding, and it is reported as a finding rather than as reassurance. Producer end green, renderer end green, all 220 metadata-admin suites green, zero reds — byte-identical counts to baseline.

The control case earns its place

Asserting fetch was called twice alongside "toast not called" is what stops case 3 from being satisfiable by a severed chain. Without it, a cut seam would satisfy the control too — and a control that a broken system passes is not a control. Your own prediction said exactly this before running.

Changeset — EMPTY frontmatter, and it matches the rule I had to correct today

The gate's own exemption language is quoted rather than reasoned about: "Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."

⚠️ Note for the record: earlier today I gave #7015's dev a wrong version of this rule (I said docs cards owe an empty-frontmatter changeset; the gate there said none was owed at all). Your handling here — quote the verdict, don't infer it — is the corrected form. The skip-changeset phantom-label note is a useful extra: an existing label object that nothing reads is exactly the sort of thing an agent would reach for and get silently nothing from.

Typecheck coverage proven, not assumed

tsc -p tsconfig.test.json --listFiles names the new file (1 hit of 4504), with a control that must hit — the sibling saveAdvisoryToast.test.ts, also 1. That forecloses the "typecheck silently excluded the test files" false green, and the 29-package closure was built first so the result is green rather than NOT MEASURED.

#7116 — filed at the right confidence level

The sibling AdapterProvider.tsx wiring is reported as grep-measured with a control, not ablation-measured, and the card says so and asks its claimant to re-measure. ⭐ That is the correct epistemic label: it is exactly the shape of this card (an unpinned middle link) but it has not been proven by cutting it, and claiming otherwise would hand the next agent a premise that might not survive.


Re-checking CI shortly; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

1 participant

@os-warren
, '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): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors - #7118

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin
Sep 1, 2026
Merged

test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors#7118
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6969

One pin over the advisory chain's middle link, asserting the CONNECTION rather than either end of it: a server-sent advisory travels the real hook, the real client factory and the real MetadataClient, and arrives at the toast — for the save door and for the publish door.

Leg 0 — the card's precondition, re-measured on current main

The card required the claimant to re-measure that the gap is still real. It is.

Seam location — assumption falsified. The card and the dispatch both name packages/app-shell/src/hooks/useMetadata.ts. That file does not exist. The real seam is packages/app-shell/src/views/metadata-admin/useMetadata.ts — the line numbers 130-135 were right, the directory was not. packages/app-shell/src/hooks/useMetadataService.ts is a different thing (it mints a MetadataService from the adapter) and carries no advisory wiring.

Ablation — predicted direction stated before running: every named suite stays GREEN, and that green IS the finding.

Cut, at the seam the card names:

- () => createConsoleMetadataClient({ previewDrafts, environmentId, onSaveAdvisory }),
+ () => createConsoleMetadataClient({ previewDrafts, environmentId }),
value
predictedall named suites stay green
observedall named suites stayed green — 226 files / 2369 tests, zero reds
blob before4bbebbf78439ca088ba313a7a708d8a007535a32 (identical to the HEAD blob)
blob after7d2ade859f42f27f76723fea95c3d4810243b7f1
mutation provenanchor count for the deleted text 1 to 0, for the injected text 0 to 1, and the blob hash moved
restore provenby STATE: blob back to 4bbebbf78...andgit diff HEAD 0 bytes

Green suites through the cut (reported because a suite that survives an ablation tells you which layer it does NOT cover):

  • metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, onSaveAdvisory.test.ts — producer end, green
  • saveAdvisoryToast.test.ts — renderer end, green
  • MetadataService.saveAdvisories.test.ts, metadata-client-auth.ratchet.test.ts — green
  • all 220 suites in views/metadata-admin/ — green, 2299 passed / 1 skipped, byte-identical to the baseline

Red suites through the cut: none.

Why nothing could see it, measured: 51 suites vi.mock('./useMetadata') — legitimately, they test pages, not plumbing — and, before this PR, exactly 0 imported the real useMetadataClient. Control: 76 suites name useMetadataClient at all, so the zero is a reading, not a dead query. The seam was mocked away everywhere it appeared.

The pin

packages/app-shell/src/views/metadata-admin/useMetadataClient.advisorySink.test.tsx — 3 cases:

  1. SAVE doorsave() through the hook's client; the toast title matches /^Saved\b/ and carries the finding's message and rule.
  2. PUBLISH doorpublishDraft() through the same client (the method the console's own usePublishAllDrafts calls through this hook); title matches /^Published\b/.
  3. CONTROL — a clean 200 with no advisories says nothing at either door, and fetch is asserted to have been called twice, so the silence is about the empty advisory list and not about a chain that never ran.

The door verb is the load-bearing assertion: it is set by the producer and read by the renderer, so it only reads correctly if the discriminator survived the whole seam.

Real: the hook, its useCallback sink, the i18n t, the PreviewModeContext read, createConsoleMetadataClient, the authenticated fetch wrapper, the MetadataClient, its response parsing, readSaveAdvisories, emitSaveAdvisories. Stubbed, only these two: sonner (the terminal sink — the hook imports it as a module binding, so it cannot be handed over the way the renderer suite hands over its own) and globalThis.fetch (the server).

Scope — deliberately not a bigger test at either end. The warning tier, the per-finding formatting and the empty-list drop stay the renderer suite's; withEnvironment clone survival and response-shape filtering stay the producer suites' (both already pin them, both doors).

Pin ablation — it fails when the middle is cut, and passes when it is intact

Both halves of the middle link were cut, separately. Predicted before running: the two door cases go RED and the CONTROL case stays GREEN, because a severed chain also satisfies "the sink was not called" — which is exactly why the control alone could never have caught this.

legfileblob before to afterobserved
AuseMetadata.ts (the hand-off)4bbebbf78 to 7d2ade8592 failed / 1 passed — as predicted
BmetadataClientFactory.ts (the pass-through into the constructor)e0f04f9a4 to fddd1d5552 failed / 1 passed — as predicted

Both legs: mutation proven on disk by anchor counts and blob-hash movement; restore proven by state — blob back to the HEAD blob and git diff HEAD 0 bytes.

⚠️ Leg B was run twice and the first run is reported as VOID, not as a result: the verification used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them), so the guard refused the reading. The mutation had in fact landed; the check was broken. Re-run with a single-line anchor, which is the row above.

Gates

Run at final HEAD b5a989f0c. Verdict lines quoted from the gates themselves, never a bare exit code.

gateverdict
union regressionTest Files 227 passed (227) · Tests 2372 passed | 1 skipped (2373)
check-changeset-presenceEvery 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-majorNo changeset declares a major bump.
check-vi-mock-specifiersOK (4074 tracked source file(s), 2349 test-named; 516 carry a mock; ...)
check-vi-mock-inheritOK (... 114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked) ...)
check-control-bytesOK (scanned 5893 tracked text file(s); skipped 85 binary).
check-shell-escape-residueOK (4/4 root(s) resolved ...)
check-lint-coveragelint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coveragetest type-check coverage: 41/41 packages compile their tests, 0 declared debt
eslint (plain form)exit 0, no output
pnpm --filter @object-ui/app-shell run type-checkexit 0; output echoed tsc --noEmit && tsc -p tsconfig.test.json, so it was not a zero-match

The typecheck actually covers the new file, proven rather than assumed: tsc -p tsconfig.test.json --listFiles names it (1 hit) out of 4504 program files, with a control that must hit — the sibling providers/saveAdvisoryToast.test.ts, also 1 hit. That config sets paths: {}, so it resolves through built .d.ts; the 29-package dependency closure was built first, otherwise the result would have been NOT MEASURED rather than green.

The changeset carries an EMPTY frontmatter — the presence gate's own named exemption for a test-only change, quoted above. No skip-changeset label was applied: in this repo that label is a phantom (the object exists from a historical mislabel, but nothing reads it, and scripts/__tests__/ci-cd-pipeline-doc.test.ts fails if anything under .github/ or scripts/ ever wires it in).

Out of scope, filed separately

#7116 — the SIBLING advisory wiring in AdapterProvider.tsx (the ObjectStackAdapter channel, built by #4237) has the same unpinned middle link. Measured by grep with a control, not by ablation, and said so on the card. Not touched here.

Notes for review


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 02:06
…s the toast, both doors
The advisory chain's middle link was pinned by nothing: the producer suites
assert the event is emitted into a sink they build themselves, and the renderer
suite asserts the message is built over an event it writes by hand. Cutting the
hand-off in useMetadata.ts silenced both doors with 226 files / 2369 tests green.
Mounts the real hook, the real client factory and the real MetadataClient, and
asserts a server-sent advisory reaches the toast for save and for publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Empty frontmatter — the presence gate's explicit exemption for a test-only
change. Nothing published moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.4 KB3191.4 KB
Main entry chunk (gzip)142.3 KB350 KB
Entry fileindex-LPKhNev8.js
StatusPASS

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

Landing NOT yet armed. 8 checks are in_progress at b5a989f0c (4 test shards, Type Check, Doc Snippet Type Check, Lint, README Export Check). in_progress is not a pass and I will not flip ready until every check is green. Conflict pre-check is already done and clean — head is behind main by exactly 1 commit (#7109), merge-tree returns 0 conflicts (tree 1e3181ee3), and the merge queue rebuilds each entry on current main, so behind is not an action item here.

⭐ The seam path was wrong, and it was wrong in MY order

I relayed the card's packages/app-shell/src/hooks/useMetadata.ts without checking it. Verified independently just now:

proberesult
packages/app-shell/src/hooks/useMetadata.tsdoes not exist
packages/app-shell/src/hooks/useMetadataService.ts⚠️control✅ exists — so hooks/ is real and the probe is live
packages/app-shell/src/views/metadata-admin/useMetadata.ts✅ exists; onSaveAdvisory at 130 / 135 / 136

So the card's line numbers were exact and its directory was wrong — a combination that reads as authoritative and is the hardest kind to catch by skimming. My order told you the line numbers might have moved and to find the real location; it should also have told you the path itself was unverified. You checked the thing I asserted rather than the thing I flagged, which is the correct instinct.

⭐ The middle link is TWO hand-offs — the falsification that mattered most

My assumption said one seam. Confirmed independently: useMetadata.ts:135 passes onSaveAdvisory into the factory, and metadataClientFactory.ts:89 passes it into the MetadataClient constructor (...(onSaveAdvisory ? { onSaveAdvisory } : {})).

Ablating both is what makes the pin trustworthy. A pin proven against only one half would leave the other half cuttable with the pin still green — i.e. it would reproduce this card's own defect one level down. Both legs red (2 failed / 1 passed, as predicted) is the difference between a pin and a decoration.

⭐ The VOID leg is the best thing in this report

Leg B's first run used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them) — and your guard refused to render a verdict on it. The mutation had landed; the check was broken.

Reporting that as VOID rather than as a result is exactly right, and it is the third instance today on this lane of the same class: a broken instrument that still prints something plausible (rg -ril silently rewriting its own output on #7015; my own landing sweep printing "queue drained" from an echo while every git call errored). ⇒ Going into the seat's standing lessons as a named class.

The zero that carries the whole finding is properly controlled

"Before this PR, exactly 0 suites imported the real useMetadataClient" is paired, in the same sweep, with 76 suites naming it at all and 51 that vi.mock it away. So the zero is a reading, and it also explains itself: the seam was mocked out everywhere it appeared — which is why 226 files and 2369 tests could stay green through the cut.

That green-through-ablation is the finding, and it is reported as a finding rather than as reassurance. Producer end green, renderer end green, all 220 metadata-admin suites green, zero reds — byte-identical counts to baseline.

The control case earns its place

Asserting fetch was called twice alongside "toast not called" is what stops case 3 from being satisfiable by a severed chain. Without it, a cut seam would satisfy the control too — and a control that a broken system passes is not a control. Your own prediction said exactly this before running.

Changeset — EMPTY frontmatter, and it matches the rule I had to correct today

The gate's own exemption language is quoted rather than reasoned about: "Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."

⚠️ Note for the record: earlier today I gave #7015's dev a wrong version of this rule (I said docs cards owe an empty-frontmatter changeset; the gate there said none was owed at all). Your handling here — quote the verdict, don't infer it — is the corrected form. The skip-changeset phantom-label note is a useful extra: an existing label object that nothing reads is exactly the sort of thing an agent would reach for and get silently nothing from.

Typecheck coverage proven, not assumed

tsc -p tsconfig.test.json --listFiles names the new file (1 hit of 4504), with a control that must hit — the sibling saveAdvisoryToast.test.ts, also 1. That forecloses the "typecheck silently excluded the test files" false green, and the 29-package closure was built first so the result is green rather than NOT MEASURED.

#7116 — filed at the right confidence level

The sibling AdapterProvider.tsx wiring is reported as grep-measured with a control, not ablation-measured, and the card says so and asks its claimant to re-measure. ⭐ That is the correct epistemic label: it is exactly the shape of this card (an unpinned middle link) but it has not been proven by cutting it, and claiming otherwise would hand the next agent a premise that might not survive.


Re-checking CI shortly; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

1 participant

@os-warren
, '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): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors - #7118

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin
Sep 1, 2026
Merged

test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors#7118
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6969

One pin over the advisory chain's middle link, asserting the CONNECTION rather than either end of it: a server-sent advisory travels the real hook, the real client factory and the real MetadataClient, and arrives at the toast — for the save door and for the publish door.

Leg 0 — the card's precondition, re-measured on current main

The card required the claimant to re-measure that the gap is still real. It is.

Seam location — assumption falsified. The card and the dispatch both name packages/app-shell/src/hooks/useMetadata.ts. That file does not exist. The real seam is packages/app-shell/src/views/metadata-admin/useMetadata.ts — the line numbers 130-135 were right, the directory was not. packages/app-shell/src/hooks/useMetadataService.ts is a different thing (it mints a MetadataService from the adapter) and carries no advisory wiring.

Ablation — predicted direction stated before running: every named suite stays GREEN, and that green IS the finding.

Cut, at the seam the card names:

- () => createConsoleMetadataClient({ previewDrafts, environmentId, onSaveAdvisory }),
+ () => createConsoleMetadataClient({ previewDrafts, environmentId }),
value
predictedall named suites stay green
observedall named suites stayed green — 226 files / 2369 tests, zero reds
blob before4bbebbf78439ca088ba313a7a708d8a007535a32 (identical to the HEAD blob)
blob after7d2ade859f42f27f76723fea95c3d4810243b7f1
mutation provenanchor count for the deleted text 1 to 0, for the injected text 0 to 1, and the blob hash moved
restore provenby STATE: blob back to 4bbebbf78...andgit diff HEAD 0 bytes

Green suites through the cut (reported because a suite that survives an ablation tells you which layer it does NOT cover):

  • metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, onSaveAdvisory.test.ts — producer end, green
  • saveAdvisoryToast.test.ts — renderer end, green
  • MetadataService.saveAdvisories.test.ts, metadata-client-auth.ratchet.test.ts — green
  • all 220 suites in views/metadata-admin/ — green, 2299 passed / 1 skipped, byte-identical to the baseline

Red suites through the cut: none.

Why nothing could see it, measured: 51 suites vi.mock('./useMetadata') — legitimately, they test pages, not plumbing — and, before this PR, exactly 0 imported the real useMetadataClient. Control: 76 suites name useMetadataClient at all, so the zero is a reading, not a dead query. The seam was mocked away everywhere it appeared.

The pin

packages/app-shell/src/views/metadata-admin/useMetadataClient.advisorySink.test.tsx — 3 cases:

  1. SAVE doorsave() through the hook's client; the toast title matches /^Saved\b/ and carries the finding's message and rule.
  2. PUBLISH doorpublishDraft() through the same client (the method the console's own usePublishAllDrafts calls through this hook); title matches /^Published\b/.
  3. CONTROL — a clean 200 with no advisories says nothing at either door, and fetch is asserted to have been called twice, so the silence is about the empty advisory list and not about a chain that never ran.

The door verb is the load-bearing assertion: it is set by the producer and read by the renderer, so it only reads correctly if the discriminator survived the whole seam.

Real: the hook, its useCallback sink, the i18n t, the PreviewModeContext read, createConsoleMetadataClient, the authenticated fetch wrapper, the MetadataClient, its response parsing, readSaveAdvisories, emitSaveAdvisories. Stubbed, only these two: sonner (the terminal sink — the hook imports it as a module binding, so it cannot be handed over the way the renderer suite hands over its own) and globalThis.fetch (the server).

Scope — deliberately not a bigger test at either end. The warning tier, the per-finding formatting and the empty-list drop stay the renderer suite's; withEnvironment clone survival and response-shape filtering stay the producer suites' (both already pin them, both doors).

Pin ablation — it fails when the middle is cut, and passes when it is intact

Both halves of the middle link were cut, separately. Predicted before running: the two door cases go RED and the CONTROL case stays GREEN, because a severed chain also satisfies "the sink was not called" — which is exactly why the control alone could never have caught this.

legfileblob before to afterobserved
AuseMetadata.ts (the hand-off)4bbebbf78 to 7d2ade8592 failed / 1 passed — as predicted
BmetadataClientFactory.ts (the pass-through into the constructor)e0f04f9a4 to fddd1d5552 failed / 1 passed — as predicted

Both legs: mutation proven on disk by anchor counts and blob-hash movement; restore proven by state — blob back to the HEAD blob and git diff HEAD 0 bytes.

⚠️ Leg B was run twice and the first run is reported as VOID, not as a result: the verification used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them), so the guard refused the reading. The mutation had in fact landed; the check was broken. Re-run with a single-line anchor, which is the row above.

Gates

Run at final HEAD b5a989f0c. Verdict lines quoted from the gates themselves, never a bare exit code.

gateverdict
union regressionTest Files 227 passed (227) · Tests 2372 passed | 1 skipped (2373)
check-changeset-presenceEvery 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-majorNo changeset declares a major bump.
check-vi-mock-specifiersOK (4074 tracked source file(s), 2349 test-named; 516 carry a mock; ...)
check-vi-mock-inheritOK (... 114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked) ...)
check-control-bytesOK (scanned 5893 tracked text file(s); skipped 85 binary).
check-shell-escape-residueOK (4/4 root(s) resolved ...)
check-lint-coveragelint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coveragetest type-check coverage: 41/41 packages compile their tests, 0 declared debt
eslint (plain form)exit 0, no output
pnpm --filter @object-ui/app-shell run type-checkexit 0; output echoed tsc --noEmit && tsc -p tsconfig.test.json, so it was not a zero-match

The typecheck actually covers the new file, proven rather than assumed: tsc -p tsconfig.test.json --listFiles names it (1 hit) out of 4504 program files, with a control that must hit — the sibling providers/saveAdvisoryToast.test.ts, also 1 hit. That config sets paths: {}, so it resolves through built .d.ts; the 29-package dependency closure was built first, otherwise the result would have been NOT MEASURED rather than green.

The changeset carries an EMPTY frontmatter — the presence gate's own named exemption for a test-only change, quoted above. No skip-changeset label was applied: in this repo that label is a phantom (the object exists from a historical mislabel, but nothing reads it, and scripts/__tests__/ci-cd-pipeline-doc.test.ts fails if anything under .github/ or scripts/ ever wires it in).

Out of scope, filed separately

#7116 — the SIBLING advisory wiring in AdapterProvider.tsx (the ObjectStackAdapter channel, built by #4237) has the same unpinned middle link. Measured by grep with a control, not by ablation, and said so on the card. Not touched here.

Notes for review


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 02:06
…s the toast, both doors
The advisory chain's middle link was pinned by nothing: the producer suites
assert the event is emitted into a sink they build themselves, and the renderer
suite asserts the message is built over an event it writes by hand. Cutting the
hand-off in useMetadata.ts silenced both doors with 226 files / 2369 tests green.
Mounts the real hook, the real client factory and the real MetadataClient, and
asserts a server-sent advisory reaches the toast for save and for publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Empty frontmatter — the presence gate's explicit exemption for a test-only
change. Nothing published moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.4 KB3191.4 KB
Main entry chunk (gzip)142.3 KB350 KB
Entry fileindex-LPKhNev8.js
StatusPASS

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

Landing NOT yet armed. 8 checks are in_progress at b5a989f0c (4 test shards, Type Check, Doc Snippet Type Check, Lint, README Export Check). in_progress is not a pass and I will not flip ready until every check is green. Conflict pre-check is already done and clean — head is behind main by exactly 1 commit (#7109), merge-tree returns 0 conflicts (tree 1e3181ee3), and the merge queue rebuilds each entry on current main, so behind is not an action item here.

⭐ The seam path was wrong, and it was wrong in MY order

I relayed the card's packages/app-shell/src/hooks/useMetadata.ts without checking it. Verified independently just now:

proberesult
packages/app-shell/src/hooks/useMetadata.tsdoes not exist
packages/app-shell/src/hooks/useMetadataService.ts⚠️control✅ exists — so hooks/ is real and the probe is live
packages/app-shell/src/views/metadata-admin/useMetadata.ts✅ exists; onSaveAdvisory at 130 / 135 / 136

So the card's line numbers were exact and its directory was wrong — a combination that reads as authoritative and is the hardest kind to catch by skimming. My order told you the line numbers might have moved and to find the real location; it should also have told you the path itself was unverified. You checked the thing I asserted rather than the thing I flagged, which is the correct instinct.

⭐ The middle link is TWO hand-offs — the falsification that mattered most

My assumption said one seam. Confirmed independently: useMetadata.ts:135 passes onSaveAdvisory into the factory, and metadataClientFactory.ts:89 passes it into the MetadataClient constructor (...(onSaveAdvisory ? { onSaveAdvisory } : {})).

Ablating both is what makes the pin trustworthy. A pin proven against only one half would leave the other half cuttable with the pin still green — i.e. it would reproduce this card's own defect one level down. Both legs red (2 failed / 1 passed, as predicted) is the difference between a pin and a decoration.

⭐ The VOID leg is the best thing in this report

Leg B's first run used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them) — and your guard refused to render a verdict on it. The mutation had landed; the check was broken.

Reporting that as VOID rather than as a result is exactly right, and it is the third instance today on this lane of the same class: a broken instrument that still prints something plausible (rg -ril silently rewriting its own output on #7015; my own landing sweep printing "queue drained" from an echo while every git call errored). ⇒ Going into the seat's standing lessons as a named class.

The zero that carries the whole finding is properly controlled

"Before this PR, exactly 0 suites imported the real useMetadataClient" is paired, in the same sweep, with 76 suites naming it at all and 51 that vi.mock it away. So the zero is a reading, and it also explains itself: the seam was mocked out everywhere it appeared — which is why 226 files and 2369 tests could stay green through the cut.

That green-through-ablation is the finding, and it is reported as a finding rather than as reassurance. Producer end green, renderer end green, all 220 metadata-admin suites green, zero reds — byte-identical counts to baseline.

The control case earns its place

Asserting fetch was called twice alongside "toast not called" is what stops case 3 from being satisfiable by a severed chain. Without it, a cut seam would satisfy the control too — and a control that a broken system passes is not a control. Your own prediction said exactly this before running.

Changeset — EMPTY frontmatter, and it matches the rule I had to correct today

The gate's own exemption language is quoted rather than reasoned about: "Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."

⚠️ Note for the record: earlier today I gave #7015's dev a wrong version of this rule (I said docs cards owe an empty-frontmatter changeset; the gate there said none was owed at all). Your handling here — quote the verdict, don't infer it — is the corrected form. The skip-changeset phantom-label note is a useful extra: an existing label object that nothing reads is exactly the sort of thing an agent would reach for and get silently nothing from.

Typecheck coverage proven, not assumed

tsc -p tsconfig.test.json --listFiles names the new file (1 hit of 4504), with a control that must hit — the sibling saveAdvisoryToast.test.ts, also 1. That forecloses the "typecheck silently excluded the test files" false green, and the 29-package closure was built first so the result is green rather than NOT MEASURED.

#7116 — filed at the right confidence level

The sibling AdapterProvider.tsx wiring is reported as grep-measured with a control, not ablation-measured, and the card says so and asks its claimant to re-measure. ⭐ That is the correct epistemic label: it is exactly the shape of this card (an unpinned middle link) but it has not been proven by cutting it, and claiming otherwise would hand the next agent a premise that might not survive.


Re-checking CI shortly; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

1 participant

@os-warren
, '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): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors - #7118

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin
Sep 1, 2026
Merged

test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors#7118
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6969

One pin over the advisory chain's middle link, asserting the CONNECTION rather than either end of it: a server-sent advisory travels the real hook, the real client factory and the real MetadataClient, and arrives at the toast — for the save door and for the publish door.

Leg 0 — the card's precondition, re-measured on current main

The card required the claimant to re-measure that the gap is still real. It is.

Seam location — assumption falsified. The card and the dispatch both name packages/app-shell/src/hooks/useMetadata.ts. That file does not exist. The real seam is packages/app-shell/src/views/metadata-admin/useMetadata.ts — the line numbers 130-135 were right, the directory was not. packages/app-shell/src/hooks/useMetadataService.ts is a different thing (it mints a MetadataService from the adapter) and carries no advisory wiring.

Ablation — predicted direction stated before running: every named suite stays GREEN, and that green IS the finding.

Cut, at the seam the card names:

- () => createConsoleMetadataClient({ previewDrafts, environmentId, onSaveAdvisory }),
+ () => createConsoleMetadataClient({ previewDrafts, environmentId }),
value
predictedall named suites stay green
observedall named suites stayed green — 226 files / 2369 tests, zero reds
blob before4bbebbf78439ca088ba313a7a708d8a007535a32 (identical to the HEAD blob)
blob after7d2ade859f42f27f76723fea95c3d4810243b7f1
mutation provenanchor count for the deleted text 1 to 0, for the injected text 0 to 1, and the blob hash moved
restore provenby STATE: blob back to 4bbebbf78...andgit diff HEAD 0 bytes

Green suites through the cut (reported because a suite that survives an ablation tells you which layer it does NOT cover):

  • metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, onSaveAdvisory.test.ts — producer end, green
  • saveAdvisoryToast.test.ts — renderer end, green
  • MetadataService.saveAdvisories.test.ts, metadata-client-auth.ratchet.test.ts — green
  • all 220 suites in views/metadata-admin/ — green, 2299 passed / 1 skipped, byte-identical to the baseline

Red suites through the cut: none.

Why nothing could see it, measured: 51 suites vi.mock('./useMetadata') — legitimately, they test pages, not plumbing — and, before this PR, exactly 0 imported the real useMetadataClient. Control: 76 suites name useMetadataClient at all, so the zero is a reading, not a dead query. The seam was mocked away everywhere it appeared.

The pin

packages/app-shell/src/views/metadata-admin/useMetadataClient.advisorySink.test.tsx — 3 cases:

  1. SAVE doorsave() through the hook's client; the toast title matches /^Saved\b/ and carries the finding's message and rule.
  2. PUBLISH doorpublishDraft() through the same client (the method the console's own usePublishAllDrafts calls through this hook); title matches /^Published\b/.
  3. CONTROL — a clean 200 with no advisories says nothing at either door, and fetch is asserted to have been called twice, so the silence is about the empty advisory list and not about a chain that never ran.

The door verb is the load-bearing assertion: it is set by the producer and read by the renderer, so it only reads correctly if the discriminator survived the whole seam.

Real: the hook, its useCallback sink, the i18n t, the PreviewModeContext read, createConsoleMetadataClient, the authenticated fetch wrapper, the MetadataClient, its response parsing, readSaveAdvisories, emitSaveAdvisories. Stubbed, only these two: sonner (the terminal sink — the hook imports it as a module binding, so it cannot be handed over the way the renderer suite hands over its own) and globalThis.fetch (the server).

Scope — deliberately not a bigger test at either end. The warning tier, the per-finding formatting and the empty-list drop stay the renderer suite's; withEnvironment clone survival and response-shape filtering stay the producer suites' (both already pin them, both doors).

Pin ablation — it fails when the middle is cut, and passes when it is intact

Both halves of the middle link were cut, separately. Predicted before running: the two door cases go RED and the CONTROL case stays GREEN, because a severed chain also satisfies "the sink was not called" — which is exactly why the control alone could never have caught this.

legfileblob before to afterobserved
AuseMetadata.ts (the hand-off)4bbebbf78 to 7d2ade8592 failed / 1 passed — as predicted
BmetadataClientFactory.ts (the pass-through into the constructor)e0f04f9a4 to fddd1d5552 failed / 1 passed — as predicted

Both legs: mutation proven on disk by anchor counts and blob-hash movement; restore proven by state — blob back to the HEAD blob and git diff HEAD 0 bytes.

⚠️ Leg B was run twice and the first run is reported as VOID, not as a result: the verification used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them), so the guard refused the reading. The mutation had in fact landed; the check was broken. Re-run with a single-line anchor, which is the row above.

Gates

Run at final HEAD b5a989f0c. Verdict lines quoted from the gates themselves, never a bare exit code.

gateverdict
union regressionTest Files 227 passed (227) · Tests 2372 passed | 1 skipped (2373)
check-changeset-presenceEvery 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-majorNo changeset declares a major bump.
check-vi-mock-specifiersOK (4074 tracked source file(s), 2349 test-named; 516 carry a mock; ...)
check-vi-mock-inheritOK (... 114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked) ...)
check-control-bytesOK (scanned 5893 tracked text file(s); skipped 85 binary).
check-shell-escape-residueOK (4/4 root(s) resolved ...)
check-lint-coveragelint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coveragetest type-check coverage: 41/41 packages compile their tests, 0 declared debt
eslint (plain form)exit 0, no output
pnpm --filter @object-ui/app-shell run type-checkexit 0; output echoed tsc --noEmit && tsc -p tsconfig.test.json, so it was not a zero-match

The typecheck actually covers the new file, proven rather than assumed: tsc -p tsconfig.test.json --listFiles names it (1 hit) out of 4504 program files, with a control that must hit — the sibling providers/saveAdvisoryToast.test.ts, also 1 hit. That config sets paths: {}, so it resolves through built .d.ts; the 29-package dependency closure was built first, otherwise the result would have been NOT MEASURED rather than green.

The changeset carries an EMPTY frontmatter — the presence gate's own named exemption for a test-only change, quoted above. No skip-changeset label was applied: in this repo that label is a phantom (the object exists from a historical mislabel, but nothing reads it, and scripts/__tests__/ci-cd-pipeline-doc.test.ts fails if anything under .github/ or scripts/ ever wires it in).

Out of scope, filed separately

#7116 — the SIBLING advisory wiring in AdapterProvider.tsx (the ObjectStackAdapter channel, built by #4237) has the same unpinned middle link. Measured by grep with a control, not by ablation, and said so on the card. Not touched here.

Notes for review


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 02:06
…s the toast, both doors
The advisory chain's middle link was pinned by nothing: the producer suites
assert the event is emitted into a sink they build themselves, and the renderer
suite asserts the message is built over an event it writes by hand. Cutting the
hand-off in useMetadata.ts silenced both doors with 226 files / 2369 tests green.
Mounts the real hook, the real client factory and the real MetadataClient, and
asserts a server-sent advisory reaches the toast for save and for publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Empty frontmatter — the presence gate's explicit exemption for a test-only
change. Nothing published moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.4 KB3191.4 KB
Main entry chunk (gzip)142.3 KB350 KB
Entry fileindex-LPKhNev8.js
StatusPASS

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

Landing NOT yet armed. 8 checks are in_progress at b5a989f0c (4 test shards, Type Check, Doc Snippet Type Check, Lint, README Export Check). in_progress is not a pass and I will not flip ready until every check is green. Conflict pre-check is already done and clean — head is behind main by exactly 1 commit (#7109), merge-tree returns 0 conflicts (tree 1e3181ee3), and the merge queue rebuilds each entry on current main, so behind is not an action item here.

⭐ The seam path was wrong, and it was wrong in MY order

I relayed the card's packages/app-shell/src/hooks/useMetadata.ts without checking it. Verified independently just now:

proberesult
packages/app-shell/src/hooks/useMetadata.tsdoes not exist
packages/app-shell/src/hooks/useMetadataService.ts⚠️control✅ exists — so hooks/ is real and the probe is live
packages/app-shell/src/views/metadata-admin/useMetadata.ts✅ exists; onSaveAdvisory at 130 / 135 / 136

So the card's line numbers were exact and its directory was wrong — a combination that reads as authoritative and is the hardest kind to catch by skimming. My order told you the line numbers might have moved and to find the real location; it should also have told you the path itself was unverified. You checked the thing I asserted rather than the thing I flagged, which is the correct instinct.

⭐ The middle link is TWO hand-offs — the falsification that mattered most

My assumption said one seam. Confirmed independently: useMetadata.ts:135 passes onSaveAdvisory into the factory, and metadataClientFactory.ts:89 passes it into the MetadataClient constructor (...(onSaveAdvisory ? { onSaveAdvisory } : {})).

Ablating both is what makes the pin trustworthy. A pin proven against only one half would leave the other half cuttable with the pin still green — i.e. it would reproduce this card's own defect one level down. Both legs red (2 failed / 1 passed, as predicted) is the difference between a pin and a decoration.

⭐ The VOID leg is the best thing in this report

Leg B's first run used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them) — and your guard refused to render a verdict on it. The mutation had landed; the check was broken.

Reporting that as VOID rather than as a result is exactly right, and it is the third instance today on this lane of the same class: a broken instrument that still prints something plausible (rg -ril silently rewriting its own output on #7015; my own landing sweep printing "queue drained" from an echo while every git call errored). ⇒ Going into the seat's standing lessons as a named class.

The zero that carries the whole finding is properly controlled

"Before this PR, exactly 0 suites imported the real useMetadataClient" is paired, in the same sweep, with 76 suites naming it at all and 51 that vi.mock it away. So the zero is a reading, and it also explains itself: the seam was mocked out everywhere it appeared — which is why 226 files and 2369 tests could stay green through the cut.

That green-through-ablation is the finding, and it is reported as a finding rather than as reassurance. Producer end green, renderer end green, all 220 metadata-admin suites green, zero reds — byte-identical counts to baseline.

The control case earns its place

Asserting fetch was called twice alongside "toast not called" is what stops case 3 from being satisfiable by a severed chain. Without it, a cut seam would satisfy the control too — and a control that a broken system passes is not a control. Your own prediction said exactly this before running.

Changeset — EMPTY frontmatter, and it matches the rule I had to correct today

The gate's own exemption language is quoted rather than reasoned about: "Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."

⚠️ Note for the record: earlier today I gave #7015's dev a wrong version of this rule (I said docs cards owe an empty-frontmatter changeset; the gate there said none was owed at all). Your handling here — quote the verdict, don't infer it — is the corrected form. The skip-changeset phantom-label note is a useful extra: an existing label object that nothing reads is exactly the sort of thing an agent would reach for and get silently nothing from.

Typecheck coverage proven, not assumed

tsc -p tsconfig.test.json --listFiles names the new file (1 hit of 4504), with a control that must hit — the sibling saveAdvisoryToast.test.ts, also 1. That forecloses the "typecheck silently excluded the test files" false green, and the 29-package closure was built first so the result is green rather than NOT MEASURED.

#7116 — filed at the right confidence level

The sibling AdapterProvider.tsx wiring is reported as grep-measured with a control, not ablation-measured, and the card says so and asks its claimant to re-measure. ⭐ That is the correct epistemic label: it is exactly the shape of this card (an unpinned middle link) but it has not been proven by cutting it, and claiming otherwise would hand the next agent a premise that might not survive.


Re-checking CI shortly; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

1 participant

@os-warren
, '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): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors - #7118

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin
Sep 1, 2026
Merged

test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors#7118
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6969

One pin over the advisory chain's middle link, asserting the CONNECTION rather than either end of it: a server-sent advisory travels the real hook, the real client factory and the real MetadataClient, and arrives at the toast — for the save door and for the publish door.

Leg 0 — the card's precondition, re-measured on current main

The card required the claimant to re-measure that the gap is still real. It is.

Seam location — assumption falsified. The card and the dispatch both name packages/app-shell/src/hooks/useMetadata.ts. That file does not exist. The real seam is packages/app-shell/src/views/metadata-admin/useMetadata.ts — the line numbers 130-135 were right, the directory was not. packages/app-shell/src/hooks/useMetadataService.ts is a different thing (it mints a MetadataService from the adapter) and carries no advisory wiring.

Ablation — predicted direction stated before running: every named suite stays GREEN, and that green IS the finding.

Cut, at the seam the card names:

- () => createConsoleMetadataClient({ previewDrafts, environmentId, onSaveAdvisory }),
+ () => createConsoleMetadataClient({ previewDrafts, environmentId }),
value
predictedall named suites stay green
observedall named suites stayed green — 226 files / 2369 tests, zero reds
blob before4bbebbf78439ca088ba313a7a708d8a007535a32 (identical to the HEAD blob)
blob after7d2ade859f42f27f76723fea95c3d4810243b7f1
mutation provenanchor count for the deleted text 1 to 0, for the injected text 0 to 1, and the blob hash moved
restore provenby STATE: blob back to 4bbebbf78...andgit diff HEAD 0 bytes

Green suites through the cut (reported because a suite that survives an ablation tells you which layer it does NOT cover):

  • metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, onSaveAdvisory.test.ts — producer end, green
  • saveAdvisoryToast.test.ts — renderer end, green
  • MetadataService.saveAdvisories.test.ts, metadata-client-auth.ratchet.test.ts — green
  • all 220 suites in views/metadata-admin/ — green, 2299 passed / 1 skipped, byte-identical to the baseline

Red suites through the cut: none.

Why nothing could see it, measured: 51 suites vi.mock('./useMetadata') — legitimately, they test pages, not plumbing — and, before this PR, exactly 0 imported the real useMetadataClient. Control: 76 suites name useMetadataClient at all, so the zero is a reading, not a dead query. The seam was mocked away everywhere it appeared.

The pin

packages/app-shell/src/views/metadata-admin/useMetadataClient.advisorySink.test.tsx — 3 cases:

  1. SAVE doorsave() through the hook's client; the toast title matches /^Saved\b/ and carries the finding's message and rule.
  2. PUBLISH doorpublishDraft() through the same client (the method the console's own usePublishAllDrafts calls through this hook); title matches /^Published\b/.
  3. CONTROL — a clean 200 with no advisories says nothing at either door, and fetch is asserted to have been called twice, so the silence is about the empty advisory list and not about a chain that never ran.

The door verb is the load-bearing assertion: it is set by the producer and read by the renderer, so it only reads correctly if the discriminator survived the whole seam.

Real: the hook, its useCallback sink, the i18n t, the PreviewModeContext read, createConsoleMetadataClient, the authenticated fetch wrapper, the MetadataClient, its response parsing, readSaveAdvisories, emitSaveAdvisories. Stubbed, only these two: sonner (the terminal sink — the hook imports it as a module binding, so it cannot be handed over the way the renderer suite hands over its own) and globalThis.fetch (the server).

Scope — deliberately not a bigger test at either end. The warning tier, the per-finding formatting and the empty-list drop stay the renderer suite's; withEnvironment clone survival and response-shape filtering stay the producer suites' (both already pin them, both doors).

Pin ablation — it fails when the middle is cut, and passes when it is intact

Both halves of the middle link were cut, separately. Predicted before running: the two door cases go RED and the CONTROL case stays GREEN, because a severed chain also satisfies "the sink was not called" — which is exactly why the control alone could never have caught this.

legfileblob before to afterobserved
AuseMetadata.ts (the hand-off)4bbebbf78 to 7d2ade8592 failed / 1 passed — as predicted
BmetadataClientFactory.ts (the pass-through into the constructor)e0f04f9a4 to fddd1d5552 failed / 1 passed — as predicted

Both legs: mutation proven on disk by anchor counts and blob-hash movement; restore proven by state — blob back to the HEAD blob and git diff HEAD 0 bytes.

⚠️ Leg B was run twice and the first run is reported as VOID, not as a result: the verification used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them), so the guard refused the reading. The mutation had in fact landed; the check was broken. Re-run with a single-line anchor, which is the row above.

Gates

Run at final HEAD b5a989f0c. Verdict lines quoted from the gates themselves, never a bare exit code.

gateverdict
union regressionTest Files 227 passed (227) · Tests 2372 passed | 1 skipped (2373)
check-changeset-presenceEvery 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-majorNo changeset declares a major bump.
check-vi-mock-specifiersOK (4074 tracked source file(s), 2349 test-named; 516 carry a mock; ...)
check-vi-mock-inheritOK (... 114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked) ...)
check-control-bytesOK (scanned 5893 tracked text file(s); skipped 85 binary).
check-shell-escape-residueOK (4/4 root(s) resolved ...)
check-lint-coveragelint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coveragetest type-check coverage: 41/41 packages compile their tests, 0 declared debt
eslint (plain form)exit 0, no output
pnpm --filter @object-ui/app-shell run type-checkexit 0; output echoed tsc --noEmit && tsc -p tsconfig.test.json, so it was not a zero-match

The typecheck actually covers the new file, proven rather than assumed: tsc -p tsconfig.test.json --listFiles names it (1 hit) out of 4504 program files, with a control that must hit — the sibling providers/saveAdvisoryToast.test.ts, also 1 hit. That config sets paths: {}, so it resolves through built .d.ts; the 29-package dependency closure was built first, otherwise the result would have been NOT MEASURED rather than green.

The changeset carries an EMPTY frontmatter — the presence gate's own named exemption for a test-only change, quoted above. No skip-changeset label was applied: in this repo that label is a phantom (the object exists from a historical mislabel, but nothing reads it, and scripts/__tests__/ci-cd-pipeline-doc.test.ts fails if anything under .github/ or scripts/ ever wires it in).

Out of scope, filed separately

#7116 — the SIBLING advisory wiring in AdapterProvider.tsx (the ObjectStackAdapter channel, built by #4237) has the same unpinned middle link. Measured by grep with a control, not by ablation, and said so on the card. Not touched here.

Notes for review


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 02:06
…s the toast, both doors
The advisory chain's middle link was pinned by nothing: the producer suites
assert the event is emitted into a sink they build themselves, and the renderer
suite asserts the message is built over an event it writes by hand. Cutting the
hand-off in useMetadata.ts silenced both doors with 226 files / 2369 tests green.
Mounts the real hook, the real client factory and the real MetadataClient, and
asserts a server-sent advisory reaches the toast for save and for publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Empty frontmatter — the presence gate's explicit exemption for a test-only
change. Nothing published moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.4 KB3191.4 KB
Main entry chunk (gzip)142.3 KB350 KB
Entry fileindex-LPKhNev8.js
StatusPASS

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

Landing NOT yet armed. 8 checks are in_progress at b5a989f0c (4 test shards, Type Check, Doc Snippet Type Check, Lint, README Export Check). in_progress is not a pass and I will not flip ready until every check is green. Conflict pre-check is already done and clean — head is behind main by exactly 1 commit (#7109), merge-tree returns 0 conflicts (tree 1e3181ee3), and the merge queue rebuilds each entry on current main, so behind is not an action item here.

⭐ The seam path was wrong, and it was wrong in MY order

I relayed the card's packages/app-shell/src/hooks/useMetadata.ts without checking it. Verified independently just now:

proberesult
packages/app-shell/src/hooks/useMetadata.tsdoes not exist
packages/app-shell/src/hooks/useMetadataService.ts⚠️control✅ exists — so hooks/ is real and the probe is live
packages/app-shell/src/views/metadata-admin/useMetadata.ts✅ exists; onSaveAdvisory at 130 / 135 / 136

So the card's line numbers were exact and its directory was wrong — a combination that reads as authoritative and is the hardest kind to catch by skimming. My order told you the line numbers might have moved and to find the real location; it should also have told you the path itself was unverified. You checked the thing I asserted rather than the thing I flagged, which is the correct instinct.

⭐ The middle link is TWO hand-offs — the falsification that mattered most

My assumption said one seam. Confirmed independently: useMetadata.ts:135 passes onSaveAdvisory into the factory, and metadataClientFactory.ts:89 passes it into the MetadataClient constructor (...(onSaveAdvisory ? { onSaveAdvisory } : {})).

Ablating both is what makes the pin trustworthy. A pin proven against only one half would leave the other half cuttable with the pin still green — i.e. it would reproduce this card's own defect one level down. Both legs red (2 failed / 1 passed, as predicted) is the difference between a pin and a decoration.

⭐ The VOID leg is the best thing in this report

Leg B's first run used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them) — and your guard refused to render a verdict on it. The mutation had landed; the check was broken.

Reporting that as VOID rather than as a result is exactly right, and it is the third instance today on this lane of the same class: a broken instrument that still prints something plausible (rg -ril silently rewriting its own output on #7015; my own landing sweep printing "queue drained" from an echo while every git call errored). ⇒ Going into the seat's standing lessons as a named class.

The zero that carries the whole finding is properly controlled

"Before this PR, exactly 0 suites imported the real useMetadataClient" is paired, in the same sweep, with 76 suites naming it at all and 51 that vi.mock it away. So the zero is a reading, and it also explains itself: the seam was mocked out everywhere it appeared — which is why 226 files and 2369 tests could stay green through the cut.

That green-through-ablation is the finding, and it is reported as a finding rather than as reassurance. Producer end green, renderer end green, all 220 metadata-admin suites green, zero reds — byte-identical counts to baseline.

The control case earns its place

Asserting fetch was called twice alongside "toast not called" is what stops case 3 from being satisfiable by a severed chain. Without it, a cut seam would satisfy the control too — and a control that a broken system passes is not a control. Your own prediction said exactly this before running.

Changeset — EMPTY frontmatter, and it matches the rule I had to correct today

The gate's own exemption language is quoted rather than reasoned about: "Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."

⚠️ Note for the record: earlier today I gave #7015's dev a wrong version of this rule (I said docs cards owe an empty-frontmatter changeset; the gate there said none was owed at all). Your handling here — quote the verdict, don't infer it — is the corrected form. The skip-changeset phantom-label note is a useful extra: an existing label object that nothing reads is exactly the sort of thing an agent would reach for and get silently nothing from.

Typecheck coverage proven, not assumed

tsc -p tsconfig.test.json --listFiles names the new file (1 hit of 4504), with a control that must hit — the sibling saveAdvisoryToast.test.ts, also 1. That forecloses the "typecheck silently excluded the test files" false green, and the 29-package closure was built first so the result is green rather than NOT MEASURED.

#7116 — filed at the right confidence level

The sibling AdapterProvider.tsx wiring is reported as grep-measured with a control, not ablation-measured, and the card says so and asks its claimant to re-measure. ⭐ That is the correct epistemic label: it is exactly the shape of this card (an unpinned middle link) but it has not been proven by cutting it, and claiming otherwise would hand the next agent a premise that might not survive.


Re-checking CI shortly; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

1 participant

@os-warren
, '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): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors - #7118

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin
Sep 1, 2026
Merged

test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors#7118
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6969

One pin over the advisory chain's middle link, asserting the CONNECTION rather than either end of it: a server-sent advisory travels the real hook, the real client factory and the real MetadataClient, and arrives at the toast — for the save door and for the publish door.

Leg 0 — the card's precondition, re-measured on current main

The card required the claimant to re-measure that the gap is still real. It is.

Seam location — assumption falsified. The card and the dispatch both name packages/app-shell/src/hooks/useMetadata.ts. That file does not exist. The real seam is packages/app-shell/src/views/metadata-admin/useMetadata.ts — the line numbers 130-135 were right, the directory was not. packages/app-shell/src/hooks/useMetadataService.ts is a different thing (it mints a MetadataService from the adapter) and carries no advisory wiring.

Ablation — predicted direction stated before running: every named suite stays GREEN, and that green IS the finding.

Cut, at the seam the card names:

- () => createConsoleMetadataClient({ previewDrafts, environmentId, onSaveAdvisory }),
+ () => createConsoleMetadataClient({ previewDrafts, environmentId }),
value
predictedall named suites stay green
observedall named suites stayed green — 226 files / 2369 tests, zero reds
blob before4bbebbf78439ca088ba313a7a708d8a007535a32 (identical to the HEAD blob)
blob after7d2ade859f42f27f76723fea95c3d4810243b7f1
mutation provenanchor count for the deleted text 1 to 0, for the injected text 0 to 1, and the blob hash moved
restore provenby STATE: blob back to 4bbebbf78...andgit diff HEAD 0 bytes

Green suites through the cut (reported because a suite that survives an ablation tells you which layer it does NOT cover):

  • metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, onSaveAdvisory.test.ts — producer end, green
  • saveAdvisoryToast.test.ts — renderer end, green
  • MetadataService.saveAdvisories.test.ts, metadata-client-auth.ratchet.test.ts — green
  • all 220 suites in views/metadata-admin/ — green, 2299 passed / 1 skipped, byte-identical to the baseline

Red suites through the cut: none.

Why nothing could see it, measured: 51 suites vi.mock('./useMetadata') — legitimately, they test pages, not plumbing — and, before this PR, exactly 0 imported the real useMetadataClient. Control: 76 suites name useMetadataClient at all, so the zero is a reading, not a dead query. The seam was mocked away everywhere it appeared.

The pin

packages/app-shell/src/views/metadata-admin/useMetadataClient.advisorySink.test.tsx — 3 cases:

  1. SAVE doorsave() through the hook's client; the toast title matches /^Saved\b/ and carries the finding's message and rule.
  2. PUBLISH doorpublishDraft() through the same client (the method the console's own usePublishAllDrafts calls through this hook); title matches /^Published\b/.
  3. CONTROL — a clean 200 with no advisories says nothing at either door, and fetch is asserted to have been called twice, so the silence is about the empty advisory list and not about a chain that never ran.

The door verb is the load-bearing assertion: it is set by the producer and read by the renderer, so it only reads correctly if the discriminator survived the whole seam.

Real: the hook, its useCallback sink, the i18n t, the PreviewModeContext read, createConsoleMetadataClient, the authenticated fetch wrapper, the MetadataClient, its response parsing, readSaveAdvisories, emitSaveAdvisories. Stubbed, only these two: sonner (the terminal sink — the hook imports it as a module binding, so it cannot be handed over the way the renderer suite hands over its own) and globalThis.fetch (the server).

Scope — deliberately not a bigger test at either end. The warning tier, the per-finding formatting and the empty-list drop stay the renderer suite's; withEnvironment clone survival and response-shape filtering stay the producer suites' (both already pin them, both doors).

Pin ablation — it fails when the middle is cut, and passes when it is intact

Both halves of the middle link were cut, separately. Predicted before running: the two door cases go RED and the CONTROL case stays GREEN, because a severed chain also satisfies "the sink was not called" — which is exactly why the control alone could never have caught this.

legfileblob before to afterobserved
AuseMetadata.ts (the hand-off)4bbebbf78 to 7d2ade8592 failed / 1 passed — as predicted
BmetadataClientFactory.ts (the pass-through into the constructor)e0f04f9a4 to fddd1d5552 failed / 1 passed — as predicted

Both legs: mutation proven on disk by anchor counts and blob-hash movement; restore proven by state — blob back to the HEAD blob and git diff HEAD 0 bytes.

⚠️ Leg B was run twice and the first run is reported as VOID, not as a result: the verification used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them), so the guard refused the reading. The mutation had in fact landed; the check was broken. Re-run with a single-line anchor, which is the row above.

Gates

Run at final HEAD b5a989f0c. Verdict lines quoted from the gates themselves, never a bare exit code.

gateverdict
union regressionTest Files 227 passed (227) · Tests 2372 passed | 1 skipped (2373)
check-changeset-presenceEvery 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-majorNo changeset declares a major bump.
check-vi-mock-specifiersOK (4074 tracked source file(s), 2349 test-named; 516 carry a mock; ...)
check-vi-mock-inheritOK (... 114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked) ...)
check-control-bytesOK (scanned 5893 tracked text file(s); skipped 85 binary).
check-shell-escape-residueOK (4/4 root(s) resolved ...)
check-lint-coveragelint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coveragetest type-check coverage: 41/41 packages compile their tests, 0 declared debt
eslint (plain form)exit 0, no output
pnpm --filter @object-ui/app-shell run type-checkexit 0; output echoed tsc --noEmit && tsc -p tsconfig.test.json, so it was not a zero-match

The typecheck actually covers the new file, proven rather than assumed: tsc -p tsconfig.test.json --listFiles names it (1 hit) out of 4504 program files, with a control that must hit — the sibling providers/saveAdvisoryToast.test.ts, also 1 hit. That config sets paths: {}, so it resolves through built .d.ts; the 29-package dependency closure was built first, otherwise the result would have been NOT MEASURED rather than green.

The changeset carries an EMPTY frontmatter — the presence gate's own named exemption for a test-only change, quoted above. No skip-changeset label was applied: in this repo that label is a phantom (the object exists from a historical mislabel, but nothing reads it, and scripts/__tests__/ci-cd-pipeline-doc.test.ts fails if anything under .github/ or scripts/ ever wires it in).

Out of scope, filed separately

#7116 — the SIBLING advisory wiring in AdapterProvider.tsx (the ObjectStackAdapter channel, built by #4237) has the same unpinned middle link. Measured by grep with a control, not by ablation, and said so on the card. Not touched here.

Notes for review


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 02:06
…s the toast, both doors
The advisory chain's middle link was pinned by nothing: the producer suites
assert the event is emitted into a sink they build themselves, and the renderer
suite asserts the message is built over an event it writes by hand. Cutting the
hand-off in useMetadata.ts silenced both doors with 226 files / 2369 tests green.
Mounts the real hook, the real client factory and the real MetadataClient, and
asserts a server-sent advisory reaches the toast for save and for publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Empty frontmatter — the presence gate's explicit exemption for a test-only
change. Nothing published moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.4 KB3191.4 KB
Main entry chunk (gzip)142.3 KB350 KB
Entry fileindex-LPKhNev8.js
StatusPASS

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

Landing NOT yet armed. 8 checks are in_progress at b5a989f0c (4 test shards, Type Check, Doc Snippet Type Check, Lint, README Export Check). in_progress is not a pass and I will not flip ready until every check is green. Conflict pre-check is already done and clean — head is behind main by exactly 1 commit (#7109), merge-tree returns 0 conflicts (tree 1e3181ee3), and the merge queue rebuilds each entry on current main, so behind is not an action item here.

⭐ The seam path was wrong, and it was wrong in MY order

I relayed the card's packages/app-shell/src/hooks/useMetadata.ts without checking it. Verified independently just now:

proberesult
packages/app-shell/src/hooks/useMetadata.tsdoes not exist
packages/app-shell/src/hooks/useMetadataService.ts⚠️control✅ exists — so hooks/ is real and the probe is live
packages/app-shell/src/views/metadata-admin/useMetadata.ts✅ exists; onSaveAdvisory at 130 / 135 / 136

So the card's line numbers were exact and its directory was wrong — a combination that reads as authoritative and is the hardest kind to catch by skimming. My order told you the line numbers might have moved and to find the real location; it should also have told you the path itself was unverified. You checked the thing I asserted rather than the thing I flagged, which is the correct instinct.

⭐ The middle link is TWO hand-offs — the falsification that mattered most

My assumption said one seam. Confirmed independently: useMetadata.ts:135 passes onSaveAdvisory into the factory, and metadataClientFactory.ts:89 passes it into the MetadataClient constructor (...(onSaveAdvisory ? { onSaveAdvisory } : {})).

Ablating both is what makes the pin trustworthy. A pin proven against only one half would leave the other half cuttable with the pin still green — i.e. it would reproduce this card's own defect one level down. Both legs red (2 failed / 1 passed, as predicted) is the difference between a pin and a decoration.

⭐ The VOID leg is the best thing in this report

Leg B's first run used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them) — and your guard refused to render a verdict on it. The mutation had landed; the check was broken.

Reporting that as VOID rather than as a result is exactly right, and it is the third instance today on this lane of the same class: a broken instrument that still prints something plausible (rg -ril silently rewriting its own output on #7015; my own landing sweep printing "queue drained" from an echo while every git call errored). ⇒ Going into the seat's standing lessons as a named class.

The zero that carries the whole finding is properly controlled

"Before this PR, exactly 0 suites imported the real useMetadataClient" is paired, in the same sweep, with 76 suites naming it at all and 51 that vi.mock it away. So the zero is a reading, and it also explains itself: the seam was mocked out everywhere it appeared — which is why 226 files and 2369 tests could stay green through the cut.

That green-through-ablation is the finding, and it is reported as a finding rather than as reassurance. Producer end green, renderer end green, all 220 metadata-admin suites green, zero reds — byte-identical counts to baseline.

The control case earns its place

Asserting fetch was called twice alongside "toast not called" is what stops case 3 from being satisfiable by a severed chain. Without it, a cut seam would satisfy the control too — and a control that a broken system passes is not a control. Your own prediction said exactly this before running.

Changeset — EMPTY frontmatter, and it matches the rule I had to correct today

The gate's own exemption language is quoted rather than reasoned about: "Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."

⚠️ Note for the record: earlier today I gave #7015's dev a wrong version of this rule (I said docs cards owe an empty-frontmatter changeset; the gate there said none was owed at all). Your handling here — quote the verdict, don't infer it — is the corrected form. The skip-changeset phantom-label note is a useful extra: an existing label object that nothing reads is exactly the sort of thing an agent would reach for and get silently nothing from.

Typecheck coverage proven, not assumed

tsc -p tsconfig.test.json --listFiles names the new file (1 hit of 4504), with a control that must hit — the sibling saveAdvisoryToast.test.ts, also 1. That forecloses the "typecheck silently excluded the test files" false green, and the 29-package closure was built first so the result is green rather than NOT MEASURED.

#7116 — filed at the right confidence level

The sibling AdapterProvider.tsx wiring is reported as grep-measured with a control, not ablation-measured, and the card says so and asks its claimant to re-measure. ⭐ That is the correct epistemic label: it is exactly the shape of this card (an unpinned middle link) but it has not been proven by cutting it, and claiming otherwise would hand the next agent a premise that might not survive.


Re-checking CI shortly; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

1 participant

@os-warren
, '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): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors - #7118

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin
Sep 1, 2026
Merged

test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors#7118
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6969

One pin over the advisory chain's middle link, asserting the CONNECTION rather than either end of it: a server-sent advisory travels the real hook, the real client factory and the real MetadataClient, and arrives at the toast — for the save door and for the publish door.

Leg 0 — the card's precondition, re-measured on current main

The card required the claimant to re-measure that the gap is still real. It is.

Seam location — assumption falsified. The card and the dispatch both name packages/app-shell/src/hooks/useMetadata.ts. That file does not exist. The real seam is packages/app-shell/src/views/metadata-admin/useMetadata.ts — the line numbers 130-135 were right, the directory was not. packages/app-shell/src/hooks/useMetadataService.ts is a different thing (it mints a MetadataService from the adapter) and carries no advisory wiring.

Ablation — predicted direction stated before running: every named suite stays GREEN, and that green IS the finding.

Cut, at the seam the card names:

- () => createConsoleMetadataClient({ previewDrafts, environmentId, onSaveAdvisory }),
+ () => createConsoleMetadataClient({ previewDrafts, environmentId }),
value
predictedall named suites stay green
observedall named suites stayed green — 226 files / 2369 tests, zero reds
blob before4bbebbf78439ca088ba313a7a708d8a007535a32 (identical to the HEAD blob)
blob after7d2ade859f42f27f76723fea95c3d4810243b7f1
mutation provenanchor count for the deleted text 1 to 0, for the injected text 0 to 1, and the blob hash moved
restore provenby STATE: blob back to 4bbebbf78...andgit diff HEAD 0 bytes

Green suites through the cut (reported because a suite that survives an ablation tells you which layer it does NOT cover):

  • metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, onSaveAdvisory.test.ts — producer end, green
  • saveAdvisoryToast.test.ts — renderer end, green
  • MetadataService.saveAdvisories.test.ts, metadata-client-auth.ratchet.test.ts — green
  • all 220 suites in views/metadata-admin/ — green, 2299 passed / 1 skipped, byte-identical to the baseline

Red suites through the cut: none.

Why nothing could see it, measured: 51 suites vi.mock('./useMetadata') — legitimately, they test pages, not plumbing — and, before this PR, exactly 0 imported the real useMetadataClient. Control: 76 suites name useMetadataClient at all, so the zero is a reading, not a dead query. The seam was mocked away everywhere it appeared.

The pin

packages/app-shell/src/views/metadata-admin/useMetadataClient.advisorySink.test.tsx — 3 cases:

  1. SAVE doorsave() through the hook's client; the toast title matches /^Saved\b/ and carries the finding's message and rule.
  2. PUBLISH doorpublishDraft() through the same client (the method the console's own usePublishAllDrafts calls through this hook); title matches /^Published\b/.
  3. CONTROL — a clean 200 with no advisories says nothing at either door, and fetch is asserted to have been called twice, so the silence is about the empty advisory list and not about a chain that never ran.

The door verb is the load-bearing assertion: it is set by the producer and read by the renderer, so it only reads correctly if the discriminator survived the whole seam.

Real: the hook, its useCallback sink, the i18n t, the PreviewModeContext read, createConsoleMetadataClient, the authenticated fetch wrapper, the MetadataClient, its response parsing, readSaveAdvisories, emitSaveAdvisories. Stubbed, only these two: sonner (the terminal sink — the hook imports it as a module binding, so it cannot be handed over the way the renderer suite hands over its own) and globalThis.fetch (the server).

Scope — deliberately not a bigger test at either end. The warning tier, the per-finding formatting and the empty-list drop stay the renderer suite's; withEnvironment clone survival and response-shape filtering stay the producer suites' (both already pin them, both doors).

Pin ablation — it fails when the middle is cut, and passes when it is intact

Both halves of the middle link were cut, separately. Predicted before running: the two door cases go RED and the CONTROL case stays GREEN, because a severed chain also satisfies "the sink was not called" — which is exactly why the control alone could never have caught this.

legfileblob before to afterobserved
AuseMetadata.ts (the hand-off)4bbebbf78 to 7d2ade8592 failed / 1 passed — as predicted
BmetadataClientFactory.ts (the pass-through into the constructor)e0f04f9a4 to fddd1d5552 failed / 1 passed — as predicted

Both legs: mutation proven on disk by anchor counts and blob-hash movement; restore proven by state — blob back to the HEAD blob and git diff HEAD 0 bytes.

⚠️ Leg B was run twice and the first run is reported as VOID, not as a result: the verification used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them), so the guard refused the reading. The mutation had in fact landed; the check was broken. Re-run with a single-line anchor, which is the row above.

Gates

Run at final HEAD b5a989f0c. Verdict lines quoted from the gates themselves, never a bare exit code.

gateverdict
union regressionTest Files 227 passed (227) · Tests 2372 passed | 1 skipped (2373)
check-changeset-presenceEvery 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-majorNo changeset declares a major bump.
check-vi-mock-specifiersOK (4074 tracked source file(s), 2349 test-named; 516 carry a mock; ...)
check-vi-mock-inheritOK (... 114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked) ...)
check-control-bytesOK (scanned 5893 tracked text file(s); skipped 85 binary).
check-shell-escape-residueOK (4/4 root(s) resolved ...)
check-lint-coveragelint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coveragetest type-check coverage: 41/41 packages compile their tests, 0 declared debt
eslint (plain form)exit 0, no output
pnpm --filter @object-ui/app-shell run type-checkexit 0; output echoed tsc --noEmit && tsc -p tsconfig.test.json, so it was not a zero-match

The typecheck actually covers the new file, proven rather than assumed: tsc -p tsconfig.test.json --listFiles names it (1 hit) out of 4504 program files, with a control that must hit — the sibling providers/saveAdvisoryToast.test.ts, also 1 hit. That config sets paths: {}, so it resolves through built .d.ts; the 29-package dependency closure was built first, otherwise the result would have been NOT MEASURED rather than green.

The changeset carries an EMPTY frontmatter — the presence gate's own named exemption for a test-only change, quoted above. No skip-changeset label was applied: in this repo that label is a phantom (the object exists from a historical mislabel, but nothing reads it, and scripts/__tests__/ci-cd-pipeline-doc.test.ts fails if anything under .github/ or scripts/ ever wires it in).

Out of scope, filed separately

#7116 — the SIBLING advisory wiring in AdapterProvider.tsx (the ObjectStackAdapter channel, built by #4237) has the same unpinned middle link. Measured by grep with a control, not by ablation, and said so on the card. Not touched here.

Notes for review


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 02:06
…s the toast, both doors
The advisory chain's middle link was pinned by nothing: the producer suites
assert the event is emitted into a sink they build themselves, and the renderer
suite asserts the message is built over an event it writes by hand. Cutting the
hand-off in useMetadata.ts silenced both doors with 226 files / 2369 tests green.
Mounts the real hook, the real client factory and the real MetadataClient, and
asserts a server-sent advisory reaches the toast for save and for publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Empty frontmatter — the presence gate's explicit exemption for a test-only
change. Nothing published moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.4 KB3191.4 KB
Main entry chunk (gzip)142.3 KB350 KB
Entry fileindex-LPKhNev8.js
StatusPASS

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

Landing NOT yet armed. 8 checks are in_progress at b5a989f0c (4 test shards, Type Check, Doc Snippet Type Check, Lint, README Export Check). in_progress is not a pass and I will not flip ready until every check is green. Conflict pre-check is already done and clean — head is behind main by exactly 1 commit (#7109), merge-tree returns 0 conflicts (tree 1e3181ee3), and the merge queue rebuilds each entry on current main, so behind is not an action item here.

⭐ The seam path was wrong, and it was wrong in MY order

I relayed the card's packages/app-shell/src/hooks/useMetadata.ts without checking it. Verified independently just now:

proberesult
packages/app-shell/src/hooks/useMetadata.tsdoes not exist
packages/app-shell/src/hooks/useMetadataService.ts⚠️control✅ exists — so hooks/ is real and the probe is live
packages/app-shell/src/views/metadata-admin/useMetadata.ts✅ exists; onSaveAdvisory at 130 / 135 / 136

So the card's line numbers were exact and its directory was wrong — a combination that reads as authoritative and is the hardest kind to catch by skimming. My order told you the line numbers might have moved and to find the real location; it should also have told you the path itself was unverified. You checked the thing I asserted rather than the thing I flagged, which is the correct instinct.

⭐ The middle link is TWO hand-offs — the falsification that mattered most

My assumption said one seam. Confirmed independently: useMetadata.ts:135 passes onSaveAdvisory into the factory, and metadataClientFactory.ts:89 passes it into the MetadataClient constructor (...(onSaveAdvisory ? { onSaveAdvisory } : {})).

Ablating both is what makes the pin trustworthy. A pin proven against only one half would leave the other half cuttable with the pin still green — i.e. it would reproduce this card's own defect one level down. Both legs red (2 failed / 1 passed, as predicted) is the difference between a pin and a decoration.

⭐ The VOID leg is the best thing in this report

Leg B's first run used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them) — and your guard refused to render a verdict on it. The mutation had landed; the check was broken.

Reporting that as VOID rather than as a result is exactly right, and it is the third instance today on this lane of the same class: a broken instrument that still prints something plausible (rg -ril silently rewriting its own output on #7015; my own landing sweep printing "queue drained" from an echo while every git call errored). ⇒ Going into the seat's standing lessons as a named class.

The zero that carries the whole finding is properly controlled

"Before this PR, exactly 0 suites imported the real useMetadataClient" is paired, in the same sweep, with 76 suites naming it at all and 51 that vi.mock it away. So the zero is a reading, and it also explains itself: the seam was mocked out everywhere it appeared — which is why 226 files and 2369 tests could stay green through the cut.

That green-through-ablation is the finding, and it is reported as a finding rather than as reassurance. Producer end green, renderer end green, all 220 metadata-admin suites green, zero reds — byte-identical counts to baseline.

The control case earns its place

Asserting fetch was called twice alongside "toast not called" is what stops case 3 from being satisfiable by a severed chain. Without it, a cut seam would satisfy the control too — and a control that a broken system passes is not a control. Your own prediction said exactly this before running.

Changeset — EMPTY frontmatter, and it matches the rule I had to correct today

The gate's own exemption language is quoted rather than reasoned about: "Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."

⚠️ Note for the record: earlier today I gave #7015's dev a wrong version of this rule (I said docs cards owe an empty-frontmatter changeset; the gate there said none was owed at all). Your handling here — quote the verdict, don't infer it — is the corrected form. The skip-changeset phantom-label note is a useful extra: an existing label object that nothing reads is exactly the sort of thing an agent would reach for and get silently nothing from.

Typecheck coverage proven, not assumed

tsc -p tsconfig.test.json --listFiles names the new file (1 hit of 4504), with a control that must hit — the sibling saveAdvisoryToast.test.ts, also 1. That forecloses the "typecheck silently excluded the test files" false green, and the 29-package closure was built first so the result is green rather than NOT MEASURED.

#7116 — filed at the right confidence level

The sibling AdapterProvider.tsx wiring is reported as grep-measured with a control, not ablation-measured, and the card says so and asks its claimant to re-measure. ⭐ That is the correct epistemic label: it is exactly the shape of this card (an unpinned middle link) but it has not been proven by cutting it, and claiming otherwise would hand the next agent a premise that might not survive.


Re-checking CI shortly; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

1 participant

@os-warren
, '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): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors - #7118

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin
Sep 1, 2026
Merged

test(app-shell): pin the metadata advisory sink seam — an emitted event reaches the toast, both doors#7118
os-warren merged 2 commits into
mainfrom
claude/issue-6969-advisory-sink-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6969

One pin over the advisory chain's middle link, asserting the CONNECTION rather than either end of it: a server-sent advisory travels the real hook, the real client factory and the real MetadataClient, and arrives at the toast — for the save door and for the publish door.

Leg 0 — the card's precondition, re-measured on current main

The card required the claimant to re-measure that the gap is still real. It is.

Seam location — assumption falsified. The card and the dispatch both name packages/app-shell/src/hooks/useMetadata.ts. That file does not exist. The real seam is packages/app-shell/src/views/metadata-admin/useMetadata.ts — the line numbers 130-135 were right, the directory was not. packages/app-shell/src/hooks/useMetadataService.ts is a different thing (it mints a MetadataService from the adapter) and carries no advisory wiring.

Ablation — predicted direction stated before running: every named suite stays GREEN, and that green IS the finding.

Cut, at the seam the card names:

- () => createConsoleMetadataClient({ previewDrafts, environmentId, onSaveAdvisory }),
+ () => createConsoleMetadataClient({ previewDrafts, environmentId }),
value
predictedall named suites stay green
observedall named suites stayed green — 226 files / 2369 tests, zero reds
blob before4bbebbf78439ca088ba313a7a708d8a007535a32 (identical to the HEAD blob)
blob after7d2ade859f42f27f76723fea95c3d4810243b7f1
mutation provenanchor count for the deleted text 1 to 0, for the injected text 0 to 1, and the blob hash moved
restore provenby STATE: blob back to 4bbebbf78...andgit diff HEAD 0 bytes

Green suites through the cut (reported because a suite that survives an ablation tells you which layer it does NOT cover):

  • metadata-client.saveAdvisories.test.ts, metadata-client.publishAdvisories.test.ts, onSaveAdvisory.test.ts — producer end, green
  • saveAdvisoryToast.test.ts — renderer end, green
  • MetadataService.saveAdvisories.test.ts, metadata-client-auth.ratchet.test.ts — green
  • all 220 suites in views/metadata-admin/ — green, 2299 passed / 1 skipped, byte-identical to the baseline

Red suites through the cut: none.

Why nothing could see it, measured: 51 suites vi.mock('./useMetadata') — legitimately, they test pages, not plumbing — and, before this PR, exactly 0 imported the real useMetadataClient. Control: 76 suites name useMetadataClient at all, so the zero is a reading, not a dead query. The seam was mocked away everywhere it appeared.

The pin

packages/app-shell/src/views/metadata-admin/useMetadataClient.advisorySink.test.tsx — 3 cases:

  1. SAVE doorsave() through the hook's client; the toast title matches /^Saved\b/ and carries the finding's message and rule.
  2. PUBLISH doorpublishDraft() through the same client (the method the console's own usePublishAllDrafts calls through this hook); title matches /^Published\b/.
  3. CONTROL — a clean 200 with no advisories says nothing at either door, and fetch is asserted to have been called twice, so the silence is about the empty advisory list and not about a chain that never ran.

The door verb is the load-bearing assertion: it is set by the producer and read by the renderer, so it only reads correctly if the discriminator survived the whole seam.

Real: the hook, its useCallback sink, the i18n t, the PreviewModeContext read, createConsoleMetadataClient, the authenticated fetch wrapper, the MetadataClient, its response parsing, readSaveAdvisories, emitSaveAdvisories. Stubbed, only these two: sonner (the terminal sink — the hook imports it as a module binding, so it cannot be handed over the way the renderer suite hands over its own) and globalThis.fetch (the server).

Scope — deliberately not a bigger test at either end. The warning tier, the per-finding formatting and the empty-list drop stay the renderer suite's; withEnvironment clone survival and response-shape filtering stay the producer suites' (both already pin them, both doors).

Pin ablation — it fails when the middle is cut, and passes when it is intact

Both halves of the middle link were cut, separately. Predicted before running: the two door cases go RED and the CONTROL case stays GREEN, because a severed chain also satisfies "the sink was not called" — which is exactly why the control alone could never have caught this.

legfileblob before to afterobserved
AuseMetadata.ts (the hand-off)4bbebbf78 to 7d2ade8592 failed / 1 passed — as predicted
BmetadataClientFactory.ts (the pass-through into the constructor)e0f04f9a4 to fddd1d5552 failed / 1 passed — as predicted

Both legs: mutation proven on disk by anchor counts and blob-hash movement; restore proven by state — blob back to the HEAD blob and git diff HEAD 0 bytes.

⚠️ Leg B was run twice and the first run is reported as VOID, not as a result: the verification used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them), so the guard refused the reading. The mutation had in fact landed; the check was broken. Re-run with a single-line anchor, which is the row above.

Gates

Run at final HEAD b5a989f0c. Verdict lines quoted from the gates themselves, never a bare exit code.

gateverdict
union regressionTest Files 227 passed (227) · Tests 2372 passed | 1 skipped (2373)
check-changeset-presenceEvery 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-majorNo changeset declares a major bump.
check-vi-mock-specifiersOK (4074 tracked source file(s), 2349 test-named; 516 carry a mock; ...)
check-vi-mock-inheritOK (... 114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked) ...)
check-control-bytesOK (scanned 5893 tracked text file(s); skipped 85 binary).
check-shell-escape-residueOK (4/4 root(s) resolved ...)
check-lint-coveragelint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coveragetest type-check coverage: 41/41 packages compile their tests, 0 declared debt
eslint (plain form)exit 0, no output
pnpm --filter @object-ui/app-shell run type-checkexit 0; output echoed tsc --noEmit && tsc -p tsconfig.test.json, so it was not a zero-match

The typecheck actually covers the new file, proven rather than assumed: tsc -p tsconfig.test.json --listFiles names it (1 hit) out of 4504 program files, with a control that must hit — the sibling providers/saveAdvisoryToast.test.ts, also 1 hit. That config sets paths: {}, so it resolves through built .d.ts; the 29-package dependency closure was built first, otherwise the result would have been NOT MEASURED rather than green.

The changeset carries an EMPTY frontmatter — the presence gate's own named exemption for a test-only change, quoted above. No skip-changeset label was applied: in this repo that label is a phantom (the object exists from a historical mislabel, but nothing reads it, and scripts/__tests__/ci-cd-pipeline-doc.test.ts fails if anything under .github/ or scripts/ ever wires it in).

Out of scope, filed separately

#7116 — the SIBLING advisory wiring in AdapterProvider.tsx (the ObjectStackAdapter channel, built by #4237) has the same unpinned middle link. Measured by grep with a control, not by ablation, and said so on the card. Not touched here.

Notes for review


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 02:06
…s the toast, both doors
The advisory chain's middle link was pinned by nothing: the producer suites
assert the event is emitted into a sink they build themselves, and the renderer
suite asserts the message is built over an event it writes by hand. Cutting the
hand-off in useMetadata.ts silenced both doors with 226 files / 2369 tests green.
Mounts the real hook, the real client factory and the real MetadataClient, and
asserts a server-sent advisory reaches the toast for save and for publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Empty frontmatter — the presence gate's explicit exemption for a test-only
change. Nothing published moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.4 KB3191.4 KB
Main entry chunk (gzip)142.3 KB350 KB
Entry fileindex-LPKhNev8.js
StatusPASS

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

Landing NOT yet armed. 8 checks are in_progress at b5a989f0c (4 test shards, Type Check, Doc Snippet Type Check, Lint, README Export Check). in_progress is not a pass and I will not flip ready until every check is green. Conflict pre-check is already done and clean — head is behind main by exactly 1 commit (#7109), merge-tree returns 0 conflicts (tree 1e3181ee3), and the merge queue rebuilds each entry on current main, so behind is not an action item here.

⭐ The seam path was wrong, and it was wrong in MY order

I relayed the card's packages/app-shell/src/hooks/useMetadata.ts without checking it. Verified independently just now:

proberesult
packages/app-shell/src/hooks/useMetadata.tsdoes not exist
packages/app-shell/src/hooks/useMetadataService.ts⚠️control✅ exists — so hooks/ is real and the probe is live
packages/app-shell/src/views/metadata-admin/useMetadata.ts✅ exists; onSaveAdvisory at 130 / 135 / 136

So the card's line numbers were exact and its directory was wrong — a combination that reads as authoritative and is the hardest kind to catch by skimming. My order told you the line numbers might have moved and to find the real location; it should also have told you the path itself was unverified. You checked the thing I asserted rather than the thing I flagged, which is the correct instinct.

⭐ The middle link is TWO hand-offs — the falsification that mattered most

My assumption said one seam. Confirmed independently: useMetadata.ts:135 passes onSaveAdvisory into the factory, and metadataClientFactory.ts:89 passes it into the MetadataClient constructor (...(onSaveAdvisory ? { onSaveAdvisory } : {})).

Ablating both is what makes the pin trustworthy. A pin proven against only one half would leave the other half cuttable with the pin still green — i.e. it would reproduce this card's own defect one level down. Both legs red (2 failed / 1 passed, as predicted) is the difference between a pin and a decoration.

⭐ The VOID leg is the best thing in this report

Leg B's first run used grep -c -F with a newline inside the pattern, which degenerates to matching every line (92 of them) — and your guard refused to render a verdict on it. The mutation had landed; the check was broken.

Reporting that as VOID rather than as a result is exactly right, and it is the third instance today on this lane of the same class: a broken instrument that still prints something plausible (rg -ril silently rewriting its own output on #7015; my own landing sweep printing "queue drained" from an echo while every git call errored). ⇒ Going into the seat's standing lessons as a named class.

The zero that carries the whole finding is properly controlled

"Before this PR, exactly 0 suites imported the real useMetadataClient" is paired, in the same sweep, with 76 suites naming it at all and 51 that vi.mock it away. So the zero is a reading, and it also explains itself: the seam was mocked out everywhere it appeared — which is why 226 files and 2369 tests could stay green through the cut.

That green-through-ablation is the finding, and it is reported as a finding rather than as reassurance. Producer end green, renderer end green, all 220 metadata-admin suites green, zero reds — byte-identical counts to baseline.

The control case earns its place

Asserting fetch was called twice alongside "toast not called" is what stops case 3 from being satisfiable by a severed chain. Without it, a cut seam would satisfy the control too — and a control that a broken system passes is not a control. Your own prediction said exactly this before running.

Changeset — EMPTY frontmatter, and it matches the rule I had to correct today

The gate's own exemption language is quoted rather than reasoned about: "Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."

⚠️ Note for the record: earlier today I gave #7015's dev a wrong version of this rule (I said docs cards owe an empty-frontmatter changeset; the gate there said none was owed at all). Your handling here — quote the verdict, don't infer it — is the corrected form. The skip-changeset phantom-label note is a useful extra: an existing label object that nothing reads is exactly the sort of thing an agent would reach for and get silently nothing from.

Typecheck coverage proven, not assumed

tsc -p tsconfig.test.json --listFiles names the new file (1 hit of 4504), with a control that must hit — the sibling saveAdvisoryToast.test.ts, also 1. That forecloses the "typecheck silently excluded the test files" false green, and the 29-package closure was built first so the result is green rather than NOT MEASURED.

#7116 — filed at the right confidence level

The sibling AdapterProvider.tsx wiring is reported as grep-measured with a control, not ablation-measured, and the card says so and asks its claimant to re-measure. ⭐ That is the correct epistemic label: it is exactly the shape of this card (an unpinned middle link) but it has not been proven by cutting it, and claiming otherwise would hand the next agent a premise that might not survive.


Re-checking CI shortly; I arm on green.


Generated by Claude Code

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

Labels

Projects

None yet

1 participant

@os-warren