The bce evidence format — hash-chained, re-derivable, tamper-evident#

bce run --emit turns every conformance run into one immutable EvidenceRecord — a link in an append-only, tamper-evident hash chain. A sequence of runs over a subsystem becomes a ledger a reviewer can verify with node alone (tools/verify-chain.mjs, zero dependencies), and re-derive byte-for-byte from the run's inputs. This document is the format contract.

Machine-readable schema: spec/schemas/evidence-record.schema.json ($id: https://blueprint-conformance.github.io/bce/schemas/evidence-record.schema.json). Producing implementation: src/emit.ts. Committed worked example: evidence/example-chain/ (three records produced by real runs, with recorded verify/tamper transcripts).

An evidence chain starts from the all-zero genesis previous hash. Record 001 commits to report A and produces a hash; record 002 includes that hash; record 003 includes record 002's hash. Editing an earlier record breaks its own hash, while rehashing or removing it breaks the next link. Producer authentication remains a separate signature layer.

1. The record#

{
  "schemaVersion": "1",
  "id": "evidence:<blueprintRef>:<hash[0..16]>",
  "traceId": "<blueprint id — one chain per subsystem>",
  "blueprintRef": "<blueprint id>@<version>",
  "ctRepoRevision": "<the pinned git revision the run graded>",
  "score": 0,
  "verdict": "pass | fail",
  "violationCount": 0,
  "reportEvidenceRef": "architecture-graph.json@sha256:<graph hash>",
  "toolchain": {
    "engine": { "name": "bce-engine", "version": "<exact version>" },
    "dependencyLock": { "file": "npm-shrinkwrap.json", "sha256": "<64 hex>" },
    "runtime": { "node": "<version>", "npm": "<version>", "platform": "<os>", "arch": "<architecture>" },
    "extractor": { "kind": "ast", "profile": "plugin-surface", "provider": "typescript-ts-morph", "version": "<engine version>" }
  },
  "previousHash": "<sha256 of the PREVIOUS record, or the genesis sentinel>",
  "hash": "<sha256 of THIS record's canonical body>"
}

2. Content hash: derived-field stripping#

hash is computed over the record's body — the record with its two derived fields stripped:

Everything else, including previousHash, is hashed:

hash = sha256( stableStringify({ schemaVersion, traceId, blueprintRef,
                                 ctRepoRevision, score, verdict, violationCount,
                                 reportEvidenceRef, toolchain, previousHash }) )

Because previousHash is inside the hashed body, each link commits to its whole ancestry — editing ANY field of ANY earlier record invalidates every later hash. The field set is closed: a record with extra fields does not verify (the verifier fail-closes on unknown keys), so nothing volatile can ride along outside the hash.

There is deliberately no timestamp and no hostname/user field anywhere in the record. Current records do include the reproducibility-relevant toolchain environment, so record hashes can differ across platforms even when the underlying report hash is identical. Wall-clock and operator identity, where needed, belong to signed transport/provenance rather than being guessed from a local record.

3. Stable serialization#

The canonical bytes are produced by stableStringify (src/report.ts, mirrored verbatim in tools/verify-chain.mjs):

  1. object keys sorted lexicographically, recursively (arrays keep their order);
  2. JSON.stringify(…, null, 2) — 2-space indent;
  3. one trailing newline;
  4. serialization of a cyclic value is a hard error.

Two records with equal contents therefore have byte-identical canonical forms, independent of key insertion order — which is what makes the hash meaningful. For a fixed report, previous hash, and toolchain identity, the evidence record is byte-identical.

4. Re-derivation contract#

Evidence is only evidence if an outsider can re-derive it. Two levels:

Level 1 — verify (node alone, no build, no install):

node tools/verify-chain.mjs evidence/example-chain    # a directory of records
node tools/verify-chain.mjs some-record.json          # a single genesis record

The verifier independently re-implements §2 + §3 (it imports nothing from src/ or dist/) and checks, per record: shape (closed field set), hash re-derivation, content-derived id, and the previousHash link — starting from the genesis sentinel. Exit 0 = chain intact; exit 1 = first broken record named with the reason; exit 2 = usage error. Directory mode reads every *.json in lexicographic filename order — the filename order IS the claimed chain order.

Level 2 — reproduce (with the engine): the record is a pure function of (ComplianceReport, previousHash), and the report is a pure function of (blueprint, repo tree @ ctRepoRevision). Re-running

bce run --blueprint <blueprint> --ct-repo <repo> --ref <ctRepoRevision> \
  --emit --prev-hash <previousHash> --emit-evidence-out rederived.json

against the same revision and declared toolchain yields a byte-identical record. The report remains content-deterministic across supported environments; the outer record intentionally identifies the environment rather than hiding it. No wall-clock or randomness enters the pipeline (tests/determinism-proof.test.ts and tests/emit.test.ts lock this).

Release producer identity#

The hash chain above proves integrity and ordering; by itself it does not prove who produced the record. Release workflows additionally attach release-evidence-record.sigstore, a keyless Sigstore bundle created with GitHub Actions OIDC after the release gate passes. Verify both layers:

node tools/verify-chain.mjs release-evidence-record.json
sigstore verify release-evidence-record.sigstore \
  --certificate-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-uri \
  https://github.com/blueprint-conformance/bce/.github/workflows/release.yml@refs/tags/vX.Y.Z

Use the exact release tag in the identity URI. The issuer and workflow constraints are mandatory: verification without them establishes that some accepted Sigstore identity signed the attestation, not that this repository's release workflow did. Local evidence remains unsigned by default and must not be described as authenticated producer provenance.

5. Redaction: whole-record exclusion, never field editing#

A published chain may need to withhold sensitive runs. The ONLY sanctioned redaction is exclusion of whole records — never editing fields inside a record. An edited record no longer hash-verifies (§2), which would silently destroy the very property the chain exists to provide; a "redacted" field is indistinguishable from tampering, and the verifier treats it as such.

Consequences of the link structure (§2):

Records are shipped byte-intact or not at all.

6. Evidence posture at v1 (explicit)#

The v1 posture is on-demand evidence, verified on demand:

This is deliberate, not an omission. A committed chain is a historical artifact bound to the revisions it graded; automating appends from CI would create merge-racing writers over an append-only artifact and manufacture volume without adding trust. What v1 ships instead: the emit path (bce run --emit --prev-hash), the zero-dependency verifier, a real committed example chain, and this contract — enough for any consumer to build their own evidence pipeline on top, at their own cadence, in their own storage. Revisiting this posture (e.g. a gate-integrated emit) is future work and would be introduced as an opt-in, never a silent default.

7. Worked example#

See evidence/example-chain/: three records produced by real bce run --emit invocations of this engine over its own self-blueprint, chained genesis → 001 → 002 → 003, with recorded transcripts of the intact verify (exit 0), a field-tamper refusal (exit 1), and a record-excision refusal (exit 1). tests/verify-chain-tool.test.ts locks all three outcomes in CI.

Read Markdown · Source: docs/evidence-format.md