/* RFC-0215: derived-content staleness. A translation/copy is a `derived` claim that remembers which source it came from (derivedFrom) and a normalized hash of that source at derivation time (sourceHash). When the source changes, the stored hash no longer matches → the derivative is `outdated`. Framework-free hashing + compare, shared with the enriched-content frozen-provenance model (RFC-0197/0207). Do not read the filesystem or parse sidecars — the kernel command does that. Do not translate or regenerate content — only detect staleness. RFC-0215: initial normalized hasher + state compare. */ import { createHash } from "node:crypto"; export type DerivedState = "current" | "outdated" | "source-missing"; /** * Normalize a source value so cosmetic edits (whitespace reflow, markdown emphasis) * do not falsely mark a derivative outdated, while wording/number changes do. * Whitespace is collapsed; common markdown punctuation is stripped; result trimmed. */ export function normalizeForHash(value: string): string { return value .replace(/[*_`~#>]/g, "") .replace(/[[\]()]/g, "") .replace(/\s+/g, " ") .trim(); } /** Hash a value (after normalization) into a stable "sha256:" string. */ export function hashSourceValue(value: string): string { const hex = createHash("sha256").update(normalizeForHash(value), "utf8").digest("hex"); return `sha256:${hex}`; } /** * Compare a derivative's stored sourceHash against a freshly computed hash of the * current source value. A missing stored hash counts as `outdated` (never stamped). */ export function derivedState( storedHash: string | undefined, currentSourceValue: string | undefined, ): { state: DerivedState; currentHash?: string } { if (currentSourceValue === undefined) return { state: "source-missing" }; const currentHash = hashSourceValue(currentSourceValue); if (!storedHash || storedHash !== currentHash) return { state: "outdated", currentHash }; return { state: "current", currentHash }; }