{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../../../src/core/evaluation/artifacts.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgD,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnG,eAAO,MAAM,6BAA6B,GAAa,CAAC;AACxD,eAAO,MAAM,+BAA+B,aAAmD,CAAC;AAGhG,MAAM,WAAW,uBAAuB;IACvC,aAAa,EAAE,OAAO,6BAA6B,CAAC;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,MAAM,kBAAkB,GAC3B,QAAQ,GACR,OAAO,GACP,aAAa,GACb,UAAU,GACV,qBAAqB,GACrB,kBAAkB,GAClB,kBAAkB,GAClB,oBAAoB,GACpB,mBAAmB,CAAC;AAEvB,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,2BAA2B,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,KAAK,EAAE,kBAAkB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE,CAAC,CAAC;CACjB;AAMD,wBAAgB,cAAc,CAC7B,KAAK,EAAE,IAAI,CAAC,kBAAkB,EAAE,eAAe,GAAG,kBAAkB,GAAG,cAAc,CAAC,GACpF,kBAAkB,CAGpB;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAGpE;AAgDD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwC/F;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAOhG;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAO/E;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAoHzF","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { mkdir, open, readdir, readFile, rename, rm, stat } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { sha256, stableStringify } from \"./identity.js\";\nimport { EVALUATION_SCHEMA_VERSION, EVALUATOR_VERSION, type EvaluationArtifact } from \"./types.js\";\n\nexport const ARTIFACT_STORE_SCHEMA_VERSION = 1 as const;\nexport const RESERVED_EVALUATION_DIRECTORIES = new Set([\"artifacts\", \"baselines\", \"temporary\"]);\nconst ARTIFACT_ID_PATTERN = /^[a-f0-9]{64}$/;\n\nexport interface EvaluationStoreManifest {\n\tschemaVersion: typeof ARTIFACT_STORE_SCHEMA_VERSION;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tartifacts: string[];\n}\n\nexport type ArtifactStoreState =\n\t| \"absent\"\n\t| \"empty\"\n\t| \"initialized\"\n\t| \"degraded\"\n\t| \"artifact_incomplete\"\n\t| \"artifact_corrupt\"\n\t| \"artifact_missing\"\n\t| \"schema_unsupported\"\n\t| \"permission_denied\";\n\nexport interface ArtifactStoreDiagnostic {\n\tname: \"evaluation-artifact-store\";\n\tstatus: \"pass\" | \"warn\" | \"fail\";\n\tstate: ArtifactStoreState;\n\tstoreRoot: string;\n\tartifactCount: number;\n\ttemporaryCount: number;\n\tindexedArtifactIds: string[];\n\tdiscoveredArtifactIds: string[];\n\terrors: string[];\n\twarnings: string[];\n\tmutationCount: 0;\n}\n\nfunction artifactPayload(artifact: Omit<EvaluationArtifact, \"artifactHash\">): string {\n\treturn stableStringify(artifact);\n}\n\nexport function createArtifact(\n\tinput: Omit<EvaluationArtifact, \"schemaVersion\" | \"evaluatorVersion\" | \"artifactHash\">,\n): EvaluationArtifact {\n\tconst unsigned = { ...input, schemaVersion: EVALUATION_SCHEMA_VERSION, evaluatorVersion: EVALUATOR_VERSION };\n\treturn { ...unsigned, artifactHash: sha256(artifactPayload(unsigned)) };\n}\n\nexport function verifyArtifact(artifact: EvaluationArtifact): boolean {\n\tconst { artifactHash, ...unsigned } = artifact;\n\treturn artifact.schemaVersion === EVALUATION_SCHEMA_VERSION && artifactHash === sha256(artifactPayload(unsigned));\n}\n\nfunction assertArtifactId(artifactId: string): void {\n\tif (!ARTIFACT_ID_PATTERN.test(artifactId)) throw new Error(`invalid evaluation artifact id: ${artifactId}`);\n}\n\nfunction storeManifestPath(root: string): string {\n\treturn join(root, \"store.json\");\n}\n\nasync function writeDurably(path: string, contents: string): Promise<void> {\n\tconst handle = await open(path, \"wx\");\n\ttry {\n\t\tawait handle.writeFile(contents, \"utf8\");\n\t\tawait handle.sync();\n\t} finally {\n\t\tawait handle.close();\n\t}\n}\n\nasync function readStoreManifest(root: string): Promise<EvaluationStoreManifest | undefined> {\n\ttry {\n\t\treturn JSON.parse(await readFile(storeManifestPath(root), \"utf8\")) as EvaluationStoreManifest;\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n\t\tthrow error;\n\t}\n}\n\nasync function updateStoreManifest(root: string, artifactId: string): Promise<void> {\n\tconst path = storeManifestPath(root);\n\tconst current = (await readStoreManifest(root)) ?? {\n\t\tschemaVersion: ARTIFACT_STORE_SCHEMA_VERSION,\n\t\tcreatedAt: new Date().toISOString(),\n\t\tupdatedAt: new Date().toISOString(),\n\t\tartifacts: [],\n\t};\n\tif (current.schemaVersion !== ARTIFACT_STORE_SCHEMA_VERSION) throw new Error(\"unsupported evaluation store schema\");\n\tconst manifest: EvaluationStoreManifest = {\n\t\t...current,\n\t\tupdatedAt: new Date().toISOString(),\n\t\tartifacts: [...new Set([...current.artifacts, artifactId])].sort(),\n\t};\n\tconst temporaryPath = `${path}.${randomUUID()}.tmp`;\n\tawait writeDurably(temporaryPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n\tawait rename(temporaryPath, path);\n}\n\nexport async function writeArtifact(root: string, artifact: EvaluationArtifact): Promise<string> {\n\tif (!verifyArtifact(artifact)) throw new Error(\"cannot write invalid evaluation artifact\");\n\tassertArtifactId(artifact.artifactHash);\n\tconst storeRoot = resolve(root);\n\tconst artifactDirectory = join(storeRoot, \"artifacts\", artifact.artifactHash);\n\tconst temporaryDirectory = join(storeRoot, \"temporary\", `${artifact.artifactHash}-${randomUUID()}`);\n\tawait mkdir(join(storeRoot, \"artifacts\"), { recursive: true });\n\tawait mkdir(join(storeRoot, \"baselines\"), { recursive: true });\n\tawait mkdir(join(storeRoot, \"temporary\"), { recursive: true });\n\ttry {\n\t\tconst resultPath = join(temporaryDirectory, \"result.json\");\n\t\tconst manifestPath = join(temporaryDirectory, \"manifest.json\");\n\t\tawait mkdir(temporaryDirectory, { recursive: true });\n\t\tconst result = `${JSON.stringify(artifact, null, 2)}\\n`;\n\t\tawait writeDurably(resultPath, result);\n\t\tawait writeDurably(\n\t\t\tmanifestPath,\n\t\t\t`${JSON.stringify(\n\t\t\t\t{\n\t\t\t\t\tschemaVersion: ARTIFACT_STORE_SCHEMA_VERSION,\n\t\t\t\t\tartifactId: artifact.artifactHash,\n\t\t\t\t\tresultHash: sha256(result),\n\t\t\t\t\tcreatedAt: artifact.provenance.createdAt,\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t)}\\n`,\n\t\t);\n\t\tawait rename(temporaryDirectory, artifactDirectory);\n\t\tawait updateStoreManifest(storeRoot, artifact.artifactHash);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"EEXIST\") {\n\t\t\tawait rm(temporaryDirectory, { recursive: true, force: true });\n\t\t\tconst existing = await readArtifact(storeRoot, artifact.artifactHash);\n\t\t\tif (!verifyArtifact(existing)) throw new Error(\"existing artifact is corrupt\");\n\t\t\treturn artifact.artifactHash;\n\t\t}\n\t\tawait rm(temporaryDirectory, { recursive: true, force: true });\n\t\tthrow error;\n\t}\n\treturn artifact.artifactHash;\n}\n\nexport async function readArtifact(root: string, artifactId: string): Promise<EvaluationArtifact> {\n\tassertArtifactId(artifactId);\n\tconst artifact = JSON.parse(\n\t\tawait readFile(join(resolve(root), \"artifacts\", artifactId, \"result.json\"), \"utf8\"),\n\t) as EvaluationArtifact;\n\tif (!verifyArtifact(artifact)) throw new Error(`invalid evaluation artifact: ${artifactId}`);\n\treturn artifact;\n}\n\nexport async function listArtifacts(root: string): Promise<EvaluationArtifact[]> {\n\tconst diagnostic = await inspectArtifactStore(root);\n\tconst artifacts: EvaluationArtifact[] = [];\n\tfor (const artifactId of diagnostic.discoveredArtifactIds.sort()) {\n\t\tartifacts.push(await readArtifact(root, artifactId));\n\t}\n\treturn artifacts;\n}\n\nexport async function inspectArtifactStore(root: string): Promise<ArtifactStoreDiagnostic> {\n\tconst storeRoot = resolve(root);\n\tconst diagnostic: ArtifactStoreDiagnostic = {\n\t\tname: \"evaluation-artifact-store\",\n\t\tstatus: \"pass\",\n\t\tstate: \"absent\",\n\t\tstoreRoot,\n\t\tartifactCount: 0,\n\t\ttemporaryCount: 0,\n\t\tindexedArtifactIds: [],\n\t\tdiscoveredArtifactIds: [],\n\t\terrors: [],\n\t\twarnings: [],\n\t\tmutationCount: 0,\n\t};\n\tlet rootStat: Awaited<ReturnType<typeof stat>>;\n\ttry {\n\t\trootStat = await stat(storeRoot);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return diagnostic;\n\t\tif ((error as NodeJS.ErrnoException).code === \"EACCES\") {\n\t\t\tdiagnostic.status = \"fail\";\n\t\t\tdiagnostic.state = \"permission_denied\";\n\t\t\tdiagnostic.errors.push(\"permission denied reading evaluation store\");\n\t\t\treturn diagnostic;\n\t\t}\n\t\tthrow error;\n\t}\n\tif (!rootStat.isDirectory()) {\n\t\tdiagnostic.status = \"fail\";\n\t\tdiagnostic.state = \"artifact_corrupt\";\n\t\tdiagnostic.errors.push(\"evaluation store root is not a directory\");\n\t\treturn diagnostic;\n\t}\n\tlet store: EvaluationStoreManifest | undefined;\n\ttry {\n\t\tstore = await readStoreManifest(storeRoot);\n\t} catch (error) {\n\t\tdiagnostic.status = \"fail\";\n\t\tdiagnostic.state = \"artifact_corrupt\";\n\t\tdiagnostic.errors.push(`store manifest: ${error instanceof Error ? error.message : String(error)}`);\n\t\treturn diagnostic;\n\t}\n\tif (store && store.schemaVersion !== ARTIFACT_STORE_SCHEMA_VERSION) {\n\t\tdiagnostic.status = \"fail\";\n\t\tdiagnostic.state = \"schema_unsupported\";\n\t\tdiagnostic.errors.push(`unsupported evaluation store schema: ${store.schemaVersion}`);\n\t\treturn diagnostic;\n\t}\n\tdiagnostic.indexedArtifactIds = store?.artifacts ?? [];\n\tconst artifactRoot = join(storeRoot, \"artifacts\");\n\tconst temporaryRoot = join(storeRoot, \"temporary\");\n\tconst artifactEntries = await readdir(artifactRoot, { withFileTypes: true }).catch((error: unknown) => {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\tthrow error;\n\t});\n\tconst temporaryEntries = await readdir(temporaryRoot, { withFileTypes: true }).catch((error: unknown) => {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\tthrow error;\n\t});\n\tdiagnostic.temporaryCount = temporaryEntries.length;\n\tif (diagnostic.temporaryCount > 0) {\n\t\tdiagnostic.status = \"warn\";\n\t\tdiagnostic.state = \"degraded\";\n\t\tdiagnostic.warnings.push(`${diagnostic.temporaryCount} temporary artifact(s) require recovery or pruning`);\n\t}\n\tfor (const entry of artifactEntries.sort((left, right) => left.name.localeCompare(right.name))) {\n\t\tif (!entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\tdiagnostic.status = \"fail\";\n\t\t\tdiagnostic.state = \"artifact_corrupt\";\n\t\t\tdiagnostic.errors.push(`ready artifact entry is not a directory: ${entry.name}`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst artifactId = entry.name;\n\t\tdiagnostic.discoveredArtifactIds.push(artifactId);\n\t\tdiagnostic.artifactCount += 1;\n\t\tif (!ARTIFACT_ID_PATTERN.test(artifactId)) {\n\t\t\tdiagnostic.status = \"fail\";\n\t\t\tdiagnostic.state = \"artifact_corrupt\";\n\t\t\tdiagnostic.errors.push(`invalid artifact id: ${artifactId}`);\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tconst manifest = JSON.parse(await readFile(join(artifactRoot, artifactId, \"manifest.json\"), \"utf8\")) as {\n\t\t\t\tschemaVersion: number;\n\t\t\t\tartifactId: string;\n\t\t\t\tresultHash: string;\n\t\t\t};\n\t\t\tconst result = await readFile(join(artifactRoot, artifactId, \"result.json\"));\n\t\t\tconst artifact = JSON.parse(result.toString(\"utf8\")) as EvaluationArtifact;\n\t\t\tif (\n\t\t\t\tmanifest.schemaVersion !== ARTIFACT_STORE_SCHEMA_VERSION ||\n\t\t\t\tmanifest.artifactId !== artifactId ||\n\t\t\t\tmanifest.resultHash !== sha256(result) ||\n\t\t\t\t!verifyArtifact(artifact)\n\t\t\t)\n\t\t\t\tthrow new Error(\"manifest or result hash mismatch\");\n\t\t} catch (error) {\n\t\t\tdiagnostic.status = \"fail\";\n\t\t\tdiagnostic.state = \"artifact_incomplete\";\n\t\t\tdiagnostic.errors.push(`${artifactId}: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\tfor (const artifactId of diagnostic.indexedArtifactIds) {\n\t\tif (!diagnostic.discoveredArtifactIds.includes(artifactId)) {\n\t\t\tdiagnostic.status = \"fail\";\n\t\t\tdiagnostic.state = \"artifact_missing\";\n\t\t\tdiagnostic.errors.push(`store index references missing artifact: ${artifactId}`);\n\t\t}\n\t}\n\tif (!store && diagnostic.artifactCount === 0 && diagnostic.temporaryCount === 0) {\n\t\tdiagnostic.state = \"empty\";\n\t} else if (diagnostic.status === \"pass\") {\n\t\tdiagnostic.state = diagnostic.artifactCount === 0 ? \"empty\" : \"initialized\";\n\t}\n\treturn diagnostic;\n}\n"]}