oc_chat
Loading...
Searching...
No Matches
seed_142_changelog_reseed.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tools/seed_142_changelog_reseed.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/seed/test_changelog_reseed.py"
6"""
7Full changelog re-seed (byte-faithful): ROADMAP-CHANGELOG-v3.38.md ->
8source/changelog/** (Plan 142 prose-preservation Phase 1, §9 completeness).
9
10Purpose
11-------
12Tile the COMPLETE authoritative changelog (the WHOLE file: preamble + every
13``# ▶`` era/section banner + every ``## `` block + the pre-v2.27 historical
14tables + the velocity reference + the end note) into one record per top-level
15segment, each record **body = the verbatim original slice**. The render
16(``changelog._render_verbatim``) concatenates the bodies in ``changelog_seq``
17order joined by ``\\n\\n---\\n\\n`` -> byte-identical to the original (modulo only
18the generated provenance header). No frontmatter duplication (no junk
19``entries``, no re-typed ``notes``) — the body is the single source of visible
20prose, so the ``option_c_specs``-class transcription error cannot recur.
21
22Keying (surfaced at the gate): one ``kind: roadmap`` record per segment, ordered
23by ``extensions.changelog_seq``; ``version`` = the real ``vN.NN`` for a bumped
24round (creates a ``roadmap_versions`` row for the lean/extended render), the real
25product SemVer for a ``v0.7.x`` release (no store row), or a synthesized
26``v0.0.<seq>`` SemVer for the framing / no-version-bump / summary segments (no
27store row, unique). ``era`` = ``v3`` (metadata; the visible era banners live in
28the segment bodies, so the renderer needs no era-grouping logic).
29
30Responsibilities
31----------------
32- ``segments`` (split on the ``---`` rule), ``classify``, ``reseed``, ``coverage``.
33
34Diagnostics
35-----------
36Domain: OC_CHAT.MAIN
37Levels:
38 L2 - reseed lifecycle
39 L3 - per-segment classification
40 L4 - per-record emission
41
42Contracts
43---------
44- Deterministic; never writes outside the source tree's ``changelog/``; every
45 record validates; the segment bodies tile the original byte-for-byte.
46
47Dependencies
48------------
49- oc_diagnostics (structured debug logging via _dbg)
50- oc_chat.registry.regenerate (imported for the contracts bootstrap it runs)
51- option_c_registry_contracts (the Version model, SCHEMA_VERSION and ROADMAP_VERSION_RE for each changelog record)
52- oc_chat.registry.source.canonical (format_record_text - writes each record's frontmatter and verbatim body)
53"""
54from __future__ import annotations
55
56import re
57from dataclasses import dataclass, field
58from pathlib import Path
59
60from oc_diagnostics import _dbg
61
62import oc_chat.registry.regenerate # noqa: F401 (contracts bootstrap)
63import option_c_registry_contracts as rc
64
65from oc_chat.registry.source.canonical import format_record_text
66
67_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
68
69_ERA = "v3"
70_SEPARATOR = "\n\n---\n\n"
71_BUMP_RE = re.compile(r"→\s*v(\d+\.\d{2})\b")
72_SEMVER_RE = re.compile(r"\bv(\d+\.\d+\.\d+)\b")
73_ISO_DATE_RE = re.compile(r"(\d{4}-\d{2}-\d{2})")
74_DEFAULT_DATE = "2026-06-02"
75
76
77@dataclass
79 records: int = 0
80 segments: int = 0
81 store_versions: int = 0
82 coverage: list = field(default_factory=list) # (seq, version, header_excerpt)
83
84
85def segments(text: str) -> list[str]:
86 """Split the changelog into top-level segments on the ``---`` horizontal rule.
87
88 Every ``---`` in the file separates two top-level sections (preamble, era
89 banners, ``## `` blocks, the historical/velocity tables, the end note); there
90 are no ``---`` inside a block, so this tiling is exact + lossless.
91 """
92 body = text.replace("\r\n", "\n").strip("\n")
93 return [chunk.strip("\n") for chunk in body.split(_SEPARATOR)]
94
95
96def _iso_date(segment: str) -> str:
97 match = _ISO_DATE_RE.search(segment)
98 return match.group(1) if match else _DEFAULT_DATE
99
100
101def classify(segment: str, seq: int) -> str:
102 """Return the ``version`` for a segment (real vN.NN/SemVer or synthesized)."""
103 header = segment.splitlines()[0] if segment else ""
104 bump = _BUMP_RE.search(header)
105 if bump is not None:
106 return f"v{bump.group(1)}" # bumped round -> roadmap-axis (store row)
107 if header.startswith("## ") and "v3.00 ROADMAP supersession" in header:
108 return "v3.00"
109 semver = _SEMVER_RE.search(header)
110 if header.startswith("## ") and semver is not None:
111 return f"v{semver.group(1)}" # v0.7.x product release (no store row)
112 return f"v0.0.{seq}" # framing / no-version-bump / summary (no store row)
113
114
115def _slug(segment: str) -> str:
116 header = (segment.splitlines()[0] if segment else "").lstrip("# ")
117 slug = re.sub(r"[^a-z0-9]+", "-", header.lower()).strip("-")
118 return slug[:48] or "segment"
119
120
121def reseed(changelog_md: Path, source_dir: Path) -> ReseedReport:
122 """Tile the legacy changelog into verbatim ``source/changelog/roadmap/*`` records."""
123 func = "reseed"
124 text = Path(changelog_md).read_text(encoding="utf-8")
125 chunks = segments(text)
126 out = Path(source_dir) / "changelog" / "roadmap"
127 for stale in (Path(source_dir) / "changelog").rglob("*.md"):
128 stale.unlink()
129 out.mkdir(parents=True, exist_ok=True)
130
131 report = ReseedReport(segments=len(chunks))
132 for index, chunk in enumerate(chunks, start=1):
133 version = classify(chunk, index)
134 if re.fullmatch(rc.ROADMAP_VERSION_RE, version):
135 report.store_versions += 1
136 model = rc.Version(
137 schema_version=rc.SCHEMA_VERSION,
138 version=version,
139 date=_iso_date(chunk),
140 era=_ERA,
141 kind="roadmap",
142 extensions={"changelog_seq": index},
143 )
144 path = out / f"{index:03d}-{_slug(chunk)}.md"
145 path.write_text(format_record_text(model, body=chunk), encoding="utf-8", newline="")
146 header = chunk.splitlines()[0] if chunk else ""
147 report.coverage.append((index, version, header[:80]))
148 report.records = len(chunks)
149 _dbg("OC_CHAT.MAIN", func, f"reseeded {len(chunks)} segments", 2)
150 return report
str classify(str segment, int seq)
ReseedReport reseed(Path changelog_md, Path source_dir)