/*
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);
}