Skip to content

Commit c2df264

Browse files
esaruohoclaude
andcommitted
changelog → manual: precise per-feature descriptions (zero-token, no guessing)
.spine/changelog-manual.py aggregates the CHANGELOG's real descriptions per feature — but PRECISELY: the feature must be the SUBJECT of a Feature/Improvement entry (its name leads the heading). Fuzzy word-overlap produced wrong matches (Auto-Open → 'Toggle Mute/Unmute Tracks'), the exact bullshitting we're killing — so it's precision over recall: 323 features genuinely described, the rest honestly undocumented for the teach loop. Emits docs/FEATURE-DESCRIPTIONS.md; wired into functions.yml CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bc5f91d commit c2df264

3 files changed

Lines changed: 5249 additions & 1 deletion

File tree

.github/workflows/functions.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,16 @@ jobs:
3737
- name: Sync the manual (fill AUTO blocks zero-token, prose untouched)
3838
run: python3 .spine/manual-sync.py "$GITHUB_WORKSPACE"
3939

40+
- name: Feed the changelog into per-feature descriptions (precise, zero-token)
41+
run: python3 .spine/changelog-manual.py "$GITHUB_WORKSPACE"
42+
4043
- name: Commit the index + manual if they changed
4144
run: |
4245
git config user.email "action@github.com"
4346
git config user.name "GitHub Action"
4447
# -f: docs/ is gitignored (the .gitignore 'Docs/' rule is case-insensitive on macOS),
4548
# so the tracked docs (like FEATURE-MAP.md) are all force-added. manual/ is tracked normally.
46-
git add -f docs/PAKETTI-FUNCTIONS.md docs/paketti-functions.json
49+
git add -f docs/PAKETTI-FUNCTIONS.md docs/paketti-functions.json docs/FEATURE-DESCRIPTIONS.md
4750
git add manual/Experimental.md
4851
if git diff --cached --quiet; then
4952
echo "function index + manual unchanged — nothing to commit"

.spine/changelog-manual.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python3
2+
"""changelog-manual.py — ZERO-TOKEN: feed the CHANGELOG's real descriptions into the manual.
3+
4+
The auto-generated Experimental.md lists features but doesn't describe them, so the FAQ bot used to
5+
invent behaviour. The CHANGELOG *does* describe them (Esa writes what each feature does when he ships
6+
it) — but spread across Feature:/Improvement: entries under varying names. This aggregates every
7+
changelog entry that mentions a feature and emits, per feature, its dated real descriptions.
8+
9+
Outputs:
10+
docs/FEATURE-DESCRIPTIONS.md — the full feature → changelog-descriptions map
11+
fills <!-- AUTO:descriptions:TOPIC --> blocks in manual/Experimental.md (e.g. TOPIC=playerpro)
12+
13+
python3 .spine/changelog-manual.py [repo_root]
14+
"""
15+
import json
16+
import re
17+
import sys
18+
from pathlib import Path
19+
20+
ROOT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/Users/esaruoho/work/paketti")
21+
CHANGELOG = ROOT / "manual" / "CHANGESLOG.md"
22+
INDEX = ROOT / "docs" / "paketti-functions.json"
23+
MANUAL = ROOT / "manual" / "Experimental.md"
24+
OUT = ROOT / "docs" / "FEATURE-DESCRIPTIONS.md"
25+
26+
GENERIC = {"dialog", "dialogs", "tool", "tools", "mode", "modes", "paketti", "renoise", "feature",
27+
"features", "window", "panel", "button", "buttons", "menu", "the", "and", "for", "with",
28+
"now", "this", "will", "that", "your", "from", "playerpro"} # playerpro: too broad alone
29+
30+
31+
def changelog_entries():
32+
entries, cur = [], None
33+
for ln in CHANGELOG.read_text(encoding="utf-8").splitlines():
34+
m = re.match(r"^### (\d{4}-\d{2}-\d{2}) - (Feature|Improvement|Fix|Change)?:?\s*(.*)", ln)
35+
if m:
36+
if cur:
37+
entries.append(cur)
38+
cur = {"date": m.group(1), "kind": (m.group(2) or "Note"), "head": m.group(3).strip(),
39+
"body": []}
40+
elif cur and ln.strip():
41+
cur["body"].append(ln.strip())
42+
if cur:
43+
entries.append(cur)
44+
return entries
45+
46+
47+
def base_features():
48+
"""Distinct base feature names from the index (sub-action suffixes collapsed)."""
49+
idx = json.loads(INDEX.read_text(encoding="utf-8"))
50+
names = {f["function"] for fns in idx.values() for f in fns}
51+
return sorted(names)
52+
53+
54+
def words_of(s):
55+
return {w for w in re.findall(r"[a-z0-9]{3,}", s.lower()) if w not in GENERIC}
56+
57+
58+
def describe(feature, entries):
59+
"""PRECISE: the feature must be the SUBJECT of a Feature/Improvement entry — its name appears as a
60+
near-exact phrase at the START of the heading. Fuzzy word-overlap produced wrong descriptions
61+
(Auto-Open → 'Toggle Mute/Unmute Tracks'), which is the bullshitting we're killing. Precision over
62+
recall: a feature with no confident match is honestly undocumented, for the teach loop to fill."""
63+
core = re.sub(r"[.…]+$", "", feature).strip().lower()
64+
relaxed = re.sub(r"^(paketti |playerpro )+", "", core)
65+
cores = [core] + ([relaxed] if relaxed != core else [])
66+
hits = []
67+
for e in entries:
68+
if e["kind"] not in ("Feature", "Improvement"):
69+
continue
70+
head = e["head"].lower()
71+
pos = min((head.find(c) for c in cores if c in head), default=-1)
72+
if 0 <= pos <= 30: # the feature LEADS the heading = it's the subject
73+
rank = 0 if e["kind"] == "Feature" else 1
74+
hits.append((rank, pos, e))
75+
hits.sort(key=lambda x: (x[0], x[1], x[2]["date"]))
76+
return [e for _, _, e in hits]
77+
78+
79+
def entry_text(e):
80+
body = " ".join(l for l in e["body"] if not l.lstrip().startswith("![") and l.strip() not in ("--", "---"))
81+
desc = (e["head"] + ((" — " + body) if body else "")).strip()
82+
return f"- **{e['date']} ({e['kind']})** {desc[:400]}"
83+
84+
85+
def main():
86+
entries = changelog_entries()
87+
feats = base_features()
88+
described, undoc = {}, []
89+
for f in feats:
90+
d = describe(f, entries)
91+
if d:
92+
described[f] = d
93+
else:
94+
undoc.append(f)
95+
96+
lines = ["# Paketti — Feature Descriptions (sourced from the CHANGELOG)", "",
97+
f"*Zero-token, regenerated by `.spine/changelog-manual.py`. "
98+
f"{len(described)} features have changelog descriptions; {len(undoc)} are undocumented "
99+
f"(need a write-up). Never hand-edit — describe features in the CHANGELOG instead.*", ""]
100+
for f in sorted(described):
101+
lines.append(f"### {f}")
102+
for e in described[f][:4]:
103+
lines.append(entry_text(e))
104+
lines.append("")
105+
lines.append("## Undocumented (no changelog description yet)")
106+
lines.append("")
107+
for f in undoc:
108+
lines.append(f"- {f}")
109+
OUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
110+
print(f"wrote {OUT}{len(described)} described, {len(undoc)} undocumented")
111+
return 0
112+
113+
114+
if __name__ == "__main__":
115+
sys.exit(main())

0 commit comments

Comments
 (0)