From 4fb543796b417b25a13bad04dc070f7bfa80b4c9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 04:39:29 +0100 Subject: [PATCH] refactor(plugin-sdk): annotate dormant reserved subpaths --- docs/plugins/sdk-migration.md | 7 +- docs/plugins/sdk-subpaths.md | 4 +- scripts/plugin-boundary-report.ts | 44 +++ src/plugin-sdk/entrypoints.ts | 364 +++++++++++++++--- ...in-sdk-package-contract-guardrails.test.ts | 41 ++ test/scripts/plugin-boundary-report.test.ts | 2 + 6 files changed, 414 insertions(+), 48 deletions(-) diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 415d3e8c755..70931149540 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -96,8 +96,11 @@ compatibility records, cross-owner reserved SDK imports, or unused reserved SDK subpaths without a dormant classification. The report groups deprecated compatibility records by removal date, counts local code/docs references, surfaces cross-owner reserved SDK imports, classifies dormant reserved SDK -subpaths, and summarizes the private memory-host SDK bridge so compatibility -cleanup stays explicit instead of relying on ad hoc searches. +subpaths with owner/replacement/remove-after metadata, and summarizes the +private memory-host SDK bridge so compatibility cleanup stays explicit instead +of relying on ad hoc searches. Dormant reserved SDK subpaths are package exports +with no tracked repo imports; keep them until their recorded removal date unless +a separate compatibility review proves the external import never shipped. If a manifest field is still accepted, plugin authors can keep using it until the docs and diagnostics say otherwise. New code should prefer the documented diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index e27d47900e0..4099f82b7c5 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -11,7 +11,9 @@ This page catalogs the commonly used subpaths grouped by purpose. The generated full list of 200+ subpaths lives in `scripts/lib/plugin-sdk-entrypoints.json`; reserved bundled-plugin helper subpaths appear there but are implementation detail unless a doc page explicitly promotes them. Maintainers can audit active -and dormant reserved helper subpaths with `pnpm plugins:boundary-report:summary`. +and dormant reserved helper subpaths with `pnpm plugins:boundary-report:summary`; +the full JSON report includes dormant helper owner, replacement, and +remove-after metadata. For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview). diff --git a/scripts/plugin-boundary-report.ts b/scripts/plugin-boundary-report.ts index 72628a39f79..10f63794c2b 100644 --- a/scripts/plugin-boundary-report.ts +++ b/scripts/plugin-boundary-report.ts @@ -3,11 +3,13 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join, relative, resolve } from "node:path"; import { dormantReservedBundledPluginSdkEntrypoints, + dormantReservedBundledPluginSdkEntrypointRecords, pluginSdkEntrypoints, publicPluginOwnedSdkEntrypoints, reservedBundledPluginSdkEntrypoints, supportedBundledFacadeSdkEntrypoints, } from "../src/plugin-sdk/entrypoints.ts"; +import type { DormantReservedBundledPluginSdkEntrypointRecord } from "../src/plugin-sdk/entrypoints.ts"; import { PLUGIN_COMPAT_RECORDS } from "../src/plugins/compat/registry.ts"; import type { PluginCompatRecord } from "../src/plugins/compat/types.ts"; @@ -75,7 +77,9 @@ type BoundaryReport = { crossOwnerReservedImports: ReservedSdkImport[]; unusedReservedSubpaths: string[]; dormantReservedSubpaths: string[]; + dormantReservedRecords: DormantReservedBundledPluginSdkEntrypointRecord[]; unclassifiedUnusedReservedSubpaths: string[]; + dormantReservedEligibleForRemovalSubpaths: string[]; }; memoryHostSdk: { privatePackage: boolean; @@ -106,6 +110,8 @@ type BoundaryReportSummary = { dormantReservedCountInUnused: number; unclassifiedUnusedReservedCount: number; unclassifiedUnusedReservedSubpaths: string[]; + dormantReservedEligibleForRemovalCount: number; + dormantReservedEligibleForRemovalSubpaths: string[]; crossOwnerReservedImports: ReservedSdkImport[]; }; memoryHostSdk: { @@ -381,6 +387,10 @@ function resolveMemoryHostImplementation( return "mixed"; } +function isDateDue(removeAfter: string, today = new Date()): boolean { + return new Date(`${removeAfter}T00:00:00Z`) <= today; +} + function buildSummary(report: BoundaryReport, owner?: string): BoundaryReportSummary { const eligibleForRemoval = report.compat.records .filter((record) => record.eligibleForRemoval) @@ -410,6 +420,10 @@ function buildSummary(report: BoundaryReport, owner?: string): BoundaryReportSum dormantReservedCountInUnused: report.pluginSdk.dormantReservedSubpaths.length, unclassifiedUnusedReservedCount: report.pluginSdk.unclassifiedUnusedReservedSubpaths.length, unclassifiedUnusedReservedSubpaths: report.pluginSdk.unclassifiedUnusedReservedSubpaths, + dormantReservedEligibleForRemovalCount: + report.pluginSdk.dormantReservedEligibleForRemovalSubpaths.length, + dormantReservedEligibleForRemovalSubpaths: + report.pluginSdk.dormantReservedEligibleForRemovalSubpaths, crossOwnerReservedImports: report.pluginSdk.crossOwnerReservedImports, }, memoryHostSdk: { @@ -434,6 +448,9 @@ function buildReport(options: Pick = {}): BoundaryReport { ); const usedReserved = new Set(reservedImports.map((entry) => entry.subpath)); const dormantReserved = new Set(dormantReservedBundledPluginSdkEntrypoints); + const dormantReservedRecords = dormantReservedBundledPluginSdkEntrypointRecords.filter((record) => + matchesOwner(options.owner, record.owner), + ); const unusedReservedSubpaths = reservedBundledPluginSdkEntrypoints .filter( (subpath) => @@ -444,6 +461,11 @@ function buildReport(options: Pick = {}): BoundaryReport { const dormantReservedSubpaths = unusedReservedSubpaths .filter((subpath) => dormantReserved.has(subpath)) .toSorted(); + const dormantReservedEligibleForRemovalSubpaths = dormantReservedRecords + .filter((record) => unusedReservedSubpaths.includes(record.subpath)) + .filter((record) => isDateDue(record.removeAfter)) + .map((record) => record.subpath) + .toSorted(); return { generatedAt: new Date().toISOString(), compat: { @@ -463,9 +485,11 @@ function buildReport(options: Pick = {}): BoundaryReport { ), unusedReservedSubpaths, dormantReservedSubpaths, + dormantReservedRecords, unclassifiedUnusedReservedSubpaths: unusedReservedSubpaths .filter((subpath) => !dormantReserved.has(subpath)) .toSorted(), + dormantReservedEligibleForRemovalSubpaths, }, memoryHostSdk: collectMemoryHostBoundary(files), }; @@ -487,9 +511,15 @@ function renderSummaryText(summary: BoundaryReportSummary): string { lines.push( ` dormantUnused=${summary.pluginSdk.dormantReservedCountInUnused} unclassifiedUnused=${summary.pluginSdk.unclassifiedUnusedReservedCount}`, ); + lines.push( + ` dormantEligibleForRemoval=${summary.pluginSdk.dormantReservedEligibleForRemovalCount}`, + ); for (const subpath of summary.pluginSdk.unclassifiedUnusedReservedSubpaths) { lines.push(` unclassified-unused ${subpath}`); } + for (const subpath of summary.pluginSdk.dormantReservedEligibleForRemovalSubpaths) { + lines.push(` dormant-due ${subpath}`); + } for (const entry of summary.pluginSdk.crossOwnerReservedImports) { lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`); } @@ -521,9 +551,15 @@ function renderText(report: BoundaryReport, owner?: string): string { lines.push( ` dormantUnused=${report.pluginSdk.dormantReservedSubpaths.length} unclassifiedUnused=${report.pluginSdk.unclassifiedUnusedReservedSubpaths.length}`, ); + lines.push( + ` dormantEligibleForRemoval=${report.pluginSdk.dormantReservedEligibleForRemovalSubpaths.length}`, + ); for (const subpath of report.pluginSdk.unclassifiedUnusedReservedSubpaths) { lines.push(` unclassified-unused ${subpath}`); } + for (const subpath of report.pluginSdk.dormantReservedEligibleForRemovalSubpaths) { + lines.push(` dormant-due ${subpath}`); + } for (const entry of report.pluginSdk.crossOwnerReservedImports) { lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`); } @@ -554,6 +590,14 @@ function collectFailures(report: BoundaryReport, options: CliOptions): string[] `${report.compat.eligibleForRemovalCount} compatibility record(s) are due for removal`, ); } + if ( + options.failOnEligibleCompat && + report.pluginSdk.dormantReservedEligibleForRemovalSubpaths.length > 0 + ) { + failures.push( + `${report.pluginSdk.dormantReservedEligibleForRemovalSubpaths.length} dormant reserved SDK subpath(s) are due for removal`, + ); + } return failures; } diff --git a/src/plugin-sdk/entrypoints.ts b/src/plugin-sdk/entrypoints.ts index 15fcb7f8864..eed4ee0a01d 100644 --- a/src/plugin-sdk/entrypoints.ts +++ b/src/plugin-sdk/entrypoints.ts @@ -60,53 +60,327 @@ export const reservedBundledPluginSdkEntrypoints = [ "zalouser", ] as const; +export type DormantReservedBundledPluginSdkEntrypointReason = + | "bundled-plugin-compat" + | "external-compat" + | "owner-facade-compat"; + +export type DormantReservedBundledPluginSdkEntrypointRecord = { + subpath: string; + owner: string; + reason: DormantReservedBundledPluginSdkEntrypointReason; + removeAfter: "2026-07-24"; + replacement: string; +}; + // Reserved compatibility/helper subpaths with no current tracked imports. // Keeping them classified avoids treating dormant compatibility as unknown debt. -export const dormantReservedBundledPluginSdkEntrypoints = [ - "bluebubbles", - "bluebubbles-policy", - "browser-cdp", - "browser-control-auth", - "browser-profiles", - "browser-support", - "diagnostics-otel", - "diagnostics-prometheus", - "diffs", - "feishu", - "feishu-conversation", - "feishu-setup", - "github-copilot-login", - "googlechat", - "googlechat-runtime-shared", - "irc", - "irc-surface", - "line", - "line-core", - "line-runtime", - "line-surface", - "llm-task", - "matrix", - "matrix-helper", - "matrix-runtime-heavy", - "matrix-runtime-surface", - "matrix-surface", - "matrix-thread-bindings", - "mattermost", - "mattermost-policy", - "memory-lancedb", - "msteams", - "nextcloud-talk", - "nostr", - "opencode", - "telegram-command-ui", - "thread-ownership", - "tlon", - "twitch", - "voice-call", - "zalo", - "zalo-setup", - "zalouser", -] as const; +export const dormantReservedBundledPluginSdkEntrypointRecords = [ + { + subpath: "bluebubbles", + owner: "bluebubbles", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "BlueBubbles local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "bluebubbles-policy", + owner: "bluebubbles", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "BlueBubbles local api/runtime-api plus plugin-sdk/channel-policy", + }, + { + subpath: "browser-cdp", + owner: "browser", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "plugin-sdk/browser-config plus browser local config helpers", + }, + { + subpath: "browser-control-auth", + owner: "browser", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "plugin-sdk/browser-config plus browser local control-auth helpers", + }, + { + subpath: "browser-profiles", + owner: "browser", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "plugin-sdk/browser-config plus browser local profile helpers", + }, + { + subpath: "browser-support", + owner: "browser", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "focused browser SDK subpaths", + }, + { + subpath: "diagnostics-otel", + owner: "diagnostics-otel", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "diagnostics-otel local api plus plugin-sdk/diagnostic-runtime", + }, + { + subpath: "diagnostics-prometheus", + owner: "diagnostics-prometheus", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "diagnostics-prometheus local api plus plugin-sdk/diagnostic-runtime", + }, + { + subpath: "diffs", + owner: "diffs", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "diffs local api/runtime-api plus generic plugin SDK subpaths", + }, + { + subpath: "feishu", + owner: "feishu", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Feishu local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "feishu-conversation", + owner: "feishu", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Feishu local contract-api plus plugin-sdk/conversation-runtime", + }, + { + subpath: "feishu-setup", + owner: "feishu", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Feishu local setup-api plus plugin-sdk/channel-setup", + }, + { + subpath: "github-copilot-login", + owner: "github-copilot", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "GitHub Copilot local api plus plugin-sdk/provider-auth-login", + }, + { + subpath: "googlechat", + owner: "googlechat", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Google Chat local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "googlechat-runtime-shared", + owner: "googlechat", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Google Chat local runtime-api plus plugin-sdk/config-types", + }, + { + subpath: "irc", + owner: "irc", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "IRC local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "irc-surface", + owner: "irc", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "IRC local api plus plugin-sdk/channel-setup", + }, + { + subpath: "line", + owner: "line", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "LINE local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "line-core", + owner: "line", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "LINE local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "line-runtime", + owner: "line", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "LINE local runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "line-surface", + owner: "line", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "LINE local runtime-api plus plugin-sdk/channel-setup", + }, + { + subpath: "llm-task", + owner: "llm-task", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "llm-task local api plus plugin-sdk/plugin-entry", + }, + { + subpath: "matrix", + owner: "matrix", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Matrix local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "matrix-helper", + owner: "matrix", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Matrix local api plus plugin-sdk/config-types", + }, + { + subpath: "matrix-runtime-heavy", + owner: "matrix", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Matrix local runtime-api plus doctor/fix migration paths", + }, + { + subpath: "matrix-runtime-surface", + owner: "matrix", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Matrix local runtime-api plus plugin-sdk/config-types", + }, + { + subpath: "matrix-surface", + owner: "matrix", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Matrix local contract/runtime API plus generic channel SDK subpaths", + }, + { + subpath: "matrix-thread-bindings", + owner: "matrix", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Matrix local api plus plugin-sdk/thread-bindings-runtime", + }, + { + subpath: "mattermost", + owner: "mattermost", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Mattermost local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "mattermost-policy", + owner: "mattermost", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Mattermost local policy-api plus plugin-sdk/channel-policy", + }, + { + subpath: "memory-lancedb", + owner: "memory-lancedb", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "memory-lancedb local api plus plugin-sdk/plugin-entry", + }, + { + subpath: "msteams", + owner: "msteams", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Microsoft Teams local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "nextcloud-talk", + owner: "nextcloud-talk", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Nextcloud Talk local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "nostr", + owner: "nostr", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Nostr local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "opencode", + owner: "opencode", + reason: "external-compat", + removeAfter: "2026-07-24", + replacement: "plugin-sdk/provider-auth-api-key plus OpenCode local provider helpers", + }, + { + subpath: "telegram-command-ui", + owner: "telegram", + reason: "external-compat", + removeAfter: "2026-07-24", + replacement: "plugin-sdk/telegram-command-config plus Telegram local command UI helpers", + }, + { + subpath: "thread-ownership", + owner: "thread-ownership", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "thread-ownership local api plus plugin-sdk/plugin-entry", + }, + { + subpath: "tlon", + owner: "tlon", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Tlon local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "twitch", + owner: "twitch", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Twitch local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "voice-call", + owner: "voice-call", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Voice Call local api plus plugin-sdk/plugin-entry", + }, + { + subpath: "zalo", + owner: "zalo", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Zalo local api/runtime-api plus generic channel SDK subpaths", + }, + { + subpath: "zalo-setup", + owner: "zalo", + reason: "owner-facade-compat", + removeAfter: "2026-07-24", + replacement: "Zalo local setup/contract APIs plus plugin-sdk/channel-setup", + }, + { + subpath: "zalouser", + owner: "zalouser", + reason: "bundled-plugin-compat", + removeAfter: "2026-07-24", + replacement: "Zalo user local api/runtime-api plus generic channel SDK subpaths", + }, +] as const satisfies readonly DormantReservedBundledPluginSdkEntrypointRecord[]; + +export const dormantReservedBundledPluginSdkEntrypoints = + dormantReservedBundledPluginSdkEntrypointRecords.map((record) => record.subpath); // Supported SDK facades backed by bundled plugins. These are intentionally public // until they move to generic, plugin-neutral contracts. diff --git a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts index 39d698b6795..13052c4cc59 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { dormantReservedBundledPluginSdkEntrypoints, + dormantReservedBundledPluginSdkEntrypointRecords, pluginSdkEntrypoints, publicPluginOwnedSdkEntrypoints, reservedBundledPluginSdkEntrypoints, @@ -514,6 +515,42 @@ function collectReservedSdkSubpathImports(): string[] { return [...imports].toSorted(); } +function collectDormantReservedMetadataDrift(): string[] { + const failures: string[] = []; + const recordsBySubpath = new Map< + string, + (typeof dormantReservedBundledPluginSdkEntrypointRecords)[number] + >(); + for (const record of dormantReservedBundledPluginSdkEntrypointRecords) { + if (recordsBySubpath.has(record.subpath)) { + failures.push(`${record.subpath}: duplicate dormant metadata record`); + continue; + } + recordsBySubpath.set(record.subpath, record); + if (record.replacement.trim().length === 0) { + failures.push(`${record.subpath}: missing replacement`); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(record.removeAfter)) { + failures.push(`${record.subpath}: invalid removeAfter ${record.removeAfter}`); + } + if (record.owner.trim().length === 0) { + failures.push(`${record.subpath}: missing owner`); + } + const resolvedOwner = resolvePluginOwnerFromEntrypoint(record.subpath); + if (resolvedOwner && resolvedOwner !== record.owner) { + failures.push(`${record.subpath}: owner ${record.owner} should be ${resolvedOwner}`); + } + } + + const recordSubpaths = [...recordsBySubpath.keys()].toSorted(); + const derivedSubpaths = [...dormantReservedBundledPluginSdkEntrypoints].toSorted(); + if (JSON.stringify(recordSubpaths) !== JSON.stringify(derivedSubpaths)) { + failures.push("dormant subpath list must be derived from dormant metadata records"); + } + + return failures.toSorted(); +} + describe("plugin-sdk package contract guardrails", () => { it("keeps plugin-sdk entrypoint metadata unique", () => { const counts = new Map(); @@ -564,6 +601,10 @@ describe("plugin-sdk package contract guardrails", () => { }); }); + it("keeps dormant reserved SDK compatibility subpaths annotated for retirement", () => { + expect(collectDormantReservedMetadataDrift()).toEqual([]); + }); + it("keeps plugin-owned SDK subpaths explicitly classified and documented", () => { const entrypoints = new Set(pluginSdkEntrypoints); const reserved = new Set(reservedBundledPluginSdkEntrypoints); diff --git a/test/scripts/plugin-boundary-report.test.ts b/test/scripts/plugin-boundary-report.test.ts index 69bdc930227..19579ff9789 100644 --- a/test/scripts/plugin-boundary-report.test.ts +++ b/test/scripts/plugin-boundary-report.test.ts @@ -27,6 +27,7 @@ describe("plugin-boundary-report", () => { const summary = JSON.parse(output) as { pluginSdk?: { crossOwnerReservedImportCount?: unknown; + dormantReservedEligibleForRemovalCount?: unknown; unclassifiedUnusedReservedCount?: unknown; }; memoryHostSdk?: { @@ -35,6 +36,7 @@ describe("plugin-boundary-report", () => { }; expect(summary.pluginSdk?.crossOwnerReservedImportCount).toBe(0); + expect(summary.pluginSdk?.dormantReservedEligibleForRemovalCount).toBe(0); expect(summary.pluginSdk?.unclassifiedUnusedReservedCount).toBe(0); expect(summary.memoryHostSdk?.implementation).toBe("private-core-bridge"); });