oc_chat
Loading...
Searching...
No Matches
seed_142_framing_notes.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tools/seed_142_framing_notes.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/seed/test_framing_notes.py"
6"""
7Framing-section -> note records (Plan 142 prose-preservation Phase 3, GATE3 Part 2).
8
9Purpose
10-------
11Migrate the FIVE roadmap-level framing sections of the legacy
12``oc_chat/roadmap/ROADMAP-v3.38.md`` — rationale / decision content that must live
13in the registry before cutover — into ``source/notes/<id>.md`` records, mirroring
14the Phase-1 changelog-framing pattern (verbatim body, no prose in the renderer):
15
16 ``## Sources Referenced`` · ``## Reconciliation notes`` · ``## Execution Order``
17 · ``## Out Of Scope (v3.00)`` · ``## Git Workflow & Version Convention``
18
19Each becomes a ``Note`` record (``id`` + ``title``, §3.5 — EXTENDED-only, never a
20``roadmap_items_v3`` row) whose **body is the verbatim section** (heading + content,
21trailing ``---`` rule stripped). The EXTENDED render (``render.roadmap.notes``)
22emits the bodies as their own sections; the lean ROADMAP stays lean.
23
24No contract change: ``Note`` is a pre-existing P1.1 model; the scanner + pipeline
25already classify ``notes/``. Idempotent; canonical-stable.
26
27Responsibilities
28----------------
29- ``parse_framing_sections`` (slice the 5 ``## `` sections), ``backfill`` (write
30 the note records), ``coverage`` (the section -> record map for the audit).
31
32Diagnostics
33-----------
34Domain: OC_CHAT.MAIN
35Levels:
36 L2 - seed lifecycle
37 L3 - per-section extraction
38 L4 - per-record emission
39
40Contracts
41---------
42- Deterministic; writes only ``source/notes/<id>.md``; never touches the live
43 legacy roadmap; every record validates + re-canonicalises.
44
45Dependencies
46------------
47- oc_diagnostics (structured debug logging via _dbg)
48- oc_chat.registry.regenerate (imported for the contracts bootstrap it runs)
49- option_c_registry_contracts (the Note model and SCHEMA_VERSION for each framing note record)
50- oc_chat.registry.source.canonical (format_record_text - writes each note's frontmatter and verbatim body)
51"""
52from __future__ import annotations
53
54import re
55import sys
56from dataclasses import dataclass, field
57from pathlib import Path
58
59# Force the WORKTREE-local oc_chat ahead of the editable main-repo install.
60sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
61
62from oc_diagnostics import _dbg
63
64import oc_chat.registry.regenerate # noqa: F401 (contracts bootstrap)
65import option_c_registry_contracts as rc
66
67from oc_chat.registry.source.canonical import format_record_text
68
69_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
70
71#: (heading-prefix, note id) for the 5 framing sections, in legacy reading order.
72#: Each prefix is unique in the file (the ``## What's New`` blocks never match).
73_SECTIONS: tuple[tuple[str, str], ...] = (
74 ("## Sources Referenced", "sources-referenced"),
75 ("## Reconciliation notes", "reconciliation-notes"),
76 ("## Execution Order", "execution-order"),
77 ("## Out Of Scope", "out-of-scope"),
78 ("## Git Workflow", "git-workflow"),
79)
80#: A section ends at the next top-level heading / phase / the file's end note.
81_BOUNDARY_RE = re.compile(r"^(# PHASE |## |\*End of )", re.MULTILINE)
82_TRAILING_RULE_RE = re.compile(r"\n*-{3,}\s*$")
83
84
85@dataclass
87 notes_written: int = 0
88 sections: dict = field(default_factory=dict) # id -> (title, body)
89
90
91def parse_framing_sections(roadmap_text: str) -> dict[str, tuple[str, str]]:
92 """Return ``{note_id: (title, verbatim_body)}`` for the 5 framing sections."""
93 text = roadmap_text.replace("\r\n", "\n")
94 out: dict[str, tuple[str, str]] = {}
95 for prefix, note_id in _SECTIONS:
96 match = re.search(rf"^{re.escape(prefix)}.*$", text, re.MULTILINE)
97 if match is None:
98 continue
99 title = match.group(0)[3:].strip() # heading text minus "## "
100 nxt = _BOUNDARY_RE.search(text, match.end())
101 end = nxt.start() if nxt else len(text)
102 body = text[match.start():end].strip("\n")
103 body = _TRAILING_RULE_RE.sub("", body).strip("\n")
104 out[note_id] = (title, body)
105 return out
106
107
108def backfill(roadmap_md: Path, source_dir: Path) -> FramingReport:
109 """Write each framing section to ``source/notes/<id>.md`` (verbatim body)."""
110 func = "backfill"
111 sections = parse_framing_sections(Path(roadmap_md).read_text(encoding="utf-8"))
112 out_dir = Path(source_dir) / "notes"
113 out_dir.mkdir(parents=True, exist_ok=True)
114 report = FramingReport()
115 for note_id, (title, body) in sections.items():
116 model = rc.Note(schema_version=rc.SCHEMA_VERSION, id=note_id, title=title)
117 (out_dir / f"{note_id}.md").write_text(
118 format_record_text(model, body=body), encoding="utf-8", newline=""
119 )
120 report.notes_written += 1
121 report.sections[note_id] = (title, body)
122 _dbg("OC_CHAT.MAIN", func, f"wrote note {note_id} ({len(body)} chars)", 3)
123 _dbg("OC_CHAT.MAIN", func, f"wrote {report.notes_written} framing notes", 2)
124 return report
125
126
127def _main() -> int:
128 func = "_main"
129 repo = Path(__file__).resolve().parents[1]
130 roadmap = repo / "oc_chat" / "roadmap" / "ROADMAP-v3.38.md"
131 source = repo / "oc_chat" / "roadmap" / "source"
132 report = backfill(roadmap, source)
133 _dbg("OC_CHAT.MAIN", func, f"notes={report.notes_written}", 2)
134 for note_id, (title, body) in report.sections.items():
135 print(f"{note_id}: {title!r} ({len(body)} chars)")
136 print(f"notes_written={report.notes_written}")
137 return 0
138
139
140if __name__ == "__main__":
141 raise SystemExit(_main())
dict[str, tuple[str, str]] parse_framing_sections(str roadmap_text)
FramingReport backfill(Path roadmap_md, Path source_dir)