oc_chat
Loading...
Searching...
No Matches
verified_projection.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/regenerate/verified_projection.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/regenerate/test_verified_projection.py"
6# OCT-LINT: disable=no_hardcoded_secrets reason="Column names + stamp constants are schema constants"
7"""
8The verified-projection publish-gate: stamp + central reader guard (§5.3.1).
9
10Purpose
11-------
12Write a durable verification STAMP into the temp DB BEFORE the atomic swap (so
13the data + stamp become visible atomically), and gate EVERY canonical reader of
14an authored projection through one check (Registry Source & Storage Spec V0.22
15§5.3.1 — verify-before-USE). A missing/failed stamp on an authored projection is
16``REGEN_UNVERIFIED_PROJECTION`` (AC §8.26/49). Live stores do not use this guard
17(they use the P1.2 ``store_scope`` guard).
18
19Responsibilities
20----------------
21- ``write_verification_stamp(conn, ...)`` — create + populate the stamp row.
22- ``assert_verified_projection(conn)`` — raise on an unverified authored projection.
23- ``open_roadmap_reader(db_path)`` — open a guarded read connection.
24
25Diagnostics
26-----------
27Domain: OC_CHAT.MAIN
28Levels:
29 L2 - stamp/guard lifecycle
30 L3 - stamp validity check
31 L4 - column probes
32
33Contracts
34---------
35- The stamp lives in ``registry_verification_stamp`` (NOT a roadmap_* table, so
36 it never enters the §3.2 logical-state hash).
37- The guard is a fast in-DB validity check (status + internal-hash consistency),
38 NOT a source-tree recomputation (that is ``render --check`` / ``regenerate``).
39
40Dependencies
41------------
42- oc_diagnostics (structured debug logging via _dbg)
43- oc_chat.registry.regenerate.envelope (REGEN_UNVERIFIED_PROJECTION)
44"""
45from __future__ import annotations
46
47import sqlite3
48from pathlib import Path
49from typing import Optional
50
51from oc_diagnostics import _dbg
52
54 REGEN_UNVERIFIED_PROJECTION,
55 RegenerateError,
56)
57
58_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
59
60VERIFICATION_SCHEMA_VERSION = 1
61STAMP_TABLE = "registry_verification_stamp"
62_AUTHORED_PROJECTION = "authored_projection"
63_PASSED = "passed"
64
65_CREATE_STAMP = f"""
66CREATE TABLE IF NOT EXISTS {STAMP_TABLE} (
67 id INTEGER PRIMARY KEY CHECK(id = 1),
68 store_scope TEXT NOT NULL,
69 source_hash TEXT NOT NULL,
70 logical_state_hash TEXT NOT NULL,
71 derive_rule_set_hash TEXT,
72 constraint_inventory_hash TEXT,
73 verified_at_step TEXT NOT NULL,
74 verification_schema_version INTEGER NOT NULL,
75 verification_status TEXT NOT NULL
76)
77"""
78
79
80def _store_scope(conn: sqlite3.Connection) -> Optional[str]:
81 try:
82 row = conn.execute("SELECT store_scope FROM registry_meta WHERE id = 1").fetchone()
83 except sqlite3.OperationalError:
84 return None
85 if row is None:
86 return None
87 return str(row[0])
88
89
91 conn: sqlite3.Connection,
92 *,
93 source_hash: str,
94 logical_state_hash: str,
95 store_scope: str = _AUTHORED_PROJECTION,
96 derive_rule_set_hash: Optional[str] = None,
97 constraint_inventory_hash: Optional[str] = None,
98 status: str = _PASSED,
99) -> None:
100 """Write the single-row verification stamp into the temp DB (pre-swap)."""
101 conn.execute(_CREATE_STAMP)
102 conn.execute(f"DELETE FROM {STAMP_TABLE}")
103 conn.execute(
104 f"INSERT INTO {STAMP_TABLE}(id, store_scope, source_hash, logical_state_hash, "
105 "derive_rule_set_hash, constraint_inventory_hash, verified_at_step, "
106 "verification_schema_version, verification_status) VALUES(1,?,?,?,?,?,?,?,?)",
107 (store_scope, source_hash, logical_state_hash, derive_rule_set_hash,
108 constraint_inventory_hash, "step-7", VERIFICATION_SCHEMA_VERSION, status),
109 )
110 conn.commit()
111
112
113def assert_verified_projection(conn: sqlite3.Connection) -> None:
114 """Raise ``REGEN_UNVERIFIED_PROJECTION`` if an authored projection is unverified.
115
116 A live store (or pre-v0.3.2 DB) is not gated here. An authored projection
117 must carry a ``verification_status='passed'`` stamp.
118 """
119 if _store_scope(conn) != _AUTHORED_PROJECTION:
120 return
121 try:
122 row = conn.execute(
123 f"SELECT verification_status FROM {STAMP_TABLE} WHERE id = 1"
124 ).fetchone()
125 except sqlite3.OperationalError as error:
126 raise RegenerateError(
127 "authored projection has no verification stamp (unverified)",
128 code=REGEN_UNVERIFIED_PROJECTION,
129 hint="run 'regenerate' to build + verify before reading",
130 ) from error
131 if row is None or str(row[0]) != _PASSED:
132 raise RegenerateError(
133 "authored projection verification stamp is missing or not 'passed'",
134 code=REGEN_UNVERIFIED_PROJECTION,
135 hint="run 'regenerate' to build + verify before reading",
136 )
137
138
139def stamp_hashes(conn: sqlite3.Connection) -> Optional[tuple[str, str]]:
140 """Return the stamped ``(source_hash, logical_state_hash)``, fail-closed for
141 an authored projection (Plan 142i R2(a), §5.4 [R9] closure).
142
143 - An ``authored_projection`` store MUST carry a
144 ``verification_status='passed'`` stamp: a missing stamp OR a non-``passed``
145 status (N-c) raises ``REGEN_UNVERIFIED_PROJECTION`` — the header's hashes
146 are never emitted from an unverified authored projection.
147 - Any other store (a legacy ``live_store`` / pre-v0.3.2 DB — the deliberate
148 F7 pass-through) returns the stamp values if a stamp exists, else ``None``
149 so the caller applies its deterministic fallback (never gated).
150
151 This is defence-in-depth for direct / future callers: the render open path
152 already raises at open time via ``assert_verified_projection`` for a
153 stampless authored projection (G5), so through ``open_verified`` the raise
154 never fires — the fail-closed unit test calls this on a raw connection.
155 """
156 scope = _store_scope(conn)
157 try:
158 row = conn.execute(
159 f"SELECT source_hash, logical_state_hash, verification_status "
160 f"FROM {STAMP_TABLE} WHERE id = 1"
161 ).fetchone()
162 except sqlite3.OperationalError:
163 row = None
164 if scope == _AUTHORED_PROJECTION:
165 if row is None or str(row[2]) != _PASSED:
166 raise RegenerateError(
167 "authored projection stamp is missing or not 'passed' — "
168 "refusing to emit unverified header hashes",
169 code=REGEN_UNVERIFIED_PROJECTION,
170 hint="run 'regenerate' to build + verify before reading",
171 )
172 return str(row[0]), str(row[1])
173 if row is not None:
174 return str(row[0]), str(row[1])
175 return None
176
177
178def stamp_source_hash_if_authored(conn: sqlite3.Connection) -> Optional[str]:
179 """Return the stamped canonical ``source_hash`` IFF this is an
180 ``authored_projection`` store carrying a ``passed`` verification stamp;
181 else ``None`` (Plan 142i R2(b) stamp-conditional gate).
182
183 A stampless legacy live store (or any non-authored / non-``passed`` store)
184 returns ``None`` so ``render --check``'s whole-tree ``source_hash`` recompute
185 is skipped wholesale — a legacy store carries no canonical-source contract to
186 enforce and an unconditional compare would false-drift by construction (H1).
187 """
188 if _store_scope(conn) != _AUTHORED_PROJECTION:
189 return None
190 try:
191 row = conn.execute(
192 f"SELECT source_hash, verification_status FROM {STAMP_TABLE} WHERE id = 1"
193 ).fetchone()
194 except sqlite3.OperationalError:
195 return None
196 if row is None or str(row[1]) != _PASSED:
197 return None
198 return str(row[0])
199
200
201def open_roadmap_reader(db_path: Path) -> sqlite3.Connection:
202 """Open a read connection through the verified-projection guard."""
203 conn = sqlite3.connect(str(db_path))
204 conn.row_factory = sqlite3.Row
205 try:
207 except Exception:
208 conn.close()
209 raise
210 return conn
Optional[str] _store_scope(sqlite3.Connection conn)
sqlite3.Connection open_roadmap_reader(Path db_path)
Optional[str] stamp_source_hash_if_authored(sqlite3.Connection conn)
None write_verification_stamp(sqlite3.Connection conn, *, str source_hash, str logical_state_hash, str store_scope=_AUTHORED_PROJECTION, Optional[str] derive_rule_set_hash=None, Optional[str] constraint_inventory_hash=None, str status=_PASSED)
Optional[tuple[str, str]] stamp_hashes(sqlite3.Connection conn)
None assert_verified_projection(sqlite3.Connection conn)