#!/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(); 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(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( options.functionDecisionPath, ); const callDecisions = readJson( 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( options.moduleManifestPath, ); const migrationCatalog = readJson( options.migrationDecisionPath, ); const migrationAudit = auditLive2DModuleMigrations({ decisions: migrationCatalog, implementationRoot: options.implementationRoot, moduleManifest, source, sourceFunctions: sourceAudit.functions, workspaceRoot, }); const behaviorCatalog = readJson( 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); }