oc_chat
Loading...
Searching...
No Matches
sections.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/render/roadmap/sections.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/roadmap/test_render_living_set.py"
6"""
7Per-section body renderers shared by the lean V4 + EXTENDED V3 views (P1.6, §6).
8
9Purpose
10-------
11Render each ``roadmap_render_sections`` body section by ``query_kind`` — the
12milestone (m_phase) table, per-phase sub-phase detail, the non-phase item
13tables, and the era overview — so the lean and EXTENDED views are produced
14**deterministically from the seeded render-section manifest** (the P1.4↔P1.6
15seam, umbrella §12 wave-3). All enum→glyph mapping flows through ``_common``
16(§7, AC §8.7); all reads flow through ``store_read`` (the §8.30 guard).
17
18Responsibilities
19----------------
20- ``render_section(conn, query_kind, section_path, params)`` — dispatch one
21 manifest section to its body markdown (a list of lines).
22- The four v1 body query kinds: ``prose_only`` (era overview),
23 ``m_phase_table``, ``phase_section_with_subrows``, ``backlog_track_table`` /
24 ``git_workflow_table`` (item tables).
25
26Diagnostics
27-----------
28Domain: OC_CHAT.MAIN
29Levels:
30 L2 - section lifecycle
31 L3 - section selection
32 L4 - per-row rendering
33
34Contracts
35---------
36- Deterministic: identical store state → identical bytes (no wall-clock).
37- A live-scope phase-detail section emits detail only for non-terminal phases;
38 an ``all`` scope emits every phase (DESIGN §3 lean-vs-EXTENDED split).
39
40Dependencies
41------------
42- oc_diagnostics (structured debug logging via _dbg)
43- oc_chat.registry.render.roadmap.store_read (guarded reads of eras, milestones, sub-items and non-phase items)
44- oc_chat.registry.render.roadmap._common (status and priority glyphs, status labels and terminal statuses)
45"""
46from __future__ import annotations
47
48import sqlite3
49from typing import Any, Optional
50
51from oc_diagnostics import _dbg
52
53from oc_chat.registry.render.roadmap import store_read
55 TERMINAL_STATUSES,
56 priority_glyph,
57 status_glyph,
58 status_label,
59)
60
61_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
62
63_MILESTONE_HEADER = (
64 "| Phase | Priority | State | Done | Depends On | Title | Purpose |\n"
65 "|-------|----------|-------|------|------------|-------|---------|"
66)
67_ITEM_HEADER = (
68 "| State | Priority | Item | Title | Purpose |\n"
69 "|-------|----------|------|-------|---------|"
70)
71_SUBROW_HEADER = "| Sub-phase | State | Description |\n|-----------|-------|-------------|"
72_ERA_HEADER = (
73 "| Era | Phases | Started | Ended | Note |\n"
74 "|-----|--------|---------|-------|------|"
75)
76
77
78def escape(value: object) -> str:
79 """Collapse a cell value to a single safe markdown-table line."""
80 text = "" if value is None else str(value)
81 return text.replace("|", "\\|").replace("\n", " ").strip()
82
83
84def _done_cell(done: int, total: int) -> str:
85 return f"{done}/{total}" if total else "—"
86
87
89 conn: sqlite3.Connection,
90 *,
91 query_kind: str,
92 section_path: str,
93 params: Optional[dict[str, Any]],
94) -> list[str]:
95 """Render one manifest section's body markdown (heading + content)."""
96 func = "render_section"
97 params = params or {}
98 if query_kind == "prose_only" and section_path == "era-overview":
99 return _era_overview(conn)
100 if query_kind == "m_phase_table":
101 return _milestone_table(conn)
102 if query_kind == "phase_section_with_subrows":
103 return _phase_detail(conn, scope=str(params.get("scope", "all")))
104 if query_kind in ("backlog_track_table", "git_workflow_table"):
105 return _item_tables(conn, query_kind, params)
106 _dbg("OC_CHAT.MAIN", func, f"unhandled {query_kind}/{section_path}", 2)
107 return []
108
109
110def _era_overview(conn: sqlite3.Connection) -> list[str]:
111 rows = store_read.era_rows(conn)
112 if not rows:
113 return []
114 lines = ["## Era Overview", "", _ERA_HEADER]
115 for row in rows:
116 lines.append(
117 "| {era} | {label} | {started} | {ended} | {note} |".format(
118 era=escape(row["era_id"]),
119 label=escape(row["era_label"]),
120 started=escape(row["started_at"]),
121 ended=escape(row["ended_at"] or "—"),
122 note=escape(row["major_bump_reason"] or ""),
123 )
124 )
125 return lines
126
127
128def _milestone_table(conn: sqlite3.Connection) -> list[str]:
129 milestones = store_read.milestone_rows(conn)
130 if not milestones:
131 return []
132 lines = ["## Milestones", "", _MILESTONE_HEADER]
133 for milestone in milestones:
134 has_successor = milestone.successor is not None
135 lines.append(
136 "| **{phase}** | {prio} | {state} | {done} | {deps} | {title} | {purpose} |".format(
137 phase=escape(milestone.phase_code),
138 prio=priority_glyph(milestone.priority),
139 state=status_glyph(milestone.status, has_successor=has_successor),
140 done=_done_cell(milestone.done, milestone.total),
141 deps=escape(", ".join(milestone.depends_on) or "—"),
142 title=escape(milestone.title),
143 purpose=escape(milestone.purpose),
144 )
145 )
146 return lines
147
148
150 blocked_by: tuple[str, ...],
151 status: str,
152 titles: dict[str, str],
153 statuses: dict[str, str],
154) -> str:
155 """The "🔒 Blocked by `<id> — <title>`[, ...]" annotation for a non-terminal
156 item with blocks/gates predecessors. Only an *incomplete* predecessor is an
157 active block — a satisfied (terminal) predecessor is filtered out (a done
158 dependency does not block); empty for a terminal/unblocked item or one whose
159 blockers are all satisfied."""
160 if status in TERMINAL_STATUSES or not blocked_by:
161 return ""
162 active = [bid for bid in blocked_by if statuses.get(bid, "") not in TERMINAL_STATUSES]
163 if not active:
164 return ""
165 named = ", ".join(f"`{bid} — {titles.get(bid, bid)}`" for bid in active)
166 return f"🔒 Blocked by {named}"
167
168
170 milestone: "store_read.Milestone", titles: dict[str, str], statuses: dict[str, str]
171) -> str:
172 """The phase status line. When the status is ``blocked`` AND a named blocker
173 exists, the named blocker REPLACES the generic "🔒 Blocked" label (one lock):
174 ``🔒 Blocked by `M2 — …` · **1/9** complete``. A ``blocked`` item with no named
175 blocker keeps ``🔒 Blocked``; any other item appends the blocker after progress."""
176 done = _done_cell(milestone.done, milestone.total)
177 names = _blocker_names(milestone.depends_on, milestone.status, titles, statuses)
178 if milestone.status == "blocked" and names:
179 return f"{names} · **{done}** complete"
180 glyph = status_glyph(milestone.status, has_successor=milestone.successor is not None)
181 label = status_label(
182 milestone.status, successor=milestone.successor, superseded_by=milestone.superseded_by
183 )
184 suffix = f" · {names}" if names else ""
185 return f"{glyph} {label} · **{done}** complete{suffix}"
186
187
188def _phase_detail(conn: sqlite3.Connection, *, scope: str) -> list[str]:
189 milestones = store_read.milestone_rows(conn)
190 titles = store_read.item_titles(conn)
191 statuses = store_read.item_statuses(conn)
192 blocks: list[str] = []
193 for milestone in milestones:
194 if scope == "live" and milestone.status in TERMINAL_STATUSES:
195 continue
196 children = store_read.sub_items(conn, milestone.phase_code)
197 # The EXTENDED (scope="all") view emits the verbatim per-phase prose
198 # (the legacy `# PHASE` section: metadata + the full deliverables table
199 # with each item's status provenance + acceptance) when present; the lean
200 # view keeps the compact structured sub-phase table (DESIGN §3 split).
201 prose = (milestone.prose or "").strip() if scope == "all" else ""
202 if not children and not prose:
203 continue
204 blocks.extend(
205 [
206 f"### {milestone.phase_code} — {escape(milestone.title)}",
207 "",
208 _phase_status_line(milestone, titles, statuses),
209 "",
210 ]
211 )
212 if prose:
213 blocks.extend(prose.split("\n"))
214 blocks.append("")
215 continue
216 blocks.append(_SUBROW_HEADER)
217 for child in children:
218 child_glyph = status_glyph(child.status, has_successor=child.successor is not None)
219 child_names = _blocker_names(child.blocked_by, child.status, titles, statuses)
220 child_suffix = f" · {child_names}" if child_names else ""
221 blocks.append(
222 "| {sub} | {state} | {desc} |".format(
223 sub=escape(child.item_id),
224 state=f"{child_glyph} {status_label(child.status, successor=child.successor, superseded_by=child.superseded_by)}",
225 desc=escape(f"{child.purpose}{child_suffix}"),
226 )
227 )
228 blocks.append("")
229 if not blocks:
230 return []
231 heading = "Live Phase Detail" if scope == "live" else "Phase Detail"
232 return ["## " + heading, "", *blocks[:-1]]
233
234
236 conn: sqlite3.Connection, query_kind: str, params: dict[str, Any]
237) -> list[str]:
238 if query_kind == "git_workflow_table":
239 track_kinds: tuple[str, ...] = ("git_workflow",)
240 heading = "Git Workflow"
241 else:
242 track_kinds = tuple(params.get("track_kinds", ()))
243 heading = "Backlog & Non-Phase Items"
244 items = store_read.non_phase_rows(conn, track_kinds)
245 if not items:
246 return []
247 lines = [f"## {heading}", "", _ITEM_HEADER]
248 for item in items:
249 glyph = status_glyph(item.status, has_successor=item.successor is not None)
250 lines.append(
251 "| {state} | {prio} | {item} | {title} | {purpose} |".format(
252 state=glyph,
253 prio=priority_glyph(item.priority),
254 item=escape(item.item_id),
255 title=escape(item.title),
256 purpose=escape(item.purpose),
257 )
258 )
259 return lines
260
261
262__all__ = ["escape", "render_section"]
str _phase_status_line("store_read.Milestone" milestone, dict[str, str] titles, dict[str, str] statuses)
Definition sections.py:171
list[str] _phase_detail(sqlite3.Connection conn, *, str scope)
Definition sections.py:188
list[str] _milestone_table(sqlite3.Connection conn)
Definition sections.py:128
list[str] render_section(sqlite3.Connection conn, *, str query_kind, str section_path, Optional[dict[str, Any]] params)
Definition sections.py:94
str _blocker_names(tuple[str,...] blocked_by, str status, dict[str, str] titles, dict[str, str] statuses)
Definition sections.py:154
list[str] _item_tables(sqlite3.Connection conn, str query_kind, dict[str, Any] params)
Definition sections.py:237
list[str] _era_overview(sqlite3.Connection conn)
Definition sections.py:110
str _done_cell(int done, int total)
Definition sections.py:84