refactor(plugin-sdk): annotate dormant reserved subpaths

This commit is contained in:
Peter Steinberger
2026-04-28 04:39:29 +01:00
parent 0ff60d162c
commit 4fb543796b
6 changed files with 414 additions and 48 deletions

View File

@@ -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

View File

@@ -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).

View File

@@ -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<CliOptions, "owner"> = {}): BoundaryReport {
);
const usedReserved = new Set(reservedImports.map((entry) => entry.subpath));
const dormantReserved = new Set<string>(dormantReservedBundledPluginSdkEntrypoints);
const dormantReservedRecords = dormantReservedBundledPluginSdkEntrypointRecords.filter((record) =>
matchesOwner(options.owner, record.owner),
);
const unusedReservedSubpaths = reservedBundledPluginSdkEntrypoints
.filter(
(subpath) =>
@@ -444,6 +461,11 @@ function buildReport(options: Pick<CliOptions, "owner"> = {}): 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<CliOptions, "owner"> = {}): 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;
}

View File

@@ -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.

View File

@@ -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<string, number>();
@@ -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<string>(reservedBundledPluginSdkEntrypoints);

View File

@@ -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");
});