Originally created by: fu351
Give every policy a version an auditor can match a decision to. A policy version is pv1: + the SHA-256 of a canonical snapshot of the effective policy — the saved policy document minus cosmetic text, the active role's globs, the ledger-verified enforcement state, and the engine version. Content identity, not a label: OPA's decision logs, SLSA, and OSCAL all converged on writing a hash of a canonical serialization inline; systems that stamped only a mutable name (IAM policy versions vs CloudTrail, Kubernetes admission audit annotations) had to bolt a side history on later.
src/doberman/storage/policy_catalogue.py (new): the frozen PolicySnapshotV1 model, the canonical JSON form (same shape tool_pins already uses), the pv1: id, and an append-only stdlib-sqlite3 store at .doberman/policies.db — policy_versions (each snapshot once) + policy_observations (a row whenever the version in force was seen to change). No UPDATE/DELETE statement exists in the module, and a test enforces that. Plain SHA-256, deliberately not the keyed HMAC: anyone holding the snapshot can recompute the id.config.save_policy records every saved policy as the version now in force, linked to the policy_changes row that authorised it: ChangeOutcome gains a ts, and every gated caller (enforcement, mode, prefs, egress-velocity, both default-role toggles, the revealed-preference nudge, setup's first run) threads it through. Enforcement on the save path is resolved by the pure timer rule — resolve_enforcement_sync fails closed to enforce inside a running event loop, and the dashboard reaches save_policy inside one, so using it there would record a false version.doberman policy-versions: list (records an observed entry first, which is how a hand-edited policies.yaml/role.yaml enters the timeline), --show <id> (full id or ≥8 hex chars), --json (allowlist only, never snapshot content), and a read-only --verify that recomputes every stored digest and reports ok / mismatch / drift. doberman doctor gains a non-critical Policy version row.docs/POLICY_VERSIONS.md (what is hashed, the matching contract, what this does not prove), a published JSON Schema at docs/schemas/policy-snapshot.v1.json kept in sync by a drift test, CLI/README/TUNING rows.Design record: ADR 0088 (maintainer workspace). This is the store the upcoming "stamp policy_version on every decision row" issue matches against.
tests/unit/test_policy_catalogue.py (golden vector pinning the serialization, per-field sensitivity, redaction, store semantics, version_at, verify states), tests/unit/test_cli_policy_versions.py (listing, --show prefix/full-id/exit codes, read-only --verify), doctor/help/schema-doc/standalone additions, linkage tests at every chokepoint including establish_ok first-run. Full suite locally: 3410+ passed; the only failures are this box's two known environment-only ones (test_version_is_exposed dist metadata; the numpy2/pandas benchmark import), both green in CI.
🤖 Generated with Claude Code
Ticket changed by: fu351