diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 70931149540..1fa28aaa668 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -90,7 +90,8 @@ For external plugins, compatibility work follows this order: Maintainers can audit the current migration queue with `pnpm plugins:boundary-report`. Use `pnpm plugins:boundary-report:summary` for -compact counts, `--owner ` for one plugin or compatibility owner, and +compact counts, `--owner ` for one plugin or compatibility owner, +`--retirement-plan` for an issue/PR-ready dormant SDK checklist, and `pnpm plugins:boundary-report:ci` when a CI gate should fail on due compatibility records, cross-owner reserved SDK imports, or unused reserved SDK subpaths without a dormant classification. The report groups deprecated diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 4099f82b7c5..0ddf83809e4 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -12,8 +12,9 @@ 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`; -the full JSON report includes dormant helper owner, replacement, and -remove-after metadata. +`pnpm plugins:boundary-report --retirement-plan` emits an issue/PR-ready +checklist grouped by owner, and 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 10f63794c2b..801294c8046 100644 --- a/scripts/plugin-boundary-report.ts +++ b/scripts/plugin-boundary-report.ts @@ -30,6 +30,7 @@ const PLUGIN_SDK_SPECIFIER_PATTERN = type CliOptions = { json: boolean; summary: boolean; + retirementPlan: boolean; owner?: string; failOnCrossOwner: boolean; failOnEligibleCompat: boolean; @@ -162,6 +163,7 @@ function parseArgs(args: readonly string[]): CliOptions { const options: CliOptions = { json: false, summary: false, + retirementPlan: false, failOnCrossOwner: false, failOnEligibleCompat: false, failOnUnclassifiedUnusedReserved: false, @@ -173,6 +175,8 @@ function parseArgs(args: readonly string[]): CliOptions { options.json = true; } else if (arg === "--summary") { options.summary = true; + } else if (arg === "--retirement-plan") { + options.retirementPlan = true; } else if (arg === "--owner") { const owner = args[index + 1]; if (!owner || owner.startsWith("--")) { @@ -192,6 +196,9 @@ function parseArgs(args: readonly string[]): CliOptions { throw new Error(`Unknown argument: ${arg}`); } } + if (options.retirementPlan && (options.json || options.summary)) { + throw new Error("--retirement-plan cannot be combined with --summary or --json"); + } return options; } @@ -202,6 +209,7 @@ function renderHelp(): string { "Options:", " --summary Print compact counts only.", " --json Emit JSON instead of text.", + " --retirement-plan Emit an issue/PR-ready dormant SDK subpath retirement checklist.", " --owner Filter compat/imports/reserved shims by owner id.", " --fail-on-cross-owner Exit non-zero on cross-owner reserved SDK imports.", " --fail-on-eligible-compat Exit non-zero when deprecated compat is due for removal.", @@ -570,6 +578,49 @@ function renderText(report: BoundaryReport, owner?: string): string { return lines.join("\n"); } +function renderRetirementPlan(report: BoundaryReport, owner?: string): string { + const records = report.pluginSdk.dormantReservedRecords.toSorted( + (left, right) => + left.owner.localeCompare(right.owner) || + left.removeAfter.localeCompare(right.removeAfter) || + left.subpath.localeCompare(right.subpath), + ); + const dueSubpaths = new Set(report.pluginSdk.dormantReservedEligibleForRemovalSubpaths); + const owners = [...new Set(records.map((record) => record.owner))]; + const removeAfterDates = [...new Set(records.map((record) => record.removeAfter))]; + const lines: string[] = []; + lines.push(`# Plugin SDK Dormant Reserved Subpath Retirement Plan`); + lines.push(""); + lines.push(`Generated: ${report.generatedAt}`); + if (owner) { + lines.push(`Owner filter: \`${owner}\``); + } + lines.push(""); + lines.push("## Summary"); + lines.push(""); + lines.push(`- Dormant reserved subpaths: ${records.length}`); + lines.push(`- Owners: ${owners.length}`); + lines.push(`- Removal dates: ${removeAfterDates.join(", ") || "none"}`); + lines.push(`- Eligible for removal now: ${dueSubpaths.size}`); + lines.push(`- CI gate: \`pnpm plugins:boundary-report:ci\``); + if (records.length === 0) { + lines.push(""); + lines.push("No dormant reserved SDK subpaths match this filter."); + return lines.join("\n"); + } + for (const ownerId of owners) { + lines.push(""); + lines.push(`## ${ownerId}`); + for (const record of records.filter((candidate) => candidate.owner === ownerId)) { + const status = dueSubpaths.has(record.subpath) ? "due" : "waiting"; + lines.push( + `- [ ] \`openclaw/plugin-sdk/${record.subpath}\` remove after \`${record.removeAfter}\` (${status}); reason=\`${record.reason}\`; replacement: ${record.replacement}`, + ); + } + } + return lines.join("\n"); +} + function collectFailures(report: BoundaryReport, options: CliOptions): string[] { const failures: string[] = []; if (options.failOnCrossOwner && report.pluginSdk.crossOwnerReservedImports.length > 0) { @@ -618,7 +669,9 @@ if (options.help) { const report = buildReport(options); const summary = buildSummary(report, options.owner); -if (options.json) { +if (options.retirementPlan) { + process.stdout.write(`${renderRetirementPlan(report, options.owner)}\n`); +} else if (options.json) { process.stdout.write(`${JSON.stringify(options.summary ? summary : report, null, 2)}\n`); } else if (options.summary) { process.stdout.write(`${renderSummaryText(summary)}\n`); diff --git a/test/scripts/plugin-boundary-report.test.ts b/test/scripts/plugin-boundary-report.test.ts index 19579ff9789..d6b8f1a386a 100644 --- a/test/scripts/plugin-boundary-report.test.ts +++ b/test/scripts/plugin-boundary-report.test.ts @@ -40,4 +40,18 @@ describe("plugin-boundary-report", () => { expect(summary.pluginSdk?.unclassifiedUnusedReservedCount).toBe(0); expect(summary.memoryHostSdk?.implementation).toBe("private-core-bridge"); }); + + it("emits an owner-scoped dormant SDK retirement plan", () => { + const output = runBoundaryReport("--retirement-plan", "--owner", "matrix"); + + expect(output).toContain("# Plugin SDK Dormant Reserved Subpath Retirement Plan"); + expect(output).toContain("Owner filter: `matrix`"); + expect(output).toContain("Dormant reserved subpaths: 6"); + expect(output).toContain( + "`openclaw/plugin-sdk/matrix-runtime-heavy` remove after `2026-07-24`", + ); + expect(output).toContain( + "replacement: Matrix local runtime-api plus doctor/fix migration paths", + ); + }); });