Compliance core
generateEvidencePack won't produce a pack while any control's evidence is unresolved, what it does hand you is a byte-stable, SHA-256-verifiable ZIP.
What it is
compliance-core is Caisson's evidence engine: generateEvidencePack composes typed EvidenceCollector results into a deterministic, byte-stable evidence pack, refusing to assemble anything while a control's evidence stays unresolved (flag-never-guess). computeCrosswalkRollup joins every framework pack's crosswalk pointers into one cross-framework view. It depends on and re-exports oscal-spine so existing OSCAL imports keep resolving while the dedicated package owns the formats.
What ships in the module
Flag-never-guess pack generation
generateEvidencePack scans every control for an unresolved collector result before assembling anything; if any exist it throws EvidencePackBlockedError (HTTP 422) carrying the full BLOCKED-case report. The throw runs before any assembly and the module touches no filesystem, so a partial pack is structurally impossible, not just policy.
Browser-safe entry point
Import @caisson/compliance-core/browser inside a client bundle for the collector contract with its result constructors, the four pure collectors (FORCE-RLS, WORM retention, risk register, impersonation dual trail), the pack format, the crosswalk rollup, and assembleEvidenceManifest, the same flag-never-guess refusal and derived-readiness assembly generateEvidencePack composes. The archive and digest phase, the chain-verify collector, and the field-crypto collector stay on the main entry: each needs Node. Every browser-entry export is also on the main entry.
Deterministic, byte-stable archive
buildDeterministicZip fixes every entry to the 1980-epoch DOS mtime, name-sorts entries, and pins the deflate level over canonicalize()'d contents, so identical evidence always serializes to the identical SHA-256 on EvidencePack.sha256, regardless of when or by whom it was generated. The injected now clock is stamped only on the generatedAt envelope field, never hashed into the body.
Cross-framework evidence rollup
computeCrosswalkRollup joins every framework pack's crosswalk[] pointers into one flat cell list. A cell renders claim: "implements" only when every contributing control is ready, every contributing verification is reviewed/expert-reviewed and non-stale, and a matching regime-crosswalk row already claims implements, anything short of that defaults to maps-to, mechanically, never editorially.
Pluggable EvidenceCollector contract, mandatory reasons
EvidenceCollector.collect(fact) is pure, no I/O, no clock, no DB. passResult ships a satisfied check; flaggedResult and unresolvedResult both throw ValidationError on an empty reason, so a recorded deficiency can never reach a pack without a stated cause.
Detached external-anchor grade tagging
buildExternalAnchorEntry attaches the newest anchor receipt as its own archive entry plus a trusted-timestamped or externally-transparent grade tag on the result envelope, never a field in the canonical manifest.json (the receipt is non-deterministic; hashing it would break byte-stability). anchorGradePhrase keeps a private RFC-3161 receipt from ever claiming the public-transparency language reserved for the externally-transparent grade.
Source-compatible OSCAL boundary
compliance-core depends on and re-exports @caisson/oscal-spine. Existing assessment-plan, assessment-results, POA&M, catalog, XML, and ISO 27001 SoA imports keep resolving through this package, while one dedicated package owns their implementation and conformance fixtures.
export function assembleEvidenceManifest(
input: AssembleEvidenceManifestInput,
): EvidencePackManifest {
// PHASE 1 — flag-never-guess. Scan EVERY control for unresolved evidence before assembling
// anything; refuse the whole pack if any is found. No filesystem touch here → no partial pack.
const unresolved: Array<{
controlId: string;
collectorId: string;
reason: string | undefined;
}> = [];
for (const control of input.controls) {
for (const result of control.evidence) {
if (result.status === "unresolved") {
unresolved.push({
controlId: control.controlId,
collectorId: result.item.collectorId,
reason: result.reason,
});
}
}
}
if (unresolved.length > 0) {
const sortedUnresolved = [...unresolved].sort(
(a, b) =>
cmp(a.controlId, b.controlId) || cmp(a.collectorId, b.collectorId),
);
const report = parseEvidencePackBlocked({
formatVersion: EVIDENCE_PACK_FORMAT_VERSION,
tenantId: input.tenantId,
framework: input.framework,
blocked: true,
unresolved: sortedUnresolved,
});
throw new EvidencePackBlockedError(report);
}- The scan over input.controls runs BEFORE any assembly starts, every control is checked for an unresolved result first, so a partial pack is never even started.
- EvidencePackBlockedError carries the full sorted report (every unresolved controlId + collectorId), not just a boolean, the caller sees exactly what's missing.
- sortedUnresolved is deterministically ordered by cmp(), the same set of gaps always reports in the same order, run to run.