7Shared plumbing for the living roadmap GeneratedView set (Plan 142 P1.6, §6).
11The pieces every roadmap GeneratedView shares: the **parameterized generated
12header** (build + parse), the canonical **renderer-id** set, the **glyph/label
13maps** for the §7 status/priority enums, and the **render guard** that forbids
14reading a runtime-excluded table or an EXCLUDED column from a source-only
15projection (Registry Source & Storage Spec V0.22 §3.2 / §6 / §7 — AC §8.30).
16The crash-safe atomic writer is reused from ``registry/render/_common.py``
17(the §6 shared ``atomic_write_bytes``).
21- ``build_generated_header`` / ``parse_generated_header`` — the §6 four-line
22 HTML-comment header carrying ``source_hash`` / ``logical_state_hash`` /
24- ``status_glyph`` / ``status_label`` / ``priority_glyph`` — the §7 enum→glyph
25 mapping (single source for every renderer; AC §8.7).
26- ``assert_render_query_safe`` / ``guarded_query`` — reject a render query that
27 reads a runtime-excluded table or an EXCLUDED column (AC §8.30).
28- ``atomic_write_text`` — atomic UTF-8 (LF, no BOM) write via the shared writer.
35 L3 - parsed header fields
36 L4 - per-query guard probes
40- The header is output-only metadata; ``render --check`` parses it and treats a
41 drifted/absent header (a hand-edit) as ``E_ROADMAP_UNTRACKED_EDIT`` drift.
42- A source-only GeneratedView reads only INCLUDED columns of source-rebuildable
43 tables; the guard fails closed on any runtime-excluded table/column token.
47- oc_diagnostics (structured debug logging via _dbg)
48- oc_chat.registry.render._common (the shared atomic writer)
50from __future__
import annotations
55from pathlib
import Path
56from typing
import Optional
58from oc_diagnostics
import _dbg
62_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
65RENDERER_LEAN =
"roadmap-lean-v4"
66RENDERER_EXTENDED =
"roadmap-extended-v3"
67RENDERER_CHANGELOG =
"roadmap-changelog-v1"
68RENDERER_ROOT_CHANGELOG =
"changelog-product-v2"
69RENDERER_IDS = frozenset(
70 {RENDERER_LEAN, RENDERER_EXTENDED, RENDERER_CHANGELOG, RENDERER_ROOT_CHANGELOG}
76LIVING_SET_FILENAMES: dict[str, str] = {
77 "roadmap":
"ROADMAP.md",
78 "roadmap_extended":
"ROADMAP-EXTENDED.md",
79 "roadmap_changelog":
"ROADMAP-CHANGELOG.md",
80 "root_changelog":
"CHANGELOG.md",
84_FOOTER_LABELS: dict[str, str] = {
85 "roadmap":
"Lean roadmap",
86 "roadmap_extended":
"Extended record",
87 "roadmap_changelog":
"Roadmap changelog",
88 "root_changelog":
"Product changelog",
94_ARTIFACT_DIRS: dict[str, str] = {
95 "roadmap":
"oc_chat/roadmap",
96 "roadmap_extended":
"oc_chat/roadmap",
97 "roadmap_changelog":
"oc_chat/roadmap",
103 """The artifact-relative link from ``self_key``'s file to ``target_key``'s file:
104 a same-dir companion stays flat; a roadmap view → the root changelog becomes
105 ``../../CHANGELOG.md``; the root changelog → a roadmap view becomes
106 ``oc_chat/roadmap/ROADMAP*.md`` (purely lexical via ``posixpath``)."""
107 target = posixpath.join(_ARTIFACT_DIRS[target_key], LIVING_SET_FILENAMES[target_key])
108 return posixpath.relpath(target, _ARTIFACT_DIRS[self_key]
or ".")
112 """The generated footer for a living view: a do-not-hand-edit provenance note
113 + **artifact-relative** companion links to the *current* rendered set (never a
114 stale versioned filename, and resolvable from this artifact's own write-dir).
115 ``self_key`` is the artifact being rendered (excluded from its own list)."""
116 companions =
" · ".join(
117 f
"[{_FOOTER_LABELS[key]}]({_companion_link(self_key, key)})"
118 for key
in LIVING_SET_FILENAMES
124 "_Generated from the registry source (`oc_chat/roadmap/source`) — do not "
125 "hand-edit; edit the source records and run `oct agent roadmap render` to "
126 f
"regenerate. Companion views: {companions}._",
132RUNTIME_EXCLUDED_TABLES = frozenset(
134 "roadmap_plan_links",
135 "roadmap_item_events",
147EXCLUDED_COLUMNS = frozenset(
170 "todo":
"Not started",
171 "in_progress":
"In progress",
173 "blocked":
"Blocked",
174 "deferred":
"Deferred",
175 "superseded":
"Superseded",
188TERMINAL_STATUSES = frozenset({
"done",
"deferred",
"superseded"})
190_HEADER_OPEN =
"<!-- GENERATED by oct agent render; do not hand-edit."
192_HEADER_RE = re.compile(
194 r"<!-- GENERATED by oct agent render; do not hand-edit\.\s*"
195 r"source_hash=(?P<source_hash>\S+)\s+"
196 r"logical_state_hash=(?P<logical_state_hash>\S+)\s+"
197 r"renderer=(?P<renderer>\S+)\s*-->",
199_IDENTIFIER_RE = re.compile(
r"[A-Za-z_][A-Za-z0-9_]*")
203 """A render query touched a runtime-excluded table or EXCLUDED column."""
207 """Return the §7 glyph for ``status``.
209 ``deferred`` with a named successor renders ``↩`` (redirect); ``deferred``
210 with no successor renders ``🚫`` (no successor). ``superseded`` always has a
211 ``superseded_by`` and renders ``↩``.
213 if status ==
"deferred":
214 return "↩" if has_successor
else "🚫"
215 return _STATUS_GLYPH.get(status,
"⬜")
219 superseded_by: Optional[str] =
None) -> str:
220 """Return the §7 render label (with the redirect target when present)."""
221 if status ==
"deferred":
222 return f
"Deferred → {successor}" if successor
else "Deferred (no successor)"
223 if status ==
"superseded" and superseded_by:
224 return f
"Superseded by {superseded_by}"
225 return _STATUS_LABEL.get(status, status)
229 """Return the §7 priority glyph; ``—`` for null (not applicable)."""
232 return _PRIORITY_GLYPH.get(priority,
"—")
236 *, source_hash: str, logical_state_hash: str, renderer: str
238 """Return the §6 four-line generated header (HTML comment)."""
239 if renderer
not in RENDERER_IDS:
240 raise ValueError(f
"unknown renderer id {renderer!r}")
243 f
"source_hash={source_hash}\n"
244 f
"logical_state_hash={logical_state_hash}\n"
245 f
"renderer={renderer} {_HEADER_CLOSE}"
250 """Parse the §6 header from rendered ``text``; ``None`` if absent/malformed.
252 A ``None`` result is itself drift (a hand-edit that dropped the header).
254 func =
"parse_generated_header"
255 match = _HEADER_RE.search(text)
258 fields = match.groupdict()
259 _dbg(
"OC_CHAT.MAIN", func, f
"renderer={fields['renderer']}", 3)
264 """Raise ``RenderGuardError`` if ``sql`` reads a runtime-excluded table or
265 EXCLUDED column (AC §8.30 — the source-only render guard / column-scope guard).
267 tokens = {token.lower()
for token
in _IDENTIFIER_RE.findall(sql)}
268 bad_tables = tokens & RUNTIME_EXCLUDED_TABLES
271 "source-only render query reads runtime-excluded table(s) "
272 f
"{sorted(bad_tables)}; a runtime-checkpoint-hash header is required"
274 bad_columns = tokens & EXCLUDED_COLUMNS
277 "render query references §3.2 EXCLUDED column(s) "
278 f
"{sorted(bad_columns)}; these differ across rebuilds"
283 conn: sqlite3.Connection, sql: str, params: tuple = ()
285 """Run a render SELECT through ``assert_render_query_safe`` first (§8.30)."""
287 return conn.execute(sql, params)
291 """Atomic-write ``text`` as UTF-8 (LF, no BOM) via the shared §6 writer."""
293 atomic_write_bytes(path, text.encode(
"utf-8"))
300 "RENDERER_CHANGELOG",
301 "RENDERER_ROOT_CHANGELOG",
303 "LIVING_SET_FILENAMES",
305 "RUNTIME_EXCLUDED_TABLES",
312 "build_generated_header",
313 "parse_generated_header",
314 "assert_render_query_safe",
str _companion_link(str self_key, str target_key)
str priority_glyph(Optional[str] priority)
Path atomic_write_text(Path path, str text)
Optional[dict[str, str]] parse_generated_header(str text)
list[str] generated_footer(str self_key)
str status_label(str status, *, Optional[str] successor=None, Optional[str] superseded_by=None)
str status_glyph(str status, *, bool has_successor=False)
None assert_render_query_safe(str sql)
sqlite3.Cursor guarded_query(sqlite3.Connection conn, str sql, tuple params=())
str build_generated_header(*, str source_hash, str logical_state_hash, str renderer)