Skip to content

v1.3 provenance hardening - #105

Merged
WhatsYourWhy merged 1 commit into
mainfrom
feat/v1.3-provenance-hardening
Aug 14, 2026
Merged

v1.3 provenance hardening#105
WhatsYourWhy merged 1 commit into
mainfrom
feat/v1.3-provenance-hardening

Conversation

@WhatsYourWhy

Copy link
Copy Markdown
Owner

Stacked on #104. Base is chore/ci-and-line-ending-normalization, so this diff shows only the v1.3 work. Merge #104 first; GitHub will retarget this to main automatically.

Why

Makes every published Hardstop artifact traceable to the exact inputs that produced it, and stops unhealthy runs from publishing.

Additive only — no change to decision semantics. Classification, impact score, scope, correlation and suppression are computed exactly as in v1.2. The alerts table, the brief.v1 read model and export_schema_version: "1" stay compatible, per the v1 freeze in CHANGELOG.md.

What

Exact run-to-input lineage. New run_raw_items table records which raw items each run group fetched (NEW/DUPLICATE/RETRY). compute_raw_item_batch_digest now hashes those rows rather than approximating the batch from FETCH SourceRun counters — its own docstring called that an approximation. Changing one raw item's content now changes the digest; two batches with equal counts and status no longer collide.

Pre-publication gate. Readiness checks move out of cmd_run's inline doctor block into ops/readiness.py and now run after ingest, before the brief. Previously source health was evaluated only at Step 4 — after the brief had already printed. Strict skips the brief on BROKEN; best-effort marks it DRAFT_ONLY.

Export provenance. hardstop export emits a RunRecord; it was the only artifact-producing command without one. Manifests gain export_content_hash over the exact bytes on disk. The CSV manifest now hashes the real CSV instead of a placeholder dict containing a fresh timestamp — it changed every run and certified nothing about the CSV.

Evidence verification. load_incident_evidence_summary now compares the stored artifact_hash against the payload. It previously recomputed the hash and used it only as a fallback for a missing value, so a tampered or stale hash was trusted silently. cmd_incidents_replay and the brief read path now share one implementation.

Strict provenance failures become exit 2, with escalation placed after the try/finally so a command's own exception is never masked.

Backward compatibility

  • Run groups with no lineage rows fall back to the previous counts digest, unchanged. The fallback keys off row count, not table existence, because create_all materializes an empty table on any database
  • run_raw_items is deliberately not added to required_tables; that list feeds schema_drift → exit 2, which would make every pre-v1.3 database report BROKEN on first run
  • publication and artifact_hash_verified are added only in the non-authoritative / mismatch cases, so normal brief.v1 payloads are byte-identical
  • export_data_hash keeps its existing timestamp-independent definition so pinned manifests stay valid; export_content_hash is additive

Verified against a simulated pre-v1.3 database: readiness reports READY with no schema drift, and the digest is unchanged.

Deliberate non-goals

  • The gate covers the run pipeline only; a standalone hardstop brief --today is still ungated
  • A BLOCKED source still fetches, ingests and produces alerts, exactly as in v1.x. Gating alert creation would change frozen v1 decision behavior and needs its own proposal
  • export_sources keeps an empty artifact_hashes: sources health is derived counters, not hashed documents, so any entry would hash the same bytes export_data_hash already covers

Fixes found along the way

  • SystemExit from a sub-command skipped Step 4 of hardstop run. It is a BaseException and escaped the existing except Exception guards
  • .gitignore's bare output/ rule matches that directory name at any depth, so new files under tests/output/ were silently skipped by git add. The existing test there is tracked only because it was force-added — this PR's evidence tests were missing until it was caught
  • JSON exports now use newline=""; on Windows the bytes on disk were CRLF-translated and matched no hash of the payload

Verification

Verified in a fresh clone of this branch with core.autocrlf=true and its own venv, so no editable install could leak source from elsewhere: 237 passed, 0 failed (baseline before this work: 184 passing). 50 new tests, including a parity guard that readiness and run_status agree on every BROKEN condition, and a precedence guard that a RunRecord failure never masks a command error.

🤖 Generated with Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@WhatsYourWhy
WhatsYourWhy force-pushed the feat/v1.3-provenance-hardening branch from ee20204 to 395c7b5 Compare August 14, 2026 04:08
Makes published Hardstop artifacts traceable to the exact inputs that
produced them, and stops unhealthy runs from publishing. Additive only:
classification, impact score, scope, correlation and suppression are
computed exactly as in v1.2, and the alerts table, brief.v1 read model
and export_schema_version "1" stay compatible.

Exact run-to-input lineage. New run_raw_items table records which raw
items each run group fetched, with a NEW/DUPLICATE/RETRY action.
compute_raw_item_batch_digest now hashes those rows instead of
approximating the batch from FETCH SourceRun counters, so changing one
raw item's content changes the digest. Run groups without lineage rows
fall back to the previous counts digest unchanged, which is what keeps
pre-v1.3 databases working; the fallback keys off row count rather than
table existence, because create_all materializes an empty table on any
database. raw_id and fetch_action are excluded from the hash so a replay
of identical content digests identically.

Pre-publication gate. Readiness checks move out of cmd_run's inline
doctor block into ops/readiness.py and now run after ingest and before
the brief. Strict mode skips the brief when readiness is BROKEN;
best-effort marks it DRAFT_ONLY through an additive publication key and
a renderer banner. Alerts are still created exactly as before, including
from BLOCKED sources: only publication is gated, so v1 decision
semantics are untouched. Step 4 re-evaluates rather than reusing the
earlier result, because generating the brief can create tables and flip
the schema_drift finding.

Export provenance. hardstop export now emits a RunRecord; it was the
only artifact-producing command without one. Manifests gain
export_content_hash over the exact bytes on disk, while export_data_hash
keeps its existing timestamp-independent definition so pinned manifests
stay valid. The CSV manifest now hashes the real CSV instead of a
placeholder dict containing a fresh timestamp, which changed every run
and certified nothing. export_brief populates artifact_hashes;
export_sources deliberately does not, since sources health is derived
counters rather than hashed documents. JSON exports are written with
newline="" so Windows bytes match the serialized payload.

Evidence verification. load_incident_evidence_summary now compares the
stored artifact_hash against the payload. It previously recomputed the
hash and used it only as a fallback for a missing value, so a tampered
or stale hash was trusted silently. cmd_incidents_replay and the brief
read path now share one implementation.

Strict provenance failures. A RunRecord emission failure is fatal in
strict mode with a distinct RUN_RECORD_EMISSION_FAILED marker.
Escalation happens after the try/finally so a command's own exception is
never masked by a provenance failure.

Also fixes SystemExit from a sub-command skipping Step 4 of hardstop
run. SystemExit is a BaseException and escaped the existing
except Exception guards.

Also re-includes tests/output/ in .gitignore. The bare output/ rule
matches a directory of that name at any depth, so the new evidence
verification tests were silently skipped by git add; the existing test
in that directory is tracked only because it was force-added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@WhatsYourWhy
WhatsYourWhy force-pushed the feat/v1.3-provenance-hardening branch from 395c7b5 to fb1cbaa Compare August 14, 2026 04:12
@WhatsYourWhy
WhatsYourWhy changed the base branch from chore/ci-and-line-ending-normalization to main August 14, 2026 04:36
@WhatsYourWhy
WhatsYourWhy merged commit 6af580a into main Aug 14, 2026
5 checks passed
@WhatsYourWhy
WhatsYourWhy deleted the feat/v1.3-provenance-hardening branch August 14, 2026 05:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant