


























@@ -0,0 +1,324 @@
1+#!/usr/bin/env node
2+import { existsSync, readdirSync, readFileSync } from "node:fs";
3+import { join, relative, resolve } from "node:path";
4+import {
5+pluginSdkEntrypoints,
6+publicPluginOwnedSdkEntrypoints,
7+reservedBundledPluginSdkEntrypoints,
8+supportedBundledFacadeSdkEntrypoints,
9+} from "../src/plugin-sdk/entrypoints.ts";
10+import { PLUGIN_COMPAT_RECORDS } from "../src/plugins/compat/registry.ts";
11+import type { PluginCompatRecord } from "../src/plugins/compat/types.ts";
12+13+const REPO_ROOT = process.cwd();
14+const SOURCE_ROOTS = ["src", "extensions", "packages", "scripts", "test", "docs"] as const;
15+const SKIPPED_DIRS = new Set([
16+".artifacts",
17+".git",
18+"coverage",
19+"dist",
20+"dist-runtime",
21+"node_modules",
22+]);
23+const TEXT_FILE_PATTERN = /\.(?:[cm]?[jt]sx?|json|mdx?|ya?ml)$/u;
24+const PLUGIN_SDK_SPECIFIER_PATTERN =
25+/\b(?:from\s*["']|import\s*\(\s*["']|require\s*\(\s*["']|vi\.(?:mock|doMock)\s*\(\s*["'])(openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*))["']/g;
26+27+type CompatDebtRecord = {
28+code: string;
29+ owner: string;
30+ status: PluginCompatRecord["status"];
31+removeAfter?: string;
32+ replacement: string;
33+ docsPath: string;
34+ surfaces: readonly string[];
35+ tokens: string[];
36+ codeReferenceFiles: string[];
37+ docReferenceFiles: string[];
38+ eligibleForRemoval: boolean;
39+};
40+41+type ReservedSdkImport = {
42+file: string;
43+ specifier: string;
44+ subpath: string;
45+owner?: string;
46+consumerOwner?: string;
47+ relation: "owner" | "cross-owner" | "workspace";
48+};
49+50+type BoundaryReport = {
51+generatedAt: string;
52+ compat: {
53+ deprecatedCount: number;
54+ eligibleForRemovalCount: number;
55+ records: CompatDebtRecord[];
56+};
57+ pluginSdk: {
58+ entrypointCount: number;
59+ reservedCount: number;
60+ supportedBundledFacadeCount: number;
61+ publicPluginOwnedCount: number;
62+ reservedImports: ReservedSdkImport[];
63+ crossOwnerReservedImports: ReservedSdkImport[];
64+ unusedReservedSubpaths: string[];
65+};
66+ memoryHostSdk: {
67+ privatePackage: boolean;
68+ exportedSubpaths: string[];
69+ sourceBridgeFiles: string[];
70+ packageCoreReferenceFiles: string[];
71+};
72+};
73+74+function collectTextFiles(dir: string): string[] {
75+const files: string[] = [];
76+if (!existsSync(dir)) {
77+return files;
78+}
79+for (const entry of readdirSync(dir, { withFileTypes: true })) {
80+if (SKIPPED_DIRS.has(entry.name)) {
81+continue;
82+}
83+const nextPath = join(dir, entry.name);
84+if (entry.isDirectory()) {
85+files.push(...collectTextFiles(nextPath));
86+continue;
87+}
88+if (entry.isFile() && TEXT_FILE_PATTERN.test(entry.name)) {
89+files.push(nextPath);
90+}
91+}
92+return files;
93+}
94+95+function collectWorkspaceTextFiles(): string[] {
96+return SOURCE_ROOTS.flatMap((root) => collectTextFiles(resolve(REPO_ROOT, root))).toSorted(
97+(left, right) => relative(REPO_ROOT, left).localeCompare(relative(REPO_ROOT, right)),
98+);
99+}
100+101+function repoRelative(file: string): string {
102+return relative(REPO_ROOT, file).replaceAll("\\", "/");
103+}
104+105+function isDocsFile(file: string): boolean {
106+return file.startsWith("docs/") || file === "README.md";
107+}
108+109+function collectBundledPluginIds(): string[] {
110+return readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
111+.filter((entry) => entry.isDirectory())
112+.map((entry) => entry.name)
113+.toSorted((left, right) => right.length - left.length || left.localeCompare(right));
114+}
115+116+function resolvePluginOwner(entrypoint: string, pluginIds: readonly string[]): string | undefined {
117+return pluginIds.find(
118+(pluginId) => entrypoint === pluginId || entrypoint.startsWith(`${pluginId}-`),
119+);
120+}
121+122+function resolveConsumerOwner(file: string): string | undefined {
123+return /^extensions\/([^/]+)\//u.exec(file)?.[1];
124+}
125+126+function extractCompatTokens(record: PluginCompatRecord): string[] {
127+const tokens = new Set<string>();
128+const values = [record.code, record.replacement, ...record.surfaces, ...record.diagnostics];
129+for (const value of values) {
130+for (const match of value.matchAll(/`([^`]+)`/g)) {
131+const token = match[1]?.trim();
132+if (token && !token.includes(" ")) {
133+tokens.add(token);
134+}
135+}
136+for (const match of value.matchAll(/\bopenclaw\/[a-z0-9/-]+\b/g)) {
137+tokens.add(match[0]);
138+}
139+for (const match of value.matchAll(/\bOPENCLAW_[A-Z0-9_]+\b/g)) {
140+tokens.add(match[0]);
141+}
142+for (const match of value.matchAll(/\b[a-z][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_]+)+\b/g)) {
143+tokens.add(match[0]);
144+}
145+for (const match of value.matchAll(/\b[a-z][a-zA-Z0-9_]*_[a-zA-Z0-9_]+\b/g)) {
146+tokens.add(match[0]);
147+}
148+}
149+return [...tokens].toSorted();
150+}
151+152+function collectReferenceFiles(files: readonly string[], tokens: readonly string[]) {
153+const codeReferenceFiles = new Set<string>();
154+const docReferenceFiles = new Set<string>();
155+for (const file of files) {
156+const relativeFile = repoRelative(file);
157+if (relativeFile === "src/plugins/compat/registry.ts") {
158+continue;
159+}
160+const source = readFileSync(file, "utf8");
161+if (!tokens.some((token) => source.includes(token))) {
162+continue;
163+}
164+if (isDocsFile(relativeFile)) {
165+docReferenceFiles.add(relativeFile);
166+} else {
167+codeReferenceFiles.add(relativeFile);
168+}
169+}
170+return {
171+codeReferenceFiles: [...codeReferenceFiles].toSorted(),
172+docReferenceFiles: [...docReferenceFiles].toSorted(),
173+};
174+}
175+176+function collectCompatDebt(files: readonly string[], today = new Date()): CompatDebtRecord[] {
177+return PLUGIN_COMPAT_RECORDS.filter((record) => record.status === "deprecated")
178+.map((record) => {
179+const tokens = extractCompatTokens(record);
180+const references = collectReferenceFiles(files, tokens);
181+const eligibleForRemoval = record.removeAfter
182+ ? new Date(`${record.removeAfter}T00:00:00Z`) <= today
183+ : false;
184+return {
185+code: record.code,
186+owner: record.owner,
187+status: record.status,
188+removeAfter: record.removeAfter,
189+replacement: record.replacement,
190+docsPath: record.docsPath,
191+surfaces: record.surfaces,
192+ tokens,
193+codeReferenceFiles: references.codeReferenceFiles,
194+docReferenceFiles: references.docReferenceFiles,
195+ eligibleForRemoval,
196+};
197+})
198+.toSorted(
199+(left, right) =>
200+(left.removeAfter ?? "").localeCompare(right.removeAfter ?? "") ||
201+left.owner.localeCompare(right.owner) ||
202+left.code.localeCompare(right.code),
203+);
204+}
205+206+function collectReservedSdkImports(files: readonly string[]): ReservedSdkImport[] {
207+const reserved = new Set<string>(reservedBundledPluginSdkEntrypoints);
208+const pluginIds = collectBundledPluginIds();
209+const imports: ReservedSdkImport[] = [];
210+for (const file of files) {
211+const relativeFile = repoRelative(file);
212+const source = readFileSync(file, "utf8");
213+for (const match of source.matchAll(PLUGIN_SDK_SPECIFIER_PATTERN)) {
214+const specifier = match[1];
215+const subpath = match[2];
216+if (!specifier || !subpath || !reserved.has(subpath)) {
217+continue;
218+}
219+const owner = resolvePluginOwner(subpath, pluginIds);
220+const consumerOwner = resolveConsumerOwner(relativeFile);
221+const relation =
222+owner && consumerOwner ? (owner === consumerOwner ? "owner" : "cross-owner") : "workspace";
223+imports.push({ file: relativeFile, specifier, subpath, owner, consumerOwner, relation });
224+}
225+}
226+return imports.toSorted(
227+(left, right) =>
228+left.subpath.localeCompare(right.subpath) ||
229+left.file.localeCompare(right.file) ||
230+left.specifier.localeCompare(right.specifier),
231+);
232+}
233+234+function collectMemoryHostBoundary(files: readonly string[]): BoundaryReport["memoryHostSdk"] {
235+const packageJson = JSON.parse(
236+readFileSync(resolve(REPO_ROOT, "packages/memory-host-sdk/package.json"), "utf8"),
237+) as { private?: boolean; exports?: Record<string, string> };
238+const sourceBridgeFiles: string[] = [];
239+const packageCoreReferenceFiles = new Set<string>();
240+for (const file of files) {
241+const relativeFile = repoRelative(file);
242+if (!relativeFile.startsWith("packages/memory-host-sdk/src/")) {
243+continue;
244+}
245+const source = readFileSync(file, "utf8");
246+if (source.includes("src/memory-host-sdk/")) {
247+sourceBridgeFiles.push(relativeFile);
248+}
249+if (source.includes("../../../../src/") || source.includes("../../../src/")) {
250+packageCoreReferenceFiles.add(relativeFile);
251+}
252+}
253+return {
254+privatePackage: packageJson.private === true,
255+exportedSubpaths: Object.keys(packageJson.exports ?? {}).toSorted(),
256+sourceBridgeFiles: sourceBridgeFiles.toSorted(),
257+packageCoreReferenceFiles: [...packageCoreReferenceFiles].toSorted(),
258+};
259+}
260+261+function buildReport(): BoundaryReport {
262+const files = collectWorkspaceTextFiles();
263+const compatRecords = collectCompatDebt(files);
264+const reservedImports = collectReservedSdkImports(files);
265+const usedReserved = new Set(reservedImports.map((entry) => entry.subpath));
266+return {
267+generatedAt: new Date().toISOString(),
268+compat: {
269+deprecatedCount: compatRecords.length,
270+eligibleForRemovalCount: compatRecords.filter((record) => record.eligibleForRemoval).length,
271+records: compatRecords,
272+},
273+pluginSdk: {
274+entrypointCount: pluginSdkEntrypoints.length,
275+reservedCount: reservedBundledPluginSdkEntrypoints.length,
276+supportedBundledFacadeCount: supportedBundledFacadeSdkEntrypoints.length,
277+publicPluginOwnedCount: publicPluginOwnedSdkEntrypoints.length,
278+ reservedImports,
279+crossOwnerReservedImports: reservedImports.filter(
280+(entry) => entry.relation === "cross-owner",
281+),
282+unusedReservedSubpaths: reservedBundledPluginSdkEntrypoints
283+.filter((subpath) => !usedReserved.has(subpath))
284+.toSorted(),
285+},
286+memoryHostSdk: collectMemoryHostBoundary(files),
287+};
288+}
289+290+function renderText(report: BoundaryReport): string {
291+const lines: string[] = [];
292+lines.push("Plugin Boundary Report");
293+lines.push("");
294+lines.push(
295+`compat deprecated=${report.compat.deprecatedCount} eligibleForRemoval=${report.compat.eligibleForRemovalCount}`,
296+);
297+for (const record of report.compat.records) {
298+lines.push(
299+` ${record.removeAfter ?? "no-date"} ${record.code} owner=${record.owner} codeRefs=${record.codeReferenceFiles.length} docRefs=${record.docReferenceFiles.length}`,
300+);
301+}
302+lines.push("");
303+lines.push(
304+`plugin-sdk entrypoints=${report.pluginSdk.entrypointCount} reserved=${report.pluginSdk.reservedCount} supportedBundledFacade=${report.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${report.pluginSdk.publicPluginOwnedCount}`,
305+);
306+lines.push(
307+` reservedImports=${report.pluginSdk.reservedImports.length} crossOwnerReservedImports=${report.pluginSdk.crossOwnerReservedImports.length} unusedReserved=${report.pluginSdk.unusedReservedSubpaths.length}`,
308+);
309+for (const entry of report.pluginSdk.crossOwnerReservedImports) {
310+lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`);
311+}
312+lines.push("");
313+lines.push(
314+`memory-host-sdk private=${report.memoryHostSdk.privatePackage} exports=${report.memoryHostSdk.exportedSubpaths.length} sourceBridgeFiles=${report.memoryHostSdk.sourceBridgeFiles.length} coreReferenceFiles=${report.memoryHostSdk.packageCoreReferenceFiles.length}`,
315+);
316+return lines.join("\n");
317+}
318+319+const report = buildReport();
320+if (process.argv.includes("--json")) {
321+process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
322+} else {
323+process.stdout.write(`${renderText(report)}\n`);
324+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。