oc_chat
Loading...
Searching...
No Matches
store_read.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/render/roadmap/store_read.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/roadmap/test_store_read.py"
6"""
7Guarded read layer for the roadmap GeneratedView renderers (Plan 142 P1.6).
8
9Purpose
10-------
11Open the **verified** authored projection (P1.4 reader guard) and project the
12INCLUDED §3.2 rows the renderers need — the derived ``current`` pointer
13(AC §8.35), the milestone (m_phase) table, per-phase sub-items with ``Done N/M``
14(AC §8.50), non-phase item tables, and the era overview — every query routed
15through the §8.30 render guard so no runtime-excluded table/column is read.
16
17Responsibilities
18----------------
19- ``open_verified(db_path)`` — open through ``open_roadmap_reader`` (the
20 ``REGEN_UNVERIFIED_PROJECTION`` guard).
21- ``projection_hashes(conn)`` — the header's ``source_hash`` /
22 ``logical_state_hash`` (from the verification stamp; deterministic fallback).
23- ``current_phase_code`` / ``selected_roadmap_version`` — the §3.2 derived pointer
24 and the rendered version label.
25- ``milestone_rows`` / ``sub_items`` / ``non_phase_rows`` / ``era_rows``.
26
27Diagnostics
28-----------
29Domain: OC_CHAT.MAIN
30Levels:
31 L2 - read lifecycle
32 L3 - selection summaries
33 L4 - per-row projection tracing
34
35Contracts
36---------
37- Read-only; never mutates the store.
38- ``current`` = the first phase in canonical ``sort_order`` whose parent
39 ``m_phase`` item is non-terminal (done/deferred/superseded all terminal).
40
41Dependencies
42------------
43- oc_diagnostics (structured debug logging via _dbg)
44- oc_chat.registry.regenerate.verified_projection (the reader guard)
45- oc_chat.registry.source.hashing (logical_state_hash fallback)
46- oc_chat.registry.render.roadmap._common (the render guard + glyphs)
47"""
48from __future__ import annotations
49
50import sqlite3
51from dataclasses import dataclass
52from pathlib import Path
53from typing import Optional
54
55from oc_diagnostics import _dbg
56
57from oc_chat.registry.regenerate.verified_projection import open_roadmap_reader, stamp_hashes
58from oc_chat.registry.source.hashing import logical_state_hash
59from oc_chat.registry.render.roadmap._common import TERMINAL_STATUSES, guarded_query
60
61_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
62
63#: All-complete sentinel rendered when ``current`` is null (§3.2 byte-pinned).
64ALL_COMPLETE_LINE = "**Current:** _all phases complete_"
65
66
67@dataclass(frozen=True)
69 """A structural M-phase row (the parent ``m_phase`` ``roadmap_items_v3`` row)."""
70
71 phase_code: str
72 title: str
73 purpose: str
74 prose: Optional[str]
75 priority: Optional[str]
76 status: str
77 sort_order: int
78 target_version: Optional[str]
79 successor: Optional[str]
80 superseded_by: Optional[str]
81 done: int
82 total: int
83 depends_on: tuple[str, ...]
84
85
86@dataclass(frozen=True)
87class Item:
88 """A non-phase or sub-phase ``roadmap_items_v3`` row (INCLUDED columns only)."""
89
90 item_id: str
91 track_kind: str
92 title: str
93 purpose: str
94 prose: Optional[str]
95 status: str
96 priority: Optional[str]
97 sort_order: int
98 successor: Optional[str]
99 superseded_by: Optional[str]
100 blocked_by: tuple[str, ...] = ()
101
102
103def open_verified(db_path: Path) -> sqlite3.Connection:
104 """Open ``db_path`` through the P1.4 verified-projection reader guard."""
105 return open_roadmap_reader(Path(db_path))
106
107
108def projection_hashes(conn: sqlite3.Connection) -> tuple[str, str]:
109 """Return ``(source_hash, logical_state_hash)`` for the generated header.
110
111 Read from the durable verification stamp (written at regenerate step-7). An
112 ``authored_projection`` store MUST carry a ``passed`` stamp — a missing or
113 non-``passed`` stamp fails closed with ``REGEN_UNVERIFIED_PROJECTION``
114 (Plan 142i R2(a), via ``stamp_hashes``). For any other store (a legacy live
115 ingest store — the F7 pass-through), fall back to the recomputed logical hash
116 for both — keeps ``render --check`` deterministic.
117 """
118 hashes = stamp_hashes(conn)
119 if hashes is not None:
120 return hashes
121 computed = logical_state_hash(conn)
122 return computed, computed
123
124
125def selected_roadmap_version(conn: sqlite3.Connection) -> Optional[str]:
126 """Return the ``current`` roadmap version id, else the highest, else None."""
127 row = guarded_query(
128 conn,
129 # OCT-LINT: disable=no_hardcoded_secrets reason="Read-only SQL projection strings are schema constants, not secrets"
130 "SELECT version_id FROM roadmap_versions "
131 "ORDER BY status = 'current' DESC, version_id DESC LIMIT 1",
132 ).fetchone()
133 return None if row is None else str(row[0])
134
135
136def _child_counts(conn: sqlite3.Connection) -> dict[str, tuple[int, int]]:
137 """Return ``{phase_code: (done, total)}`` over each phase's child items."""
138 counts: dict[str, tuple[int, int]] = {}
139 for row in guarded_query(
140 conn,
141 # OCT-LINT: disable=no_hardcoded_secrets reason="Read-only SQL projection strings are schema constants, not secrets"
142 "SELECT parent_item_id AS parent, "
143 "SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS done, COUNT(*) AS total "
144 "FROM roadmap_items_v3 WHERE parent_item_id IS NOT NULL "
145 "GROUP BY parent_item_id",
146 ).fetchall():
147 counts[str(row["parent"])] = (int(row["done"]), int(row["total"]))
148 return counts
149
150
151#: Dependency kinds that constitute an active "🔒 Blocked by" relation (a soft
152#: ``enables`` edge is not a block); ``part_of`` is the parent relation, not a row.
153_BLOCKING_KINDS = ("blocks", "gates")
154
155
156def _dependencies(conn: sqlite3.Connection) -> dict[str, tuple[str, ...]]:
157 """Return ``{dependent_item_id: (predecessor_id, ...)}`` for ``blocks``/``gates``
158 edges only (§3.2 direction) — the predecessors a 🔒 blocker annotation names."""
159 deps: dict[str, list[str]] = {}
160 for row in guarded_query(
161 conn,
162 "SELECT from_item_id, to_item_id, kind FROM roadmap_item_dependencies "
163 "ORDER BY to_item_id, from_item_id",
164 ).fetchall():
165 if str(row["kind"]) not in _BLOCKING_KINDS:
166 continue
167 deps.setdefault(str(row["to_item_id"]), []).append(str(row["from_item_id"]))
168 return {key: tuple(value) for key, value in deps.items()}
169
170
171def item_titles(conn: sqlite3.Connection) -> dict[str, str]:
172 """Return ``{item_id: title}`` for the 🔒 ``Blocked by <id> — <title>`` lookup."""
173 conn.row_factory = sqlite3.Row
174 return {
175 str(row["item_id"]): str(row["title"])
176 for row in guarded_query(conn, "SELECT item_id, title FROM roadmap_items_v3").fetchall()
177 }
178
179
180def item_statuses(conn: sqlite3.Connection) -> dict[str, str]:
181 """Return ``{item_id: status}`` for the blocker filter — only an *incomplete*
182 (non-terminal) predecessor constitutes an active 🔒 block; a satisfied
183 (done/deferred/superseded) predecessor is not shown."""
184 conn.row_factory = sqlite3.Row
185 return {
186 str(row["item_id"]): str(row["status"])
187 for row in guarded_query(conn, "SELECT item_id, status FROM roadmap_items_v3").fetchall()
188 }
189
190
191def milestone_rows(conn: sqlite3.Connection) -> list[Milestone]:
192 """Return the structural M-phase rows in canonical ``sort_order`` (AC §8.50)."""
193 conn.row_factory = sqlite3.Row
194 counts = _child_counts(conn)
195 deps = _dependencies(conn)
196 rows = guarded_query(
197 conn,
198 # OCT-LINT: disable=no_hardcoded_secrets reason="Read-only SQL projection strings are schema constants, not secrets"
199 "SELECT i.item_id, i.title, i.purpose, i.prose, i.priority, i.status, "
200 "i.sort_order, i.target_version, i.successor_item_id, i.superseded_by_item_id "
201 "FROM roadmap_items_v3 i "
202 "JOIN roadmap_phases p ON p.phase_code = i.item_id "
203 "WHERE i.track_kind = 'm_phase' AND i.parent_item_id IS NULL "
204 "ORDER BY i.sort_order, i.item_id",
205 ).fetchall()
206 milestones: list[Milestone] = []
207 for row in rows:
208 done, total = counts.get(str(row["item_id"]), (0, 0))
209 milestones.append(
210 Milestone(
211 phase_code=str(row["item_id"]),
212 title=str(row["title"]),
213 purpose=str(row["purpose"]),
214 prose=row["prose"],
215 priority=row["priority"],
216 status=str(row["status"]),
217 sort_order=int(row["sort_order"]),
218 target_version=row["target_version"],
219 successor=row["successor_item_id"],
220 superseded_by=row["superseded_by_item_id"],
221 done=done,
222 total=total,
223 depends_on=deps.get(str(row["item_id"]), ()),
224 )
225 )
226 return milestones
227
228
229def current_phase_code(conn: sqlite3.Connection) -> Optional[str]:
230 """Derive the ``current`` pointer (§3.2 / AC §8.35).
231
232 The first phase, in canonical ``sort_order``, whose parent ``m_phase`` item is
233 non-terminal (``done``/``deferred``/``superseded`` are all terminal). Null
234 when every phase is terminal (all-complete behaviour).
235 """
236 func = "current_phase_code"
237 for milestone in milestone_rows(conn):
238 if milestone.status not in TERMINAL_STATUSES:
239 _dbg("OC_CHAT.MAIN", func, milestone.phase_code, 3)
240 return milestone.phase_code
241 return None
242
243
244def sub_items(conn: sqlite3.Connection, phase_code: str) -> list[Item]:
245 """Return a phase's child items (sub-phases + promoted) in ``sort_order``,
246 each carrying its ``blocks``/``gates`` predecessors (for the 🔒 annotation)."""
247 deps = _dependencies(conn)
248 conn.row_factory = sqlite3.Row
249 rows = guarded_query(
250 conn,
251 # OCT-LINT: disable=no_hardcoded_secrets reason="Read-only SQL projection strings are schema constants, not secrets"
252 "SELECT item_id, track_kind, title, purpose, prose, status, priority, "
253 "sort_order, successor_item_id, superseded_by_item_id "
254 "FROM roadmap_items_v3 WHERE parent_item_id = ? "
255 "ORDER BY sort_order, item_id",
256 (phase_code,),
257 ).fetchall()
258 return [_to_item(row, deps.get(str(row["item_id"]), ())) for row in rows]
259
260
261def non_phase_rows(conn: sqlite3.Connection, track_kinds: tuple[str, ...]) -> list[Item]:
262 """Return non-phase item rows for the given ``track_kinds`` in ``sort_order``."""
263 if not track_kinds:
264 return []
265 conn.row_factory = sqlite3.Row
266 placeholders = ",".join("?" for _ in track_kinds)
267 rows = guarded_query(
268 conn,
269 # OCT-LINT: disable=no_hardcoded_secrets reason="Read-only SQL projection strings are schema constants, not secrets"
270 "SELECT item_id, track_kind, title, purpose, prose, status, priority, "
271 "sort_order, successor_item_id, superseded_by_item_id "
272 # OCT-LINT: disable=no_hardcoded_secrets reason="Read-only SQL projection strings are schema constants, not secrets"
273 f"FROM roadmap_items_v3 WHERE track_kind IN ({placeholders}) "
274 "AND phase_id IS NULL "
275 "ORDER BY track_kind, sort_order, item_id",
276 tuple(track_kinds),
277 ).fetchall()
278 return [_to_item(row) for row in rows]
279
280
281def era_rows(conn: sqlite3.Connection) -> list[sqlite3.Row]:
282 """Return era rows in ``sort_order`` for the era-overview section."""
283 conn.row_factory = sqlite3.Row
284 return guarded_query(
285 conn,
286 "SELECT era_id, era_label, major_bump_reason, started_at, ended_at, sort_order "
287 "FROM roadmap_eras ORDER BY sort_order, era_id",
288 ).fetchall()
289
290
291def _to_item(row: sqlite3.Row, blocked_by: tuple[str, ...] = ()) -> Item:
292 return Item(
293 item_id=str(row["item_id"]),
294 track_kind=str(row["track_kind"]),
295 title=str(row["title"]),
296 purpose=str(row["purpose"]),
297 prose=row["prose"],
298 status=str(row["status"]),
299 priority=row["priority"],
300 sort_order=int(row["sort_order"]),
301 successor=row["successor_item_id"],
302 superseded_by=row["superseded_by_item_id"],
303 blocked_by=tuple(blocked_by),
304 )
305
306
307__all__ = [
308 "ALL_COMPLETE_LINE",
309 "Milestone",
310 "Item",
311 "open_verified",
312 "projection_hashes",
313 "selected_roadmap_version",
314 "milestone_rows",
315 "current_phase_code",
316 "sub_items",
317 "non_phase_rows",
318 "era_rows",
319 "item_titles",
320 "item_statuses",
321]
dict[str, tuple[int, int]] _child_counts(sqlite3.Connection conn)
dict[str, tuple[str,...]] _dependencies(sqlite3.Connection conn)
dict[str, str] item_statuses(sqlite3.Connection conn)
tuple[str, str] projection_hashes(sqlite3.Connection conn)
list[Item] sub_items(sqlite3.Connection conn, str phase_code)
Optional[str] selected_roadmap_version(sqlite3.Connection conn)
list[sqlite3.Row] era_rows(sqlite3.Connection conn)
sqlite3.Connection open_verified(Path db_path)
Optional[str] current_phase_code(sqlite3.Connection conn)
list[Milestone] milestone_rows(sqlite3.Connection conn)
list[Item] non_phase_rows(sqlite3.Connection conn, tuple[str,...] track_kinds)
dict[str, str] item_titles(sqlite3.Connection conn)
Item _to_item(sqlite3.Row row, tuple[str,...] blocked_by=())