oc_chat
Loading...
Searching...
No Matches
seed_142_fix_dependency_directions.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tools/seed_142_fix_dependency_directions.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/seed/test_dependency_directions.py"
6# OCT-LINT: disable=no_hardcoded_secrets reason="Roadmap item ids + edge kinds are not secrets"
7"""
8Dependency-direction corrections (Plan 142 prose-preservation Phase 4, GATE4 Part A).
9
10Purpose
11-------
12The proper render exposed INVERTED ``depends_on`` edges in the P1.7-ingested seed
13(a dependent "blocked by" a *later / not-started* predecessor โ€” chronologically
14impossible). This tool FIXES the clearly-inverted edges (each evidence-backed,
15see ``tools/142_dependency_direction_audit.md``); the genuinely-ambiguous ones are
16left untouched and FLAGGED in the audit for an operator decision.
17
18The final, operator-confirmed edge set (GATE4 direction fixes + the GATE4 ADDENDUM
19cleanup bundle). ``FIXES`` re-points ``depends_on``; ``STATUS_FIXES`` corrects a
20stale status:
21- ``M2.depends_on -> [M1]`` (was [M3,M7.4] inverted; + legacy "M2 Depends on: M1").
22- ``M3.depends_on -> [M2]`` RETIRED at Plan 151 T0 (operator M2->M3 gate waiver).
23- ``M7.depends_on -> [M2, M5]`` (legacy "M7 Depends on: M2 + M5"; M7.4 nested -> phase-level).
24- ``G.4a.depends_on -> []`` + ``G.5a.depends_on -> [G.4a]`` (inverted vs G.5a's prose).
25- ``B.depends_on -> []`` ยท ``C.depends_on -> []`` ยท ``M11-GATE.depends_on -> []``
26 (3 spurious chronologically-impossible edges removed).
27- ``M11-GATE.status blocked -> done`` (legacy shows M11-GATE โœ… done at Round 24).
28
29Edge ``kind`` stays ``gates`` (``gates``/``blocks`` render identically as a ๐Ÿ”’
30blocker). No item is added or removed (fidelity preserved); only ``depends_on`` +
31the one status are re-pointed. Deterministic + idempotent.
32
33Responsibilities
34----------------
35- ``apply(source_dir)`` โ€” load each affected record, re-point ``depends_on``,
36 re-emit canonically; ``FIXES`` is the declarative edit set.
37
38Diagnostics
39-----------
40Domain: OC_CHAT.MAIN
41Levels:
42 L2 - fix lifecycle
43 L3 - per-record edit
44 L4 - per-edge detail
45
46Contracts
47---------
48- Deterministic + idempotent; writes only the 5 affected ``source`` records;
49 never touches the live legacy roadmap; every record re-validates + canonicalises.
50
51Dependencies
52------------
53- oc_diagnostics (structured debug logging via _dbg)
54- oc_chat.registry.regenerate (imported for the contracts bootstrap it runs)
55- option_c_registry_contracts (DependencyEdge - the corrected depends_on edges)
56- oc_chat.registry.source.canonical (load_record and format_record_text - reads each affected record and re-emits it canonically)
57- oc_chat.registry.source.scanner (scan_source_tree - finds the phase and roadmap item records by stem)
58"""
59from __future__ import annotations
60
61import sys
62from dataclasses import dataclass, field
63from pathlib import Path
64
65# Force the WORKTREE-local oc_chat ahead of the editable main-repo install.
66sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
67
68from oc_diagnostics import _dbg
69
70import oc_chat.registry.regenerate # noqa: F401 (contracts bootstrap)
71import option_c_registry_contracts as rc
72
73from oc_chat.registry.source.canonical import format_record_text, load_record
74from oc_chat.registry.source.scanner import scan_source_tree
75
76_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
77
78#: record-id -> the corrected ``depends_on`` predecessor ids (kind ``gates``).
79#: The final, operator-confirmed edge set (GATE4 + the GATE4 ADDENDUM bundle).
80FIXES: dict[str, list[str]] = {
81 "M2": ["M1"], # was [M3, M7.4] inverted; + legacy "M2 Depends on: M1" (addendum)
82 # "M3": ["M2"] RETIRED (Plan 151 T0, 2026-07-16): the operator-ratified
83 # M2->M3 gate WAIVER (151 dispatch pin, branch A; waiver record
84 # 2026-07-16-151-t0-m2-m3-gate-waiver) removed M3's depends_on gate so the
85 # migration could start with M2 non-terminal. Re-adding the edge here made
86 # this one-shot fixer NON-idempotent against the committed seed (it
87 # re-locked M3 on every run โ€” caught at 151 T2 when the suite mutated the
88 # working tree). The 142-era evidence for the other edges is unchanged.
89 "M7": ["M2", "M5"], # legacy "M7 Depends on: M2 + M5" (M7.4 nested -> phase-level; +M5 addendum)
90 "G.4a": [], # was [G.5a] โ€” inverted vs G.5a's own prose
91 "G.5a": ["G.4a"], # G.5a body: "Depends on G.4a cleanup"
92 "B": [], # drop spurious B<-M11 (chronologically impossible, no prose link)
93 "C": [], # drop spurious C<-G.5a + C<-M11 (same)
94 "M11-GATE": [], # drop spurious M11-GATE<-M11 (it ran mid-M11, never blocked-on-complete-M11)
95}
96
97#: record-id -> corrected ``status`` (the ยง9-3b-style status correction).
98STATUS_FIXES: dict[str, str] = {
99 # Legacy Git-Workflow table shows M11-GATE โœ… done at Round 24 (merge 970d48a);
100 # the seed's "blocked" was the stale-status artifact behind the spurious M11 edge.
101 "M11-GATE": "done",
102}
103
104
105@dataclass
106class FixReport:
107 records_changed: int = 0
108 edits: list = field(default_factory=list) # (record_id, change_description)
109
110
111def _edges(ids: list[str]) -> list[rc.DependencyEdge]:
112 return [rc.DependencyEdge(id=item_id, kind="gates") for item_id in ids]
113
114
115def _before(model: object) -> list[str]:
116 out: list[str] = []
117 for edge in getattr(model, "depends_on", []) or []:
118 out.append(edge.id if isinstance(edge, rc.DependencyEdge) else str(edge))
119 return out
120
121
122def apply(source_dir: Path) -> FixReport:
123 """Apply the declarative ``depends_on`` + ``status`` fixes to the source records."""
124 func = "apply"
125 report = FixReport()
126 by_stem = {sc.stem: sc for sc in scan_source_tree(Path(source_dir)) if sc.kind in ("phase", "roadmap_item")}
127 record_ids = list(dict.fromkeys([*FIXES, *STATUS_FIXES])) # union, stable order
128 for record_id in record_ids:
129 scanned = by_stem.get(record_id)
130 if scanned is None:
131 _dbg("OC_CHAT.MAIN", func, f"record {record_id} not found โ€” skipped", 2)
132 continue
133 model, body = load_record(scanned)
134 update: dict[str, object] = {}
135 change: list[str] = []
136 if record_id in FIXES:
137 before, after = _before(model), list(FIXES[record_id])
138 if before != after:
139 update["depends_on"] = _edges(after)
140 change.append(f"depends_on {before} -> {after}")
141 if record_id in STATUS_FIXES:
142 before_status = getattr(model, "status", None)
143 if before_status != STATUS_FIXES[record_id]:
144 update["status"] = STATUS_FIXES[record_id]
145 change.append(f"status {before_status} -> {STATUS_FIXES[record_id]}")
146 if not update:
147 continue
148 updated = model.model_copy(update=update)
149 scanned.path.write_text(
150 format_record_text(updated, body=body), encoding="utf-8", newline=""
151 )
152 report.records_changed += 1
153 report.edits.append((record_id, "; ".join(change)))
154 _dbg("OC_CHAT.MAIN", func, f"{record_id}: {'; '.join(change)}", 3)
155 _dbg("OC_CHAT.MAIN", func, f"changed {report.records_changed} records", 2)
156 return report
157
158
159def _main() -> int:
160 func = "_main"
161 repo = Path(__file__).resolve().parents[1]
162 report = apply(repo / "oc_chat" / "roadmap" / "source")
163 _dbg("OC_CHAT.MAIN", func, f"records_changed={report.records_changed}", 2)
164 for record_id, change in report.edits:
165 print(f"{record_id}: {change}")
166 print(f"records_changed={report.records_changed}")
167 return 0
168
169
170if __name__ == "__main__":
171 raise SystemExit(_main())