Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,424 @@
// Analyze script supports OpenClaw repository automation.
import path from "node:path";
import ts from "typescript";
import {
canonicalSymbolInfo,
countIdentifierUsages,
countNamespacePropertyUsages,
createProgramContext,
getRepoRevision,
} from "./context.js";
import type {
ProgramContext,
PublicEntrypoint,
RankedCandidates,
ReferenceEvent,
TopologyEnvelope,
TopologyRecord,
TopologyReportName,
TopologyScope,
} from "./types.js";
function pushUnique(values: string[], next: string | null | undefined) {
if (!next) {
return;
}
if (!values.includes(next)) {
values.push(next);
}
}
function sortUnique(values: string[]) {
values.sort((left, right) => left.localeCompare(right));
}
function clampScore(value: number): number {
return Math.max(0, Math.min(100, Math.round(value)));
}
function isTypeOnlyCandidate(record: Pick<TopologyRecord, "kind">): boolean {
return record.kind === "interface" || record.kind === "type";
}
function computeSharednessScore(record: TopologyRecord): number {
const extensionWeight = record.productionExtensions.length * 30;
const packageWeight = record.productionPackages.length * 20;
const internalWeight = record.internalRefCount > 0 ? 10 : 0;
const publicSpecifierWeight = Math.min(record.publicSpecifiers.length, 4) * 5;
const typeWeight = record.isTypeOnlyCandidate ? 10 : 0;
const testOnlyPenalty = record.productionRefCount === 0 && record.testRefCount > 0 ? 25 : 0;
return clampScore(
extensionWeight +
packageWeight +
internalWeight +
publicSpecifierWeight +
typeWeight -
testOnlyPenalty,
);
}
function computeMoveBackToOwnerScore(record: TopologyRecord): number {
const singleExtensionWeight = record.productionExtensions.length === 1 ? 45 : 0;
const noNonExtensionOwnersWeight = record.productionPackages.length === 0 ? 20 : 0;
const runtimeWeight = record.isTypeOnlyCandidate ? 0 : 10;
const usedWeight = record.productionRefCount > 0 ? 10 : 0;
const publicSpecifierWeight = record.publicSpecifiers.length > 1 ? 5 : 0;
const multiOwnerPenalty = record.productionOwners.length > 1 ? 35 : 0;
const packagePenalty = record.productionPackages.length > 0 ? 25 : 0;
return clampScore(
singleExtensionWeight +
noNonExtensionOwnersWeight +
runtimeWeight +
usedWeight +
publicSpecifierWeight -
multiOwnerPenalty -
packagePenalty,
);
}
function createRecord(info: ReturnType<typeof canonicalSymbolInfo>): TopologyRecord {
return {
canonicalKey: info.canonicalKey,
declarationPath: info.declarationPath,
declarationLine: info.declarationLine,
kind: info.kind,
aliasName: info.aliasName,
entrypoints: [],
exportNames: [],
publicSpecifiers: [],
internalRefCount: 0,
productionRefCount: 0,
testRefCount: 0,
internalImportCount: 0,
productionImportCount: 0,
testImportCount: 0,
internalConsumers: [],
productionConsumers: [],
testConsumers: [],
productionExtensions: [],
productionPackages: [],
productionOwners: [],
isTypeOnlyCandidate: info.kind === "interface" || info.kind === "type",
sharednessScore: 0,
moveBackToOwnerScore: 0,
};
}
function bucketConsumer(record: TopologyRecord, event: ReferenceEvent) {
if (event.bucket === "internal") {
record.internalImportCount += event.importCount;
record.internalRefCount += event.usageCount;
pushUnique(record.internalConsumers, event.consumerPath);
return;
}
if (event.bucket === "test") {
record.testImportCount += event.importCount;
record.testRefCount += event.usageCount;
pushUnique(record.testConsumers, event.consumerPath);
return;
}
record.productionImportCount += event.importCount;
record.productionRefCount += event.usageCount;
pushUnique(record.productionConsumers, event.consumerPath);
pushUnique(record.productionExtensions, event.extensionId);
pushUnique(record.productionPackages, event.packageOwner);
pushUnique(record.productionOwners, event.owner);
}
function addEntrypointMetadata(
record: TopologyRecord,
entrypoint: PublicEntrypoint,
exportName: string,
aliasName?: string,
) {
pushUnique(record.entrypoints, entrypoint.entrypoint);
pushUnique(record.exportNames, exportName);
pushUnique(record.publicSpecifiers, entrypoint.importSpecifier);
if (aliasName) {
pushUnique(record.exportNames, aliasName);
}
}
function buildScopeMaps(context: ProgramContext, scope: TopologyScope) {
const recordByCanonicalKey = new Map<string, TopologyRecord>();
const recordBySpecifierAndExportName = new Map<string, Map<string, TopologyRecord>>();
for (const entrypoint of scope.entrypoints) {
const absolutePath = path.join(context.repoRoot, entrypoint.sourcePath);
const sourceFile = context.program.getSourceFile(absolutePath);
if (!sourceFile) {
continue;
}
const moduleSymbol = context.checker.getSymbolAtLocation(sourceFile);
if (!moduleSymbol) {
continue;
}
const exportMap = new Map<string, TopologyRecord>();
for (const exportedSymbol of context.checker.getExportsOfModule(moduleSymbol)) {
const info = canonicalSymbolInfo(context, exportedSymbol);
let record = recordByCanonicalKey.get(info.canonicalKey);
if (!record) {
record = createRecord(info);
recordByCanonicalKey.set(info.canonicalKey, record);
}
addEntrypointMetadata(record, entrypoint, exportedSymbol.getName(), info.aliasName);
exportMap.set(exportedSymbol.getName(), record);
}
recordBySpecifierAndExportName.set(entrypoint.importSpecifier, exportMap);
}
return { recordByCanonicalKey, recordBySpecifierAndExportName };
}
function collectReferenceEvents(
context: ProgramContext,
scope: TopologyScope,
recordBySpecifierAndExportName: Map<string, Map<string, TopologyRecord>>,
includeTests: boolean,
): ReferenceEvent[] {
const events: ReferenceEvent[] = [];
for (const sourceFile of context.program.getSourceFiles()) {
if (sourceFile.isDeclarationFile) {
continue;
}
const normalizedFileName = context.normalizePath(sourceFile.fileName);
if (!normalizedFileName.startsWith(context.normalizePath(context.repoRoot))) {
continue;
}
const relPath = context.relativeToRepo(sourceFile.fileName);
const bucket = scope.classifyUsageBucket(relPath);
if (!includeTests && bucket === "test") {
continue;
}
for (const statement of sourceFile.statements) {
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
continue;
}
const importSpecifier = statement.moduleSpecifier.text.trim();
if (!scope.importFilter(importSpecifier)) {
continue;
}
const recordMap = recordBySpecifierAndExportName.get(importSpecifier);
if (!recordMap) {
continue;
}
const clause = statement.importClause;
if (!clause?.namedBindings) {
continue;
}
if (clause.isTypeOnly) {
continue;
}
if (ts.isNamedImports(clause.namedBindings)) {
for (const element of clause.namedBindings.elements) {
if (element.isTypeOnly) {
continue;
}
const importedName = element.propertyName?.text ?? element.name.text;
const record = recordMap.get(importedName);
if (!record) {
continue;
}
const localSymbol = context.checker.getSymbolAtLocation(element.name);
if (!localSymbol) {
continue;
}
events.push({
canonicalKey: record.canonicalKey,
bucket,
consumerPath: relPath,
usageCount: countIdentifierUsages(context, sourceFile, localSymbol, element.name.text),
importCount: 1,
importSpecifier,
owner: bucket === "production" ? scope.ownerForPath(relPath) : null,
extensionId: bucket === "production" ? scope.extensionForPath(relPath) : null,
packageOwner: bucket === "production" ? scope.packageOwnerForPath(relPath) : null,
});
}
continue;
}
if (ts.isNamespaceImport(clause.namedBindings)) {
const namespaceSymbol = context.checker.getSymbolAtLocation(clause.namedBindings.name);
if (!namespaceSymbol) {
continue;
}
for (const [exportedName, record] of recordMap.entries()) {
const usageCount = countNamespacePropertyUsages(
context,
sourceFile,
namespaceSymbol,
exportedName,
);
if (usageCount <= 0) {
continue;
}
events.push({
canonicalKey: record.canonicalKey,
bucket,
consumerPath: relPath,
usageCount,
importCount: 1,
importSpecifier,
owner: bucket === "production" ? scope.ownerForPath(relPath) : null,
extensionId: bucket === "production" ? scope.extensionForPath(relPath) : null,
packageOwner: bucket === "production" ? scope.packageOwnerForPath(relPath) : null,
});
}
}
}
}
return events;
}
function finalizeRecords(records: TopologyRecord[]) {
for (const record of records) {
sortUnique(record.entrypoints);
sortUnique(record.exportNames);
sortUnique(record.publicSpecifiers);
sortUnique(record.internalConsumers);
sortUnique(record.productionConsumers);
sortUnique(record.testConsumers);
sortUnique(record.productionExtensions);
sortUnique(record.productionPackages);
sortUnique(record.productionOwners);
record.isTypeOnlyCandidate = isTypeOnlyCandidate(record);
record.sharednessScore = computeSharednessScore(record);
record.moveBackToOwnerScore = computeMoveBackToOwnerScore(record);
}
return records.toSorted((left, right) => {
const byRefs =
right.productionRefCount +
right.testRefCount +
right.internalRefCount -
(left.productionRefCount + left.testRefCount + left.internalRefCount);
if (byRefs !== 0) {
return byRefs;
}
return (
left.publicSpecifiers[0].localeCompare(right.publicSpecifiers[0]) ||
left.exportNames[0].localeCompare(right.exportNames[0])
);
});
}
function buildRankedCandidates(records: TopologyRecord[], limit: number): RankedCandidates {
return {
candidateToMove: records
.filter(
(record) =>
record.productionOwners.length === 1 &&
record.productionExtensions.length === 1 &&
record.productionRefCount > 0,
)
.toSorted((left, right) => right.moveBackToOwnerScore - left.moveBackToOwnerScore)
.slice(0, limit),
duplicatedPublicExports: records
.filter((record) => record.publicSpecifiers.length > 1)
.toSorted((left, right) => right.publicSpecifiers.length - left.publicSpecifiers.length)
.slice(0, limit),
singleOwnerShared: records
.filter((record) => record.productionOwners.length === 1 && record.productionImportCount > 0)
.toSorted((left, right) => right.productionRefCount - left.productionRefCount)
.slice(0, limit),
};
}
export function analyzeTopology(options: {
repoRoot: string;
scope: TopologyScope;
report: TopologyReportName;
includeTests?: boolean;
limit?: number;
tsconfigName?: string;
}): TopologyEnvelope {
const includeTests = options.includeTests ?? true;
const limit = options.limit ?? 25;
const context = createProgramContext(options.repoRoot, options.tsconfigName);
const { recordByCanonicalKey, recordBySpecifierAndExportName } = buildScopeMaps(
context,
options.scope,
);
const events = collectReferenceEvents(
context,
options.scope,
recordBySpecifierAndExportName,
includeTests,
);
for (const event of events) {
const record = recordByCanonicalKey.get(event.canonicalKey);
if (record) {
bucketConsumer(record, event);
}
}
const allRecords = finalizeRecords([...recordByCanonicalKey.values()]);
const filteredRecords = filterRecordsForReport(allRecords, options.report);
return {
metadata: {
tool: "ts-topology",
version: 1,
generatedAt: new Date().toISOString(),
repoRevision: getRepoRevision(options.repoRoot),
tsconfigPath: context.tsconfigPath,
},
scope: {
id: options.scope.id,
description: options.scope.description,
repoRoot: options.repoRoot,
entrypoints: options.scope.entrypoints,
includeTests,
},
report: options.report,
totals: {
exports: allRecords.length,
usedByProduction: allRecords.filter((record) => record.productionImportCount > 0).length,
usedByTests: allRecords.filter((record) => record.testImportCount > 0).length,
usedInternally: allRecords.filter((record) => record.internalImportCount > 0).length,
singleOwnerShared: allRecords.filter(
(record) => record.productionOwners.length === 1 && record.productionImportCount > 0,
).length,
unused: allRecords.filter(
(record) =>
record.productionImportCount === 0 &&
record.testImportCount === 0 &&
record.internalImportCount === 0,
).length,
},
rankedCandidates: buildRankedCandidates(allRecords, limit),
records: filteredRecords,
};
}
export function filterRecordsForReport(
records: TopologyRecord[],
report: TopologyReportName,
): TopologyRecord[] {
switch (report) {
case "owner-map":
return records.filter((record) => record.productionImportCount > 0);
case "single-owner-shared":
return records.filter(
(record) => record.productionOwners.length === 1 && record.productionImportCount > 0,
);
case "unused-public-surface":
return records.filter(
(record) =>
record.productionImportCount === 0 &&
record.testImportCount === 0 &&
record.internalImportCount === 0,
);
case "consumer-topology":
return records.filter(
(record) =>
record.productionImportCount > 0 ||
record.testImportCount > 0 ||
record.internalImportCount > 0,
);
case "public-surface-usage":
return records;
}
throw new Error("Unsupported topology report");
}

View File

@@ -0,0 +1,176 @@
// Context script supports OpenClaw repository automation.
import { execFileSync } from "node:child_process";
import path from "node:path";
import ts from "typescript";
import type { CanonicalSymbol, ProgramContext, SymbolKind } from "./types.js";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function normalizePath(filePath: string): string {
return filePath.split(path.sep).join(path.posix.sep);
}
export function createProgramContext(
repoRoot: string,
tsconfigName = "tsconfig.json",
): ProgramContext {
const configPath = ts.findConfigFile(
repoRoot,
(candidate) => ts.sys.fileExists(candidate),
tsconfigName,
);
assert(configPath, `Could not find ${tsconfigName}`);
const configFile = ts.readConfigFile(configPath, (candidate) => ts.sys.readFile(candidate));
if (configFile.error) {
throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"));
}
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, repoRoot);
const program = ts.createProgram(parsed.fileNames, parsed.options);
return {
repoRoot,
tsconfigPath: normalizePath(path.relative(repoRoot, configPath)),
program,
checker: program.getTypeChecker(),
normalizePath,
relativeToRepo(filePath: string) {
return normalizePath(path.relative(repoRoot, filePath));
},
};
}
function comparableSymbol(
checker: ts.TypeChecker,
symbol: ts.Symbol | undefined,
): ts.Symbol | undefined {
if (!symbol) {
return undefined;
}
return symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
}
function symbolKind(symbol: ts.Symbol, declaration: ts.Declaration | undefined): SymbolKind {
if (declaration) {
switch (declaration.kind) {
case ts.SyntaxKind.FunctionDeclaration:
return "function";
case ts.SyntaxKind.ClassDeclaration:
return "class";
case ts.SyntaxKind.InterfaceDeclaration:
return "interface";
case ts.SyntaxKind.TypeAliasDeclaration:
return "type";
case ts.SyntaxKind.EnumDeclaration:
return "enum";
case ts.SyntaxKind.VariableDeclaration:
return "variable";
default:
break;
}
}
if (symbol.flags & ts.SymbolFlags.Function) {
return "function";
}
if (symbol.flags & ts.SymbolFlags.Class) {
return "class";
}
if (symbol.flags & ts.SymbolFlags.Interface) {
return "interface";
}
if (symbol.flags & ts.SymbolFlags.TypeAlias) {
return "type";
}
if (symbol.flags & ts.SymbolFlags.Enum) {
return "enum";
}
if (symbol.flags & ts.SymbolFlags.Variable) {
return "variable";
}
return "unknown";
}
export function canonicalSymbolInfo(context: ProgramContext, symbol: ts.Symbol): CanonicalSymbol {
const resolved = comparableSymbol(context.checker, symbol) ?? symbol;
const declaration =
resolved.getDeclarations()?.find((candidate) => candidate.kind !== ts.SyntaxKind.SourceFile) ??
symbol.getDeclarations()?.find((candidate) => candidate.kind !== ts.SyntaxKind.SourceFile);
assert(declaration, `Missing declaration for symbol ${symbol.getName()}`);
const sourceFile = declaration.getSourceFile();
const declarationPath = context.relativeToRepo(sourceFile.fileName);
const declarationLine = sourceFile.getLineAndCharacterOfPosition(declaration.getStart()).line + 1;
return {
canonicalKey: `${declarationPath}:${declarationLine}:${resolved.getName()}`,
declarationPath,
declarationLine,
kind: symbolKind(resolved, declaration),
aliasName: symbol.getName() !== resolved.getName() ? symbol.getName() : undefined,
};
}
export function countIdentifierUsages(
context: ProgramContext,
sourceFile: ts.SourceFile,
importedSymbol: ts.Symbol,
localName: string,
): number {
const targetSymbol = comparableSymbol(context.checker, importedSymbol);
let count = 0;
const visit = (node: ts.Node) => {
if (ts.isIdentifier(node) && node.text === localName) {
const symbol = comparableSymbol(context.checker, context.checker.getSymbolAtLocation(node));
if (
symbol === targetSymbol &&
!ts.isImportClause(node.parent) &&
!ts.isImportSpecifier(node.parent)
) {
count += 1;
}
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sourceFile, visit);
return count;
}
export function countNamespacePropertyUsages(
context: ProgramContext,
sourceFile: ts.SourceFile,
namespaceSymbol: ts.Symbol,
exportedName: string,
): number {
const targetSymbol = comparableSymbol(context.checker, namespaceSymbol);
let count = 0;
const visit = (node: ts.Node) => {
if (
ts.isPropertyAccessExpression(node) &&
ts.isIdentifier(node.expression) &&
node.name.text === exportedName
) {
const symbol = comparableSymbol(
context.checker,
context.checker.getSymbolAtLocation(node.expression),
);
if (symbol === targetSymbol) {
count += 1;
}
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sourceFile, visit);
return count;
}
export function getRepoRevision(repoRoot: string): string | null {
try {
return execFileSync("git", ["rev-parse", "HEAD"], {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
} catch {
return null;
}
}

View File

@@ -0,0 +1,119 @@
// Reports script supports OpenClaw repository automation.
import type { ReportModule, TopologyEnvelope, TopologyRecord } from "./types.js";
function canonicalExportName(record: TopologyRecord): string {
const finalColon = record.canonicalKey.lastIndexOf(":");
return finalColon >= 0
? record.canonicalKey.slice(finalColon + 1)
: (record.exportNames[0] ?? "<unknown>");
}
function primarySymbol(record: TopologyRecord): string {
return `${record.publicSpecifiers[0] ?? "<unknown>"}:${canonicalExportName(record)}`;
}
function formatRecordLine(record: TopologyRecord): string {
return (
`- ${primarySymbol(record)} -> ${record.declarationPath}:${record.declarationLine} ` +
`(prodRefs=${record.productionRefCount}, owners=${record.productionOwners.join(",") || "-"}, ` +
`sharedness=${record.sharednessScore}, move=${record.moveBackToOwnerScore})`
);
}
const reportModules: Record<ReportModule["name"], ReportModule> = {
"public-surface-usage": {
name: "public-surface-usage",
describe(envelope, limit) {
const candidates = envelope.rankedCandidates?.candidateToMove ?? [];
const duplicateExports = envelope.rankedCandidates?.duplicatedPublicExports ?? [];
return [
`Scope: ${envelope.scope.id}`,
`Public exports analyzed: ${envelope.totals.exports}`,
`Production-used exports: ${envelope.totals.usedByProduction}`,
`Single-owner shared exports: ${envelope.totals.singleOwnerShared}`,
`Unused public exports: ${envelope.totals.unused}`,
"",
`Top ${Math.min(limit, candidates.length)} candidate-to-move exports:`,
...candidates.slice(0, limit).map(formatRecordLine),
"",
`Top ${Math.min(limit, duplicateExports.length)} duplicated public exports:`,
...duplicateExports
.slice(0, limit)
.map(
(record) =>
`- ${primarySymbol(record)} via ${record.publicSpecifiers.join(", ")} ` +
`(${record.declarationPath}:${record.declarationLine})`,
),
].join("\n");
},
},
"owner-map": {
name: "owner-map",
describe(envelope, limit) {
return [
`Scope: ${envelope.scope.id}`,
`Production-owned records: ${envelope.records.length}`,
"",
`Top ${Math.min(limit, envelope.records.length)} owner-map records:`,
...envelope.records
.slice(0, limit)
.map(
(record) =>
`- ${primarySymbol(record)} owners=${record.productionOwners.join(",")} ` +
`extensions=${record.productionExtensions.join(",") || "-"} ` +
`packages=${record.productionPackages.join(",") || "-"}`,
),
].join("\n");
},
},
"single-owner-shared": {
name: "single-owner-shared",
describe(envelope, limit) {
return [
`Scope: ${envelope.scope.id}`,
`Single-owner shared exports: ${envelope.records.length}`,
"",
`Top ${Math.min(limit, envelope.records.length)} single-owner shared exports:`,
...envelope.records.slice(0, limit).map(formatRecordLine),
].join("\n");
},
},
"unused-public-surface": {
name: "unused-public-surface",
describe(envelope, limit) {
return [
`Scope: ${envelope.scope.id}`,
`Unused public exports: ${envelope.records.length}`,
"",
`Top ${Math.min(limit, envelope.records.length)} unused exports:`,
...envelope.records.slice(0, limit).map(formatRecordLine),
].join("\n");
},
},
"consumer-topology": {
name: "consumer-topology",
describe(envelope, limit) {
return [
`Scope: ${envelope.scope.id}`,
`Records with consumers: ${envelope.records.length}`,
"",
`Top ${Math.min(limit, envelope.records.length)} consumer-topology records:`,
...envelope.records
.slice(0, limit)
.map(
(record) =>
`- ${primarySymbol(record)} prod=${record.productionConsumers.length} ` +
`test=${record.testConsumers.length} internal=${record.internalConsumers.length}`,
),
].join("\n");
},
},
};
export function renderTextReport(envelope: TopologyEnvelope, limit: number): string {
const reportModule = reportModules[envelope.report];
if (!reportModule) {
throw new Error(`Unsupported topology report: ${envelope.report}`);
}
return reportModule.describe(envelope, limit);
}

View File

@@ -0,0 +1,163 @@
// Scope script supports OpenClaw repository automation.
import fs from "node:fs";
import path from "node:path";
import { BUNDLED_PLUGIN_PATH_PREFIX } from "../bundled-plugin-paths.mjs";
import { publicPluginSdkEntrypoints } from "../plugin-sdk-entries.mjs";
import type { ConsumerScope, PublicEntrypoint, TopologyScope, UsageBucket } from "./types.js";
function isTestFile(relPath: string): boolean {
return (
relPath.startsWith("test/") ||
relPath.includes("/__tests__/") ||
relPath.includes(".test.") ||
relPath.includes(".spec.") ||
relPath.includes(".e2e.") ||
relPath.includes(".suite.") ||
relPath.includes("test-harness") ||
relPath.includes("test-support") ||
relPath.includes("test-helper") ||
relPath.includes("test-utils")
);
}
function classifyScope(relPath: string): ConsumerScope {
if (relPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
return "extension";
}
if (relPath.startsWith("packages/")) {
return "package";
}
if (relPath.startsWith("apps/")) {
return "app";
}
if (relPath.startsWith("ui/")) {
return "ui";
}
if (relPath.startsWith("scripts/")) {
return "script";
}
if (relPath.startsWith("src/")) {
return "src";
}
if (relPath.startsWith("test/")) {
return "test";
}
return "other";
}
function classifyUsageBucketForRoots(internalRoots: string[], relPath: string): UsageBucket {
if (internalRoots.some((root) => relPath === root || relPath.startsWith(`${root}/`))) {
return "internal";
}
return isTestFile(relPath) ? "test" : "production";
}
function extractOwner(relPath: string): string | null {
const scope = classifyScope(relPath);
const parts = relPath.split("/");
switch (scope) {
case "extension":
return parts[1] ? `extension:${parts[1]}` : "extension";
case "package":
return parts[1] ? `package:${parts[1]}` : "package";
case "app":
return parts[1] ? `app:${parts[1]}` : "app";
case "src":
return "src";
case "ui":
return "ui";
case "script":
return "scripts";
case "other":
return parts[0] || "other";
case "test":
return null;
}
throw new Error("Unsupported topology scope");
}
function extractExtensionId(relPath: string): string | null {
if (!relPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
return null;
}
const parts = relPath.split("/");
return parts[1] ?? null;
}
function extractPackageOwner(relPath: string): string | null {
const owner = extractOwner(relPath);
return owner?.startsWith("extension:") ? null : owner;
}
function buildScopeFromEntrypoints(
id: string,
description: string,
entrypoints: PublicEntrypoint[],
): TopologyScope {
const internalRoots = [
...new Set(entrypoints.map((entrypoint) => path.posix.dirname(entrypoint.sourcePath))),
];
const publicSpecifiers = new Set(entrypoints.map((entrypoint) => entrypoint.importSpecifier));
return {
id,
description,
entrypoints,
importFilter(specifier: string) {
return publicSpecifiers.has(specifier);
},
classifyUsageBucket(relPath: string) {
return classifyUsageBucketForRoots(internalRoots, relPath);
},
classifyScope,
ownerForPath(relPath: string) {
return extractOwner(relPath);
},
extensionForPath(relPath: string) {
return extractExtensionId(relPath);
},
packageOwnerForPath(relPath: string) {
return extractPackageOwner(relPath);
},
};
}
export function createPluginSdkScope(_repoRoot: string): TopologyScope {
const entrypoints = publicPluginSdkEntrypoints.map((entrypoint) => ({
entrypoint,
sourcePath: `src/plugin-sdk/${entrypoint}.ts`,
importSpecifier:
entrypoint === "index" ? "openclaw/plugin-sdk" : `openclaw/plugin-sdk/${entrypoint}`,
}));
return buildScopeFromEntrypoints("plugin-sdk", "OpenClaw plugin-sdk public surface", entrypoints);
}
export function createFilesystemPublicSurfaceScope(
repoRoot: string,
options: {
id: string;
description?: string;
entrypointRoot: string;
importPrefix: string;
},
): TopologyScope {
const absoluteRoot = path.join(repoRoot, options.entrypointRoot);
const entries = fs
.readdirSync(absoluteRoot, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
.map((entry) => entry.name)
.toSorted();
const publicEntrypoints = entries.map((fileName) => {
const entrypoint = fileName.replace(/\.ts$/, "");
return {
entrypoint,
sourcePath: path.posix.join(options.entrypointRoot, fileName),
importSpecifier:
entrypoint === "index" ? options.importPrefix : `${options.importPrefix}/${entrypoint}`,
};
});
return buildScopeFromEntrypoints(
options.id,
options.description ?? `Public surface rooted at ${options.entrypointRoot}`,
publicEntrypoints,
);
}

View File

@@ -0,0 +1,138 @@
// Types script supports OpenClaw repository automation.
import type ts from "typescript";
export type UsageBucket = "internal" | "production" | "test";
export type ConsumerScope =
| "src"
| "extension"
| "package"
| "app"
| "ui"
| "script"
| "test"
| "other";
export type TopologyReportName =
| "public-surface-usage"
| "owner-map"
| "single-owner-shared"
| "unused-public-surface"
| "consumer-topology";
export type SymbolKind =
| "function"
| "class"
| "interface"
| "type"
| "enum"
| "variable"
| "unknown";
export type ProgramContext = {
repoRoot: string;
tsconfigPath: string;
program: ts.Program;
checker: ts.TypeChecker;
normalizePath: (filePath: string) => string;
relativeToRepo: (filePath: string) => string;
};
export type CanonicalSymbol = {
canonicalKey: string;
declarationPath: string;
declarationLine: number;
kind: SymbolKind;
aliasName?: string;
};
export type PublicEntrypoint = {
entrypoint: string;
sourcePath: string;
importSpecifier: string;
};
export type ReferenceEvent = {
canonicalKey: string;
bucket: UsageBucket;
consumerPath: string;
usageCount: number;
importCount: number;
importSpecifier: string;
owner: string | null;
extensionId: string | null;
packageOwner: string | null;
};
export type TopologyRecord = CanonicalSymbol & {
entrypoints: string[];
exportNames: string[];
publicSpecifiers: string[];
internalRefCount: number;
productionRefCount: number;
testRefCount: number;
internalImportCount: number;
productionImportCount: number;
testImportCount: number;
internalConsumers: string[];
productionConsumers: string[];
testConsumers: string[];
productionExtensions: string[];
productionPackages: string[];
productionOwners: string[];
isTypeOnlyCandidate: boolean;
sharednessScore: number;
moveBackToOwnerScore: number;
};
export type TopologyScope = {
id: string;
description: string;
entrypoints: PublicEntrypoint[];
importFilter: (specifier: string) => boolean;
classifyUsageBucket: (relPath: string) => UsageBucket;
classifyScope: (relPath: string) => ConsumerScope;
ownerForPath: (relPath: string) => string | null;
extensionForPath: (relPath: string) => string | null;
packageOwnerForPath: (relPath: string) => string | null;
};
export type RankedCandidates = {
candidateToMove: TopologyRecord[];
duplicatedPublicExports: TopologyRecord[];
singleOwnerShared: TopologyRecord[];
};
export type TopologyEnvelope = {
metadata: {
tool: "ts-topology";
version: 1;
generatedAt: string;
repoRevision: string | null;
tsconfigPath: string;
};
scope: {
id: string;
description: string;
repoRoot: string;
entrypoints: PublicEntrypoint[];
includeTests: boolean;
};
report: TopologyReportName;
totals: {
exports: number;
usedByProduction: number;
usedByTests: number;
usedInternally: number;
singleOwnerShared: number;
unused: number;
};
rankedCandidates?: RankedCandidates;
records: TopologyRecord[];
};
export type ReportModule = {
name: TopologyReportName;
describe: (envelope: TopologyEnvelope, limit: number) => string;
filterRecords?: (record: TopologyRecord) => boolean;
};