#!/usr/bin/env node import { createHash } from "node:crypto"; 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 { analyzeLive2DBootstrapGraph } from "../src/tools/live2dBootstrapGraph.js"; import { findMissingLive2DImplementationTargets } from "../src/tools/live2dImplementationTargets.js"; import { buildLive2DModuleReviewPack, type Live2DModuleReviewPackManifest, } from "../src/tools/live2dModuleReviewPack.js"; import { buildLive2DModuleGraphAnalysis, buildLive2DModuleSplitCatalog, type Live2DModuleGraphAnalysis, type Live2DModuleCouplingEdge, type Live2DModuleSplitCatalog, validateLive2DModuleSplitCatalog, } from "../src/tools/live2dModuleSplits.js"; import { auditLive2DModuleMigrations, type Live2DModuleMigrationDecisionCatalog, } from "../src/tools/live2dModuleMigrations.js"; import { analyzeLive2DSourceBindings } from "../src/tools/live2dSourceBindings.js"; interface CliOptions { bindingOutputPath: string; bootstrapOutputPath: string; callDecisionPath: string; draftOutputPath: string; functionDecisionPath: string; finalOutputPath: string | null; graphOutputPath: string; implementationRoot: string; migrationDecisionPath: string; reviewResultPaths: string[]; reviewJsonOutputPath: string; reviewMarkdownOutputPath: string; sourcePath: string; } interface Live2DModuleOwnerReviewRow { decision: "accept-owner" | "change-owner" | "omitted-unreachable"; evidence: string[]; functionId: string; ownerModule: string | null; reason: string; reviewStatus: "reviewed"; } interface Live2DModuleOwnerReviewResult { findings: Array<{ severity: "Critical" | "Important" | "Minor" } & Record>; functionIdsSha256: string; partitionId: string; reviewedRows: Live2DModuleOwnerReviewRow[]; rowCount: number; rowsSha256: string; schemaVersion: 1 | "live2d-module-owner-review-result/v1"; sourceSha256: string; summary: unknown; } const dirname = path.dirname(fileURLToPath(import.meta.url)); const workspaceRoot = path.resolve( process.env.KT_WORKSPACE_ROOT || path.join(dirname, "../../.."), ); const generatedRoot = ".kt-workspace/deobfuscate/blog-live2d-cubism2-minjs/live2d.min"; /** 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 source/catalog/output surface for the module-split audit. */ 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 { bindingOutputPath: resolveWorkspacePath( args.get("binding-output") ?? `${generatedRoot}/module-source-bindings.generated.json`, ), bootstrapOutputPath: resolveWorkspacePath( args.get("bootstrap-output") ?? `${generatedRoot}/module-bootstrap.generated.json`, ), callDecisionPath: resolveWorkspacePath( args.get("call-decisions") ?? "docs/live2d-deobfuscation/call-resolutions.json", ), draftOutputPath: resolveWorkspacePath( args.get("draft-output") ?? `${generatedRoot}/module-splits.draft.generated.json`, ), functionDecisionPath: resolveWorkspacePath( args.get("function-decisions") ?? "docs/live2d-deobfuscation/function-name-decisions.json", ), finalOutputPath: args.has("final-output") ? resolveWorkspacePath(args.get("final-output")!) : null, graphOutputPath: resolveWorkspacePath( args.get("graph-output") ?? `${generatedRoot}/module-graph.generated.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", ), reviewJsonOutputPath: resolveWorkspacePath( args.get("review-json-output") ?? `${generatedRoot}/module-review-pack.generated.json`, ), reviewResultPaths: ["a", "b", "c"].map((suffix) => resolveWorkspacePath( args.get(`review-${suffix}`) ?? `.kt-workspace/subagents/results/2026-07-15-live2d-task4-review-${suffix}.json`, ), ), reviewMarkdownOutputPath: resolveWorkspacePath( args.get("review-markdown-output") ?? ".kt-workspace/subagents/2026-07-15-live2d-task4-module-owner-source-pack.md", ), sourcePath: resolveWorkspacePath( args.get("source") ?? "Vue/kt-blog-web/public/live2d/wordpress-moc/live2d.min.js", ), }; } /** Reads a typed JSON catalog while leaving semantic validation to the builders. */ function readJson(filePath: string): T { return JSON.parse(readFileSync(filePath, "utf8")) as T; } /** Writes deterministic pretty JSON with one trailing LF. */ function writeJson(filePath: string, value: unknown): void { mkdirSync(path.dirname(filePath), { recursive: true }); writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } /** Writes a generated UTF-8 text artifact after creating its evidence directory. */ function writeText(filePath: string, value: string): void { mkdirSync(path.dirname(filePath), { recursive: true }); writeFileSync(filePath, value.endsWith("\n") ? value : `${value}\n`, "utf8"); } /** Adapts only bootstrap prototype ownership into structural function clusters. */ function buildBootstrapStructuralCouplingEdges( bootstrapAnalysis: ReturnType, ): Live2DModuleCouplingEdge[] { return bootstrapAnalysis.couplingEdges .filter((edge) => edge.kind === "prototype-owner") .map((edge) => ({ couplingKey: edge.couplingKey, functionIds: edge.functionIds, kind: "prototype-owner" as const, })); } /** Produces a stable SHA-256 for a generated text payload. */ function sha256(value: string): string { return createHash("sha256").update(value).digest("hex"); } /** Validates the three disjoint reviewer outputs and indexes their 627 decisions. */ function validateModuleOwnerReviews( reviewPack: Live2DModuleReviewPackManifest, results: readonly Live2DModuleOwnerReviewResult[], ): Map { if (results.length !== reviewPack.partitions.length) { throw new Error("Live2D module review result partition count mismatch"); } const decisions = new Map< string, Live2DModuleOwnerReviewRow & { partitionId: string } >(); for (const partition of reviewPack.partitions) { const result = results.find( (candidate) => candidate.partitionId === partition.partitionId, ); if (!result) { throw new Error(`Missing Live2D module review result: ${partition.partitionId}`); } if ( (result.schemaVersion !== 1 && result.schemaVersion !== "live2d-module-owner-review-result/v1") || result.sourceSha256 !== reviewPack.sourceSha256 || result.rowCount !== partition.rowCount || result.functionIdsSha256 !== partition.functionIdsSha256 || result.rowsSha256 !== partition.rowsSha256 || result.reviewedRows.length !== partition.rows.length ) { throw new Error(`Live2D module review seals mismatch: ${partition.partitionId}`); } const expectedById = new Map( partition.rows.map((row) => [row.functionId, row]), ); for (const row of result.reviewedRows) { const expected = expectedById.get(row.functionId); if (!expected || decisions.has(row.functionId)) { throw new Error(`Duplicate or foreign Live2D module review row: ${row.functionId}`); } if ( row.reviewStatus !== "reviewed" || row.reason.trim().length === 0 || row.evidence.length === 0 ) { throw new Error(`Incomplete Live2D module review row: ${row.functionId}`); } if (expected.ownerCandidate === null && expected.reachability !== "reachable") { if (row.decision !== "omitted-unreachable" || row.ownerModule !== null) { throw new Error(`Reachability mismatch in module review: ${row.functionId}`); } } else if (row.ownerModule === null || row.decision === "omitted-unreachable") { throw new Error(`Reachable function lacks module owner: ${row.functionId}`); } else if ( row.decision === "accept-owner" && row.ownerModule !== expected.ownerCandidate ) { throw new Error(`Accepted owner differs from candidate: ${row.functionId}`); } else if ( row.decision === "change-owner" && row.ownerModule === expected.ownerCandidate ) { throw new Error(`Changed owner still equals candidate: ${row.functionId}`); } decisions.set(row.functionId, { ...row, partitionId: partition.partitionId, }); } } if (decisions.size !== reviewPack.summary.functionCount) { throw new Error(`Live2D module review coverage is ${decisions.size}/627`); } return decisions; } /** Rebuilds module dependency rows after applying the reviewed owner decisions. */ function buildReviewedModules( functions: Array< Live2DModuleSplitCatalog["functions"][number] & { ownerReview: Live2DModuleOwnerReviewRow & { partitionId: string }; } >, graph: Live2DModuleGraphAnalysis, ): Array> { const functionById = new Map(functions.map((entry) => [entry.functionId, entry])); const modules = new Map< string, { clusterIds: Set; couplingKeys: Set; couplingModules: Set; dependencyModules: Set; implementationFunctionIds: string[]; ownedFunctionIds: string[]; } >(); for (const entry of functions) { const modulePaths = new Set([ ...(entry.ownerModule ? [entry.ownerModule] : []), ...entry.implementationModules, ]); for (const modulePath of modulePaths) { const module = modules.get(modulePath) ?? { clusterIds: new Set(), couplingKeys: new Set(), couplingModules: new Set(), dependencyModules: new Set(), implementationFunctionIds: [], ownedFunctionIds: [], }; module.clusterIds.add(entry.clusterId); if (entry.implementationModules.includes(modulePath)) { module.implementationFunctionIds.push(entry.functionId); } if (entry.ownerModule === modulePath) { module.ownedFunctionIds.push(entry.functionId); } modules.set(modulePath, module); } if (entry.ownerModule) { for (const implementationModule of entry.implementationModules) { if (implementationModule !== entry.ownerModule) { modules .get(entry.ownerModule)! .dependencyModules.add(implementationModule); } } } } for (const edge of graph.callEdges) { const caller = functionById.get(edge.callerFunctionId)?.ownerModule; const target = functionById.get(edge.targetFunctionId)?.ownerModule; if (caller && target && caller !== target) { modules.get(caller)!.dependencyModules.add(target); } } for (const edge of graph.couplingEdges) { const owners = [ ...new Set( edge.functionIds .map((functionId) => functionById.get(functionId)?.ownerModule) .filter((owner): owner is string => owner !== null && owner !== undefined), ), ]; for (const owner of owners) { const module = modules.get(owner)!; module.couplingKeys.add(edge.couplingKey); for (const peer of owners) { if (peer !== owner) { module.couplingModules.add(peer); } } } } return [...modules.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([modulePath, module]) => ({ clusterIds: [...module.clusterIds].sort(), couplingKeys: [...module.couplingKeys].sort(), couplingModules: [...module.couplingModules].sort(), dependencyModules: [...module.dependencyModules].sort(), implementationFunctionIds: module.implementationFunctionIds.sort( (left, right) => functionById.get(left)!.sourceOrder - functionById.get(right)!.sourceOrder, ), ownedFunctionIds: module.ownedFunctionIds.sort( (left, right) => functionById.get(left)!.sourceOrder - functionById.get(right)!.sourceOrder, ), modulePath, })); } /** Builds the durable reviewed split manifest with ownership and initialization evidence. */ function buildReviewedModuleSplitManifest( reviewPack: Live2DModuleReviewPackManifest, draft: Live2DModuleSplitCatalog, graph: Live2DModuleGraphAnalysis, bindingAnalysis: ReturnType, bootstrapAnalysis: ReturnType, results: readonly Live2DModuleOwnerReviewResult[], ): Record { const decisions = validateModuleOwnerReviews(reviewPack, results); const functions = draft.functions.map((entry) => { const ownerReview = decisions.get(entry.functionId)!; const ownerModule = ownerReview.ownerModule; return { ...entry, ownerModule, ownerReview, ownershipStatus: ownerModule === null ? ("omitted-unreachable" as const) : ("owned" as const), }; }); const criticalOrImportant = results.flatMap((result) => result.findings).filter( (finding) => finding.severity === "Critical" || finding.severity === "Important", ); if (criticalOrImportant.length > 0) { throw new Error("Live2D module owner review has blocking findings"); } const modules = buildReviewedModules(functions, graph); if (modules.length !== draft.modules.length) { throw new Error( `Live2D reviewed manifest lost implementation modules: ${modules.length}/${draft.modules.length}`, ); } return { bootstrap: bootstrapAnalysis, functions, graphSummary: graph.summary, modules, reviews: results.map((result) => ({ findings: result.findings, functionIdsSha256: result.functionIdsSha256, partitionId: result.partitionId, rowCount: result.rowCount, rowsSha256: result.rowsSha256, summary: result.summary, })), schemaVersion: "live2d-module-splits/v1", sharedImplementations: draft.sharedImplementations, sourceBindings: bindingAnalysis, sourcePath: reviewPack.sourcePath, sourceSha256: reviewPack.sourceSha256, summary: { blockingFindingCount: criticalOrImportant.length, functionCount: functions.length, moduleCount: modules.length, ownerModuleCount: new Set( functions.map((entry) => entry.ownerModule).filter(Boolean), ).size, omittedFunctionCount: functions.filter((entry) => entry.ownerModule === null) .length, ownedFunctionCount: functions.filter((entry) => entry.ownerModule !== null) .length, reviewedFunctionCount: decisions.size, sharedImplementationCount: draft.sharedImplementations.length, }, }; } /** Adds source-sealed semantic migration state to every reviewed owner decision. */ function attachModuleMigrationState( manifest: Record, source: string, sourceFunctions: ReturnType["functions"], options: CliOptions, ): Record { const migrationAudit = auditLive2DModuleMigrations({ decisions: readJson(options.migrationDecisionPath), implementationRoot: options.implementationRoot, moduleManifest: manifest as unknown as Parameters< typeof auditLive2DModuleMigrations >[0]["moduleManifest"], source, sourceFunctions, workspaceRoot, }); const migrationByFunctionId = new Map( migrationAudit.functions.map((entry) => [entry.functionId, entry.migrationStatus]), ); const functions = (manifest.functions as Array>).map((entry) => ({ ...entry, migrationStatus: migrationByFunctionId.get(String(entry.functionId)), })); return { ...manifest, functions, schemaVersion: "live2d-module-splits/v2", summary: { ...(manifest.summary as Record), migratedFunctionCount: migrationAudit.summary.migrated, omittedMigrationFunctionCount: migrationAudit.summary.omittedUnreachable, pendingMigrationFunctionCount: migrationAudit.summary.pending, }, }; } /** Generates all source-sealed Task 4 audits and the exhaustive three-way review pack. */ function main(): void { const options = parseArgs(process.argv.slice(2)); const source = readFileSync(options.sourcePath, "utf8"); const sourceLabel = path .relative(workspaceRoot, options.sourcePath) .replaceAll("\\", "/"); const sourceAudit = analyzeLive2DMinJsSource(source, { sourcePath: sourceLabel, }); const functionCatalog = buildFunctionNameReviewCatalog( sourceAudit, buildLegacyFunctionRenameCatalog(sourceAudit, []), readJson(options.functionDecisionPath), ); const implementationRoot = path.resolve(options.implementationRoot); const implementationRootPrefix = `${implementationRoot}${path.sep}`; const missingImplementationTargets = findMissingLive2DImplementationTargets( functionCatalog.functions, (modulePath) => { const implementationPath = path.resolve(implementationRoot, modulePath); if (!implementationPath.startsWith(implementationRootPrefix)) { return null; } return readFileSync(implementationPath, "utf8"); }, ); if (missingImplementationTargets.length > 0) { throw new Error( `Live2D reviewed implementation targets are missing: ${missingImplementationTargets .map((entry) => `${entry.functionId} -> ${entry.targetSymbol} (${entry.reason})`) .join(", ")}`, ); } const callCatalog = buildCallResolutionReviewCatalog( sourceAudit, functionCatalog, readJson(options.callDecisionPath), ); const bindingAnalysis = analyzeLive2DSourceBindings( source, sourceAudit, functionCatalog, ); const bootstrapAnalysis = analyzeLive2DBootstrapGraph( source, sourceAudit, functionCatalog, callCatalog, ); const graph = buildLive2DModuleGraphAnalysis( functionCatalog, callCatalog, [ ...bindingAnalysis.couplingEdges, ...buildBootstrapStructuralCouplingEdges(bootstrapAnalysis), ], ); const draft = buildLive2DModuleSplitCatalog(functionCatalog, graph); const validation = validateLive2DModuleSplitCatalog( functionCatalog, graph, draft, ); const reviewPack = buildLive2DModuleReviewPack( functionCatalog, graph, draft, ); writeJson(options.bindingOutputPath, bindingAnalysis); writeJson(options.bootstrapOutputPath, bootstrapAnalysis); writeJson(options.graphOutputPath, graph); writeJson(options.draftOutputPath, draft); writeText(options.reviewJsonOutputPath, reviewPack.json); writeText(options.reviewMarkdownOutputPath, reviewPack.markdown); let finalOutput: { path: string; sha256: string } | null = null; if (options.finalOutputPath) { const reviewedManifest = attachModuleMigrationState( buildReviewedModuleSplitManifest( reviewPack.manifest, draft, graph, bindingAnalysis, bootstrapAnalysis, options.reviewResultPaths.map((filePath) => readJson(filePath), ), ), source, sourceAudit.functions, options, ); const reviewedJson = `${JSON.stringify(reviewedManifest, null, 2)}\n`; writeText(options.finalOutputPath, reviewedJson); finalOutput = { path: options.finalOutputPath, sha256: sha256(reviewedJson), }; } console.log( JSON.stringify( { bindingSummary: bindingAnalysis.summary, bootstrapSummary: bootstrapAnalysis.summary, graphSummary: graph.summary, moduleValidation: validation, outputs: { binding: options.bindingOutputPath, bootstrap: options.bootstrapOutputPath, draft: options.draftOutputPath, graph: options.graphOutputPath, reviewJson: options.reviewJsonOutputPath, reviewMarkdown: options.reviewMarkdownOutputPath, reviewedManifest: finalOutput, }, reviewPack: { jsonSha256: reviewPack.jsonSha256, markdownSha256: reviewPack.markdownSha256, summary: reviewPack.manifest.summary, }, sourceSha256: sourceAudit.sourceSha256, }, null, 2, ), ); } main();