7Idempotent ROADMAP corpus ingest into the registry.
11Populate the roadmap v0.3 registry tables from the on-disk ROADMAP corpus.
15- Walk and parse the four ROADMAP artifact families.
16- Seed static version, phase, track, and render-section metadata.
17- Emit roadmap item and dependency operations only for semantic changes.
24 L3 - file and row counts
25 L4 - per-record idempotence decisions
29- Re-running against unchanged inputs creates no new operation_log rows.
30- Dry-run reports pending changes without writing registry rows.
34- oc_chat.registry.roadmap.parser (markdown parsing)
35- oc_chat.registry.roadmap.operations (mutating seam helpers)
36- oc_chat.registry.api (Registry facade the operations are executed through)
37- oc_chat.registry.backends.sqlite (SQLiteBackend connections and transactions for the metadata upserts)
38- oc_chat.registry.canonical (canonical JSON for the idempotence hash)
39- oc_diagnostics (structured debug logging via _dbg)
41from __future__
import annotations
43from dataclasses
import dataclass
44from pathlib
import Path
46from typing
import Mapping
48from oc_diagnostics
import _dbg
54 roadmap_item_add_dependency,
66_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
69@dataclass(frozen=True)
77 errors: tuple[str, ...] = ()
78 warnings: tuple[str, ...] = ()
88 "errors": list(self.
errors),
95 roadmap_dir: str | Path,
97 actor_slot: str |
None =
None,
98 actor_type: str =
"human_operator",
99 dry_run: bool =
False,
101 """Parse and ingest every ROADMAP artifact under *roadmap_dir*."""
104 slot = actor_slot
or backend.active_operator_slot()
113 errors=(
"no active operator slot exists",),
115 path = Path(roadmap_dir)
117 parsed: list[ParsedRoadmap] = []
118 errors: list[str] = []
119 for file_path
in files:
121 parsed.append(parse_roadmap_file(file_path))
122 except Exception
as exc:
123 errors.append(f
"{file_path}: {exc}")
132 errors=tuple(errors),
139 with backend.connect()
as conn:
140 for roadmap
in parsed:
141 for item
in roadmap.items:
143 if action ==
"create":
145 elif action ==
"update":
158 with backend.transaction(
"immediate")
as conn:
161 for roadmap
in parsed:
162 for item
in roadmap.items:
163 with backend.connect()
as conn:
170 item_id=item.item_id,
172 track_kind=item.track_kind,
173 phase_code=item.phase_code,
174 parent_item_id=item.parent_item_id,
177 priority=item.priority,
178 sort_order=item.sort_order,
179 target_version=item.target_version,
180 introduced_in_version_id=roadmap.version_id,
182 actor_type=actor_type,
183 command=
"roadmap import",
186 if action ==
"create":
191 with backend.transaction(
"immediate")
as conn:
194 for roadmap
in parsed:
195 for dependency
in roadmap.dependencies:
196 with backend.connect()
as conn:
197 exists = conn.execute(
199 "SELECT 1 FROM roadmap_item_dependencies "
200 "WHERE from_item_id = ? AND to_item_id = ? AND kind = ?",
202 dependency.from_item_id,
203 dependency.to_item_id,
207 if exists
is not None:
209 roadmap_item_add_dependency(
211 from_item_id=dependency.from_item_id,
212 to_item_id=dependency.to_item_id,
213 kind=dependency.kind,
214 in_version_id=roadmap.version_id,
216 actor_type=actor_type,
217 command=
"roadmap import",
232 if not path.is_dir():
233 raise FileNotFoundError(path)
236 for item
in path.glob(
"ROADMAP*.md")
237 if item.is_file()
and "-v" in item.stem
242 conn: sqlite3.Connection,
243 roadmaps: list[ParsedRoadmap],
246 latest_by_era: dict[str, str] = {}
247 for roadmap
in sorted(roadmaps, key=
lambda item: item.version_id):
250 latest_by_era[roadmap.era_id] = roadmap.version_id
251 for track
in roadmap.tracks:
253 for phase
in roadmap.phases:
255 for section
in roadmap.render_sections:
257 for era_id, version_id
in latest_by_era.items():
260 "UPDATE roadmap_versions SET status = 'draft' "
261 "WHERE era_id = ? AND version_id != ? AND status = 'current'",
262 (era_id, version_id),
266 "UPDATE roadmap_versions SET status = 'current' WHERE version_id = ?",
271def _upsert_era(conn: sqlite3.Connection, roadmap: ParsedRoadmap, now: str) ->
None:
274 "INSERT INTO roadmap_eras("
275 "era_id, era_label, major_bump_reason, started_at, ended_at, archived_under, sort_order"
276 ") VALUES (?, ?, ?, ?, NULL, NULL, ?) "
277 "ON CONFLICT(era_id) DO UPDATE SET "
278 "era_label = excluded.era_label",
281 f
"{roadmap.era_id} roadmap era",
282 None if roadmap.era_id !=
"v3" else "Migration era",
283 roadmap.created_at
or now,
290 conn: sqlite3.Connection,
291 roadmap: ParsedRoadmap,
296 "INSERT INTO roadmap_versions("
297 "version_id, era_id, released_at, status, parent_version_id, "
298 "supersedes_version_id, superseded_by_version_id, last_updated_at, "
299 "source_round_no, bump_kind, notes"
300 ") VALUES (?, ?, ?, ?, NULL, NULL, NULL, ?, NULL, ?, ?) "
301 "ON CONFLICT(version_id) DO UPDATE SET "
302 "released_at = excluded.released_at, "
303 "last_updated_at = excluded.last_updated_at, "
304 "bump_kind = excluded.bump_kind, "
305 "notes = excluded.notes",
311 roadmap.last_updated_at,
321 "INSERT INTO roadmap_tracks(track_code, label, description, sort_order, active) "
322 "VALUES (?, ?, ?, ?, 1) "
323 "ON CONFLICT(track_code) DO UPDATE SET "
324 "label = excluded.label, "
325 "description = excluded.description, "
326 "sort_order = excluded.sort_order, "
328 (track.track_code, track.label, track.description, track.sort_order),
333 conn: sqlite3.Connection,
339 if phase.parent_phase_code
is not None:
341 "SELECT phase_id FROM roadmap_phases WHERE phase_code = ?",
342 (phase.parent_phase_code,),
345 parent_id = int(row[
"phase_id"])
348 "INSERT INTO roadmap_phases("
349 "phase_code, label, parent_phase_id, branch_suggestion, commit_prefix, "
350 "target_version, priority_label, parallel_track, full_spec_url, sort_order, "
351 "introduced_in_version_id, retired_in_version_id, created_at, updated_at"
352 ") VALUES (?, ?, ?, NULL, NULL, ?, ?, ?, NULL, ?, ?, NULL, ?, ?) "
353 "ON CONFLICT(phase_code) DO UPDATE SET "
354 "label = excluded.label, "
355 "parent_phase_id = COALESCE(roadmap_phases.parent_phase_id, excluded.parent_phase_id), "
356 "priority_label = COALESCE(excluded.priority_label, roadmap_phases.priority_label), "
357 "parallel_track = excluded.parallel_track, "
358 "sort_order = excluded.sort_order, "
359 "updated_at = excluded.updated_at",
364 phase.target_version,
365 phase.priority_label,
366 1
if phase.parallel_track
else 0,
376 conn: sqlite3.Connection,
377 section: ParsedRenderSection,
382 "DELETE FROM roadmap_render_sections WHERE file_kind = ? AND section_path = ?",
383 (section.file_kind, section.section_path),
387 "INSERT INTO roadmap_render_sections("
388 "file_kind, section_path, sort_order, query_kind, query_params_json, "
389 "required_min_version, active"
390 ") VALUES (?, ?, ?, ?, ?, ?, 1)",
393 section.section_path,
396 section.query_params_json,
403 conn: sqlite3.Connection,
404 roadmaps: list[ParsedRoadmap],
407 for roadmap
in roadmaps:
408 for item
in roadmap.items:
411 "SELECT track_id FROM roadmap_tracks WHERE track_code = ?",
418 "INSERT OR IGNORE INTO roadmap_item_tracks(item_id, track_id, created_at) "
420 (item.item_id, int(row[
"track_id"]), now),
427 "SELECT i.item_id, i.track_kind, p.phase_code, i.parent_item_id, "
428 "i.title, i.prose, i.status, i.priority, i.sort_order, i.target_version "
429 "FROM roadmap_items_v3 i "
430 "LEFT JOIN roadmap_phases p ON p.phase_id = i.phase_id "
431 "WHERE i.item_id = ?",
437 "track_kind": row[
"track_kind"],
438 "phase_code": row[
"phase_code"],
439 "parent_item_id": row[
"parent_item_id"],
440 "title": row[
"title"],
441 "prose": row[
"prose"],
442 "status": row[
"status"],
443 "priority": row[
"priority"],
444 "sort_order": int(row[
"sort_order"]),
445 "target_version": row[
"target_version"],
448 "track_kind": item.track_kind,
449 "phase_code": item.phase_code,
450 "parent_item_id": item.parent_item_id,
453 "status": item.status,
454 "priority": item.priority,
455 "sort_order": item.sort_order,
456 "target_version": item.target_version,
464 return hashlib.sha256(canonical_json(dict(value)).encode(
"utf-8")).hexdigest()
468 if item.track_kind
in {
"m_phase",
"vision",
"git_workflow",
"pre_m0_era"}:
469 return item.track_kind
470 if item.item_id.startswith(
"L"):
472 if item.item_id.startswith(
"U"):
478 text = era_id.removeprefix(
"v")
479 return int(text)
if text.isdigit()
else 999
483 backend = registry.backend
484 if not isinstance(backend, SQLiteBackend):
485 raise TypeError(
"roadmap ingest requires SQLiteBackend")
dict[str, object] to_dict(self)
None _upsert_static_metadata(sqlite3.Connection conn, list[ParsedRoadmap] roadmaps, str now)
None _upsert_phase(sqlite3.Connection conn, ParsedPhase phase, str version_id, str now)
None _upsert_render_section(sqlite3.Connection conn, ParsedRenderSection section, str version_id)
str _track_code_for_item(ParsedItem item)
list[Path] _roadmap_files(Path path)
None _upsert_version(sqlite3.Connection conn, ParsedRoadmap roadmap, str status)
None _upsert_track(sqlite3.Connection conn, ParsedTrack track)
str _canonical_hash(Mapping[str, object] value)
int _era_sort_order(str era_id)
None _upsert_era(sqlite3.Connection conn, ParsedRoadmap roadmap, str now)
str _item_action(sqlite3.Connection conn, ParsedItem item)
IngestResult ingest_corpus(Registry registry, str|Path roadmap_dir, *, str|None actor_slot=None, str actor_type="human_operator", bool dry_run=False)
None _upsert_item_tracks(sqlite3.Connection conn, list[ParsedRoadmap] roadmaps, str now)
SQLiteBackend _sqlite_backend(Registry registry)