Files
openclaw/src/plugin-sdk/api-baseline-normalization.ts
T
Peter Steinberger 98b1e8b69c fix(plugin-sdk): make the API baseline contract path-free and surface-reachable (#121514)
* fix(plugin-sdk): make the API baseline hash path-free and surface-reachable

Manifest hashes now cover only surface-reachable, path-independent facts.
Rewrite closure from whole-file/path-keyed traversal to declaration-granular reachability with fail-toward-recall fallbacks and taint-tracked cycles.
Exclude source paths from the committed hash and split declaration printing into api-baseline-declaration-print.ts.

* fix(plugin-sdk): close side-effect and namespace-import gaps in baseline closure

Address ClawSweeper findings:
- Traverse side-effect imports before collecting globals.
- Normalize repo-owned namespace import types.

* refactor(plugin-sdk): rebase precision closure onto the JSONL contract
2026-08-10 07:44:53 -07:00

73 lines
2.8 KiB
TypeScript

// API baseline normalization removes machine-local path differences from compiler output.
import path from "node:path";
/** Normalize compiler source paths into stable repo-relative or node_modules-relative paths. */
export function normalizePluginSdkApiSourcePath(repoRoot: string, filePath: string): string {
const resolvedPath = path.resolve(filePath);
const relative = path.relative(repoRoot, resolvedPath);
const relativePosix = relative.split(path.sep).join(path.posix.sep);
if (
!relative.startsWith("..") &&
!path.isAbsolute(relative) &&
!relativePosix.startsWith("node_modules/")
) {
return relativePosix;
}
const pathParts = resolvedPath.split(/[\\/]+/);
const nodeModulesIndex = pathParts.lastIndexOf("node_modules");
if (nodeModulesIndex >= 0 && nodeModulesIndex < pathParts.length - 1) {
return ["node_modules", ...pathParts.slice(nodeModulesIndex + 1)].join(path.posix.sep);
}
return relativePosix;
}
function normalizeDeclarationImportSpecifier(repoRoot: string, value: string): string {
if (!path.isAbsolute(value) && !/^[A-Za-z]:[\\/]/.test(value)) {
return value;
}
const resolvedPath = path.resolve(value);
const relative = path.relative(repoRoot, resolvedPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
return value;
}
return relative.split(path.sep).join(path.posix.sep);
}
function isRepoOwnedImportSpecifier(value: string): boolean {
return (
value.startsWith("./") ||
value.startsWith("../") ||
/^(?:apps|extensions|packages|scripts|src|test|ui)\//u.test(value)
);
}
/** Strip machine-local absolute paths from declaration text before hashing baseline output. */
export function normalizePluginSdkApiDeclarationText(repoRoot: string, value: string): string {
const repoOwnedSpecifiers = new Set<string>();
const normalized = value.replaceAll(
/import\("([^"]+)"((?:\s*,[^)]*)?)\)/g,
(match, specifier: string, suffix: string) => {
const normalizedSpecifier = normalizeDeclarationImportSpecifier(repoRoot, specifier);
if (normalizedSpecifier !== specifier || isRepoOwnedImportSpecifier(normalizedSpecifier)) {
repoOwnedSpecifiers.add(normalizedSpecifier);
}
return normalizedSpecifier === specifier
? match
: `import("${normalizedSpecifier}"${suffix})`;
},
);
const withoutRepoQualifiers = normalized.replaceAll(
/import\("([^"]+)"((?:\s*,[^)]*)?)\)((?:\.[A-Za-z_$][\w$]*)+)/g,
(match, specifier: string, _suffix: string, qualifier: string) =>
repoOwnedSpecifiers.has(specifier) ? qualifier.slice(1) : match,
);
return withoutRepoQualifiers.replaceAll(
/import\("([^"]+)"((?:\s*,[^)]*)?)\)/g,
(match, specifier: string, suffix: string) =>
repoOwnedSpecifiers.has(specifier) ? `import("<repo>"${suffix})` : match,
);
}