feat(kaizen): friction-scan section extraction + signal split
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d14639e80a
commit
859732b04d
2 changed files with 103 additions and 0 deletions
63
scripts/friction-scan.py
Normal file
63
scripts/friction-scan.py
Normal file
|
|
@ -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
|
||||
40
tests/test_friction_scan.py
Normal file
40
tests/test_friction_scan.py
Normal file
|
|
@ -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]`")
|
||||
Loading…
Add table
Reference in a new issue