{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/bundle/compile.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,QAAQ,EAAmB,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,EAAiD,cAAc,EAAE,MAAM,aAAa,CAAC;AAqIjG,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA2BjG;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC5C,KAAK,EAAE,QAAQ,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,cAAc,CAAC,CA4D1B;AAED,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/G","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { lstat, mkdir, open, readdir, readFile, rename, rm, writeFile } from \"node:fs/promises\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\nimport { validateEvoComponentSelection } from \"../components/artifact.ts\";\nimport { createDefaultEvoAbiRegistry } from \"../components/registry.ts\";\nimport { PREFERENCES_PATH, parsePreferenceMemory } from \"../memory/preferences.ts\";\nimport { type EvoPaths, ensureEvoLayout } from \"../paths.ts\";\nimport { canonicalJson, sha256 } from \"../storage.ts\";\nimport type { BundleFileEntry, BundleManifest, BundlePolicy, CompiledBundle } from \"../types.ts\";\nimport { assertAssetPath, isDigest, parseBundleManifest, parseBundlePolicy } from \"./schema.ts\";\n\nconst DEFAULT_PROMPT_BYTES = 64 * 1024;\nconst DEFAULT_SKILL_BYTES = 15 * 1024;\nconst DEFAULT_TOTAL_BYTES = 1024 * 1024;\n\nasync function syncFilesystemPath(path: string, directory: boolean): Promise<void> {\n\tlet handle: Awaited<ReturnType<typeof open>> | undefined;\n\ttry {\n\t\thandle = await open(path, \"r\");\n\t\tawait handle.sync();\n\t} catch (error) {\n\t\tif (!directory || process.platform !== \"win32\") throw error;\n\t} finally {\n\t\tawait handle?.close();\n\t}\n}\n\nasync function syncBundleDirectories(directory: string): Promise<void> {\n\tfor (const entry of await readdir(directory, { withFileTypes: true })) {\n\t\tconst path = join(directory, entry.name);\n\t\tif (entry.isDirectory()) {\n\t\t\tawait syncBundleDirectories(path);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!entry.isFile()) {\n\t\t\tthrow new Error(`Bundle staging contains a non-regular entry: ${entry.name}`);\n\t\t}\n\t}\n\tawait syncFilesystemPath(directory, true);\n}\n\nasync function writeDurableBundleFile(path: string, content: string | Uint8Array): Promise<void> {\n\tconst handle = await open(path, \"wx\", 0o600);\n\ttry {\n\t\tawait handle.writeFile(content);\n\t\tawait handle.chmod(0o444);\n\t\tawait handle.sync();\n\t} finally {\n\t\tawait handle.close();\n\t}\n}\n\nfunction normalizeRelativePath(path: string): string {\n\treturn path.split(sep).join(\"/\");\n}\n\nasync function listSourceFiles(sourceDirectory: string, currentDirectory = sourceDirectory): Promise<string[]> {\n\tconst result: string[] = [];\n\tfor (const entry of await readdir(currentDirectory, { withFileTypes: true })) {\n\t\tconst absolutePath = join(currentDirectory, entry.name);\n\t\tconst pathStat = await lstat(absolutePath);\n\t\tif (pathStat.isSymbolicLink()) throw new Error(`Bundle cannot contain symbolic links: ${entry.name}`);\n\t\tif (entry.isDirectory()) {\n\t\t\tresult.push(...(await listSourceFiles(sourceDirectory, absolutePath)));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!entry.isFile()) throw new Error(`Bundle contains a non-regular file: ${entry.name}`);\n\t\tif ((pathStat.mode & 0o111) !== 0) throw new Error(`Bundle cannot contain executable files: ${entry.name}`);\n\t\tresult.push(normalizeRelativePath(relative(sourceDirectory, absolutePath)));\n\t}\n\treturn result.sort();\n}\n\nfunction sortManifestFiles(files: BundleFileEntry[]): BundleFileEntry[] {\n\treturn [...files].sort((left, right) => {\n\t\tif (left.path === \"policy.json\") return -1;\n\t\tif (right.path === \"policy.json\") return 1;\n\t\treturn left.path.localeCompare(right.path);\n\t});\n}\n\nfunction validatePolicyAssetReferences(policy: BundlePolicy, files: Map<string, string>): void {\n\tfor (const path of policy.promptOrder ?? []) {\n\t\tif (!path.startsWith(\"prompts/\") || !files.has(path))\n\t\t\tthrow new Error(`policy.promptOrder references missing prompt: ${path}`);\n\t}\n\tfor (const path of [...(policy.stablePromptPaths ?? []), ...(policy.dynamicPromptPaths ?? [])]) {\n\t\tif (!path.startsWith(\"prompts/\") || !files.has(path))\n\t\t\tthrow new Error(`Prompt layout references missing prompt: ${path}`);\n\t}\n\tfor (const path of policy.coreAssets ?? []) {\n\t\tif (!files.has(path)) throw new Error(`policy.coreAssets references missing asset: ${path}`);\n\t}\n\tfor (const source of policy.managedSources ?? []) {\n\t\tif (!files.has(source.targetPath))\n\t\t\tthrow new Error(`policy.managedSources references missing asset: ${source.targetPath}`);\n\t}\n}\n\nasync function validateComponents(paths: EvoPaths, policy: BundlePolicy): Promise<void> {\n\tconst registry = createDefaultEvoAbiRegistry();\n\tfor (const [surface, selection] of Object.entries(policy.components ?? {})) {\n\t\tawait validateEvoComponentSelection(paths, surface, selection, registry);\n\t}\n}\n\nasync function validateStructuredMemory(directory: string, files: ReadonlySet<string>): Promise<void> {\n\tif (!files.has(PREFERENCES_PATH)) return;\n\tparsePreferenceMemory(JSON.parse(await readFile(join(directory, PREFERENCES_PATH), \"utf8\")));\n}\n\nfunction validateSizes(files: BundleFileEntry[], policy: BundlePolicy): void {\n\tconst promptLimit = Math.min(policy.limits?.promptBytes ?? DEFAULT_PROMPT_BYTES, DEFAULT_PROMPT_BYTES);\n\tconst skillLimit = Math.min(policy.limits?.skillBytes ?? DEFAULT_SKILL_BYTES, DEFAULT_SKILL_BYTES);\n\tconst totalLimit = Math.min(policy.limits?.totalBytes ?? DEFAULT_TOTAL_BYTES, DEFAULT_TOTAL_BYTES);\n\tconst promptBytes = files\n\t\t.filter((file) => file.path.startsWith(\"prompts/\"))\n\t\t.reduce((total, file) => total + file.bytes, 0);\n\tif (promptBytes > promptLimit) throw new Error(`Prompt bytes ${promptBytes} exceed limit ${promptLimit}`);\n\tfor (const file of files.filter((entry) => entry.path.startsWith(\"skills/\"))) {\n\t\tif (file.bytes > skillLimit) throw new Error(`${file.path} exceeds skill byte limit ${skillLimit}`);\n\t}\n\tconst totalBytes = files.reduce((total, file) => total + file.bytes, 0);\n\tif (totalBytes > totalLimit) throw new Error(`Bundle bytes ${totalBytes} exceed limit ${totalLimit}`);\n}\n\nasync function writeBundleDirectory(\n\tsourceDirectory: string,\n\ttemporaryDirectory: string,\n\tmanifest: BundleManifest,\n): Promise<void> {\n\tawait mkdir(temporaryDirectory, { recursive: false });\n\tfor (const file of manifest.files) {\n\t\tconst target = join(temporaryDirectory, file.path);\n\t\tawait mkdir(dirname(target), { recursive: true });\n\t\tawait writeDurableBundleFile(target, await readFile(join(sourceDirectory, file.path)));\n\t}\n\tconst manifestPath = join(temporaryDirectory, \"bundle.json\");\n\tawait writeDurableBundleFile(manifestPath, `${JSON.stringify(manifest, undefined, \"\\t\")}\\n`);\n}\n\nexport async function loadCompiledBundle(paths: EvoPaths, digest: string): Promise<CompiledBundle> {\n\tif (!isDigest(digest)) throw new Error(`Invalid bundle digest: ${digest}`);\n\tconst directory = join(paths.bundles, digest);\n\tconst manifest = parseBundleManifest(JSON.parse(await readFile(join(directory, \"bundle.json\"), \"utf8\")));\n\tif (sha256(canonicalJson(manifest)) !== digest) throw new Error(`Bundle manifest digest mismatch: ${digest}`);\n\tconst actualPaths = (await listSourceFiles(directory)).filter((path) => path !== \"bundle.json\");\n\tif (\n\t\tactualPaths.join(\"\\n\") !==\n\t\tmanifest.files\n\t\t\t.map((file) => file.path)\n\t\t\t.sort()\n\t\t\t.join(\"\\n\")\n\t) {\n\t\tthrow new Error(`Bundle file set does not match manifest: ${digest}`);\n\t}\n\tfor (const file of manifest.files) {\n\t\tconst content = await readFile(join(directory, file.path));\n\t\tif (content.byteLength !== file.bytes || sha256(content) !== file.sha256) {\n\t\t\tthrow new Error(`Bundle file digest mismatch: ${file.path}`);\n\t\t}\n\t}\n\tconst policy = parseBundlePolicy(JSON.parse(await readFile(join(directory, \"policy.json\"), \"utf8\")));\n\tvalidatePolicyAssetReferences(policy, new Map(manifest.files.map((file) => [file.path, file.sha256])));\n\tawait validateComponents(paths, policy);\n\tawait validateStructuredMemory(directory, new Set(manifest.files.map((file) => file.path)));\n\tvalidateSizes(manifest.files, policy);\n\treturn { digest, directory, manifest, policy };\n}\n\nexport async function compileBundle(options: {\n\tpaths: EvoPaths;\n\tsourceDirectory: string;\n\tparentDigest: string | null;\n\tsummary: string;\n}): Promise<CompiledBundle> {\n\tawait ensureEvoLayout(options.paths);\n\tif (options.parentDigest !== null && !isDigest(options.parentDigest))\n\t\tthrow new Error(\"parentDigest must be a digest\");\n\tif (options.summary.length === 0 || options.summary.length > 240)\n\t\tthrow new Error(\"summary must contain 1-240 characters\");\n\tconst sourceDirectory = resolve(options.sourceDirectory);\n\tconst sourcePaths = await listSourceFiles(sourceDirectory);\n\tfor (const path of sourcePaths) assertAssetPath(path);\n\tconst dataPaths = sourcePaths.filter((path) => path !== \"bundle.json\");\n\tif (!dataPaths.includes(\"policy.json\")) throw new Error(\"Bundle must contain policy.json\");\n\tconst files = sortManifestFiles(\n\t\tawait Promise.all(\n\t\t\tdataPaths.map(async (path) => {\n\t\t\t\tconst content = await readFile(join(sourceDirectory, path));\n\t\t\t\treturn { path, sha256: sha256(content), bytes: content.byteLength };\n\t\t\t}),\n\t\t),\n\t);\n\tconst policy = parseBundlePolicy(JSON.parse(await readFile(join(sourceDirectory, \"policy.json\"), \"utf8\")));\n\tvalidatePolicyAssetReferences(policy, new Map(files.map((file) => [file.path, file.sha256])));\n\tawait validateComponents(options.paths, policy);\n\tawait validateStructuredMemory(sourceDirectory, new Set(files.map((file) => file.path)));\n\tvalidateSizes(files, policy);\n\tconst manifest: BundleManifest = {\n\t\tschemaVersion: 1,\n\t\tparentDigest: options.parentDigest,\n\t\tsummary: options.summary,\n\t\tfiles,\n\t};\n\tconst digest = sha256(canonicalJson(manifest));\n\tconst destination = join(options.paths.bundles, digest);\n\ttry {\n\t\treturn await loadCompiledBundle(options.paths, digest);\n\t} catch (error) {\n\t\tif (typeof error !== \"object\" || error === null || !(\"code\" in error) || error.code !== \"ENOENT\") throw error;\n\t}\n\tconst temporaryDirectory = join(options.paths.bundles, `.tmp-${process.pid}-${randomUUID()}`);\n\ttry {\n\t\tawait writeBundleDirectory(sourceDirectory, temporaryDirectory, manifest);\n\t\tawait syncBundleDirectories(temporaryDirectory);\n\t\ttry {\n\t\t\tawait rename(temporaryDirectory, destination);\n\t\t} catch (error) {\n\t\t\tif (\n\t\t\t\ttypeof error !== \"object\" ||\n\t\t\t\terror === null ||\n\t\t\t\t!(\"code\" in error) ||\n\t\t\t\t(error.code !== \"EEXIST\" && error.code !== \"ENOTEMPTY\")\n\t\t\t) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tawait rm(temporaryDirectory, { recursive: true, force: true });\n\t\t}\n\t\tawait syncFilesystemPath(options.paths.bundles, true);\n\t\treturn await loadCompiledBundle(options.paths, digest);\n\t} catch (error) {\n\t\tawait rm(temporaryDirectory, { recursive: true, force: true }).catch(() => {});\n\t\tthrow error;\n\t}\n}\n\nexport async function materializeBundle(paths: EvoPaths, digest: string, targetDirectory: string): Promise<void> {\n\tconst bundle = await loadCompiledBundle(paths, digest);\n\tawait mkdir(targetDirectory, { recursive: true });\n\tfor (const file of bundle.manifest.files) {\n\t\tconst target = join(targetDirectory, file.path);\n\t\tawait mkdir(dirname(target), { recursive: true });\n\t\tawait writeFile(target, await readFile(join(bundle.directory, file.path)), { mode: 0o600, flag: \"wx\" });\n\t}\n}\n"]}