4"""fix_wave_references.py -- conservative auto-fix for "Wave N" session refs.
6Companion to check_no_wave_references.py. Walks every tracked source/doc
7file under the same scan roots and rewrites obvious session-bookkeeping
10 - "(Wave 70)" -> "" (delete the parenthetical)
11 - "[wave-3]" -> "" (delete the bracketed)
12 - " -- Wave 12" -> "" (trailing em-dash citation)
13 - "see Wave N" -> "see HUM" (placeholder cite; human reviews)
14 - "Wave 70 added X" -> "X" (drop the leading attribution)
15 - "Wave 70 fixed X" -> "Fixed: X"
16 - "Wave 70's X" -> "the X"
17 - "the wave 70 X" -> "the X"
18 - "during Wave 11" -> ""
22Anything else that the gate flagged but this script could not rewrite is
23reported on stderr so a human can fix it manually.
26 scripts/fix/fix_wave_references.py # dry-run (default)
27 scripts/fix/fix_wave_references.py --apply # write changes back
30 0 -- no remaining violations after the rewrite (or none to start with)
31 1 -- some violations remain that the script could not rewrite
33@copyright Copyright (c) 2026 Brighton Sikarskie
34SPDX-License-Identifier: MIT
37from __future__
import annotations
43from pathlib
import Path
46REPORT_SNIPPET_MAX_LEN = 120
60SKIP_DIR_NAMES = frozenset(
91SCAN_BASENAMES = frozenset({
"justfile",
"Dockerfile",
"CMakeLists.txt"})
93SELF_EXEMPT = frozenset(
95 "scripts/checks/check_no_wave_references.py",
96 "scripts/fix/fix_wave_references.py",
97 "docs/STYLE_GUIDE.md",
103REWRITES: list[tuple[re.Pattern[str], str]] = [
105 (re.compile(
r"\s*[\(\[][Ww]ave[\s_\-]?\d+[A-Za-z]?[\)\]]"),
""),
107 (re.compile(
r"\s*--\s*[Ww]ave[\s_\-]?\d+[A-Za-z]?[^.\n]*"),
""),
109 (re.compile(
r"[,;]\s*see\s+[Ww]ave[\s_\-]?\d+[A-Za-z]?", re.IGNORECASE),
""),
112 re.compile(
r"\bsee\s+[Ww]ave[\s_\-]?\d+[A-Za-z]?\b", re.IGNORECASE),
113 "see the relevant HUM section",
116 (re.compile(
r"\bthe\s+[Ww]ave[\s_\-]?\d+[A-Za-z]?\s+", re.IGNORECASE),
"the "),
120 r"\b(?:during|in|from|after|post[\s_\-]?)[Ww]ave[\s_\-]?\d+[A-Za-z]?\b", re.IGNORECASE
125 (re.compile(
r"\bpre[\s_\-]?[Ww]ave[\s_\-]?\d+[A-Za-z]?\b", re.IGNORECASE),
"previous"),
127 (re.compile(
r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?'s\b"),
"the"),
129 (re.compile(
r"^\s*[\*\#\-/\s]*[Ww]ave[\s_\-]?\d+[A-Za-z]?\s*:\s*", re.MULTILINE),
""),
133 r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?\s+(added|fixed|introduced|removed|"
134 r"refactored|reworked|landed|backfilled|swept|cleaned|wired|spawned|"
135 r"split|merged|deleted|created|reverted|enabled|disabled|brought\s+up)\b",
142 (re.compile(
r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?\s?"),
""),
145WAVE_RE = re.compile(
r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?\b")
148def _is_skip_dir(name: str) -> bool:
149 """Whether a directory name is build output or otherwise out of scope.
151 Matches the ``build-*`` family by prefix as well as the exact names, so a
152 CMake variant directory (build-cov, build-fuzz) is skipped without being
155 return name
in SKIP_DIR_NAMES
or name ==
"build" or name.startswith(
"build-")
158def should_scan(p: Path) -> bool:
159 """Whether this file's name or suffix puts it in scope.
161 Basename is checked as well as suffix so extensionless files the tree
162 cares about (CMakeLists.txt, justfile) are not missed.
164 return p.name
in SCAN_BASENAMES
or p.suffix
in SCAN_EXTS
167def iter_files(root: Path) -> list[Path]:
168 """Every in-scope file beneath the configured scan roots."""
170 for sr
in SCAN_ROOTS:
172 if not base.exists():
174 for dp, dn, fn
in os.walk(base):
175 dn[:] = [d
for d
in dn
if not _is_skip_dir(d)]
180 for top
in (
"justfile",
"CMakeLists.txt",
"README.md"):
187def rewrite_line(line: str) -> str:
188 """Delete session-bookkeeping "Wave N" references from one line.
190 Whitespace cleanup is deliberately conservative -- only the trailing and
191 doubled spaces the deletion itself introduced. These references sit inside
192 prose, and reflowing the surrounding sentence would produce a diff far
193 larger than the edit being made.
195 for rx, sub
in REWRITES:
196 line = rx.sub(sub, line)
199 return re.sub(
r" +",
" ", line).rstrip()
202def rewrite(text: str) -> str:
203 """Rewrite only the lines naming a wave, leaving the rest byte-identical."""
205 for ln
in text.splitlines():
206 if WAVE_RE.search(ln):
207 out.append(rewrite_line(ln))
210 return "\n".join(out) + (
"\n" if text.endswith(
"\n")
else "")
214 """Report, or with ``--apply`` perform, the wave-reference deletion.
216 Dry-run by default. Returns 0 when nothing needed rewriting and 1 when
217 something did, so the dry run doubles as the gate.
219 ap = argparse.ArgumentParser()
220 ap.add_argument(
"--apply", action=
"store_true", help=
"write changes back (default: dry-run)")
221 args = ap.parse_args()
223 root = Path(__file__).resolve().parents[2]
224 files = iter_files(root)
228 remaining: list[tuple[Path, int, str]] = []
231 rel = str(path.relative_to(root))
232 if rel
in SELF_EXEMPT:
235 old = path.read_text(encoding=
"utf-8")
236 except (OSError, UnicodeDecodeError):
238 if not WAVE_RE.search(old):
242 old_lines = old.splitlines()
243 new_lines = new.splitlines()
244 n_changed = sum(1
for a, b
in zip(old_lines, new_lines, strict=
False)
if a != b)
245 n_changed +=
abs(len(old_lines) - len(new_lines))
247 fixed_lines += n_changed
249 path.write_text(new, encoding=
"utf-8")
251 for ln, line
in enumerate(new.splitlines(), start=1):
252 if "WAVE-OK" in line:
254 if WAVE_RE.search(line):
255 remaining.append((path.relative_to(root), ln, line.rstrip()))
257 mode =
"applied" if args.apply
else "dry-run"
258 print(f
"fix-wave-refs ({mode}): rewrote {fixed_lines} lines across {fixed_files} files.")
261 f
"REMAINING: {len(remaining)} unfixable violations -- needs human edit:",
264 for rel, ln, line
in remaining[:REPORT_MAX_LINES]:
265 snippet = line
if len(line) <= REPORT_SNIPPET_MAX_LEN
else line[:117] +
"..."
266 print(f
" {rel}:{ln} {snippet}", file=sys.stderr)
267 if len(remaining) > REPORT_MAX_LINES:
268 print(f
" ... {len(remaining) - REPORT_MAX_LINES} more", file=sys.stderr)
273if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.
int abs(int j)
Compute absolute value of integer.