oc_chat
Loading...
Searching...
No Matches
render_sections.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/render/roadmap/render_sections.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/roadmap/test_render_sections.py"
6"""
7The deterministic ``roadmap_render_sections`` layout (Plan 142 P1.6 seam, §6.1).
8
9Purpose
10-------
11``roadmap_render_sections`` is in the §3.2 logical hash but has **no §3.1 source
12entity** (the P1.4 build leaves it empty on rebuild — umbrella §12 wave-3). P1.6
13owns **defining + seeding** the deterministic render-section set so the
14lean / EXTENDED / changelog / root views are produced deterministically and the
15logical-state hash is stable. The build-vs-render seam: ``regenerate``'s builder
16calls ``seed_render_sections`` so the rows land in the hashed store (the §6.1
17"re-pointed at the lean/EXTENDED/changelog file_kinds" requirement).
18
19Responsibilities
20----------------
21- ``RENDER_SECTIONS`` — the canonical, sorted, deterministic section manifest.
22- ``seed_render_sections(conn)`` — insert it into the temp DB during the build.
23- ``validate_render_sections(...)`` — reject an **active** row with a retired
24 file_kind (AC §8.24); retired kinds (``roadmap_exec_order``/``roadmap_todo``)
25 are kept ``active=0`` (L-a superset CHECK + active-only liveness rule).
26- ``sections_for(conn, file_kind)`` — the renderer's data-driven section list.
27
28Diagnostics
29-----------
30Domain: OC_CHAT.MAIN
31Levels:
32 L2 - seed lifecycle
33 L3 - per-file_kind section counts
34 L4 - per-row insert tracing
35
36Contracts
37---------
38- The manifest is a Python constant (no source entity); it is deterministic +
39 byte-stable, so seeding it keeps the logical-state hash reproducible.
40- ``query_params_json`` is stored as canonical JSON so two rebuilds match and
41 the §3.2 ``_canonical_query_params`` re-emit is a no-op.
42
43Dependencies
44------------
45- oc_diagnostics (structured debug logging via _dbg)
46- oc_chat.registry.canonical (canonical_json)
47"""
48from __future__ import annotations
49
50import sqlite3
51from dataclasses import dataclass
52from typing import Any, Optional
53
54from oc_diagnostics import _dbg
55
56from oc_chat.registry.canonical import canonical_json
57
58_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
59
60#: Active GeneratedView file_kinds (§6 file set).
61FILE_KIND_LEAN = "roadmap"
62FILE_KIND_EXTENDED = "roadmap_extended"
63FILE_KIND_CHANGELOG = "roadmap_changelog"
64FILE_KIND_ROOT_CHANGELOG = "root_changelog"
65
66#: Retired output file_kinds — kept in the schema CHECK as ``active=0`` only
67#: (L-a / [OP-1]d); no active renderer export path may emit them (AC §8.24).
68RETIRED_FILE_KINDS = frozenset({"roadmap_exec_order", "roadmap_todo"})
69
70
71@dataclass(frozen=True)
73 """One declarative render section (a ``roadmap_render_sections`` row)."""
74
75 file_kind: str
76 section_path: str
77 sort_order: int
78 query_kind: str
79 query_params: Optional[dict[str, Any]] = None
80 required_min_version: Optional[str] = None
81 active: bool = True
82
83 def params_json(self) -> Optional[str]:
84 if self.query_params is None:
85 return None
86 return canonical_json(self.query_params)
87
88
89#: Backlog-style non-phase track kinds rendered as item tables.
90_NON_PHASE_TRACKS = {"track_kinds": ["pre_m0_era", "backlog", "vision"]}
91
92#: The canonical, deterministic render-section manifest (§6.1). Section order
93#: within a file_kind is ``sort_order``; the lean view shows live-phase detail
94#: only, the EXTENDED view all phases (DESIGN §3). Changelog views read their
95#: entries from ``source/changelog/**`` directly (§3.2 store/source boundary), so
96#: they carry a single ``prose_only`` anchor section.
97RENDER_SECTIONS: tuple[RenderSection, ...] = (
98 # --- lean ROADMAP.md (V4) ---
99 RenderSection(FILE_KIND_LEAN, "era-overview", 0, "prose_only"),
100 RenderSection(FILE_KIND_LEAN, "milestones", 1, "m_phase_table"),
101 RenderSection(FILE_KIND_LEAN, "phase-detail", 2, "phase_section_with_subrows",
102 {"scope": "live"}),
103 RenderSection(FILE_KIND_LEAN, "backlog", 3, "backlog_track_table", _NON_PHASE_TRACKS),
104 # --- comprehensive ROADMAP-EXTENDED.md (V3) ---
105 RenderSection(FILE_KIND_EXTENDED, "era-overview", 0, "prose_only"),
106 RenderSection(FILE_KIND_EXTENDED, "milestones", 1, "m_phase_table"),
107 RenderSection(FILE_KIND_EXTENDED, "phase-detail", 2, "phase_section_with_subrows",
108 {"scope": "all"}),
109 RenderSection(FILE_KIND_EXTENDED, "backlog", 3, "backlog_track_table", _NON_PHASE_TRACKS),
110 RenderSection(FILE_KIND_EXTENDED, "git-workflow", 4, "git_workflow_table"),
111 # --- ROADMAP-CHANGELOG.md (source-only, kind: roadmap) ---
112 RenderSection(FILE_KIND_CHANGELOG, "changelog", 0, "prose_only"),
113 # --- verbatim root CHANGELOG.md (source-only, kind: product) ---
114 RenderSection(FILE_KIND_ROOT_CHANGELOG, "product-changelog", 0, "prose_only"),
115 # --- retired output kinds (kept active=0 only — AC §8.24) ---
116 RenderSection("roadmap_exec_order", "retired", 0, "prose_only", active=False),
117 RenderSection("roadmap_todo", "retired", 0, "prose_only", active=False),
118)
119
120
122 """An active render section names a retired output file_kind (AC §8.24)."""
123
124
125def validate_render_sections(sections: "tuple[RenderSection, ...]" = RENDER_SECTIONS) -> None:
126 """Reject any **active** section whose file_kind is retired (AC §8.24)."""
127 offenders = [
128 section.section_path
129 for section in sections
130 if section.active and section.file_kind in RETIRED_FILE_KINDS
131 ]
132 if offenders:
133 raise RetiredFileKindActiveError(
134 f"active render section(s) {offenders} use a retired file_kind "
135 f"{sorted(RETIRED_FILE_KINDS)}; retired kinds must be active=0"
136 )
137
138
139def assert_active_file_kinds_in_db(conn: sqlite3.Connection) -> None:
140 """Backstop the active-only liveness rule against the built store (AC §8.24)."""
141 rows = conn.execute(
142 # OCT-LINT: disable=no_hardcoded_secrets reason="file_kind/query_kind tokens + section ids are schema constants"
143 "SELECT file_kind FROM roadmap_render_sections "
144 "WHERE active = 1 AND file_kind IN (?, ?)",
145 tuple(sorted(RETIRED_FILE_KINDS)),
146 ).fetchall()
147 if rows:
149 f"store has active render rows for retired file_kind(s) "
150 f"{sorted({row[0] for row in rows})}"
151 )
152
153
154def seed_render_sections(conn: sqlite3.Connection) -> int:
155 """Seed the canonical render-section manifest into ``conn`` (the build seam).
156
157 Idempotent: clears + re-inserts the deterministic set. Returns the row count.
158 Called by the P1.4 builder so the rows enter the hashed store (§6.1).
159 """
160 func = "seed_render_sections"
162 conn.execute("DELETE FROM roadmap_render_sections")
163 for section in RENDER_SECTIONS:
164 conn.execute(
165 # OCT-LINT: disable=no_hardcoded_secrets reason="file_kind/query_kind tokens + section ids are schema constants"
166 "INSERT INTO roadmap_render_sections(file_kind, section_path, sort_order, "
167 "query_kind, query_params_json, required_min_version, active) "
168 "VALUES(?,?,?,?,?,?,?)",
169 (section.file_kind, section.section_path, section.sort_order,
170 section.query_kind, section.params_json(), section.required_min_version,
171 int(section.active)),
172 )
173 _dbg("OC_CHAT.MAIN", func, f"seeded {len(RENDER_SECTIONS)} rows", 3)
174 return len(RENDER_SECTIONS)
175
176
177def sections_for(conn: sqlite3.Connection, file_kind: str) -> list[sqlite3.Row]:
178 """Return the active sections for ``file_kind`` in render order (§6.1)."""
179 conn.row_factory = sqlite3.Row
180 return conn.execute(
181 # OCT-LINT: disable=no_hardcoded_secrets reason="file_kind/query_kind tokens + section ids are schema constants"
182 "SELECT section_path, sort_order, query_kind, query_params_json "
183 "FROM roadmap_render_sections WHERE file_kind = ? AND active = 1 "
184 "ORDER BY sort_order, section_path",
185 (file_kind,),
186 ).fetchall()
187
188
189__all__ = [
190 "FILE_KIND_LEAN",
191 "FILE_KIND_EXTENDED",
192 "FILE_KIND_CHANGELOG",
193 "FILE_KIND_ROOT_CHANGELOG",
194 "RETIRED_FILE_KINDS",
195 "RenderSection",
196 "RENDER_SECTIONS",
197 "RetiredFileKindActiveError",
198 "validate_render_sections",
199 "assert_active_file_kinds_in_db",
200 "seed_render_sections",
201 "sections_for",
202]
int seed_render_sections(sqlite3.Connection conn)
list[sqlite3.Row] sections_for(sqlite3.Connection conn, str file_kind)
None assert_active_file_kinds_in_db(sqlite3.Connection conn)
None validate_render_sections("tuple[RenderSection, ...]" sections=RENDER_SECTIONS)