/*
RFC-0364, RFC-0849: Lightweight hashing primitives — byte hashing, stable JSON hashing, and Sha256Digest opaque type. No parser dependencies.
- Do not implement file-type normalizers — those live in normalizers/.
- Do not implement tree fingerprinting — that lives in fingerprint.ts.
- Do not implement canonical JSON — that lives in canonical-json.ts.
- RFC-0849: add Sha256Digest branded type, isSha256Digest guard, and narrower byteHash/byteHashFile returns. Existing string consumers remain compatible.
- 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.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
*/
import { createHash } from "node:crypto";
import { createDefaultIO, type WorkspaceIO } from "../kernel/workspace-io.ts";
declare const sha256DigestBrand: unique symbol;
export type Sha256Digest = `sha256:${string}` & {
readonly [sha256DigestBrand]: true;
};
const SHA256_DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
export function isSha256Digest(value: unknown): value is Sha256Digest {
return typeof value === "string" && SHA256_DIGEST_RE.test(value);
}
export function byteHash(bytes: Uint8Array | string): Sha256Digest {
const input = typeof bytes === "string" ? Buffer.from(bytes, "utf8") : bytes;
return `sha256:${createHash("sha256").update(input).digest("hex")}` as Sha256Digest;
}
export async function byteHashFile(
absPath: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const bytes = await io.readFileBytes(absPath);
return byteHash(bytes);
}
function sortForJson(value: unknown): unknown {
if (Array.isArray(value)) return value.map(sortForJson);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value as Record)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, nested]) => [key, sortForJson(nested)]),
);
}
return value;
}
export function stableStringify(value: unknown): string {
return JSON.stringify(sortForJson(value));
}
export function stableJsonHash(value: unknown): string {
return byteHash(stableStringify(value));
}