import { createHash } from "node:crypto"; import { posix } from "node:path"; import canonicalize from "canonicalize"; import { z } from "zod"; import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u); const boundedTextSchema = z.string().min(1); const relativePathSchema = z .string() .min(1) .refine( (path) => !path.startsWith("/") && !path.includes("\\") && path .split("/") .every((part) => part !== "" && part !== "." && part !== ".."), "Expected a normalized relative POSIX path without traversal", ); const classificationSchema = z.enum([ "source", "test", "config", "manifest", "generated", "vendor", "documentation", "unknown", ]); const entryBaseShape = { path: relativePathSchema, classifications: z.array(classificationSchema).min(1), limitations: z.array(boundedTextSchema), }; const sourceFileBaseShape = { ...entryBaseShape, kind: z.literal("file"), language: z.string().min(1).nullable(), }; const sourceFileSchema = z.union([ z.strictObject({ ...sourceFileBaseShape, sha256: digestSchema, size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), content_state: z.literal("hashed"), }), z.strictObject({ ...sourceFileBaseShape, sha256: z.null(), size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).nullable(), content_state: z.enum([ "redacted-secret", "excluded", "unreadable", "unknown", ]), }), ]); const sourceDirectorySchema = z.strictObject({ ...entryBaseShape, kind: z.literal("directory"), tree_state: z.enum([ "enumerated", "partial", "excluded", "unreadable", "unknown", ]), }); const symlinkTargetSchema = z .string() .min(1) .refine( (target) => target === "" || (!target.startsWith("/") && !target.includes("\\") && posix.normalize(target) === target), "Expected a normalized POSIX symlink target", ); const sourceSymlinkSchema = z.strictObject({ ...entryBaseShape, kind: z.literal("symlink"), target: symlinkTargetSchema, target_state: z.enum([ "internal", "external", "missing", "unreadable", "unknown", ]), }); const sourceEntrySchema = z.union([ sourceFileSchema, sourceDirectorySchema, sourceSymlinkSchema, ]); const sourceRelationshipSchema = z.strictObject({ from_path: relativePathSchema, to: z.string().min(1), kind: z.enum(["imports", "requires", "references", "declares-module"]), resolution: z.enum(["internal", "external", "unresolved", "unknown"]), parse_state: z.enum(["parsed", "partial", "unknown"]), }); const parseFailureSchema = z.strictObject({ path: relativePathSchema, parser: z.string().min(1), reason: boundedTextSchema, }); const exclusionSchema = z.strictObject({ path: relativePathSchema, reason: z.enum([ "configured-secret", "symlink-escape", "size-limit", "inventory-limit", "unreadable", "caller-excluded", ]), }); const vcsSchema = z .discriminatedUnion("kind", [ z.strictObject({ kind: z.literal("git"), head: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u), dirty: z.boolean().nullable(), }), z.strictObject({ kind: z.literal("none"), head: z.null(), dirty: z.null(), }), z.strictObject({ kind: z.literal("unknown"), head: z.null(), dirty: z.null(), }), ]) .nullable(); const graphShape = { schema: z.literal("HistoricalSourceGraph"), authority: z.literal("historical-reference"), root_alias: z.literal("$REFERENCE_ROOT"), root_sha256: digestSchema, inventory_state: z.enum(["complete", "partial", "unknown"]), entries: z.array(sourceEntrySchema), relationships: z.array(sourceRelationshipSchema), parse_failures: z.array(parseFailureSchema), exclusions: z.array(exclusionSchema), languages: z.array(z.string().min(1)), manifests: z.array(relativePathSchema), vcs: vcsSchema, provenance: z.strictObject({ importer: z.string().min(1), importer_version: z.string().min(1).nullable(), caller: z .string() .min(1) .regex(/^[\w .:@/+-]+$/u), }), limitations: z.array(boundedTextSchema), }; const historicalSourceGraphStructureSchema = z .strictObject(graphShape) .superRefine(checkGraphInvariants); export const historicalSourceGraphSchema = historicalSourceGraphStructureSchema.superRefine((graph, context) => { if ( computeRootSha256(graph.entries, graph.exclusions) !== graph.root_sha256 ) context.addIssue({ code: "custom", message: "Historical source root commitment does not match entries", path: ["root_sha256"], }); }); const historicalSourceGraphInputSchema = z.strictObject({ ...graphShape, root_sha256: z.never().optional(), }); const historicalSourceManifestBaseSchema = z.strictObject({ schema: z.literal("HistoricalSourceManifest"), authority: z.literal("historical-reference"), manifest_id: z.string().regex(/^hsm_[a-f0-9]{64}$/u), graph_sha256: digestSchema, root_sha256: digestSchema, inventory_state: z.enum(["complete", "partial", "unknown"]), entry_count: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), relationship_count: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), }); export type HistoricalSourceGraph = z.infer; export type HistoricalSourceGraphInput = z.infer< typeof historicalSourceGraphInputSchema >; export type HistoricalSourceManifest = z.infer< typeof historicalSourceManifestBaseSchema >; /** Stable identity used to sort and deduplicate source parse failures. */ export const historicalSourceParseFailureKey = ( failure: HistoricalSourceGraphInput["parse_failures"][number], ): string => `${failure.path}\u0000${failure.parser}\u0000${failure.reason}`; const canonicalJson = (value: unknown): string => { const serialized = canonicalize(value); if (serialized === undefined) throw new TypeError("RFC 8785 canonicalization rejected a graph value"); return serialized; }; const digestCanonical = (value: unknown): string => createHash("sha256").update(canonicalJson(value)).digest("hex"); const relationshipKey = ( relationship: z.infer, ): string => `${relationship.from_path}\u0000${relationship.to}\u0000${relationship.kind}\u0000${relationship.resolution}\u0000${relationship.parse_state}`; const checkSortedUnique = ( values: readonly string[], field: string, context: z.RefinementCtx, ): void => { for (let index = 1; index < values.length; index += 1) if ( compareUnicodeCodePoints(values[index - 1] ?? "", values[index] ?? "") >= 0 ) { context.addIssue({ code: "custom", message: `${field} must be sorted and unique by Unicode code point`, path: [field, index], }); return; } }; /** Derive the canonical language index from inventoried source files. */ export const historicalSourceLanguages = ( entries: readonly z.infer[], ): string[] => [ ...new Set( entries.flatMap((entry) => entry.kind === "file" && entry.language !== null ? [entry.language] : [], ), ), ].sort(compareUnicodeCodePoints); /** Derive the canonical manifest path index from inventoried source files. */ export const historicalSourceManifests = ( entries: readonly z.infer[], ): string[] => entries .flatMap((entry) => entry.kind === "file" && entry.classifications.includes("manifest") ? [entry.path] : [], ) .sort(compareUnicodeCodePoints); type GraphStructure = z.infer; const checkRelationships = ( graph: GraphStructure, paths: ReadonlySet, context: z.RefinementCtx, ): void => { const files = new Set( graph.entries.flatMap((entry) => entry.kind === "file" ? [entry.path] : [], ), ); for (const [index, relationship] of graph.relationships.entries()) { if (!files.has(relationship.from_path)) context.addIssue({ code: "custom", message: "Relationship source must name an inventoried file", path: ["relationships", index, "from_path"], }); if (relationship.resolution !== "internal") continue; const parsed = relativePathSchema.safeParse(relationship.to); if (!parsed.success || !paths.has(parsed.data)) context.addIssue({ code: "custom", message: "Internal relationship target must be normalized and inventoried", path: ["relationships", index, "to"], }); } }; const checkSymlinks = ( graph: GraphStructure, paths: ReadonlySet, context: z.RefinementCtx, ): void => { for (const [index, entry] of graph.entries.entries()) { if (entry.kind !== "symlink") continue; if ( (entry.target_state === "external") !== (entry.target === "") ) context.addIssue({ code: "custom", message: "External symlink targets must use the sanitized sentinel", path: ["entries", index, "target"], }); if (entry.target_state !== "internal") continue; const resolved = posix.normalize( posix.join(posix.dirname(entry.path), entry.target), ); if (resolved.startsWith("../") || resolved === ".." || !paths.has(resolved)) context.addIssue({ code: "custom", message: "Internal symlink target must resolve to an inventoried entry", path: ["entries", index, "target"], }); } }; const checkCompleteInventory = ( graph: GraphStructure, context: z.RefinementCtx, ): void => { if (graph.inventory_state !== "complete") return; const partialEntry = graph.entries.some( (entry) => entry.limitations.length > 0 || (entry.kind === "file" && entry.content_state !== "hashed") || (entry.kind === "directory" && entry.tree_state !== "enumerated") || (entry.kind === "symlink" && entry.target_state !== "internal"), ); const partialRelationship = graph.relationships.some( ({ parse_state, resolution }) => parse_state !== "parsed" || ["unresolved", "unknown"].includes(resolution), ); if ( !partialEntry && !partialRelationship && graph.parse_failures.length === 0 && graph.exclusions.length === 0 && graph.limitations.length === 0 ) return; context.addIssue({ code: "custom", message: "Complete inventory cannot contain partial states, exclusions, failures, or limitations", path: ["inventory_state"], }); }; function checkGraphInvariants( graph: GraphStructure, context: z.RefinementCtx, ): void { checkSortedUnique( graph.entries.map(({ path }) => path), "entries", context, ); for (const [index, entry] of graph.entries.entries()) checkSortedUnique( entry.classifications, `entries.${index}.classifications`, context, ); checkSortedUnique(graph.languages, "languages", context); checkSortedUnique(graph.manifests, "manifests", context); checkSortedUnique( graph.relationships.map(relationshipKey), "relationships", context, ); checkSortedUnique( graph.parse_failures.map(historicalSourceParseFailureKey), "parse_failures", context, ); checkSortedUnique( graph.exclusions.map(({ path, reason }) => `${path}\u0000${reason}`), "exclusions", context, ); if ( canonicalJson(graph.languages) !== canonicalJson(historicalSourceLanguages(graph.entries)) ) context.addIssue({ code: "custom", message: "languages must be derived from file entries", path: ["languages"], }); if ( canonicalJson(graph.manifests) !== canonicalJson(historicalSourceManifests(graph.entries)) ) context.addIssue({ code: "custom", message: "manifests must be derived from classified file entries", path: ["manifests"], }); const paths = new Set(graph.entries.map(({ path }) => path)); checkRelationships(graph, paths, context); checkSymlinks(graph, paths, context); checkCompleteInventory(graph, context); } const rootCommitment = ( entries: readonly z.infer[], exclusions: readonly z.infer[], ): unknown => ({ entries: entries.map((entry) => { switch (entry.kind) { case "file": return { kind: entry.kind, path: entry.path, sha256: entry.sha256, size: entry.size, content_state: entry.content_state, }; case "directory": return { kind: entry.kind, path: entry.path, tree_state: entry.tree_state, }; case "symlink": return { kind: entry.kind, path: entry.path, target: entry.target, target_state: entry.target_state, }; } }), exclusions, }); const computeRootSha256 = ( entries: readonly z.infer[], exclusions: readonly z.infer[], ): string => digestCanonical(rootCommitment(entries, exclusions)); /** Build and internally commit a normalized historical source graph. */ export const createHistoricalSourceGraph = ( input: unknown, ): HistoricalSourceGraph => { const parsed = historicalSourceGraphInputSchema.parse(input); return historicalSourceGraphSchema.parse({ ...parsed, root_sha256: computeRootSha256(parsed.entries, parsed.exclusions), }); }; /** Parse a graph and reject stale or caller-invented root commitments. */ export const parseHistoricalSourceGraph = ( input: unknown, ): HistoricalSourceGraph => historicalSourceGraphSchema.parse(input); /** Compute a deterministic graph commitment containing no absolute root path. */ export const computeHistoricalSourceGraphSha256 = (input: unknown): string => digestCanonical(parseHistoricalSourceGraph(input)); /** Build a deterministic, relocation-independent manifest for a source graph. */ export const createHistoricalSourceManifest = ( input: unknown, ): HistoricalSourceManifest => { const graph = parseHistoricalSourceGraph(input); const semantic = { schema: "HistoricalSourceManifest" as const, authority: "historical-reference" as const, graph_sha256: computeHistoricalSourceGraphSha256(graph), root_sha256: graph.root_sha256, inventory_state: graph.inventory_state, entry_count: graph.entries.length, relationship_count: graph.relationships.length, }; return historicalSourceManifestBaseSchema.parse({ ...semantic, manifest_id: `hsm_${digestCanonical(semantic)}`, }); }; /** Parse and verify a historical source manifest's semantic identifier. */ export const parseHistoricalSourceManifest = ( input: unknown, ): HistoricalSourceManifest => { const manifest = historicalSourceManifestBaseSchema.parse(input); const { manifest_id: manifestId, ...semantic } = manifest; if (`hsm_${digestCanonical(semantic)}` !== manifestId) throw new TypeError("Historical source manifest identifier does not match"); return manifest; };