/* RFC-0364: Semantic fingerprint functions — file and tree fingerprinting with parser-backed normalizers. Do not implement byte-level primitives — those live in primitives.ts. Do not implement file-type normalizers — those live in normalizers/. Do not define types — those live in types.ts. RFC-0380: fingerprintTree combined input uses paths relative to tree root, making hashes portable across machines. RFC-0656: add mode: "stable" — byte hashing with targeted normalization for PDF, source map, and JSON timestamp fields. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import path from "node:path"; import { byteHash } from "@warpgogol/werkstatt-shared/fingerprint"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; import type { FingerprintFileResult, FingerprintOptions, FingerprintResult } from "./types.ts"; import { normalizeFile } from "./normalizers/index.ts"; import { normalizeFileStable } from "./normalizers/stable.ts"; import { normalizePathSep, shouldIgnore } from "./path-matcher.ts"; export async function fingerprintFile( filePath: string, options: FingerprintOptions, io: WorkspaceIO = createDefaultIO().io, ): Promise { const absPath = path.resolve(filePath); const bytes = await io.readFileBytes(absPath); if (options.mode === "byte") { return { path: normalizePathSep(absPath), mode: "byte", normalizer: "binary", hash: byteHash(bytes), }; } if (options.mode === "stable") { const distRoot = options.root ? path.resolve(options.root) : undefined; const stableResult = await normalizeFileStable(absPath, bytes, distRoot); if (stableResult) { return { path: normalizePathSep(absPath), mode: "stable", normalizer: stableResult.normalizer, hash: stableResult.hash, }; } return { path: normalizePathSep(absPath), mode: "stable", normalizer: "binary", hash: byteHash(bytes), }; } const result = await normalizeFile(absPath, bytes); return { path: normalizePathSep(absPath), mode: "semantic", normalizer: result.normalizer, hash: result.hash, }; } async function walkTree( root: string, ignore: string[], acc: string[], io: WorkspaceIO, ): Promise { const entries = await io.readdir(root); for (const entry of entries) { const abs = path.join(root, entry.name); const rel = path.relative(process.cwd(), abs); if (shouldIgnore(rel, ignore)) continue; if (entry.isDirectory) { await walkTree(abs, ignore, acc, io); } else if (entry.isFile) { acc.push(abs); } } } export async function fingerprintTree( root: string, options: FingerprintOptions, io: WorkspaceIO = createDefaultIO().io, ): Promise { const absRoot = path.resolve(root); const ignore = options.ignore ?? []; const files: string[] = []; await walkTree(absRoot, ignore, files, io); files.sort(); const results: FingerprintFileResult[] = []; const warnings: string[] = []; for (const file of files) { try { const result = await fingerprintFile(file, options, io); results.push(result); } catch (err) { const relPath = normalizePathSep(path.relative(absRoot, file)); const message = (err as Error).message?.slice(0, 200) ?? String(err); const warning = `fingerprintTree: ${options.mode} normalization failed for ${relPath} (${message}); falling back to byte hash`; console.warn(warning); warnings.push(warning); const bytes = await io.readFileBytes(file); results.push({ path: normalizePathSep(file), mode: "byte", normalizer: "binary", hash: byteHash(bytes), }); } } const combinedInput = results .map((r) => { const rel = normalizePathSep(path.relative(absRoot, r.path)); return `${rel}\n${r.hash}`; }) .join("\n"); return { algorithm: "sha256", mode: options.mode, value: byteHash(combinedInput), files: results, warnings: warnings.length > 0 ? warnings : undefined, }; }