import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { resolveInsideRoot } from "../src/core/workspace.js"; import { analyzeLive2DMinJsSource } from "../src/tools/deobfuscation.js"; import { buildLive2DModuleMigrationDecisionDrafts, Live2DModuleMigrationDraftError, retargetLive2DFunctionDecisionCatalog, type Live2DFunctionTargetDecisionCatalog, type Live2DModuleMigrationDecisionCatalog, type Live2DModuleMigrationDraftManifest, } from "../src/tools/live2dModuleMigrations.js"; interface CliOptions { decisionPath: string; functionDecisionOutputPath: string | null; functionDecisionPath: string; implementationRoot: string; issueOutputPath: string; manifestPath: string; modulePaths: Set | null; outputPath: string; refreshExisting: boolean; reviewEvidence: string[]; sourcePath: string; } /** Reads `--name value` arguments into a compact deterministic map. */ function readArgs(argv: string[]): Map { const args = new Map(); for (let index = 0; index < argv.length; index += 2) { const key = argv[index]; const value = argv[index + 1]; if (!key?.startsWith("--") || value === undefined) { throw new Error(`Invalid Live2D migration draft argument near ${key ?? ""}`); } args.set(key.slice(2), value); } return args; } /** Resolves bounded workspace defaults and the optional comma-separated module selection. */ function parseOptions(argv: string[]): CliOptions { const args = readArgs(argv); const reviewEvidence = (args.get("review-evidence") ?? "") .split(",") .map((entry) => entry.trim()) .filter(Boolean); if (reviewEvidence.length === 0) { throw new Error("--review-evidence is required for Live2D migration drafts"); } const modulePaths = args.has("modules") ? new Set( args .get("modules")! .split(",") .map((entry) => entry.trim()) .filter(Boolean), ) : null; return { decisionPath: resolveInsideRoot( args.get("decisions") ?? "docs/live2d-deobfuscation/module-migration-decisions.json", ), functionDecisionOutputPath: args.has("function-decisions-output") ? resolveInsideRoot(args.get("function-decisions-output")!) : null, functionDecisionPath: resolveInsideRoot( args.get("function-decisions") ?? "docs/live2d-deobfuscation/function-name-decisions.json", ), implementationRoot: resolveInsideRoot( args.get("implementation-root") ?? "Vue/kt-blog-web/src/components/blog/live2d/vendor/cubism2Core", ), issueOutputPath: resolveInsideRoot( args.get("issue-output") ?? ".kt-workspace/live2d-deobfuscation/module-migration-draft-issues.generated.json", ), manifestPath: resolveInsideRoot( args.get("manifest") ?? "docs/live2d-deobfuscation/module-splits.json", ), modulePaths, outputPath: resolveInsideRoot( args.get("output") ?? ".kt-workspace/live2d-deobfuscation/module-migration-decisions.draft.generated.json", ), refreshExisting: args.get("refresh-existing") === "true", reviewEvidence, sourcePath: resolveInsideRoot( args.get("source") ?? "Vue/kt-blog-web/public/live2d/wordpress-moc/live2d.min.js", ), }; } /** Parses one typed JSON artifact while leaving semantic validation to the migration builder. */ function readJson(filePath: string): T { return JSON.parse(readFileSync(filePath, "utf8")) as T; } /** Converts one module basename into the existing `Cubism2*Source.spec.ts` convention. */ function getBehaviorTestReference(modulePath: string): { file: string; name: string } { const basename = path.basename(modulePath, ".ts"); const semanticModuleName = `${basename.charAt(0).toUpperCase()}${basename.slice(1)}`; return { file: `Vue/kt-blog-web/src/__tests__/live2d/Cubism2${semanticModuleName}Source.spec.ts`, name: `preserves reviewed ${modulePath} source behavior through semantic TypeScript`, }; } /** Builds and writes a deterministic append-only draft without changing the durable decision catalog. */ function main(): void { const options = parseOptions(process.argv.slice(2)); const source = readFileSync(options.sourcePath, "utf8"); const sourceAudit = analyzeLive2DMinJsSource(source, { sourcePath: path.relative(resolveInsideRoot("."), options.sourcePath).replaceAll("\\", "/"), }); const manifest = readJson(options.manifestPath); const current = readJson(options.decisionPath); const retainedDecisions = options.refreshExisting ? [] : current.decisions; const completed = new Set(retainedDecisions.map((entry) => entry.functionId)); const selected = manifest.functions.filter( (entry) => entry.ownershipStatus === "owned" && !completed.has(entry.functionId) && (!options.modulePaths || options.modulePaths.has(entry.ownerModule!)), ); if (options.modulePaths) { const selectedModules = new Set(selected.map((entry) => entry.ownerModule)); const missingModules = [...options.modulePaths].filter( (modulePath) => !selectedModules.has(modulePath), ); if (missingModules.length > 0) { throw new Error(`Selected Live2D modules have no pending functions: ${missingModules.join(", ")}`); } } const behaviorTestsByModule = Object.fromEntries( [...new Set(selected.map((entry) => entry.ownerModule!))].map((modulePath) => [ modulePath, getBehaviorTestReference(modulePath), ]), ); let drafts: ReturnType; try { drafts = buildLive2DModuleMigrationDecisionDrafts({ behaviorTestsByModule, functionIds: selected.map((entry) => entry.functionId), implementationRoot: options.implementationRoot, moduleManifest: manifest, reviewEvidence: options.reviewEvidence, source, sourceFunctions: sourceAudit.functions, }); } catch (error) { if (!(error instanceof Live2DModuleMigrationDraftError)) { throw error; } const functions = new Map(manifest.functions.map((entry) => [entry.functionId, entry])); const issues = error.issues.map((issue) => ({ ...issue, ownerModule: functions.get(issue.functionId)?.ownerModule ?? null, semanticQualifiedName: functions.get(issue.functionId)?.semanticQualifiedName ?? null, sourceOrder: functions.get(issue.functionId)?.sourceOrder ?? null, })); const issueCountsByModule = Object.fromEntries( [...new Set(issues.map((issue) => issue.ownerModule))] .sort((left, right) => String(left).localeCompare(String(right))) .map((modulePath) => [ modulePath ?? "", issues.filter((issue) => issue.ownerModule === modulePath).length, ]), ); mkdirSync(path.dirname(options.issueOutputPath), { recursive: true }); writeFileSync( options.issueOutputPath, `${JSON.stringify( { issueCountsByModule, issues, schemaVersion: "live2d-module-migration-draft-issues/v1", sourceSha256: sourceAudit.sourceSha256, summary: { draftable: selected.length - issues.length, issueCount: issues.length, selected: selected.length, }, }, null, 2, )}\n`, "utf8", ); process.stdout.write( `${JSON.stringify({ issueCount: issues.length, issueOutputPath: path .relative(resolveInsideRoot("."), options.issueOutputPath) .replaceAll("\\", "/"), selectedCount: selected.length, })}\n`, ); process.exitCode = 2; return; } const sourceOrder = new Map( manifest.functions.map((entry) => [entry.functionId, entry.sourceOrder]), ); const catalog: Live2DModuleMigrationDecisionCatalog = { ...current, decisions: [...retainedDecisions, ...drafts].sort( (left, right) => sourceOrder.get(left.functionId)! - sourceOrder.get(right.functionId)!, ), }; mkdirSync(path.dirname(options.outputPath), { recursive: true }); writeFileSync(options.outputPath, `${JSON.stringify(catalog, null, 2)}\n`, "utf8"); let functionDecisionOutputPath: string | null = null; if (options.functionDecisionOutputPath) { const functionCatalog = readJson( options.functionDecisionPath, ); const retargetedFunctionCatalog = retargetLive2DFunctionDecisionCatalog( functionCatalog, catalog.decisions, ); mkdirSync(path.dirname(options.functionDecisionOutputPath), { recursive: true }); writeFileSync( options.functionDecisionOutputPath, `${JSON.stringify(retargetedFunctionCatalog, null, 2)}\n`, "utf8", ); functionDecisionOutputPath = path .relative(resolveInsideRoot("."), options.functionDecisionOutputPath) .replaceAll("\\", "/"); } process.stdout.write( `${JSON.stringify({ draftCount: drafts.length, functionDecisionOutputPath, outputPath: path.relative(resolveInsideRoot("."), options.outputPath).replaceAll("\\", "/"), totalDecisionCount: catalog.decisions.length, })}\n`, ); } main();