Evidence History & SHA3-256 Chain v2 Offline Verification Guide
===============================================================

Collection ID: demo-collection-006
Organization ID: aa98488a-56df-457a-bb2c-e8c661543c04
Primary Stream Hash Algorithm: SHA3-256 (v2 JSONL stream)
Legacy Ledger Hash Algorithm: SHA-256 (v1 ledger.json backward compatibility)
Verification Status: INTACT
Stream Head Digest: 9496e0defd46052a2285bfa9cdb68336eedb18f8ee6e622e8953caa690e866e9
Total Chained Stream Records: 721
  state_capture (Proof of State): 721
  change_record (Proof of Change): 0
  agency_record (Proof of Agency): 0
  declared_gap: 0
Declared Gaps in Window: 0
Executed changes not chained (no observed before and after state): 0
Autonomous decisions not chained (no recorded envelope evaluation): 0
  Both lists, with source table, row id and reason, are in manifest.json under evidenceChainV2.exclusions.
  A change record is written only when the before AND after state were observed; no hash is invented for a
  state that was not read. An agency record is written only when the decision recorded the envelope version
  digest and counterfactual digest it was evaluated against; today's envelope is never used for a past decision.
  In an agency record, beforeStateHash, afterStateHash and rollbackId are null when that state was not observed
  or no rollback id was recorded.

Files in Evidence_History/:
  - Evidence_History/ledger.jsonl       : Line-delimited JSON stream of SHA3-256 chained records (state_capture, change_record, declared_gap, agency_record)
  - Evidence_History/declared_gaps.json : Explicit declared_gap records for collector outages, failed runs, collection silence intervals, or change and agency sources that could not be read
  - Evidence_History/ledger.json        : Structured evidence history ledger with rootHash, signature metadata, and sha3Chain summary
  - Evidence_History/verify_ledger.py   : Offline verifier for ledger.jsonl (Python 3 standard library only)
  - Evidence_History/README.txt         : This offline verification guide

How to Verify Evidence_History/ledger.jsonl Offline:
Run the shipped verifier (Python 3.6 or newer, standard library only, no network):
  python3 Evidence_History/verify_ledger.py Evidence_History/ledger.jsonl --organization-id <organizationId>
  It checks every record type (state_capture, change_record, declared_gap, agency_record), the genesis link,
  every previousDigest link and every SHA3-256 digest, and compares the head digest and record counts with
  Evidence_History/ledger.json and with its signedPayload. Add --key-file <file holding the signing key> to
  check the HMAC-SHA256 signature as well. Exit code 0 means INTACT; 1 names the first broken sequence or mismatch.
Or by hand:
  1. Read Evidence_History/ledger.jsonl line by line in sequence order (0 .. N-1).
  2. Verify line 0 has previousDigest === null and each subsequent line i has previousDigest === line[i-1].digest.
  3. Recompute each record digest: SHA3-256 (lowercase hex) of JSON.stringify of the record without digest, id and
     sequence, object keys sorted recursively (arrays keep their order), previousDigest lowercased or null.
  Order: Records are ordered by their timestamp ascending (capturedAt for state_capture, change_record and agency_record; declared_at for declared_gap), then by record_type in code-unit order (agency_record, change_record, declared_gap, state_capture), then by id in code-unit order. sequence is the 0-based position in that order.

Evidence History Chain (Evidence_History/ledger.json):
------------------------------------------------------
Algorithm: SHA-256 over PostgreSQL rows; chain version 1.
Rows in this export: 0 (0 baseline, 0 changed in window, 0 chain-verified, 0 pre-chain rows without a chainHash, 0 series).
Retention: non-latest rows older than 400 days are deleted by a scheduled sweep; windows before that horizon cannot be exported.
What the chain proves: every version listed in ledger.json hashes to its recorded chainHash and links to the recorded predecessor.
What it does not prove: that no row was rewritten and re-hashed, or deleted, in the database before this export was produced.
Each evidence version is a PostgreSQL row (ato_evidence_artifacts) carrying its contentHash and chainHash. Rows are updated when a newer version supersedes them (isLatest flag) and non-latest rows older than the retention horizon are deleted by a scheduled sweep. The chain is therefore tamper-evident for the rows present in this export, not append-only storage: a party with database write access could rewrite a row and recompute every hash with this public algorithm, and deleted rows are not detectable from the rows alone. The signed rootHash below binds the exact set of versions in this export to the time it was produced; compare it against a copy kept outside PolicyCortex to detect later changes.
To recompute one chainHash:
  chainHash = SHA-256(JSON.stringify(canonical({version, organizationId, collectionId, controlId, framework, artifactType, source, resourceId, collectedAt, contentHash, bodySize, previousChainHash}))) as lowercase hex. canonical() sorts object keys recursively and leaves arrays in order. Field rules: version is the integer chain version; collectionId is null when absent; resourceId is "" when absent; collectedAt is the ISO-8601 UTC timestamp with milliseconds (toISOString); contentHash is the lowercase SHA-256 of the canonical evidence body; bodySize is the byte length of that canonical body; previousChainHash is the lowercase chainHash of the superseded version, or its contentHash when that version pre-dates chaining, or null for the first version.
To recompute the rootHash:
  rootHash = SHA-256(JSON.stringify(orderedEntries.map(e => ({id, seriesKey, seriesPosition, collectedAt, contentHash, chainHash, supersedesId})))) as lowercase hex, where orderedEntries follows the ledger entry order: series sorted by seriesKey, entries within a series in supersedes-link order.
Root signature: NOT SIGNED. No server signing key (ATO_EVIDENCE_LEDGER_SIGNING_KEY) is configured; retain the rootHash independently to detect later changes.
Collector downtime inside the window: none recorded; every collector run in the window succeeded.
