feat(kaizen): friction-scan CLI (--json default, --nudge)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6f66ee634
commit
b185ac4765
2 changed files with 69 additions and 2 deletions
45
scripts/friction-scan.py
Normal file → Executable file
45
scripts/friction-scan.py
Normal file → Executable file
|
|
@ -16,6 +16,11 @@ import re
|
|||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
FRICTION = os.path.join(REPO_ROOT, "docs", "FRICTION.md")
|
||||
|
||||
# Nudge thresholds (tunable; the /kaizen self-eval phase revisits these).
|
||||
NUDGE_MIN_OPEN = 8
|
||||
NUDGE_MAX_AGE_DAYS = 21
|
||||
NUDGE_MIN_RECURRENCE = 3
|
||||
|
||||
TAG_RE = re.compile(r"`\[(friction|gotcha|recurring|unused)\]`")
|
||||
DATE_RE = re.compile(r"(\d{4})-(\d{2})-(\d{2})")
|
||||
ORDINAL_RE = re.compile(r"(\d+)(?:st|nd|rd|th)\s+(?:occurrence|reinforcement|time)", re.I)
|
||||
|
|
@ -113,5 +118,41 @@ def parse_signal(raw, today):
|
|||
}
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover (filled in Task 4)
|
||||
pass
|
||||
def load_signals(path, today):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
text = fh.read()
|
||||
return [parse_signal(s, today) for s in split_signals(extract_open_section(text))]
|
||||
|
||||
|
||||
def nudge_line(signals):
|
||||
n = len(signals)
|
||||
ages = [s["age_days"] for s in signals if s.get("age_days") is not None]
|
||||
oldest = max(ages) if ages else 0
|
||||
max_rec = max((s["recurrence_count"] for s in signals), default=0)
|
||||
overdue = n >= NUDGE_MIN_OPEN or oldest >= NUDGE_MAX_AGE_DAYS or max_rec >= NUDGE_MIN_RECURRENCE
|
||||
status = "OVERDUE — run /kaizen" if overdue else "ok"
|
||||
return f"kaizen: {n} open signals, oldest {oldest}d, max recurrence {max_rec}x — {status}"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Parse FRICTION.md Open signals for /kaizen.")
|
||||
parser.add_argument("--nudge", action="store_true", help="print a one-line overdue summary")
|
||||
parser.add_argument("--today", help="override today's date (YYYY-MM-DD) for testing")
|
||||
parser.add_argument("--file", default=FRICTION, help="path to FRICTION.md")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.today:
|
||||
y, m, d = args.today.split("-")
|
||||
today = datetime.date(int(y), int(m), int(d))
|
||||
else:
|
||||
today = datetime.date.today()
|
||||
|
||||
signals = load_signals(args.file, today)
|
||||
if args.nudge:
|
||||
print(nudge_line(signals))
|
||||
else:
|
||||
print(json.dumps(signals, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -89,3 +89,29 @@ def test_still_exists_false_for_missing_path():
|
|||
def test_still_exists_true_for_real_path():
|
||||
sig = fs.parse_signal("`[gotcha]` **x** (2026-06-01): `scripts/repo-scan.py`", TODAY)
|
||||
assert sig["still_exists"] is True
|
||||
|
||||
|
||||
def test_nudge_line_overdue_on_recurrence():
|
||||
sigs = [{"age_days": 2, "recurrence_count": 5}]
|
||||
line = fs.nudge_line(sigs)
|
||||
assert "OVERDUE" in line
|
||||
assert "max recurrence 5x" in line
|
||||
|
||||
|
||||
def test_nudge_line_ok_when_quiet():
|
||||
sigs = [{"age_days": 3, "recurrence_count": 1}, {"age_days": 1, "recurrence_count": 1}]
|
||||
line = fs.nudge_line(sigs)
|
||||
assert "ok" in line
|
||||
assert "OVERDUE" not in line
|
||||
|
||||
|
||||
def test_nudge_line_overdue_on_count():
|
||||
sigs = [{"age_days": 1, "recurrence_count": 1} for _ in range(8)]
|
||||
assert "OVERDUE" in fs.nudge_line(sigs)
|
||||
|
||||
|
||||
def test_load_signals_reads_real_friction_file():
|
||||
path = os.path.join(os.path.dirname(__file__), "..", "docs", "FRICTION.md")
|
||||
sigs = fs.load_signals(path, TODAY)
|
||||
assert len(sigs) >= 1
|
||||
assert all(s["tag"] in {"friction", "gotcha", "recurring", "unused"} for s in sigs)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue