import { isAbsolute, resolve } from "node:path"; import { AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { javascriptRecoveryInputSchema, javascriptRecoveryResultSchema, } from "../../domain/javascript/javascriptRecovery.js"; import { jsonObjectSchema, jsonValueSchema } from "../../domain/jsonValue.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, type Result } from "../../domain/result.js"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import { completeProviderEvidence } from "../ProviderOperationExecution.js"; import type { JavaScriptRecoveryPort } from "./JavaScriptRecoveryPort.js"; const OPERATION = "recover_javascript_sources"; /** Shared CLI/MCP parsing and Evidence for derived JavaScript artifacts. */ export class JavaScriptRecoveryService { constructor(readonly provider: JavaScriptRecoveryPort) {} close(): Promise { return this.provider.close?.() ?? Promise.resolve(); } /** Recover sources and bind the result to its original immutable input. */ async recover( input: unknown, options?: ExecutionOptions, ): Promise> { if (options?.signal?.aborted === true) return err( new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), ); const parsed = javascriptRecoveryInputSchema.safeParse(input); if (!parsed.success) return err( analysisInputErrorFromIssues(OPERATION, parsed.error.issues, input, { cause: parsed.error, }), ); const invalidPaths = (["path", "output_directory"] as const).filter( (field) => !isAbsolute(parsed.data[field]), ); const invalidAdditional = (parsed.data.additional_paths ?? []).filter( (additional) => !isAbsolute(additional), ); if (invalidPaths.length > 0 || invalidAdditional.length > 0) return err( new AnalysisInputError(OPERATION, undefined, [ ...invalidPaths.map((field) => ({ path: [field], reason: "invalid_format" as const, message: `${field} must be an absolute filesystem path on this host.`, })), ...invalidAdditional.map((additional) => ({ path: ["additional_paths"], reason: "invalid_format" as const, message: `additional_paths entries must be absolute filesystem paths on this host: ${additional}`, })), ]), ); const selectedInputs = new Set( [parsed.data.path, ...(parsed.data.additional_paths ?? [])].map((path) => resolve(path), ), ); if (selectedInputs.size !== 1 + (parsed.data.additional_paths ?? []).length) return err( new AnalysisInputError(OPERATION, undefined, [ { path: ["additional_paths"], reason: "invalid_format", message: "additional_paths must not repeat path or each other.", }, ]), ); const execution = await this.provider.recover(parsed.data, options); if (!execution.ok) return execution; const result = javascriptRecoveryResultSchema.safeParse( execution.value.result, ); if (!result.success || execution.value.subject == null) return err( new AnalysisOutputError( OPERATION, "Recovery did not return a valid result bound to the original source", ), ); if ( result.data.source.path !== parsed.data.path || (result.data.additional_sources ?? []).length !== (parsed.data.additional_paths ?? []).length || (parsed.data.additional_paths ?? []).some( (path, index) => result.data.additional_sources?.[index]?.path !== path, ) || execution.value.subject.path !== result.data.source.path || execution.value.subject.sha256 !== result.data.source.sha256 || execution.value.provider.id !== result.data.engine.id || execution.value.provider.version !== result.data.engine.version ) return err( new AnalysisOutputError( OPERATION, "Recovery source or engine identity disagrees with its Evidence binding", ), ); const evidence = createEvidence( execution.value.subject, execution.value.provider, { operation: OPERATION, parameters: jsonObjectSchema.parse(parsed.data), result: jsonValueSchema.parse(result.data), rawResult: execution.value.rawResult, locations: execution.value.locations, limitations: execution.value.limitations, confidence: "derived", authority: "shipped-artifact", }, ); return completeProviderEvidence(evidence, options?.signal); } }