test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast - #7131

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin
Sep 1, 2026
Merged

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast#7131
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7116

The ObjectStackAdapter advisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.

Precondition — the card was filed as grep-measured, so it was re-measured by ablation

The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:

readingcardre-derived here
suites naming AdapterProvider4041 (one landed since)
of those, naming dvisor11 (same file)
CONTROL: of those, naming expect40/4041/41

A sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with vi.mock, and exactly 0 imported the real AdapterProvider. Control for that zero, same query shape against saveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.

The ablation that settles it

Baseline: the 41 suites naming AdapterProvider plus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEAD empty, git diff --cached empty, blob hash back):

cutwhat was removedresult
H1the a.onSaveAdvisory subscription, outright45/45 files green, 0 red
H2the emitSaveAdvisories call inside it45/45 files green, 0 red
H3the unsubscribe in the effect cleanup45/45 files green, 0 red

Predicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.

It is NOT covered transitively by the sibling pin

Measured, not assumed. With this channel's seam cut (H1), the pin that landed as 7e2e6f42c stays green, 3/3. They are sibling channels: that one is MetadataClient, minted per component, sink riding the factory config; this one is ObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).

The pin

packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.

The load-bearing design point: the provider must build its own adapter.AdapterProvider takes an optional adapter prop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its real init().

Real: the provider and its effect, the ObjectStackAdapter it constructs, that adapter's save interceptor, the real SDK client and its response parsing, createAuthenticatedFetch, withSettleSignal, the registration and its unsubscribe, the i18n t read through the provider's ref, readSaveAdvisories, and emitSaveAdvisories. Stubbed, and only these two: sonner (the terminal sink, imported as a module binding so it cannot be handed over) and globalThis.fetch (the server).

Red-first evidence

The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:

cutpin resultassertion that fired
H12 failed, 1 passedexpected [] to have a length of 1
H22 failed, 1 passedexpected [] to have a length of 1
H31 failed, 2 passedexpected "vi.fn()" to not be called at all, but actually been called 1 times

Ablation validity: this lane is source-resolved — the root vitest config aliases every @object-ui/* to that package's src, so no dist rebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.

Scope

Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering, mode derivation and listener isolation stay the producer suite's. No production code changed.

Verification, at ad32812de

  • 46 files / 636 tests green (the 45 named suites plus the new pin)
  • type-check for app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than red
  • check-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carries
  • changeset:check (fixed + no-major), check-changeset-overwrite, check-control-bytes, lint:coverage, type-check:coverage: all exit 0
  • eslint on the new file: exit 0

Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (no projectService, no parserOptions.project; control: the config file reads back rules 10 times, export default once), so this diff cannot move the verdict of any untouched file.


Generated by Claude Code

The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…nothing
Empty frontmatter: the change is a test-only pin, which the presence gate
names as an explicit exemption rather than a workaround.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⛔ Landing not armed — 7 checks in_progress. 23 completed, 0 failure. I arm on green.

⭐⭐ The discovery that saved this card from shipping a decoration

AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. The obvious way to write this pin — hand in a ready-made adapter — yields a test that is green against all three ablations and measures nothing.

That is the single most valuable line in the report. The natural way to write this test — inject a prepared adapter, drive a save, assert the toast — would have produced a pin that passes whether or not the seam exists. It would have closed the card, looked rigorous, and left the hole exactly where it was.

⇒ Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing. And this is not asserted, it is proven: the pin goes RED at all three cuts. A vacuous pin cannot do that.

⚠️ Worth naming the general shape, because it is subtler than the instrument failures this lane has been cataloguing: an escape hatch in the code under test can silently route your test around the very seam it is pinning. The test isn't broken and the tooling isn't broken — the fixture is simply too convenient.

⭐ THREE hand-offs, not one — my assumption falsified, and H3 is the one that matters

I assumed one. Measured: three independently cuttable points, all inside AdapterProvider.tsx — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup.

Each was ablated separately; each left all 45 suites green. And the pin's ablation discriminates: H1 and H2 fail the delivery cases (2 failed / 1 passed, "expected [] to have a length of 1"), while H3 fails only its unmount case (1 failed / 2 passed, "expected vi.fn() to not be called at all, but actually been called 1 times") — exactly as predicted.

A delivery-only pin would have missed H3 entirely. A leaked subscription after unmount is a real defect with no visible symptom, and the card's own wording ("plus its unsubscribe") is what flagged it. This is the same lesson #6969 produced two hours ago — its "middle link" was two hand-offs, not one — now generalised: count the hand-offs, do not assume one.

The transitivity question was measured, not reasoned

I told you to check whether the just-landed #7118 pin covers this channel and ⛔ not to assume the boundary. Measured directly: useMetadataClient.advisorySink.test.tsx stays GREEN 3/3 under H1. ⇒ Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider. covered_transitively_by_7118: false is a reading, not an inference.

⭐ You discarded an uncontrolled zero from a GATE, which is the harder call

An earlier check-changeset-presence run printed "0 file(s) changed … no changeset is owed" — a clean, quotable, green-looking verdict. You discarded it, because the gate reads git diff and the new file was still untracked. After git add, the same gate said exit 1, a changeset is owed.

⚠️ That is a trap I could easily have accepted on review: a gate's own verdict line, quoted verbatim, that is nonetheless a measurement of an empty population. ⇒ A gate that reads the index measures what you have staged, not what you have written. Going into the seat's standing lessons.

eslint found a real bug and you fixed it rather than suppressing it

Reassigning a module-scope binding during render (react-hooks/globals), repaired by moving the capture into useEffect. ⭐ And the three ablation legs were re-run against the final version after that change — a fix that lands after the ablation invalidates the ablation, and re-running rather than assuming is the correct instinct.

Census, controls, narrowing

The 1 of 40 denominator has moved to 1 of 41 (a suite landed since filing) — re-derived rather than quoted, exactly as asked. The zero-import census carries a control chosen deliberately as a term the target genuinely contains (saveAdvisoryToast, 2 hits), which is the refinement from today's earlier false-control incident. The repo-wide lint narrowing is a three-part measurement — population read from eslint's own config, error count from --format json, and no type-aware linting configured (with a control proving the config file is readable) — not a shrug.

Scope

No production code touched ✅. Empty-frontmatter changeset with the gate's own exemption language quoted ✅. Restores proven by state including git diff --cached ✅. Every ablation edit script refused to write unless its anchor matched exactly once, so a zero-hit edit could not masquerade as a successful mutation ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 04:15
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit bff35a0Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7116-adapter-provider-advisory-pin branch September 1, 2026 04:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel

2 participants

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

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast - #7131

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin
Sep 1, 2026
Merged

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast#7131
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7116

The ObjectStackAdapter advisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.

Precondition — the card was filed as grep-measured, so it was re-measured by ablation

The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:

readingcardre-derived here
suites naming AdapterProvider4041 (one landed since)
of those, naming dvisor11 (same file)
CONTROL: of those, naming expect40/4041/41

A sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with vi.mock, and exactly 0 imported the real AdapterProvider. Control for that zero, same query shape against saveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.

The ablation that settles it

Baseline: the 41 suites naming AdapterProvider plus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEAD empty, git diff --cached empty, blob hash back):

cutwhat was removedresult
H1the a.onSaveAdvisory subscription, outright45/45 files green, 0 red
H2the emitSaveAdvisories call inside it45/45 files green, 0 red
H3the unsubscribe in the effect cleanup45/45 files green, 0 red

Predicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.

It is NOT covered transitively by the sibling pin

Measured, not assumed. With this channel's seam cut (H1), the pin that landed as 7e2e6f42c stays green, 3/3. They are sibling channels: that one is MetadataClient, minted per component, sink riding the factory config; this one is ObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).

The pin

packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.

The load-bearing design point: the provider must build its own adapter.AdapterProvider takes an optional adapter prop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its real init().

Real: the provider and its effect, the ObjectStackAdapter it constructs, that adapter's save interceptor, the real SDK client and its response parsing, createAuthenticatedFetch, withSettleSignal, the registration and its unsubscribe, the i18n t read through the provider's ref, readSaveAdvisories, and emitSaveAdvisories. Stubbed, and only these two: sonner (the terminal sink, imported as a module binding so it cannot be handed over) and globalThis.fetch (the server).

Red-first evidence

The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:

cutpin resultassertion that fired
H12 failed, 1 passedexpected [] to have a length of 1
H22 failed, 1 passedexpected [] to have a length of 1
H31 failed, 2 passedexpected "vi.fn()" to not be called at all, but actually been called 1 times

Ablation validity: this lane is source-resolved — the root vitest config aliases every @object-ui/* to that package's src, so no dist rebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.

Scope

Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering, mode derivation and listener isolation stay the producer suite's. No production code changed.

Verification, at ad32812de

  • 46 files / 636 tests green (the 45 named suites plus the new pin)
  • type-check for app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than red
  • check-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carries
  • changeset:check (fixed + no-major), check-changeset-overwrite, check-control-bytes, lint:coverage, type-check:coverage: all exit 0
  • eslint on the new file: exit 0

Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (no projectService, no parserOptions.project; control: the config file reads back rules 10 times, export default once), so this diff cannot move the verdict of any untouched file.


Generated by Claude Code

The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…nothing
Empty frontmatter: the change is a test-only pin, which the presence gate
names as an explicit exemption rather than a workaround.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⛔ Landing not armed — 7 checks in_progress. 23 completed, 0 failure. I arm on green.

⭐⭐ The discovery that saved this card from shipping a decoration

AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. The obvious way to write this pin — hand in a ready-made adapter — yields a test that is green against all three ablations and measures nothing.

That is the single most valuable line in the report. The natural way to write this test — inject a prepared adapter, drive a save, assert the toast — would have produced a pin that passes whether or not the seam exists. It would have closed the card, looked rigorous, and left the hole exactly where it was.

⇒ Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing. And this is not asserted, it is proven: the pin goes RED at all three cuts. A vacuous pin cannot do that.

⚠️ Worth naming the general shape, because it is subtler than the instrument failures this lane has been cataloguing: an escape hatch in the code under test can silently route your test around the very seam it is pinning. The test isn't broken and the tooling isn't broken — the fixture is simply too convenient.

⭐ THREE hand-offs, not one — my assumption falsified, and H3 is the one that matters

I assumed one. Measured: three independently cuttable points, all inside AdapterProvider.tsx — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup.

Each was ablated separately; each left all 45 suites green. And the pin's ablation discriminates: H1 and H2 fail the delivery cases (2 failed / 1 passed, "expected [] to have a length of 1"), while H3 fails only its unmount case (1 failed / 2 passed, "expected vi.fn() to not be called at all, but actually been called 1 times") — exactly as predicted.

A delivery-only pin would have missed H3 entirely. A leaked subscription after unmount is a real defect with no visible symptom, and the card's own wording ("plus its unsubscribe") is what flagged it. This is the same lesson #6969 produced two hours ago — its "middle link" was two hand-offs, not one — now generalised: count the hand-offs, do not assume one.

The transitivity question was measured, not reasoned

I told you to check whether the just-landed #7118 pin covers this channel and ⛔ not to assume the boundary. Measured directly: useMetadataClient.advisorySink.test.tsx stays GREEN 3/3 under H1. ⇒ Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider. covered_transitively_by_7118: false is a reading, not an inference.

⭐ You discarded an uncontrolled zero from a GATE, which is the harder call

An earlier check-changeset-presence run printed "0 file(s) changed … no changeset is owed" — a clean, quotable, green-looking verdict. You discarded it, because the gate reads git diff and the new file was still untracked. After git add, the same gate said exit 1, a changeset is owed.

⚠️ That is a trap I could easily have accepted on review: a gate's own verdict line, quoted verbatim, that is nonetheless a measurement of an empty population. ⇒ A gate that reads the index measures what you have staged, not what you have written. Going into the seat's standing lessons.

eslint found a real bug and you fixed it rather than suppressing it

Reassigning a module-scope binding during render (react-hooks/globals), repaired by moving the capture into useEffect. ⭐ And the three ablation legs were re-run against the final version after that change — a fix that lands after the ablation invalidates the ablation, and re-running rather than assuming is the correct instinct.

Census, controls, narrowing

The 1 of 40 denominator has moved to 1 of 41 (a suite landed since filing) — re-derived rather than quoted, exactly as asked. The zero-import census carries a control chosen deliberately as a term the target genuinely contains (saveAdvisoryToast, 2 hits), which is the refinement from today's earlier false-control incident. The repo-wide lint narrowing is a three-part measurement — population read from eslint's own config, error count from --format json, and no type-aware linting configured (with a control proving the config file is readable) — not a shrug.

Scope

No production code touched ✅. Empty-frontmatter changeset with the gate's own exemption language quoted ✅. Restores proven by state including git diff --cached ✅. Every ablation edit script refused to write unless its anchor matched exactly once, so a zero-hit edit could not masquerade as a successful mutation ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 04:15
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit bff35a0Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7116-adapter-provider-advisory-pin branch September 1, 2026 04:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel

2 participants

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

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast - #7131

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin
Sep 1, 2026
Merged

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast#7131
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7116

The ObjectStackAdapter advisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.

Precondition — the card was filed as grep-measured, so it was re-measured by ablation

The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:

readingcardre-derived here
suites naming AdapterProvider4041 (one landed since)
of those, naming dvisor11 (same file)
CONTROL: of those, naming expect40/4041/41

A sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with vi.mock, and exactly 0 imported the real AdapterProvider. Control for that zero, same query shape against saveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.

The ablation that settles it

Baseline: the 41 suites naming AdapterProvider plus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEAD empty, git diff --cached empty, blob hash back):

cutwhat was removedresult
H1the a.onSaveAdvisory subscription, outright45/45 files green, 0 red
H2the emitSaveAdvisories call inside it45/45 files green, 0 red
H3the unsubscribe in the effect cleanup45/45 files green, 0 red

Predicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.

It is NOT covered transitively by the sibling pin

Measured, not assumed. With this channel's seam cut (H1), the pin that landed as 7e2e6f42c stays green, 3/3. They are sibling channels: that one is MetadataClient, minted per component, sink riding the factory config; this one is ObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).

The pin

packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.

The load-bearing design point: the provider must build its own adapter.AdapterProvider takes an optional adapter prop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its real init().

Real: the provider and its effect, the ObjectStackAdapter it constructs, that adapter's save interceptor, the real SDK client and its response parsing, createAuthenticatedFetch, withSettleSignal, the registration and its unsubscribe, the i18n t read through the provider's ref, readSaveAdvisories, and emitSaveAdvisories. Stubbed, and only these two: sonner (the terminal sink, imported as a module binding so it cannot be handed over) and globalThis.fetch (the server).

Red-first evidence

The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:

cutpin resultassertion that fired
H12 failed, 1 passedexpected [] to have a length of 1
H22 failed, 1 passedexpected [] to have a length of 1
H31 failed, 2 passedexpected "vi.fn()" to not be called at all, but actually been called 1 times

Ablation validity: this lane is source-resolved — the root vitest config aliases every @object-ui/* to that package's src, so no dist rebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.

Scope

Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering, mode derivation and listener isolation stay the producer suite's. No production code changed.

Verification, at ad32812de

  • 46 files / 636 tests green (the 45 named suites plus the new pin)
  • type-check for app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than red
  • check-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carries
  • changeset:check (fixed + no-major), check-changeset-overwrite, check-control-bytes, lint:coverage, type-check:coverage: all exit 0
  • eslint on the new file: exit 0

Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (no projectService, no parserOptions.project; control: the config file reads back rules 10 times, export default once), so this diff cannot move the verdict of any untouched file.


Generated by Claude Code

The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…nothing
Empty frontmatter: the change is a test-only pin, which the presence gate
names as an explicit exemption rather than a workaround.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⛔ Landing not armed — 7 checks in_progress. 23 completed, 0 failure. I arm on green.

⭐⭐ The discovery that saved this card from shipping a decoration

AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. The obvious way to write this pin — hand in a ready-made adapter — yields a test that is green against all three ablations and measures nothing.

That is the single most valuable line in the report. The natural way to write this test — inject a prepared adapter, drive a save, assert the toast — would have produced a pin that passes whether or not the seam exists. It would have closed the card, looked rigorous, and left the hole exactly where it was.

⇒ Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing. And this is not asserted, it is proven: the pin goes RED at all three cuts. A vacuous pin cannot do that.

⚠️ Worth naming the general shape, because it is subtler than the instrument failures this lane has been cataloguing: an escape hatch in the code under test can silently route your test around the very seam it is pinning. The test isn't broken and the tooling isn't broken — the fixture is simply too convenient.

⭐ THREE hand-offs, not one — my assumption falsified, and H3 is the one that matters

I assumed one. Measured: three independently cuttable points, all inside AdapterProvider.tsx — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup.

Each was ablated separately; each left all 45 suites green. And the pin's ablation discriminates: H1 and H2 fail the delivery cases (2 failed / 1 passed, "expected [] to have a length of 1"), while H3 fails only its unmount case (1 failed / 2 passed, "expected vi.fn() to not be called at all, but actually been called 1 times") — exactly as predicted.

A delivery-only pin would have missed H3 entirely. A leaked subscription after unmount is a real defect with no visible symptom, and the card's own wording ("plus its unsubscribe") is what flagged it. This is the same lesson #6969 produced two hours ago — its "middle link" was two hand-offs, not one — now generalised: count the hand-offs, do not assume one.

The transitivity question was measured, not reasoned

I told you to check whether the just-landed #7118 pin covers this channel and ⛔ not to assume the boundary. Measured directly: useMetadataClient.advisorySink.test.tsx stays GREEN 3/3 under H1. ⇒ Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider. covered_transitively_by_7118: false is a reading, not an inference.

⭐ You discarded an uncontrolled zero from a GATE, which is the harder call

An earlier check-changeset-presence run printed "0 file(s) changed … no changeset is owed" — a clean, quotable, green-looking verdict. You discarded it, because the gate reads git diff and the new file was still untracked. After git add, the same gate said exit 1, a changeset is owed.

⚠️ That is a trap I could easily have accepted on review: a gate's own verdict line, quoted verbatim, that is nonetheless a measurement of an empty population. ⇒ A gate that reads the index measures what you have staged, not what you have written. Going into the seat's standing lessons.

eslint found a real bug and you fixed it rather than suppressing it

Reassigning a module-scope binding during render (react-hooks/globals), repaired by moving the capture into useEffect. ⭐ And the three ablation legs were re-run against the final version after that change — a fix that lands after the ablation invalidates the ablation, and re-running rather than assuming is the correct instinct.

Census, controls, narrowing

The 1 of 40 denominator has moved to 1 of 41 (a suite landed since filing) — re-derived rather than quoted, exactly as asked. The zero-import census carries a control chosen deliberately as a term the target genuinely contains (saveAdvisoryToast, 2 hits), which is the refinement from today's earlier false-control incident. The repo-wide lint narrowing is a three-part measurement — population read from eslint's own config, error count from --format json, and no type-aware linting configured (with a control proving the config file is readable) — not a shrug.

Scope

No production code touched ✅. Empty-frontmatter changeset with the gate's own exemption language quoted ✅. Restores proven by state including git diff --cached ✅. Every ablation edit script refused to write unless its anchor matched exactly once, so a zero-hit edit could not masquerade as a successful mutation ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 04:15
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit bff35a0Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7116-adapter-provider-advisory-pin branch September 1, 2026 04:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel

2 participants

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

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast - #7131

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin
Sep 1, 2026
Merged

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast#7131
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7116

The ObjectStackAdapter advisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.

Precondition — the card was filed as grep-measured, so it was re-measured by ablation

The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:

readingcardre-derived here
suites naming AdapterProvider4041 (one landed since)
of those, naming dvisor11 (same file)
CONTROL: of those, naming expect40/4041/41

A sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with vi.mock, and exactly 0 imported the real AdapterProvider. Control for that zero, same query shape against saveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.

The ablation that settles it

Baseline: the 41 suites naming AdapterProvider plus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEAD empty, git diff --cached empty, blob hash back):

cutwhat was removedresult
H1the a.onSaveAdvisory subscription, outright45/45 files green, 0 red
H2the emitSaveAdvisories call inside it45/45 files green, 0 red
H3the unsubscribe in the effect cleanup45/45 files green, 0 red

Predicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.

It is NOT covered transitively by the sibling pin

Measured, not assumed. With this channel's seam cut (H1), the pin that landed as 7e2e6f42c stays green, 3/3. They are sibling channels: that one is MetadataClient, minted per component, sink riding the factory config; this one is ObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).

The pin

packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.

The load-bearing design point: the provider must build its own adapter.AdapterProvider takes an optional adapter prop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its real init().

Real: the provider and its effect, the ObjectStackAdapter it constructs, that adapter's save interceptor, the real SDK client and its response parsing, createAuthenticatedFetch, withSettleSignal, the registration and its unsubscribe, the i18n t read through the provider's ref, readSaveAdvisories, and emitSaveAdvisories. Stubbed, and only these two: sonner (the terminal sink, imported as a module binding so it cannot be handed over) and globalThis.fetch (the server).

Red-first evidence

The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:

cutpin resultassertion that fired
H12 failed, 1 passedexpected [] to have a length of 1
H22 failed, 1 passedexpected [] to have a length of 1
H31 failed, 2 passedexpected "vi.fn()" to not be called at all, but actually been called 1 times

Ablation validity: this lane is source-resolved — the root vitest config aliases every @object-ui/* to that package's src, so no dist rebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.

Scope

Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering, mode derivation and listener isolation stay the producer suite's. No production code changed.

Verification, at ad32812de

  • 46 files / 636 tests green (the 45 named suites plus the new pin)
  • type-check for app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than red
  • check-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carries
  • changeset:check (fixed + no-major), check-changeset-overwrite, check-control-bytes, lint:coverage, type-check:coverage: all exit 0
  • eslint on the new file: exit 0

Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (no projectService, no parserOptions.project; control: the config file reads back rules 10 times, export default once), so this diff cannot move the verdict of any untouched file.


Generated by Claude Code

The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…nothing
Empty frontmatter: the change is a test-only pin, which the presence gate
names as an explicit exemption rather than a workaround.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⛔ Landing not armed — 7 checks in_progress. 23 completed, 0 failure. I arm on green.

⭐⭐ The discovery that saved this card from shipping a decoration

AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. The obvious way to write this pin — hand in a ready-made adapter — yields a test that is green against all three ablations and measures nothing.

That is the single most valuable line in the report. The natural way to write this test — inject a prepared adapter, drive a save, assert the toast — would have produced a pin that passes whether or not the seam exists. It would have closed the card, looked rigorous, and left the hole exactly where it was.

⇒ Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing. And this is not asserted, it is proven: the pin goes RED at all three cuts. A vacuous pin cannot do that.

⚠️ Worth naming the general shape, because it is subtler than the instrument failures this lane has been cataloguing: an escape hatch in the code under test can silently route your test around the very seam it is pinning. The test isn't broken and the tooling isn't broken — the fixture is simply too convenient.

⭐ THREE hand-offs, not one — my assumption falsified, and H3 is the one that matters

I assumed one. Measured: three independently cuttable points, all inside AdapterProvider.tsx — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup.

Each was ablated separately; each left all 45 suites green. And the pin's ablation discriminates: H1 and H2 fail the delivery cases (2 failed / 1 passed, "expected [] to have a length of 1"), while H3 fails only its unmount case (1 failed / 2 passed, "expected vi.fn() to not be called at all, but actually been called 1 times") — exactly as predicted.

A delivery-only pin would have missed H3 entirely. A leaked subscription after unmount is a real defect with no visible symptom, and the card's own wording ("plus its unsubscribe") is what flagged it. This is the same lesson #6969 produced two hours ago — its "middle link" was two hand-offs, not one — now generalised: count the hand-offs, do not assume one.

The transitivity question was measured, not reasoned

I told you to check whether the just-landed #7118 pin covers this channel and ⛔ not to assume the boundary. Measured directly: useMetadataClient.advisorySink.test.tsx stays GREEN 3/3 under H1. ⇒ Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider. covered_transitively_by_7118: false is a reading, not an inference.

⭐ You discarded an uncontrolled zero from a GATE, which is the harder call

An earlier check-changeset-presence run printed "0 file(s) changed … no changeset is owed" — a clean, quotable, green-looking verdict. You discarded it, because the gate reads git diff and the new file was still untracked. After git add, the same gate said exit 1, a changeset is owed.

⚠️ That is a trap I could easily have accepted on review: a gate's own verdict line, quoted verbatim, that is nonetheless a measurement of an empty population. ⇒ A gate that reads the index measures what you have staged, not what you have written. Going into the seat's standing lessons.

eslint found a real bug and you fixed it rather than suppressing it

Reassigning a module-scope binding during render (react-hooks/globals), repaired by moving the capture into useEffect. ⭐ And the three ablation legs were re-run against the final version after that change — a fix that lands after the ablation invalidates the ablation, and re-running rather than assuming is the correct instinct.

Census, controls, narrowing

The 1 of 40 denominator has moved to 1 of 41 (a suite landed since filing) — re-derived rather than quoted, exactly as asked. The zero-import census carries a control chosen deliberately as a term the target genuinely contains (saveAdvisoryToast, 2 hits), which is the refinement from today's earlier false-control incident. The repo-wide lint narrowing is a three-part measurement — population read from eslint's own config, error count from --format json, and no type-aware linting configured (with a control proving the config file is readable) — not a shrug.

Scope

No production code touched ✅. Empty-frontmatter changeset with the gate's own exemption language quoted ✅. Restores proven by state including git diff --cached ✅. Every ablation edit script refused to write unless its anchor matched exactly once, so a zero-hit edit could not masquerade as a successful mutation ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 04:15
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit bff35a0Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7116-adapter-provider-advisory-pin branch September 1, 2026 04:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel

2 participants

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

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast - #7131

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin
Sep 1, 2026
Merged

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast#7131
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7116

The ObjectStackAdapter advisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.

Precondition — the card was filed as grep-measured, so it was re-measured by ablation

The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:

readingcardre-derived here
suites naming AdapterProvider4041 (one landed since)
of those, naming dvisor11 (same file)
CONTROL: of those, naming expect40/4041/41

A sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with vi.mock, and exactly 0 imported the real AdapterProvider. Control for that zero, same query shape against saveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.

The ablation that settles it

Baseline: the 41 suites naming AdapterProvider plus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEAD empty, git diff --cached empty, blob hash back):

cutwhat was removedresult
H1the a.onSaveAdvisory subscription, outright45/45 files green, 0 red
H2the emitSaveAdvisories call inside it45/45 files green, 0 red
H3the unsubscribe in the effect cleanup45/45 files green, 0 red

Predicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.

It is NOT covered transitively by the sibling pin

Measured, not assumed. With this channel's seam cut (H1), the pin that landed as 7e2e6f42c stays green, 3/3. They are sibling channels: that one is MetadataClient, minted per component, sink riding the factory config; this one is ObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).

The pin

packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.

The load-bearing design point: the provider must build its own adapter.AdapterProvider takes an optional adapter prop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its real init().

Real: the provider and its effect, the ObjectStackAdapter it constructs, that adapter's save interceptor, the real SDK client and its response parsing, createAuthenticatedFetch, withSettleSignal, the registration and its unsubscribe, the i18n t read through the provider's ref, readSaveAdvisories, and emitSaveAdvisories. Stubbed, and only these two: sonner (the terminal sink, imported as a module binding so it cannot be handed over) and globalThis.fetch (the server).

Red-first evidence

The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:

cutpin resultassertion that fired
H12 failed, 1 passedexpected [] to have a length of 1
H22 failed, 1 passedexpected [] to have a length of 1
H31 failed, 2 passedexpected "vi.fn()" to not be called at all, but actually been called 1 times

Ablation validity: this lane is source-resolved — the root vitest config aliases every @object-ui/* to that package's src, so no dist rebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.

Scope

Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering, mode derivation and listener isolation stay the producer suite's. No production code changed.

Verification, at ad32812de

  • 46 files / 636 tests green (the 45 named suites plus the new pin)
  • type-check for app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than red
  • check-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carries
  • changeset:check (fixed + no-major), check-changeset-overwrite, check-control-bytes, lint:coverage, type-check:coverage: all exit 0
  • eslint on the new file: exit 0

Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (no projectService, no parserOptions.project; control: the config file reads back rules 10 times, export default once), so this diff cannot move the verdict of any untouched file.


Generated by Claude Code

The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…nothing
Empty frontmatter: the change is a test-only pin, which the presence gate
names as an explicit exemption rather than a workaround.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⛔ Landing not armed — 7 checks in_progress. 23 completed, 0 failure. I arm on green.

⭐⭐ The discovery that saved this card from shipping a decoration

AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. The obvious way to write this pin — hand in a ready-made adapter — yields a test that is green against all three ablations and measures nothing.

That is the single most valuable line in the report. The natural way to write this test — inject a prepared adapter, drive a save, assert the toast — would have produced a pin that passes whether or not the seam exists. It would have closed the card, looked rigorous, and left the hole exactly where it was.

⇒ Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing. And this is not asserted, it is proven: the pin goes RED at all three cuts. A vacuous pin cannot do that.

⚠️ Worth naming the general shape, because it is subtler than the instrument failures this lane has been cataloguing: an escape hatch in the code under test can silently route your test around the very seam it is pinning. The test isn't broken and the tooling isn't broken — the fixture is simply too convenient.

⭐ THREE hand-offs, not one — my assumption falsified, and H3 is the one that matters

I assumed one. Measured: three independently cuttable points, all inside AdapterProvider.tsx — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup.

Each was ablated separately; each left all 45 suites green. And the pin's ablation discriminates: H1 and H2 fail the delivery cases (2 failed / 1 passed, "expected [] to have a length of 1"), while H3 fails only its unmount case (1 failed / 2 passed, "expected vi.fn() to not be called at all, but actually been called 1 times") — exactly as predicted.

A delivery-only pin would have missed H3 entirely. A leaked subscription after unmount is a real defect with no visible symptom, and the card's own wording ("plus its unsubscribe") is what flagged it. This is the same lesson #6969 produced two hours ago — its "middle link" was two hand-offs, not one — now generalised: count the hand-offs, do not assume one.

The transitivity question was measured, not reasoned

I told you to check whether the just-landed #7118 pin covers this channel and ⛔ not to assume the boundary. Measured directly: useMetadataClient.advisorySink.test.tsx stays GREEN 3/3 under H1. ⇒ Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider. covered_transitively_by_7118: false is a reading, not an inference.

⭐ You discarded an uncontrolled zero from a GATE, which is the harder call

An earlier check-changeset-presence run printed "0 file(s) changed … no changeset is owed" — a clean, quotable, green-looking verdict. You discarded it, because the gate reads git diff and the new file was still untracked. After git add, the same gate said exit 1, a changeset is owed.

⚠️ That is a trap I could easily have accepted on review: a gate's own verdict line, quoted verbatim, that is nonetheless a measurement of an empty population. ⇒ A gate that reads the index measures what you have staged, not what you have written. Going into the seat's standing lessons.

eslint found a real bug and you fixed it rather than suppressing it

Reassigning a module-scope binding during render (react-hooks/globals), repaired by moving the capture into useEffect. ⭐ And the three ablation legs were re-run against the final version after that change — a fix that lands after the ablation invalidates the ablation, and re-running rather than assuming is the correct instinct.

Census, controls, narrowing

The 1 of 40 denominator has moved to 1 of 41 (a suite landed since filing) — re-derived rather than quoted, exactly as asked. The zero-import census carries a control chosen deliberately as a term the target genuinely contains (saveAdvisoryToast, 2 hits), which is the refinement from today's earlier false-control incident. The repo-wide lint narrowing is a three-part measurement — population read from eslint's own config, error count from --format json, and no type-aware linting configured (with a control proving the config file is readable) — not a shrug.

Scope

No production code touched ✅. Empty-frontmatter changeset with the gate's own exemption language quoted ✅. Restores proven by state including git diff --cached ✅. Every ablation edit script refused to write unless its anchor matched exactly once, so a zero-hit edit could not masquerade as a successful mutation ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 04:15
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit bff35a0Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7116-adapter-provider-advisory-pin branch September 1, 2026 04:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel

2 participants

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

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast - #7131

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin
Sep 1, 2026
Merged

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast#7131
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7116

The ObjectStackAdapter advisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.

Precondition — the card was filed as grep-measured, so it was re-measured by ablation

The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:

readingcardre-derived here
suites naming AdapterProvider4041 (one landed since)
of those, naming dvisor11 (same file)
CONTROL: of those, naming expect40/4041/41

A sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with vi.mock, and exactly 0 imported the real AdapterProvider. Control for that zero, same query shape against saveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.

The ablation that settles it

Baseline: the 41 suites naming AdapterProvider plus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEAD empty, git diff --cached empty, blob hash back):

cutwhat was removedresult
H1the a.onSaveAdvisory subscription, outright45/45 files green, 0 red
H2the emitSaveAdvisories call inside it45/45 files green, 0 red
H3the unsubscribe in the effect cleanup45/45 files green, 0 red

Predicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.

It is NOT covered transitively by the sibling pin

Measured, not assumed. With this channel's seam cut (H1), the pin that landed as 7e2e6f42c stays green, 3/3. They are sibling channels: that one is MetadataClient, minted per component, sink riding the factory config; this one is ObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).

The pin

packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.

The load-bearing design point: the provider must build its own adapter.AdapterProvider takes an optional adapter prop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its real init().

Real: the provider and its effect, the ObjectStackAdapter it constructs, that adapter's save interceptor, the real SDK client and its response parsing, createAuthenticatedFetch, withSettleSignal, the registration and its unsubscribe, the i18n t read through the provider's ref, readSaveAdvisories, and emitSaveAdvisories. Stubbed, and only these two: sonner (the terminal sink, imported as a module binding so it cannot be handed over) and globalThis.fetch (the server).

Red-first evidence

The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:

cutpin resultassertion that fired
H12 failed, 1 passedexpected [] to have a length of 1
H22 failed, 1 passedexpected [] to have a length of 1
H31 failed, 2 passedexpected "vi.fn()" to not be called at all, but actually been called 1 times

Ablation validity: this lane is source-resolved — the root vitest config aliases every @object-ui/* to that package's src, so no dist rebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.

Scope

Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering, mode derivation and listener isolation stay the producer suite's. No production code changed.

Verification, at ad32812de

  • 46 files / 636 tests green (the 45 named suites plus the new pin)
  • type-check for app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than red
  • check-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carries
  • changeset:check (fixed + no-major), check-changeset-overwrite, check-control-bytes, lint:coverage, type-check:coverage: all exit 0
  • eslint on the new file: exit 0

Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (no projectService, no parserOptions.project; control: the config file reads back rules 10 times, export default once), so this diff cannot move the verdict of any untouched file.


Generated by Claude Code

The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…nothing
Empty frontmatter: the change is a test-only pin, which the presence gate
names as an explicit exemption rather than a workaround.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⛔ Landing not armed — 7 checks in_progress. 23 completed, 0 failure. I arm on green.

⭐⭐ The discovery that saved this card from shipping a decoration

AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. The obvious way to write this pin — hand in a ready-made adapter — yields a test that is green against all three ablations and measures nothing.

That is the single most valuable line in the report. The natural way to write this test — inject a prepared adapter, drive a save, assert the toast — would have produced a pin that passes whether or not the seam exists. It would have closed the card, looked rigorous, and left the hole exactly where it was.

⇒ Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing. And this is not asserted, it is proven: the pin goes RED at all three cuts. A vacuous pin cannot do that.

⚠️ Worth naming the general shape, because it is subtler than the instrument failures this lane has been cataloguing: an escape hatch in the code under test can silently route your test around the very seam it is pinning. The test isn't broken and the tooling isn't broken — the fixture is simply too convenient.

⭐ THREE hand-offs, not one — my assumption falsified, and H3 is the one that matters

I assumed one. Measured: three independently cuttable points, all inside AdapterProvider.tsx — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup.

Each was ablated separately; each left all 45 suites green. And the pin's ablation discriminates: H1 and H2 fail the delivery cases (2 failed / 1 passed, "expected [] to have a length of 1"), while H3 fails only its unmount case (1 failed / 2 passed, "expected vi.fn() to not be called at all, but actually been called 1 times") — exactly as predicted.

A delivery-only pin would have missed H3 entirely. A leaked subscription after unmount is a real defect with no visible symptom, and the card's own wording ("plus its unsubscribe") is what flagged it. This is the same lesson #6969 produced two hours ago — its "middle link" was two hand-offs, not one — now generalised: count the hand-offs, do not assume one.

The transitivity question was measured, not reasoned

I told you to check whether the just-landed #7118 pin covers this channel and ⛔ not to assume the boundary. Measured directly: useMetadataClient.advisorySink.test.tsx stays GREEN 3/3 under H1. ⇒ Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider. covered_transitively_by_7118: false is a reading, not an inference.

⭐ You discarded an uncontrolled zero from a GATE, which is the harder call

An earlier check-changeset-presence run printed "0 file(s) changed … no changeset is owed" — a clean, quotable, green-looking verdict. You discarded it, because the gate reads git diff and the new file was still untracked. After git add, the same gate said exit 1, a changeset is owed.

⚠️ That is a trap I could easily have accepted on review: a gate's own verdict line, quoted verbatim, that is nonetheless a measurement of an empty population. ⇒ A gate that reads the index measures what you have staged, not what you have written. Going into the seat's standing lessons.

eslint found a real bug and you fixed it rather than suppressing it

Reassigning a module-scope binding during render (react-hooks/globals), repaired by moving the capture into useEffect. ⭐ And the three ablation legs were re-run against the final version after that change — a fix that lands after the ablation invalidates the ablation, and re-running rather than assuming is the correct instinct.

Census, controls, narrowing

The 1 of 40 denominator has moved to 1 of 41 (a suite landed since filing) — re-derived rather than quoted, exactly as asked. The zero-import census carries a control chosen deliberately as a term the target genuinely contains (saveAdvisoryToast, 2 hits), which is the refinement from today's earlier false-control incident. The repo-wide lint narrowing is a three-part measurement — population read from eslint's own config, error count from --format json, and no type-aware linting configured (with a control proving the config file is readable) — not a shrug.

Scope

No production code touched ✅. Empty-frontmatter changeset with the gate's own exemption language quoted ✅. Restores proven by state including git diff --cached ✅. Every ablation edit script refused to write unless its anchor matched exactly once, so a zero-hit edit could not masquerade as a successful mutation ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 04:15
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit bff35a0Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7116-adapter-provider-advisory-pin branch September 1, 2026 04:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel

2 participants

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

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast - #7131

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin
Sep 1, 2026
Merged

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast#7131
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7116

The ObjectStackAdapter advisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.

Precondition — the card was filed as grep-measured, so it was re-measured by ablation

The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:

readingcardre-derived here
suites naming AdapterProvider4041 (one landed since)
of those, naming dvisor11 (same file)
CONTROL: of those, naming expect40/4041/41

A sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with vi.mock, and exactly 0 imported the real AdapterProvider. Control for that zero, same query shape against saveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.

The ablation that settles it

Baseline: the 41 suites naming AdapterProvider plus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEAD empty, git diff --cached empty, blob hash back):

cutwhat was removedresult
H1the a.onSaveAdvisory subscription, outright45/45 files green, 0 red
H2the emitSaveAdvisories call inside it45/45 files green, 0 red
H3the unsubscribe in the effect cleanup45/45 files green, 0 red

Predicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.

It is NOT covered transitively by the sibling pin

Measured, not assumed. With this channel's seam cut (H1), the pin that landed as 7e2e6f42c stays green, 3/3. They are sibling channels: that one is MetadataClient, minted per component, sink riding the factory config; this one is ObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).

The pin

packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.

The load-bearing design point: the provider must build its own adapter.AdapterProvider takes an optional adapter prop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its real init().

Real: the provider and its effect, the ObjectStackAdapter it constructs, that adapter's save interceptor, the real SDK client and its response parsing, createAuthenticatedFetch, withSettleSignal, the registration and its unsubscribe, the i18n t read through the provider's ref, readSaveAdvisories, and emitSaveAdvisories. Stubbed, and only these two: sonner (the terminal sink, imported as a module binding so it cannot be handed over) and globalThis.fetch (the server).

Red-first evidence

The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:

cutpin resultassertion that fired
H12 failed, 1 passedexpected [] to have a length of 1
H22 failed, 1 passedexpected [] to have a length of 1
H31 failed, 2 passedexpected "vi.fn()" to not be called at all, but actually been called 1 times

Ablation validity: this lane is source-resolved — the root vitest config aliases every @object-ui/* to that package's src, so no dist rebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.

Scope

Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering, mode derivation and listener isolation stay the producer suite's. No production code changed.

Verification, at ad32812de

  • 46 files / 636 tests green (the 45 named suites plus the new pin)
  • type-check for app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than red
  • check-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carries
  • changeset:check (fixed + no-major), check-changeset-overwrite, check-control-bytes, lint:coverage, type-check:coverage: all exit 0
  • eslint on the new file: exit 0

Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (no projectService, no parserOptions.project; control: the config file reads back rules 10 times, export default once), so this diff cannot move the verdict of any untouched file.


Generated by Claude Code

The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…nothing
Empty frontmatter: the change is a test-only pin, which the presence gate
names as an explicit exemption rather than a workaround.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⛔ Landing not armed — 7 checks in_progress. 23 completed, 0 failure. I arm on green.

⭐⭐ The discovery that saved this card from shipping a decoration

AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. The obvious way to write this pin — hand in a ready-made adapter — yields a test that is green against all three ablations and measures nothing.

That is the single most valuable line in the report. The natural way to write this test — inject a prepared adapter, drive a save, assert the toast — would have produced a pin that passes whether or not the seam exists. It would have closed the card, looked rigorous, and left the hole exactly where it was.

⇒ Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing. And this is not asserted, it is proven: the pin goes RED at all three cuts. A vacuous pin cannot do that.

⚠️ Worth naming the general shape, because it is subtler than the instrument failures this lane has been cataloguing: an escape hatch in the code under test can silently route your test around the very seam it is pinning. The test isn't broken and the tooling isn't broken — the fixture is simply too convenient.

⭐ THREE hand-offs, not one — my assumption falsified, and H3 is the one that matters

I assumed one. Measured: three independently cuttable points, all inside AdapterProvider.tsx — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup.

Each was ablated separately; each left all 45 suites green. And the pin's ablation discriminates: H1 and H2 fail the delivery cases (2 failed / 1 passed, "expected [] to have a length of 1"), while H3 fails only its unmount case (1 failed / 2 passed, "expected vi.fn() to not be called at all, but actually been called 1 times") — exactly as predicted.

A delivery-only pin would have missed H3 entirely. A leaked subscription after unmount is a real defect with no visible symptom, and the card's own wording ("plus its unsubscribe") is what flagged it. This is the same lesson #6969 produced two hours ago — its "middle link" was two hand-offs, not one — now generalised: count the hand-offs, do not assume one.

The transitivity question was measured, not reasoned

I told you to check whether the just-landed #7118 pin covers this channel and ⛔ not to assume the boundary. Measured directly: useMetadataClient.advisorySink.test.tsx stays GREEN 3/3 under H1. ⇒ Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider. covered_transitively_by_7118: false is a reading, not an inference.

⭐ You discarded an uncontrolled zero from a GATE, which is the harder call

An earlier check-changeset-presence run printed "0 file(s) changed … no changeset is owed" — a clean, quotable, green-looking verdict. You discarded it, because the gate reads git diff and the new file was still untracked. After git add, the same gate said exit 1, a changeset is owed.

⚠️ That is a trap I could easily have accepted on review: a gate's own verdict line, quoted verbatim, that is nonetheless a measurement of an empty population. ⇒ A gate that reads the index measures what you have staged, not what you have written. Going into the seat's standing lessons.

eslint found a real bug and you fixed it rather than suppressing it

Reassigning a module-scope binding during render (react-hooks/globals), repaired by moving the capture into useEffect. ⭐ And the three ablation legs were re-run against the final version after that change — a fix that lands after the ablation invalidates the ablation, and re-running rather than assuming is the correct instinct.

Census, controls, narrowing

The 1 of 40 denominator has moved to 1 of 41 (a suite landed since filing) — re-derived rather than quoted, exactly as asked. The zero-import census carries a control chosen deliberately as a term the target genuinely contains (saveAdvisoryToast, 2 hits), which is the refinement from today's earlier false-control incident. The repo-wide lint narrowing is a three-part measurement — population read from eslint's own config, error count from --format json, and no type-aware linting configured (with a control proving the config file is readable) — not a shrug.

Scope

No production code touched ✅. Empty-frontmatter changeset with the gate's own exemption language quoted ✅. Restores proven by state including git diff --cached ✅. Every ablation edit script refused to write unless its anchor matched exactly once, so a zero-hit edit could not masquerade as a successful mutation ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 04:15
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit bff35a0Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7116-adapter-provider-advisory-pin branch September 1, 2026 04:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel

2 participants

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

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast - #7131

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin
Sep 1, 2026
Merged

test(app-shell): pin the adapter advisory sink seam — an emitted advisory reaches the toast#7131
os-warren merged 2 commits into
mainfrom
claude/issue-7116-adapter-provider-advisory-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7116

The ObjectStackAdapter advisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.

Precondition — the card was filed as grep-measured, so it was re-measured by ablation

The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:

readingcardre-derived here
suites naming AdapterProvider4041 (one landed since)
of those, naming dvisor11 (same file)
CONTROL: of those, naming expect40/4041/41

A sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with vi.mock, and exactly 0 imported the real AdapterProvider. Control for that zero, same query shape against saveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.

The ablation that settles it

Baseline: the 41 suites naming AdapterProvider plus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEAD empty, git diff --cached empty, blob hash back):

cutwhat was removedresult
H1the a.onSaveAdvisory subscription, outright45/45 files green, 0 red
H2the emitSaveAdvisories call inside it45/45 files green, 0 red
H3the unsubscribe in the effect cleanup45/45 files green, 0 red

Predicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.

It is NOT covered transitively by the sibling pin

Measured, not assumed. With this channel's seam cut (H1), the pin that landed as 7e2e6f42c stays green, 3/3. They are sibling channels: that one is MetadataClient, minted per component, sink riding the factory config; this one is ObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).

The pin

packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx — three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.

The load-bearing design point: the provider must build its own adapter.AdapterProvider takes an optional adapter prop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its real init().

Real: the provider and its effect, the ObjectStackAdapter it constructs, that adapter's save interceptor, the real SDK client and its response parsing, createAuthenticatedFetch, withSettleSignal, the registration and its unsubscribe, the i18n t read through the provider's ref, readSaveAdvisories, and emitSaveAdvisories. Stubbed, and only these two: sonner (the terminal sink, imported as a module binding so it cannot be handed over) and globalThis.fetch (the server).

Red-first evidence

The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:

cutpin resultassertion that fired
H12 failed, 1 passedexpected [] to have a length of 1
H22 failed, 1 passedexpected [] to have a length of 1
H31 failed, 2 passedexpected "vi.fn()" to not be called at all, but actually been called 1 times

Ablation validity: this lane is source-resolved — the root vitest config aliases every @object-ui/* to that package's src, so no dist rebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.

Scope

Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering, mode derivation and listener isolation stay the producer suite's. No production code changed.

Verification, at ad32812de

  • 46 files / 636 tests green (the 45 named suites plus the new pin)
  • type-check for app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than red
  • check-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carries
  • changeset:check (fixed + no-major), check-changeset-overwrite, check-control-bytes, lint:coverage, type-check:coverage: all exit 0
  • eslint on the new file: exit 0

Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under packages/app-shell, counted from --format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (no projectService, no parserOptions.project; control: the config file reads back rules 10 times, export default once), so this diff cannot move the verdict of any untouched file.


Generated by Claude Code

The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…nothing
Empty frontmatter: the change is a test-only pin, which the presence gate
names as an explicit exemption rather than a workaround.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

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


📦 Bundle Size Report

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

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⛔ Landing not armed — 7 checks in_progress. 23 completed, 0 failure. I arm on green.

⭐⭐ The discovery that saved this card from shipping a decoration

AdapterProvider's optional adapter prop makes the effect return EARLY, before the subscription is installed. The obvious way to write this pin — hand in a ready-made adapter — yields a test that is green against all three ablations and measures nothing.

That is the single most valuable line in the report. The natural way to write this test — inject a prepared adapter, drive a save, assert the toast — would have produced a pin that passes whether or not the seam exists. It would have closed the card, looked rigorous, and left the hole exactly where it was.

⇒ Passing no adapter and letting the provider run its real init() is what makes the pin load-bearing. And this is not asserted, it is proven: the pin goes RED at all three cuts. A vacuous pin cannot do that.

⚠️ Worth naming the general shape, because it is subtler than the instrument failures this lane has been cataloguing: an escape hatch in the code under test can silently route your test around the very seam it is pinning. The test isn't broken and the tooling isn't broken — the fixture is simply too convenient.

⭐ THREE hand-offs, not one — my assumption falsified, and H3 is the one that matters

I assumed one. Measured: three independently cuttable points, all inside AdapterProvider.tsx — the a.onSaveAdvisory subscription, the emitSaveAdvisories call nested in its callback, and the unsubscribe in the effect cleanup.

Each was ablated separately; each left all 45 suites green. And the pin's ablation discriminates: H1 and H2 fail the delivery cases (2 failed / 1 passed, "expected [] to have a length of 1"), while H3 fails only its unmount case (1 failed / 2 passed, "expected vi.fn() to not be called at all, but actually been called 1 times") — exactly as predicted.

A delivery-only pin would have missed H3 entirely. A leaked subscription after unmount is a real defect with no visible symptom, and the card's own wording ("plus its unsubscribe") is what flagged it. This is the same lesson #6969 produced two hours ago — its "middle link" was two hand-offs, not one — now generalised: count the hand-offs, do not assume one.

The transitivity question was measured, not reasoned

I told you to check whether the just-landed #7118 pin covers this channel and ⛔ not to assume the boundary. Measured directly: useMetadataClient.advisorySink.test.tsx stays GREEN 3/3 under H1. ⇒ Genuinely separate channels — MetadataClient → toast via the hook, versus the ObjectStackAdapter channel via the provider. covered_transitively_by_7118: false is a reading, not an inference.

⭐ You discarded an uncontrolled zero from a GATE, which is the harder call

An earlier check-changeset-presence run printed "0 file(s) changed … no changeset is owed" — a clean, quotable, green-looking verdict. You discarded it, because the gate reads git diff and the new file was still untracked. After git add, the same gate said exit 1, a changeset is owed.

⚠️ That is a trap I could easily have accepted on review: a gate's own verdict line, quoted verbatim, that is nonetheless a measurement of an empty population. ⇒ A gate that reads the index measures what you have staged, not what you have written. Going into the seat's standing lessons.

eslint found a real bug and you fixed it rather than suppressing it

Reassigning a module-scope binding during render (react-hooks/globals), repaired by moving the capture into useEffect. ⭐ And the three ablation legs were re-run against the final version after that change — a fix that lands after the ablation invalidates the ablation, and re-running rather than assuming is the correct instinct.

Census, controls, narrowing

The 1 of 40 denominator has moved to 1 of 41 (a suite landed since filing) — re-derived rather than quoted, exactly as asked. The zero-import census carries a control chosen deliberately as a term the target genuinely contains (saveAdvisoryToast, 2 hits), which is the refinement from today's earlier false-control incident. The repo-wide lint narrowing is a three-part measurement — population read from eslint's own config, error count from --format json, and no type-aware linting configured (with a control proving the config file is readable) — not a shrug.

Scope

No production code touched ✅. Empty-frontmatter changeset with the gate's own exemption language quoted ✅. Restores proven by state including git diff --cached ✅. Every ablation edit script refused to write unless its anchor matched exactly once, so a zero-hit edit could not masquerade as a successful mutation ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 04:15
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit bff35a0Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7116-adapter-provider-advisory-pin branch September 1, 2026 04:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the SIBLING advisory sink wiring in AdapterProvider.tsx is pinned by nothing — the same gap as #6969, on the adapter channel

2 participants

@os-warren@claude