ktworkflow-mcp/scripts/audit-live2d-runtime-coverage.ts

250 lines
8.7 KiB
JavaScript

#!/usr/bin/env node
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
analyzeLive2DMinJsSource,
buildCallResolutionReviewCatalog,
buildFunctionNameReviewCatalog,
buildLegacyFunctionRenameCatalog,
type Live2DCallResolutionDecisionCatalog,
type Live2DFunctionNameDecisionCatalog,
} from "../src/tools/deobfuscation.js";
import {
auditLive2DModuleMigrations,
type Live2DModuleMigrationDecisionCatalog,
type Live2DModuleMigrationManifest,
} from "../src/tools/live2dModuleMigrations.js";
import { analyzeLive2DBootstrapGraph } from "../src/tools/live2dBootstrapGraph.js";
import {
auditLive2DRuntimeCoverage,
type Live2DRuntimeBehaviorDecisionCatalog,
} from "../src/tools/live2dRuntimeCoverage.js";
interface CliOptions {
behaviorDecisionPath: string;
callDecisionPath: string;
functionDecisionPath: string;
implementationRoot: string;
migrationDecisionPath: string;
moduleManifestPath: string;
outputPath: string;
productionRoot: string;
sourcePath: string;
}
const dirname = path.dirname(fileURLToPath(import.meta.url));
const workspaceRoot = path.resolve(
process.env.KT_WORKSPACE_ROOT || path.join(dirname, "../../.."),
);
/** Resolves a CLI path against the shared KT workspace root. */
function resolveWorkspacePath(value: string): string {
return path.isAbsolute(value) ? value : path.resolve(workspaceRoot, value);
}
/** Parses the bounded current-state runtime coverage CLI. */
function parseArgs(argv: string[]): CliOptions {
const args = new Map<string, string>();
for (let index = 0; index < argv.length; index += 1) {
const item = argv[index];
if (!item.startsWith("--")) continue;
const [key, inlineValue] = item.slice(2).split("=", 2);
if (inlineValue !== undefined) {
args.set(key, inlineValue);
continue;
}
const next = argv[index + 1];
if (next && !next.startsWith("--")) {
args.set(key, next);
index += 1;
}
}
return {
behaviorDecisionPath: resolveWorkspacePath(
args.get("behavior-decisions") ??
"docs/live2d-deobfuscation/runtime-behavior-decisions.json",
),
callDecisionPath: resolveWorkspacePath(
args.get("call-decisions") ?? "docs/live2d-deobfuscation/call-resolutions.json",
),
functionDecisionPath: resolveWorkspacePath(
args.get("function-decisions") ??
"docs/live2d-deobfuscation/function-name-decisions.json",
),
implementationRoot: resolveWorkspacePath(
args.get("implementation-root") ??
"Vue/kt-blog-web/src/components/blog/live2d/vendor/cubism2Core",
),
migrationDecisionPath: resolveWorkspacePath(
args.get("migration-decisions") ??
"docs/live2d-deobfuscation/module-migration-decisions.json",
),
moduleManifestPath: resolveWorkspacePath(
args.get("module-manifest") ?? "docs/live2d-deobfuscation/module-splits.json",
),
outputPath: resolveWorkspacePath(
args.get("output") ??
".kt-workspace/deobfuscate/blog-live2d-cubism2-minjs/live2d.min/runtime-coverage.generated.json",
),
productionRoot: resolveWorkspacePath(
args.get("production-root") ?? "Vue/kt-blog-web/src/components/blog/live2d",
),
sourcePath: resolveWorkspacePath(
args.get("source") ??
"Vue/kt-blog-web/public/live2d/wordpress-moc/live2d.min.js",
),
};
}
/** Reads one typed JSON input while semantic validation remains in the audit. */
function readJson<T>(filePath: string): T {
return JSON.parse(readFileSync(filePath, "utf8")) as T;
}
/** Writes one deterministic generated coverage artifact under `.kt-workspace`. */
function writeJson(filePath: string, value: unknown): void {
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
/** Rebuilds every current catalog and emits the complete runtime coverage report. */
function main(): void {
const options = parseArgs(process.argv.slice(2));
const source = readFileSync(options.sourcePath, "utf8");
const sourcePath = path.relative(workspaceRoot, options.sourcePath).replaceAll("\\", "/");
const sourceAudit = analyzeLive2DMinJsSource(source, { sourcePath });
const functionDecisions = readJson<Live2DFunctionNameDecisionCatalog>(
options.functionDecisionPath,
);
const callDecisions = readJson<Live2DCallResolutionDecisionCatalog>(
options.callDecisionPath,
);
const functionCatalog = buildFunctionNameReviewCatalog(
sourceAudit,
buildLegacyFunctionRenameCatalog(sourceAudit, []),
functionDecisions,
);
const callCatalog = buildCallResolutionReviewCatalog(
sourceAudit,
functionCatalog,
callDecisions,
);
const bootstrapGraph = analyzeLive2DBootstrapGraph(
source,
sourceAudit,
functionCatalog,
callCatalog,
);
const moduleManifest = readJson<Live2DModuleMigrationManifest>(
options.moduleManifestPath,
);
const migrationCatalog = readJson<Live2DModuleMigrationDecisionCatalog>(
options.migrationDecisionPath,
);
const migrationAudit = auditLive2DModuleMigrations({
decisions: migrationCatalog,
implementationRoot: options.implementationRoot,
moduleManifest,
source,
sourceFunctions: sourceAudit.functions,
workspaceRoot,
});
const behaviorCatalog = readJson<Live2DRuntimeBehaviorDecisionCatalog>(
options.behaviorDecisionPath,
);
const runtimeRoot = path.join(options.productionRoot, "runtime");
const runtimeEntryPath = path.join(options.implementationRoot, "runtimeCore.ts");
const rendererPath = path.join(runtimeRoot, "webglLive2DRenderer.ts");
const coordinatesPath = path.join(runtimeRoot, "cubism2PointerCoordinates.ts");
const coordinateSpec = resolveWorkspacePath(
"Vue/kt-blog-web/src/__tests__/live2d/Cubism2PointerCoordinates.spec.ts",
);
const trackingSpec = resolveWorkspacePath(
"Vue/kt-blog-web/src/__tests__/live2d/Cubism2PointerTracking.spec.ts",
);
const report = auditLive2DRuntimeCoverage({
behaviorCatalog,
callCatalog,
coreRoot: options.implementationRoot,
expected: {
calls: 1411,
functions: 627,
migrated: 567,
modules: 38,
omittedUnreachable: 60,
supportModules: 5,
},
functionCatalog,
fullPageGazeCoordinatesPath: coordinatesPath,
fullPageGazeRendererPath: rendererPath,
migrationAudit,
migrationCatalog,
moduleManifest,
productionRoot: options.productionRoot,
productBehaviorTests: [
{
filePath: coordinateSpec,
testName: "uses the whole viewport as a continuous page-level look-at range",
},
{
filePath: coordinateSpec,
testName: "retains page-level distance after the pointer leaves the canvas",
},
{
filePath: trackingSpec,
testName: "tracks mouse movement across the page and removes every listener on destroy",
},
],
retiredPaths: [
path.join(options.productionRoot, "vendor/cubism2Core.ts"),
path.join(runtimeRoot, "cubism2CoreLoader.ts"),
path.join(options.implementationRoot, "compatibility"),
path.join(options.implementationRoot, "coreGlobals.ts"),
path.join(options.implementationRoot, "legacyKernel.ts"),
path.join(options.implementationRoot, "sdkGlobalInstaller.ts"),
path.join(options.implementationRoot, "sdkGlobalNames.ts"),
path.join(options.implementationRoot, "autoEyeBlink.ts"),
path.join(options.implementationRoot, "legacyMotion.ts"),
path.join(options.implementationRoot, "webglAttributes.ts"),
path.join(options.implementationRoot, "webglBlendFactors.ts"),
path.join(options.implementationRoot, "webglDrawTail.ts"),
path.join(options.implementationRoot, "webglFramebuffer.ts"),
path.join(options.implementationRoot, "webglShaderLocations.ts"),
path.join(options.implementationRoot, "webglTextureBindings.ts"),
path.join(options.implementationRoot, "webglUniforms.ts"),
],
runtimeConsumerPaths: [rendererPath],
runtimeEntryPath,
sourceWindowExports: bootstrapGraph.windowExports
.slice()
.sort((left, right) => left.exportOrder - right.exportOrder)
.map((entry) => entry.exportName),
workspaceRoot,
});
writeJson(options.outputPath, report);
console.log(
JSON.stringify(
{
calls: report.calls,
modules: report.modules,
ok: true,
outputPath: path.relative(workspaceRoot, options.outputPath).replaceAll("\\", "/"),
postCoreContracts: report.postCoreContracts,
sourceSha256: report.sourceSha256,
summary: report.summary,
},
null,
2,
),
);
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}