oc_chat
Loading...
Searching...
No Matches
roadmap_main.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/render/roadmap/roadmap_main.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/roadmap/test_render_living_set.py"
6"""
7The lean V4 ``ROADMAP.md`` renderer (Plan 142 P1.6, §6 / DESIGN §3).
8
9Purpose
10-------
11Render the lean working map from the verified authored projection: the §6
12generated header → title → Version line → ``📍 Now`` current pointer → legend →
13era overview → milestone table (with ``Done N/M`` + purpose) → live-phase
14sub-phase detail. The section order + selection are driven by the seeded
15``roadmap_render_sections`` manifest for ``file_kind='roadmap'`` (the P1.4↔P1.6
16seam), so the view is produced deterministically. ``current = null`` renders the
17byte-pinned all-complete line (AC §8.52); every status/priority maps through the
18§7 glyph table (AC §8.7).
19
20Responsibilities
21----------------
22- ``render(conn, *, source_hash, logical_state_hash, published=None)`` — the lean
23 view as a single byte-deterministic markdown string (header included).
24- ``legend_lines`` — the shared §7 legend (reused by the EXTENDED view).
25
26Diagnostics
27-----------
28Domain: OC_CHAT.MAIN
29Levels:
30 L2 - render lifecycle
31 L3 - current pointer + section counts
32 L4 - per-line assembly
33
34Contracts
35---------
36- Read-only; deterministic (no wall-clock — ``Updated`` is stamped only when a
37 ``published`` boundary date is supplied).
38- Reads only INCLUDED §3.2 columns through the ``store_read`` guard.
39
40Dependencies
41------------
42- oc_diagnostics (structured debug logging via _dbg)
43- oc_chat.registry.render.roadmap._common (lean renderer id, generated header and footer)
44- oc_chat.registry.render.roadmap.render_sections (lean file kind and its render-section manifest rows)
45- oc_chat.registry.render.roadmap.sections (per-section body rendering and cell escaping)
46- oc_chat.registry.render.roadmap.store_read (selected version, current phase and milestone reads)
47"""
48from __future__ import annotations
49
50import re
51import sqlite3
52from typing import Optional
53
54from oc_diagnostics import _dbg
55
56from oc_chat.registry.render.roadmap import sections as section_render
57from oc_chat.registry.render.roadmap import store_read
59 RENDERER_LEAN,
60 build_generated_header,
61 generated_footer,
62)
63from oc_chat.registry.render.roadmap.render_sections import FILE_KIND_LEAN, sections_for
64
65_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
66
67_TITLE = "# OC Chat Roadmap"
68#: GitHub-slugger semantics: drop everything except word chars / whitespace /
69#: hyphen, then map each whitespace char to a hyphen (no collapsing).
70_SLUG_DROP_RE = re.compile(r"[^\w\s-]")
71_SLUG_WS_RE = re.compile(r"\s")
72
73
74def render(
75 conn: sqlite3.Connection,
76 *,
77 source_hash: str,
78 logical_state_hash: str,
79 published: Optional[str] = None,
80) -> str:
81 """Render the lean V4 ``ROADMAP.md`` (header + body) as deterministic bytes."""
82 func = "render"
83 header = build_generated_header(
84 source_hash=source_hash,
85 logical_state_hash=logical_state_hash,
86 renderer=RENDERER_LEAN,
87 )
88 version = store_read.selected_roadmap_version(conn)
89 body = _render_manifest_sections(conn, FILE_KIND_LEAN)
90 lines: list[str] = [
91 header,
92 "",
93 _TITLE,
94 "",
95 _version_line(version, published),
96 "",
97 "---",
98 "",
99 *_current_block(conn),
100 "",
101 "---",
102 "",
103 *legend_lines(),
104 ]
105 toc = _toc(body)
106 if toc:
107 lines.extend(["", *toc])
108 lines.extend(["", *body])
109 lines.extend(["", *generated_footer("roadmap")])
110 _dbg("OC_CHAT.MAIN", func, f"lean rendered version={version}", 2)
111 return "\n".join(lines).rstrip() + "\n"
112
113
114def _version_line(version: Optional[str], published: Optional[str]) -> str:
115 label = version or "—"
116 if published:
117 return f"**Version:** {label} · **Updated:** {published}"
118 return f"**Version:** {label}"
119
120
121def _current_block(conn: sqlite3.Connection) -> list[str]:
122 code = store_read.current_phase_code(conn)
123 if code is None:
124 return [store_read.ALL_COMPLETE_LINE]
125 milestone = next(
126 (row for row in store_read.milestone_rows(conn) if row.phase_code == code), None
127 )
128 if milestone is None: # defensive — current_phase_code derives from the same set
129 return [store_read.ALL_COMPLETE_LINE]
130 done = f"{milestone.done}/{milestone.total}" if milestone.total else "—"
131 label = f"{milestone.phase_code} — {section_render.escape(milestone.title)}"
132 # Clickable only when a phase-detail section exists to anchor to (the current
133 # phase has children -> a rendered '### {code} — {title}' heading); a childless
134 # phase has no detail section, so emit plain text rather than a dead link.
135 if milestone.total > 0:
136 anchor = _slug(f"{milestone.phase_code} — {milestone.title}")
137 label = f"[{label}](#{anchor})"
138 return [
139 f"> ### 📍 We are here now → {label}",
140 f"> {done} sub-phases complete · {section_render.escape(milestone.purpose)}",
141 ]
142
143
144def legend_lines() -> list[str]:
145 """The shared §7 legend (state + priority glyph tables)."""
146 return [
147 "## Legend",
148 "",
149 "**State:** ✅ complete · 🔄 in progress · ⬜ not started · 🔒 blocked · "
150 "↩ deferred/superseded → named successor · 🚫 deferred (no successor)",
151 "",
152 "**Priority:** 🔴 critical · 🟠 high · 🟡 medium · 🟢 lower · — n/a",
153 ]
154
155
156def _render_manifest_sections(conn: sqlite3.Connection, file_kind: str) -> list[str]:
157 """Render each active manifest section (in order) into body lines."""
158 body: list[str] = []
159 for row in sections_for(conn, file_kind):
160 params = _params(row["query_params_json"])
161 rendered = section_render.render_section(
162 conn,
163 query_kind=str(row["query_kind"]),
164 section_path=str(row["section_path"]),
165 params=params,
166 )
167 if not rendered:
168 continue
169 if body:
170 body.append("")
171 body.extend(rendered)
172 return body
173
174
175def _params(value: Optional[str]) -> Optional[dict]:
176 if not value:
177 return None
178 import json
179
180 return json.loads(value)
181
182
183def _toc(body: list[str]) -> list[str]:
184 headings = [line[3:].strip() for line in body if line.startswith("## ")]
185 if not headings:
186 return []
187 toc = ["## Contents", ""]
188 toc.extend(f"- [{heading}](#{_slug(heading)})" for heading in headings)
189 return toc
190
191
192def _slug(text: str) -> str:
193 """GitHub-slugger-accurate anchor for a heading (TOC links + the current
194 pointer's in-doc link): lowercase, drop punctuation (em-dash / ``+`` / ``&`` /
195 backticks…), then each whitespace char -> ``-`` (no run-collapsing, so a
196 ``— ``-style separator yields ``--`` exactly as GitHub/VSCode resolve it)."""
197 text = _SLUG_DROP_RE.sub("", text.strip().lower())
198 return _SLUG_WS_RE.sub("-", text)
199
200
201__all__ = ["render", "legend_lines"]
Optional[dict] _params(Optional[str] value)
str _version_line(Optional[str] version, Optional[str] published)
list[str] _render_manifest_sections(sqlite3.Connection conn, str file_kind)
list[str] _current_block(sqlite3.Connection conn)