@matt82198/aesop 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +16 -9
- package/bin/cli.js +102 -32
- package/daemons/run-watchdog.sh +6 -2
- package/docs/CONFIGURE.md +31 -20
- package/docs/FIRST-WAVE.md +29 -9
- package/docs/INSTALL.md +77 -12
- package/docs/PORTING.md +4 -0
- package/docs/README.md +3 -3
- package/docs/THE-AESOP-HYPOTHESIS.md +23 -7
- package/package.json +1 -1
- package/tools/doctor.js +124 -12
- package/tools/reproduce.js +11 -2
- package/tools/self_stats.py +61 -6
- package/tools/verify_test_suite_count.py +250 -0
- package/tools/wave_manifest_lint.py +485 -0
- package/tools/wave_scorecard.py +430 -0
- package/ui/config.py +1 -1
- package/ui/cost.py +176 -3
- package/ui/web/dist/assets/{index-CP68RIh3.js → index-4rp6B_ID.js} +1 -1
- package/ui/web/dist/assets/{index-CNQxaiOW.css → index-B2lTtpsH.css} +1 -1
- package/ui/web/dist/index.html +2 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Verify test suite counts in tests/CLAUDE.md match actual test files on disk.
|
|
4
|
+
|
|
5
|
+
Supports two modes:
|
|
6
|
+
- --check (default): Fail if counts drift from actual files (exit 1 on drift)
|
|
7
|
+
- --fix: Auto-rewrite counts in tests/CLAUDE.md to match actual files
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python tools/verify_test_suite_count.py --check [--repo ROOT]
|
|
11
|
+
python tools/verify_test_suite_count.py --fix [--dry-run] [--repo ROOT]
|
|
12
|
+
|
|
13
|
+
Modes are mutually exclusive; if neither is specified, defaults to --check.
|
|
14
|
+
Idempotent: running --fix twice produces identical results.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import re
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Tuple
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def count_git_files(*patterns: str) -> int:
|
|
26
|
+
"""Count files matching patterns using git ls-files.
|
|
27
|
+
|
|
28
|
+
Omits untracked files; uses git to ensure we count only tracked files.
|
|
29
|
+
"""
|
|
30
|
+
count = 0
|
|
31
|
+
for pattern in patterns:
|
|
32
|
+
try:
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
["git", "ls-files", pattern],
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
check=True,
|
|
38
|
+
timeout=10,
|
|
39
|
+
)
|
|
40
|
+
count += len([line for line in result.stdout.strip().split("\n") if line])
|
|
41
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
42
|
+
pass
|
|
43
|
+
return count
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_actual_counts(repo_root: Path) -> Tuple[int, int, int]:
|
|
47
|
+
"""Get actual test suite counts from disk.
|
|
48
|
+
|
|
49
|
+
Returns: (node_count, shell_count, python_count)
|
|
50
|
+
"""
|
|
51
|
+
node_count = count_git_files("tests/*.test.mjs")
|
|
52
|
+
shell_count = count_git_files("tests/*.test.sh", "tests/test_*.sh", "tests/test-*.sh")
|
|
53
|
+
python_count = count_git_files("tests/test_*.py")
|
|
54
|
+
|
|
55
|
+
return node_count, shell_count, python_count
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_documented_counts(claudemd_path: Path) -> Tuple[int, int, int]:
|
|
59
|
+
"""Extract documented counts from tests/CLAUDE.md.
|
|
60
|
+
|
|
61
|
+
Returns: (node_count, shell_count, python_count) or raises ValueError if not found.
|
|
62
|
+
"""
|
|
63
|
+
content = claudemd_path.read_text(encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
# Match "**<Type> (N suites?)**:" patterns
|
|
66
|
+
node_match = re.search(r"\*\*Node \((\d+) suites?\)\*\*:", content)
|
|
67
|
+
shell_match = re.search(r"\*\*Shell \((\d+) suites?\)\*\*:", content)
|
|
68
|
+
python_match = re.search(r"\*\*Python \((\d+) suites?\)\*\*:", content)
|
|
69
|
+
|
|
70
|
+
if not (node_match and shell_match and python_match):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"Could not find one or more test suite count lines in tests/CLAUDE.md. "
|
|
73
|
+
"Expected: **Node (N suites)**: **Shell (N suites)**: **Python (N suites):**"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
int(node_match.group(1)),
|
|
78
|
+
int(shell_match.group(1)),
|
|
79
|
+
int(python_match.group(1)),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def check_mode(claudemd_path: Path) -> int:
|
|
84
|
+
"""Verify counts match. Exit 0 if clean, 1 if drift detected.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
claudemd_path: Path to tests/CLAUDE.md
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
0 if counts match, 1 if drift detected
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
documented = get_documented_counts(claudemd_path)
|
|
94
|
+
actual = get_actual_counts(claudemd_path.parent.parent)
|
|
95
|
+
|
|
96
|
+
if documented == actual:
|
|
97
|
+
print("[OK] Test suite counts match")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
doc_node, doc_shell, doc_python = documented
|
|
101
|
+
act_node, act_shell, act_python = actual
|
|
102
|
+
|
|
103
|
+
print("[DRIFT] Test suite count mismatch:")
|
|
104
|
+
if doc_node != act_node:
|
|
105
|
+
print(f" Node: CLAUDE.md says {doc_node}, actual is {act_node}")
|
|
106
|
+
if doc_shell != act_shell:
|
|
107
|
+
print(f" Shell: CLAUDE.md says {doc_shell}, actual is {act_shell}")
|
|
108
|
+
if doc_python != act_python:
|
|
109
|
+
print(f" Python: CLAUDE.md says {doc_python}, actual is {act_python}")
|
|
110
|
+
print(f"\nRun: python tools/verify_test_suite_count.py --fix")
|
|
111
|
+
return 1
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
print(f"[ERROR] {e}", file=sys.stderr)
|
|
114
|
+
return 2
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def fix_mode(claudemd_path: Path, dry_run: bool = False) -> int:
|
|
118
|
+
"""Auto-rewrite counts in tests/CLAUDE.md to match actual files.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
claudemd_path: Path to tests/CLAUDE.md
|
|
122
|
+
dry_run: If True, show what would change but don't write
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
0 if successful (or dry_run shows what would change), 1 on error
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
actual = get_actual_counts(claudemd_path.parent.parent)
|
|
129
|
+
act_node, act_shell, act_python = actual
|
|
130
|
+
|
|
131
|
+
content = claudemd_path.read_text(encoding="utf-8")
|
|
132
|
+
original_content = content
|
|
133
|
+
|
|
134
|
+
# Rewrite count patterns
|
|
135
|
+
content = re.sub(
|
|
136
|
+
r"\*\*Node \(\d+ suites?\)\*\*:",
|
|
137
|
+
f"**Node ({act_node} suites)**:",
|
|
138
|
+
content,
|
|
139
|
+
)
|
|
140
|
+
content = re.sub(
|
|
141
|
+
r"\*\*Shell \(\d+ suites?\)\*\*:",
|
|
142
|
+
f"**Shell ({act_shell} suites)**:",
|
|
143
|
+
content,
|
|
144
|
+
)
|
|
145
|
+
content = re.sub(
|
|
146
|
+
r"\*\*Python \(\d+ suites?\)\*\*:",
|
|
147
|
+
f"**Python ({act_python} suites)**:",
|
|
148
|
+
content,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if content == original_content:
|
|
152
|
+
print("[OK] Counts already match, no changes needed")
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
if dry_run:
|
|
156
|
+
print(f"[DRY-RUN] Would update counts:")
|
|
157
|
+
print(f" Node: {re.search(r'Node \\((\\d+)', original_content).group(1)} → {act_node}")
|
|
158
|
+
print(f" Shell: {re.search(r'Shell \\((\\d+)', original_content).group(1)} → {act_shell}")
|
|
159
|
+
print(f" Python: {re.search(r'Python \\((\\d+)', original_content).group(1)} → {act_python}")
|
|
160
|
+
print()
|
|
161
|
+
print("Run without --dry-run to apply changes.")
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
# Write the updated content
|
|
165
|
+
claudemd_path.write_text(content, encoding="utf-8")
|
|
166
|
+
|
|
167
|
+
doc_node, doc_shell, doc_python = get_documented_counts(claudemd_path)
|
|
168
|
+
print(f"[FIXED] Updated tests/CLAUDE.md:")
|
|
169
|
+
print(f" Node: {doc_node} suites")
|
|
170
|
+
print(f" Shell: {doc_shell} suites")
|
|
171
|
+
print(f" Python: {doc_python} suites")
|
|
172
|
+
return 0
|
|
173
|
+
except ValueError as e:
|
|
174
|
+
print(f"[ERROR] {e}", file=sys.stderr)
|
|
175
|
+
return 1
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def main():
|
|
179
|
+
"""Main entry point."""
|
|
180
|
+
parser = argparse.ArgumentParser(
|
|
181
|
+
description=__doc__,
|
|
182
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--check",
|
|
187
|
+
action="store_true",
|
|
188
|
+
help="Verify counts match (exit 1 if drift); default if neither --check nor --fix specified",
|
|
189
|
+
)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--fix",
|
|
192
|
+
action="store_true",
|
|
193
|
+
help="Auto-rewrite counts to match actual files",
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--dry-run",
|
|
197
|
+
action="store_true",
|
|
198
|
+
help="With --fix: show what would change but don't write (implies --fix)",
|
|
199
|
+
)
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"--claudemd",
|
|
202
|
+
type=Path,
|
|
203
|
+
default=None,
|
|
204
|
+
help="Path to tests/CLAUDE.md (default: auto-detect from repo root)",
|
|
205
|
+
)
|
|
206
|
+
parser.add_argument(
|
|
207
|
+
"--repo",
|
|
208
|
+
type=Path,
|
|
209
|
+
default=None,
|
|
210
|
+
help="Repository root (default: current directory)",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
args = parser.parse_args()
|
|
214
|
+
|
|
215
|
+
# Validate mutually exclusive modes
|
|
216
|
+
if args.check and args.fix:
|
|
217
|
+
print("[ERROR] --check and --fix are mutually exclusive", file=sys.stderr)
|
|
218
|
+
return 1
|
|
219
|
+
|
|
220
|
+
# --dry-run implies --fix
|
|
221
|
+
if args.dry_run and not args.fix:
|
|
222
|
+
args.fix = True
|
|
223
|
+
|
|
224
|
+
# Default to --check if neither specified
|
|
225
|
+
if not args.check and not args.fix:
|
|
226
|
+
args.check = True
|
|
227
|
+
|
|
228
|
+
# Determine repo root
|
|
229
|
+
repo_root = args.repo or Path.cwd()
|
|
230
|
+
repo_root = repo_root.resolve()
|
|
231
|
+
|
|
232
|
+
# Determine CLAUDE.md path
|
|
233
|
+
if args.claudemd:
|
|
234
|
+
claudemd_path = args.claudemd.resolve()
|
|
235
|
+
else:
|
|
236
|
+
claudemd_path = repo_root / "tests" / "CLAUDE.md"
|
|
237
|
+
|
|
238
|
+
if not claudemd_path.exists():
|
|
239
|
+
print(f"[ERROR] {claudemd_path} not found", file=sys.stderr)
|
|
240
|
+
return 2
|
|
241
|
+
|
|
242
|
+
# Run the selected mode
|
|
243
|
+
if args.check:
|
|
244
|
+
return check_mode(claudemd_path)
|
|
245
|
+
else:
|
|
246
|
+
return fix_mode(claudemd_path, dry_run=args.dry_run)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
sys.exit(main())
|