diff --git a/scripts/friction-scan.py b/scripts/friction-scan.py new file mode 100644 index 0000000..69481c6 --- /dev/null +++ b/scripts/friction-scan.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Parse docs/FRICTION.md 'Open signals' into structured data for /kaizen. + +Stdlib only. Modes: + --json (default): emit the open signals as JSON (Phase-0 input for /kaizen) + --nudge : print a one-line 'loop overdue?' summary + +Authoritative design: docs/superpowers/specs/2026-06-14-kaizen-command-design.md +""" +import argparse +import datetime +import json +import os +import re + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FRICTION = os.path.join(REPO_ROOT, "docs", "FRICTION.md") + + +def extract_open_section(text): + """Return the body between '## Open signals' and the next '## ' heading.""" + lines = text.splitlines() + start = None + for i, line in enumerate(lines): + if line.strip().lower() == "## open signals": + start = i + 1 + break + if start is None: + return "" + end = len(lines) + for j in range(start, len(lines)): + if lines[j].startswith("## "): + end = j + break + return "\n".join(lines[start:end]) + + +def split_signals(section): + """Split the Open-signals body into raw per-signal blocks. + + A signal starts with a top-level '- ' bullet; indented or blank lines are + continuations. Returns a list of multi-line strings with the leading '- ' + stripped from the first line.""" + signals = [] + current = None + for line in section.splitlines(): + if line.startswith("- "): + if current is not None: + signals.append("\n".join(current).strip()) + current = [line[2:]] + elif current is not None: + if line.strip() == "" or line.startswith(" "): + current.append(line.strip()) + else: + signals.append("\n".join(current).strip()) + current = None + if current is not None: + signals.append("\n".join(current).strip()) + return [s for s in signals if s] + + +if __name__ == "__main__": # pragma: no cover (filled in Task 4) + pass diff --git a/tests/test_friction_scan.py b/tests/test_friction_scan.py new file mode 100644 index 0000000..3fcc4d8 --- /dev/null +++ b/tests/test_friction_scan.py @@ -0,0 +1,40 @@ +import importlib.util +import os + +_SPEC = importlib.util.spec_from_file_location( + "friction_scan", + os.path.join(os.path.dirname(__file__), "..", "scripts", "friction-scan.py"), +) +fs = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(fs) + +SAMPLE = """# FRICTION.md + +## Open signals + +_(append new raw signals here)_ + +- `[gotcha]` **First thing** (2026-06-01): body line one. + continuation line two. +- `[friction]` **Second thing** (2026-06-10): only one line. + +--- + +## Kaizen reviews — decisions ledger + +- `[gotcha]` **Should not be parsed** (2026-01-01): in the ledger. +""" + + +def test_extract_open_section_stops_at_next_heading(): + section = fs.extract_open_section(SAMPLE) + assert "First thing" in section + assert "Second thing" in section + assert "Should not be parsed" not in section + + +def test_split_signals_finds_two_items_and_joins_continuations(): + signals = fs.split_signals(fs.extract_open_section(SAMPLE)) + assert len(signals) == 2 + assert "continuation line two" in signals[0] + assert signals[1].startswith("`[friction]`")