/* component identity — compute manifest hashes for stable component identity. Do not declare components — this module only hashes manifests. 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 { snapshotCanonicalJsonObjectV1, canonicalJsonHashV1, } from "@warpgogol/werkstatt-shared/fingerprint"; import { type Sha256Digest } from "@warpgogol/werkstatt-shared/fingerprint"; import type { ComponentDeclaration, IsolationTier, ResolvedComponentIdentityV1, ResolvedComponentSetV1, } from "@warpgogol/werkstatt-shared/component"; function sortByIdentity(items: T[], keyFn: (item: T) => string): T[] { return [...items].sort((a, b) => { const ka = keyFn(a); const kb = keyFn(b); return ka < kb ? -1 : ka > kb ? 1 : 0; }); } function manifestIdentityPayload(manifest: ComponentDeclaration): unknown { return { schema: manifest.schema, componentId: manifest.componentId, version: manifest.version, artifactHash: manifest.artifactHash, provides: sortByIdentity(manifest.provides, (p) => `${p.capability}@${p.version}`), requires: sortByIdentity(manifest.requires, (r) => `${r.capability}@${r.compatibility}`), requestedGrants: sortByIdentity(manifest.requestedGrants, (g) => `${g.scope}:${g.resource}`), effects: sortByIdentity(manifest.effects, (e) => e.effectClass), isolation: { tier: manifest.isolation.tier, adapterId: manifest.isolation.adapterId, }, resources: sortByIdentity(manifest.resources, (r) => `${r.kind}:${r.owner}:${r.lifecycle}`), }; } export function computeManifestHash(manifest: ComponentDeclaration): Sha256Digest { const result = snapshotCanonicalJsonObjectV1(manifestIdentityPayload(manifest)); if (!result.ok) { throw new Error(`CERT-CANONICAL-${result.code}: failed to snapshot manifest identity payload`); } return canonicalJsonHashV1(result.value); } function resolvedComponentIdentityPayload(c: ResolvedComponentIdentityV1): unknown { return { componentId: c.componentId, version: c.version, artifactHash: c.artifactHash, }; } function resolvedSetIdentityPayload(set: Omit): unknown { const sortedComponents = sortByIdentity(set.components, (c) => `${c.componentId}@${c.version}`); return { schema: set.schema, profileId: set.profileId, components: sortedComponents.map(resolvedComponentIdentityPayload), dependencyGraphHash: set.dependencyGraphHash, grantSetHash: set.grantSetHash, effectPolicyHash: set.effectPolicyHash, isolationPolicyHash: set.isolationPolicyHash, }; } export function computeSetHash(set: Omit): Sha256Digest { const result = snapshotCanonicalJsonObjectV1(resolvedSetIdentityPayload(set)); if (!result.ok) { throw new Error( `CERT-CANONICAL-${result.code}: failed to snapshot resolved set identity payload`, ); } return canonicalJsonHashV1(result.value); } export function verifySetHash(set: ResolvedComponentSetV1): boolean { const { setHash: declared, ...rest } = set; const recomputed = computeSetHash(rest); return recomputed === declared; } export interface SetHashMismatchViolation { rule: string; path: string; message: string; expected: string; actual: string; } export function verifySetHashStrict(set: ResolvedComponentSetV1): { valid: boolean; violation: SetHashMismatchViolation | null; } { const { setHash: declared, ...rest } = set; const recomputed = computeSetHash(rest); if (recomputed === declared) { return { valid: true, violation: null }; } return { valid: false, violation: { rule: "COMPONENT-CONTRACT-07", path: "setHash", message: "recomputed setHash does not match declared setHash", expected: recomputed, actual: declared, }, }; } export function computeDependencyGraphHash( edges: Array<{ from: string; to: string }>, ): Sha256Digest { const sorted = sortByIdentity(edges, (e) => `${e.from}->${e.to}`); const payload = { edges: sorted.map((e) => ({ from: e.from, to: e.to })) }; const result = snapshotCanonicalJsonObjectV1(payload); if (!result.ok) { throw new Error(`CERT-CANONICAL-${result.code}: failed to snapshot dependency graph`); } return canonicalJsonHashV1(result.value); } export function computeGrantSetHash( grants: Array<{ componentId: string; scope: string; resource: string; attenuated: boolean }>, ): Sha256Digest { const sorted = sortByIdentity(grants, (g) => `${g.componentId}:${g.scope}:${g.resource}`); const payload = { grants: sorted }; const result = snapshotCanonicalJsonObjectV1(payload); if (!result.ok) { throw new Error(`CERT-CANONICAL-${result.code}: failed to snapshot grant set`); } return canonicalJsonHashV1(result.value); } export function computeEffectPolicyHash( effects: Array<{ componentId: string; effectClass: string }>, ): Sha256Digest { const sorted = sortByIdentity(effects, (e) => `${e.componentId}:${e.effectClass}`); const payload = { effects: sorted }; const result = snapshotCanonicalJsonObjectV1(payload); if (!result.ok) { throw new Error(`CERT-CANONICAL-${result.code}: failed to snapshot effect policy`); } return canonicalJsonHashV1(result.value); } export function computeIsolationPolicyHash( isolations: Array<{ componentId: string; tier: IsolationTier; adapterId: string | null }>, ): Sha256Digest { const sorted = sortByIdentity(isolations, (i) => `${i.componentId}:${i.tier}`); const payload = { isolations: sorted }; const result = snapshotCanonicalJsonObjectV1(payload); if (!result.ok) { throw new Error(`CERT-CANONICAL-${result.code}: failed to snapshot isolation policy`); } return canonicalJsonHashV1(result.value); }