7Per-section body renderers shared by the lean V4 + EXTENDED V3 views (P1.6, §6).
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).
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).
30 L2 - section lifecycle
31 L3 - section selection
32 L4 - per-row rendering
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).
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)
46from __future__
import annotations
49from typing
import Any, Optional
51from oc_diagnostics
import _dbg
61_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
64 "| Phase | Priority | State | Done | Depends On | Title | Purpose |\n"
65 "|-------|----------|-------|------|------------|-------|---------|"
68 "| State | Priority | Item | Title | Purpose |\n"
69 "|-------|----------|------|-------|---------|"
71_SUBROW_HEADER =
"| Sub-phase | State | Description |\n|-----------|-------|-------------|"
73 "| Era | Phases | Started | Ended | Note |\n"
74 "|-----|--------|---------|-------|------|"
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()
85 return f
"{done}/{total}" if total
else "—"
89 conn: sqlite3.Connection,
93 params: Optional[dict[str, Any]],
95 """Render one manifest section's body markdown (heading + content)."""
96 func =
"render_section"
98 if query_kind ==
"prose_only" and section_path ==
"era-overview":
100 if query_kind ==
"m_phase_table":
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"):
106 _dbg(
"OC_CHAT.MAIN", func, f
"unhandled {query_kind}/{section_path}", 2)
111 rows = store_read.era_rows(conn)
114 lines = [
"## Era Overview",
"", _ERA_HEADER]
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 ""),
129 milestones = store_read.milestone_rows(conn)
132 lines = [
"## Milestones",
"", _MILESTONE_HEADER]
133 for milestone
in milestones:
134 has_successor = milestone.successor
is not None
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),
150 blocked_by: tuple[str, ...],
152 titles: dict[str, str],
153 statuses: dict[str, 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:
162 active = [bid
for bid
in blocked_by
if statuses.get(bid,
"")
not in TERMINAL_STATUSES]
165 named =
", ".join(f
"`{bid} — {titles.get(bid, bid)}`" for bid
in active)
166 return f
"🔒 Blocked by {named}"
170 milestone:
"store_read.Milestone", titles: dict[str, str], statuses: dict[str, 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
184 suffix = f
" · {names}" if names
else ""
185 return f
"{glyph} {label} · **{done}** complete{suffix}"
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:
196 children = store_read.sub_items(conn, milestone.phase_code)
201 prose = (milestone.prose
or "").strip()
if scope ==
"all" else ""
202 if not children
and not prose:
206 f
"### {milestone.phase_code} — {escape(milestone.title)}",
213 blocks.extend(prose.split(
"\n"))
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 ""
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}"),
231 heading =
"Live Phase Detail" if scope ==
"live" else "Phase Detail"
232 return [
"## " + heading,
"", *blocks[:-1]]
236 conn: sqlite3.Connection, query_kind: str, params: dict[str, Any]
238 if query_kind ==
"git_workflow_table":
239 track_kinds: tuple[str, ...] = (
"git_workflow",)
240 heading =
"Git Workflow"
242 track_kinds = tuple(params.get(
"track_kinds", ()))
243 heading =
"Backlog & Non-Phase Items"
244 items = store_read.non_phase_rows(conn, track_kinds)
247 lines = [f
"## {heading}",
"", _ITEM_HEADER]
249 glyph = status_glyph(item.status, has_successor=item.successor
is not None)
251 "| {state} | {prio} | {item} | {title} | {purpose} |".format(
253 prio=priority_glyph(item.priority),
254 item=
escape(item.item_id),
256 purpose=
escape(item.purpose),
262__all__ = [
"escape",
"render_section"]
str _phase_status_line("store_read.Milestone" milestone, dict[str, str] titles, dict[str, str] statuses)
list[str] _phase_detail(sqlite3.Connection conn, *, str scope)
list[str] _milestone_table(sqlite3.Connection conn)
list[str] render_section(sqlite3.Connection conn, *, str query_kind, str section_path, Optional[dict[str, Any]] params)
str _blocker_names(tuple[str,...] blocked_by, str status, dict[str, str] titles, dict[str, str] statuses)
list[str] _item_tables(sqlite3.Connection conn, str query_kind, dict[str, Any] params)
list[str] _era_overview(sqlite3.Connection conn)
str _done_cell(int done, int total)