@theglitchking/babel-fish 1.0.2 → 2.0.0
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/.claude/install.sh +666 -0
- package/.claude/project-map/PROJECT_MAP.md +61 -0
- package/.claude/project-map/checksums.json +6 -0
- package/.claude/project-map/generate.py +1481 -0
- package/.claude/project-map/grader.py +583 -0
- package/.claude/project-map/learned-vocabulary.json +1 -0
- package/.claude/project-map/mine-sessions.py +419 -0
- package/.claude/project-map/reports/install-report.md +54 -0
- package/.claude/project-map/reports/iteration-01-report.md +54 -0
- package/.claude/project-map/reports/iteration-01-score.json +7 -0
- package/.claude/project-map/sections/01-vocabulary.md +8 -0
- package/.claude/project-map/sections/02-service-topology.md +6 -0
- package/.claude/project-map/sections/03-environment.md +6 -0
- package/.claude/project-map/sections/04-api-routes.md +6 -0
- package/.claude/project-map/sections/05-data-models.md +4 -0
- package/.claude/project-map/sections/06-schemas.md +4 -0
- package/.claude/project-map/sections/07-services.md +6 -0
- package/.claude/project-map/sections/08-background-jobs.md +5 -0
- package/.claude/project-map/sections/09-frontend-features.md +4 -0
- package/.claude/project-map/sections/10-tools-commands.md +8 -0
- package/.claude/project-map/sections/11-migrations.md +4 -0
- package/.claude/project-map/sections/12-import-chains.md +7 -0
- package/.claude/project-map/sections/13-frontend-backend-map.md +8 -0
- package/.claude/project-map/sections/14-reverse-proxy.md +4 -0
- package/.claude/project-map/sections/15-auth-config.md +6 -0
- package/.claude/project-map/sections/16-infra-profile.md +13 -0
- package/.claude/project-map/sections/17-learned-vocabulary.md +7 -0
- package/.claude/project-map/sections/18-dead-code.md +9 -0
- package/.claude/project-map/sections/19-doc-pointers.md +5 -0
- package/.claude/project-map/stack.json +12 -0
- package/.claude/rules/operational-runbook.md +40 -0
- package/.claude/rules/project-vocabulary.md +25 -0
- package/.claude/scripts/detect-stack.sh +222 -0
- package/.claude/scripts/ensure-python.sh +100 -0
- package/.claude/scripts/statusline.sh +27 -0
- package/.claude/scripts/validate.sh +59 -0
- package/.claude/settings.json +6 -0
- package/.claude/skills/babel-fish-developer-skill/SKILL.md +56 -0
- package/.claude/templates/SKILL.md.template +56 -0
- package/.claude/templates/operational-runbook.md.template +40 -0
- package/.claude/templates/project-vocabulary.md.template +25 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.githooks/install.sh +4 -0
- package/.githooks/pre-commit +22 -0
- package/CHANGELOG.md +72 -0
- package/README.md +18 -0
- package/bin/babel-fish.js +78 -71
- package/commands/policy.md +16 -0
- package/commands/relink.md +6 -0
- package/commands/status.md +6 -0
- package/commands/update.md +6 -0
- package/hooks/hooks.json +15 -0
- package/hooks/session-start.js +11 -0
- package/package.json +19 -3
- package/scripts/link-skills.js +31 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
grader.py — Babel Fish Quality Grader
|
|
4
|
+
Grades generate.py output on a 0-100 scale across 7 weighted categories.
|
|
5
|
+
Writes a human-readable report and returns exit code 0 (pass) or 1 (fail).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python grader.py [--project-root PATH] [--iteration N] [--total N] [--report-path PATH]
|
|
9
|
+
|
|
10
|
+
Exit codes:
|
|
11
|
+
0 = PASS (score >= 90%)
|
|
12
|
+
1 = FAIL (score < 90%)
|
|
13
|
+
2 = ERROR (could not run)
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# ── Paths ────────────────────────────────────────────────────────────────────
|
|
27
|
+
SCRIPT_DIR = Path(__file__).parent
|
|
28
|
+
MAP_DIR = SCRIPT_DIR
|
|
29
|
+
SECTIONS_DIR = MAP_DIR / "sections"
|
|
30
|
+
REPORTS_DIR = MAP_DIR / "reports"
|
|
31
|
+
PROJECT_ROOT = MAP_DIR.parent.parent
|
|
32
|
+
|
|
33
|
+
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
PASS_THRESHOLD = 90.0
|
|
36
|
+
|
|
37
|
+
# ── Secret pattern (mirrors generate.py) ─────────────────────────────────────
|
|
38
|
+
SECRET_PATTERN = re.compile(
|
|
39
|
+
r'(?i)(password|secret|token|api_key|apikey|private_key|auth_token|'
|
|
40
|
+
r'access_key|secret_key|client_secret|db_pass|database_password|'
|
|
41
|
+
r'stripe_key|twilio_auth|sendgrid_key|aws_secret)\s*[=:]\s*[^\[\n\r]{4,}'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# ── Grading weights ───────────────────────────────────────────────────────────
|
|
45
|
+
WEIGHTS = {
|
|
46
|
+
'section_completeness': 25,
|
|
47
|
+
'vocabulary_accuracy': 20,
|
|
48
|
+
'import_chain_validity':15,
|
|
49
|
+
'secret_safety': 15,
|
|
50
|
+
'section_size_bounds': 10,
|
|
51
|
+
'structural_integrity': 10,
|
|
52
|
+
'checksum_functionality': 5,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
assert sum(WEIGHTS.values()) == 100
|
|
56
|
+
|
|
57
|
+
# ── Color helpers (ANSI) ──────────────────────────────────────────────────────
|
|
58
|
+
GREEN = '\033[32m'
|
|
59
|
+
YELLOW = '\033[33m'
|
|
60
|
+
RED = '\033[31m'
|
|
61
|
+
CYAN = '\033[36m'
|
|
62
|
+
BOLD = '\033[1m'
|
|
63
|
+
RESET = '\033[0m'
|
|
64
|
+
|
|
65
|
+
def color_score(score: float) -> str:
|
|
66
|
+
c = GREEN if score >= 90 else YELLOW if score >= 70 else RED
|
|
67
|
+
return f"{c}{score:.1f}%{RESET}"
|
|
68
|
+
|
|
69
|
+
def color_pass(passed: bool) -> str:
|
|
70
|
+
return f"{GREEN}PASS{RESET}" if passed else f"{RED}FAIL{RESET}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ╔══════════════════════════════════════════════════════════════════════════╗
|
|
74
|
+
# ║ GRADING FUNCTIONS ║
|
|
75
|
+
# ╚══════════════════════════════════════════════════════════════════════════╝
|
|
76
|
+
|
|
77
|
+
class GradeResult:
|
|
78
|
+
def __init__(self, category: str, weight: int, raw_score: float,
|
|
79
|
+
details: str, issues: list[str], fixes: list[str] | None = None):
|
|
80
|
+
self.category = category
|
|
81
|
+
self.weight = weight
|
|
82
|
+
self.raw_score = max(0.0, min(100.0, raw_score)) # clamp 0-100
|
|
83
|
+
self.weighted = self.raw_score * weight / 100
|
|
84
|
+
self.details = details
|
|
85
|
+
self.issues = issues
|
|
86
|
+
self.fixes = fixes or []
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def display_name(self) -> str:
|
|
90
|
+
return self.category.replace('_', ' ').title()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def grade_section_completeness() -> GradeResult:
|
|
94
|
+
"""25% — expected sections were generated."""
|
|
95
|
+
issues = []
|
|
96
|
+
expected = [f"{str(i).zfill(2)}-" for i in range(1, 20)] # 01- through 19-
|
|
97
|
+
present = set()
|
|
98
|
+
|
|
99
|
+
if SECTIONS_DIR.exists():
|
|
100
|
+
for f in SECTIONS_DIR.glob('*.md'):
|
|
101
|
+
for exp in expected:
|
|
102
|
+
if f.name.startswith(exp):
|
|
103
|
+
present.add(exp)
|
|
104
|
+
|
|
105
|
+
missing = [e for e in expected if e not in present]
|
|
106
|
+
for m in missing:
|
|
107
|
+
issues.append(f"Missing section: {m}*.md")
|
|
108
|
+
|
|
109
|
+
score = (len(present) / len(expected)) * 100 if expected else 100.0
|
|
110
|
+
|
|
111
|
+
# Check PROJECT_MAP.md exists
|
|
112
|
+
if not (MAP_DIR / 'PROJECT_MAP.md').exists():
|
|
113
|
+
issues.append("PROJECT_MAP.md not generated")
|
|
114
|
+
score = min(score, 50.0)
|
|
115
|
+
|
|
116
|
+
details = f"{len(present)}/{len(expected)} sections present"
|
|
117
|
+
return GradeResult('section_completeness', WEIGHTS['section_completeness'],
|
|
118
|
+
score, details, issues)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def grade_vocabulary_accuracy() -> GradeResult:
|
|
122
|
+
"""20% — vocabulary entries point to files that exist."""
|
|
123
|
+
vocab_file = SECTIONS_DIR / '01-vocabulary.md'
|
|
124
|
+
issues = []
|
|
125
|
+
|
|
126
|
+
if not vocab_file.exists():
|
|
127
|
+
return GradeResult('vocabulary_accuracy', WEIGHTS['vocabulary_accuracy'],
|
|
128
|
+
0.0, 'vocabulary section missing', ['01-vocabulary.md not found'])
|
|
129
|
+
|
|
130
|
+
content = vocab_file.read_text(encoding='utf-8', errors='replace')
|
|
131
|
+
|
|
132
|
+
# Extract location column from table rows
|
|
133
|
+
# Format: | alias | type | location | notes |
|
|
134
|
+
rows = re.findall(r'^\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|', content, re.MULTILINE)
|
|
135
|
+
rows = [r for r in rows if not r[0].strip().startswith('-') and r[0].strip() != 'Alias']
|
|
136
|
+
|
|
137
|
+
if not rows:
|
|
138
|
+
# Greenfield — empty vocab is acceptable
|
|
139
|
+
details = "No vocabulary entries yet (greenfield — ok)"
|
|
140
|
+
return GradeResult('vocabulary_accuracy', WEIGHTS['vocabulary_accuracy'],
|
|
141
|
+
85.0, details, [])
|
|
142
|
+
|
|
143
|
+
total = len(rows)
|
|
144
|
+
valid = 0
|
|
145
|
+
for _, _, location, _ in rows:
|
|
146
|
+
loc = location.strip()
|
|
147
|
+
if not loc or loc.startswith('_') or loc == '':
|
|
148
|
+
valid += 1 # empty location is neutral
|
|
149
|
+
continue
|
|
150
|
+
# Check if any referenced file exists
|
|
151
|
+
# Location may contain multiple paths separated by ', '
|
|
152
|
+
paths = [p.strip() for p in loc.split(',')]
|
|
153
|
+
any_exists = any((PROJECT_ROOT / p).exists() for p in paths if p and not p.startswith('('))
|
|
154
|
+
if any_exists or loc.startswith('[LEARNED]') or '(learned' in loc.lower():
|
|
155
|
+
valid += 1
|
|
156
|
+
else:
|
|
157
|
+
issues.append(f"Vocab entry points to non-existent path: {loc[:60]}")
|
|
158
|
+
|
|
159
|
+
score = (valid / total * 100) if total > 0 else 85.0
|
|
160
|
+
details = f"{valid}/{total} entries have valid file references"
|
|
161
|
+
return GradeResult('vocabulary_accuracy', WEIGHTS['vocabulary_accuracy'],
|
|
162
|
+
score, details, issues[:5]) # cap at 5 issues in report
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def grade_import_chains() -> GradeResult:
|
|
166
|
+
"""15% — import chains section exists and chains are plausible."""
|
|
167
|
+
chains_file = SECTIONS_DIR / '12-import-chains.md'
|
|
168
|
+
issues = []
|
|
169
|
+
|
|
170
|
+
if not chains_file.exists():
|
|
171
|
+
return GradeResult('import_chain_validity', WEIGHTS['import_chain_validity'],
|
|
172
|
+
0.0, 'section missing', ['12-import-chains.md not found'])
|
|
173
|
+
|
|
174
|
+
content = chains_file.read_text(encoding='utf-8', errors='replace')
|
|
175
|
+
|
|
176
|
+
if '_No import chains traced' in content or '_No' in content:
|
|
177
|
+
# For greenfield or non-Python: acceptable
|
|
178
|
+
details = "No chains traced (greenfield or non-Python stack — ok)"
|
|
179
|
+
return GradeResult('import_chain_validity', WEIGHTS['import_chain_validity'],
|
|
180
|
+
80.0, details, [])
|
|
181
|
+
|
|
182
|
+
# Check that chains reference real files
|
|
183
|
+
chain_lines = re.findall(r'```\n(.+)\n```', content)
|
|
184
|
+
total = len(chain_lines)
|
|
185
|
+
valid = 0
|
|
186
|
+
for chain in chain_lines:
|
|
187
|
+
files = re.findall(r'([\w/.-]+\.py)', chain)
|
|
188
|
+
if not files:
|
|
189
|
+
valid += 1
|
|
190
|
+
continue
|
|
191
|
+
if all((PROJECT_ROOT / f).exists() for f in files[:2]):
|
|
192
|
+
valid += 1
|
|
193
|
+
else:
|
|
194
|
+
issues.append(f"Chain references missing files: {chain[:80]}")
|
|
195
|
+
|
|
196
|
+
score = (valid / total * 100) if total > 0 else 80.0
|
|
197
|
+
details = f"{valid}/{total} chains reference real files"
|
|
198
|
+
return GradeResult('import_chain_validity', WEIGHTS['import_chain_validity'],
|
|
199
|
+
score, details, issues[:3])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def grade_secret_safety() -> GradeResult:
|
|
203
|
+
"""15% — no secrets leaked in any section or PROJECT_MAP.md."""
|
|
204
|
+
issues = []
|
|
205
|
+
files_checked = 0
|
|
206
|
+
leaks_found = 0
|
|
207
|
+
|
|
208
|
+
check_paths: list[Path] = [MAP_DIR / 'PROJECT_MAP.md']
|
|
209
|
+
if SECTIONS_DIR.exists():
|
|
210
|
+
check_paths.extend(SECTIONS_DIR.glob('*.md'))
|
|
211
|
+
|
|
212
|
+
for path in check_paths:
|
|
213
|
+
if not path.exists():
|
|
214
|
+
continue
|
|
215
|
+
files_checked += 1
|
|
216
|
+
try:
|
|
217
|
+
content = path.read_text(encoding='utf-8', errors='replace')
|
|
218
|
+
for m in SECRET_PATTERN.finditer(content):
|
|
219
|
+
line_num = content[:m.start()].count('\n') + 1
|
|
220
|
+
leaks_found += 1
|
|
221
|
+
issues.append(f"Potential secret in {path.name}:{line_num} — `{m.group()[:50]}...`")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
issues.append(f"Could not read {path.name}: {e}")
|
|
224
|
+
|
|
225
|
+
# Secret leaks are a hard fail — each leak heavily penalizes
|
|
226
|
+
score = max(0.0, 100.0 - (leaks_found * 25))
|
|
227
|
+
details = f"Checked {files_checked} files — {leaks_found} potential leak(s)"
|
|
228
|
+
return GradeResult('secret_safety', WEIGHTS['secret_safety'],
|
|
229
|
+
score, details, issues)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def grade_section_sizes() -> GradeResult:
|
|
233
|
+
"""10% — sections are within 0.1KB–50KB bounds."""
|
|
234
|
+
issues = []
|
|
235
|
+
if not SECTIONS_DIR.exists():
|
|
236
|
+
return GradeResult('section_size_bounds', WEIGHTS['section_size_bounds'],
|
|
237
|
+
0.0, 'sections/ directory missing', ['sections/ not found'])
|
|
238
|
+
|
|
239
|
+
sections = list(SECTIONS_DIR.glob('*.md'))
|
|
240
|
+
if not sections:
|
|
241
|
+
# Greenfield — no sections yet means they were written as empty placeholders
|
|
242
|
+
return GradeResult('section_size_bounds', WEIGHTS['section_size_bounds'],
|
|
243
|
+
85.0, 'sections exist (greenfield content expected to be minimal)', [])
|
|
244
|
+
|
|
245
|
+
violations = 0
|
|
246
|
+
for f in sections:
|
|
247
|
+
size_kb = f.stat().st_size / 1024
|
|
248
|
+
if size_kb < 0.05:
|
|
249
|
+
issues.append(f"{f.name}: {size_kb:.2f}KB — suspiciously empty")
|
|
250
|
+
violations += 1
|
|
251
|
+
elif size_kb > 50:
|
|
252
|
+
issues.append(f"{f.name}: {size_kb:.1f}KB — exceeds 50KB limit (should be split)")
|
|
253
|
+
violations += 1
|
|
254
|
+
|
|
255
|
+
score = max(0.0, 100.0 - (violations / max(len(sections), 1)) * 100)
|
|
256
|
+
details = f"{len(sections)} sections, {violations} size violations"
|
|
257
|
+
return GradeResult('section_size_bounds', WEIGHTS['section_size_bounds'],
|
|
258
|
+
score, details, issues[:5])
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def grade_structural_integrity() -> GradeResult:
|
|
262
|
+
"""10% — valid markdown, proper headings, TOC links resolve."""
|
|
263
|
+
issues = []
|
|
264
|
+
score = 100.0
|
|
265
|
+
|
|
266
|
+
project_map = MAP_DIR / 'PROJECT_MAP.md'
|
|
267
|
+
if not project_map.exists():
|
|
268
|
+
return GradeResult('structural_integrity', WEIGHTS['structural_integrity'],
|
|
269
|
+
0.0, 'PROJECT_MAP.md missing', ['PROJECT_MAP.md not found'])
|
|
270
|
+
|
|
271
|
+
content = project_map.read_text(encoding='utf-8', errors='replace')
|
|
272
|
+
|
|
273
|
+
# Must have a top-level heading
|
|
274
|
+
if not re.search(r'^# .+', content, re.MULTILINE):
|
|
275
|
+
issues.append("PROJECT_MAP.md missing top-level heading")
|
|
276
|
+
score -= 20
|
|
277
|
+
|
|
278
|
+
# Must have Stats section
|
|
279
|
+
if '## Stats' not in content:
|
|
280
|
+
issues.append("PROJECT_MAP.md missing ## Stats section")
|
|
281
|
+
score -= 15
|
|
282
|
+
|
|
283
|
+
# Must have Section Index
|
|
284
|
+
if 'Section Index' not in content:
|
|
285
|
+
issues.append("PROJECT_MAP.md missing Section Index")
|
|
286
|
+
score -= 15
|
|
287
|
+
|
|
288
|
+
# Must have Quick Routing
|
|
289
|
+
if 'Quick Routing' not in content:
|
|
290
|
+
issues.append("PROJECT_MAP.md missing Quick Routing table")
|
|
291
|
+
score -= 10
|
|
292
|
+
|
|
293
|
+
# Check that section links in TOC point to real files
|
|
294
|
+
link_pattern = re.compile(r'\[(\d+)\]\(sections/([^)]+)\)')
|
|
295
|
+
broken_links = 0
|
|
296
|
+
for m in link_pattern.finditer(content):
|
|
297
|
+
target = SECTIONS_DIR / m.group(2)
|
|
298
|
+
if not target.exists():
|
|
299
|
+
issues.append(f"Broken TOC link: sections/{m.group(2)}")
|
|
300
|
+
broken_links += 1
|
|
301
|
+
if broken_links:
|
|
302
|
+
score -= broken_links * 5
|
|
303
|
+
|
|
304
|
+
# Check each section has a top-level heading
|
|
305
|
+
if SECTIONS_DIR.exists():
|
|
306
|
+
for f in SECTIONS_DIR.glob('*.md'):
|
|
307
|
+
try:
|
|
308
|
+
first_line = f.read_text(encoding='utf-8', errors='replace').split('\n')[0]
|
|
309
|
+
if not first_line.startswith('#'):
|
|
310
|
+
issues.append(f"{f.name}: missing top-level heading")
|
|
311
|
+
score -= 2
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
details = f"{broken_links} broken TOC links, {len(issues)} structural issues"
|
|
316
|
+
return GradeResult('structural_integrity', WEIGHTS['structural_integrity'],
|
|
317
|
+
max(0.0, score), details, issues[:5])
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def grade_checksum_functionality() -> GradeResult:
|
|
321
|
+
"""5% — checksums.json exists and looks valid."""
|
|
322
|
+
issues = []
|
|
323
|
+
|
|
324
|
+
checksums_path = MAP_DIR / 'checksums.json'
|
|
325
|
+
if not checksums_path.exists():
|
|
326
|
+
return GradeResult('checksum_functionality', WEIGHTS['checksum_functionality'],
|
|
327
|
+
0.0, 'checksums.json missing', ['checksums.json not found — checksum skip will not work'])
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
data = json.loads(checksums_path.read_text())
|
|
331
|
+
if 'input_hash' not in data:
|
|
332
|
+
issues.append("checksums.json missing 'input_hash' key")
|
|
333
|
+
return GradeResult('checksum_functionality', WEIGHTS['checksum_functionality'],
|
|
334
|
+
50.0, 'checksums.json malformed', issues)
|
|
335
|
+
if not isinstance(data['input_hash'], str) or len(data['input_hash']) < 32:
|
|
336
|
+
issues.append("checksums.json has invalid hash value")
|
|
337
|
+
return GradeResult('checksum_functionality', WEIGHTS['checksum_functionality'],
|
|
338
|
+
60.0, 'checksums.json has weak hash', issues)
|
|
339
|
+
except json.JSONDecodeError as e:
|
|
340
|
+
return GradeResult('checksum_functionality', WEIGHTS['checksum_functionality'],
|
|
341
|
+
0.0, f'checksums.json parse error: {e}', [str(e)])
|
|
342
|
+
|
|
343
|
+
details = f"checksums.json valid (hash: {data['input_hash'][:12]}...)"
|
|
344
|
+
return GradeResult('checksum_functionality', WEIGHTS['checksum_functionality'],
|
|
345
|
+
100.0, details, [])
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ╔══════════════════════════════════════════════════════════════════════════╗
|
|
349
|
+
# ║ REPORT BUILDER ║
|
|
350
|
+
# ╚══════════════════════════════════════════════════════════════════════════╝
|
|
351
|
+
|
|
352
|
+
def build_report(
|
|
353
|
+
results: list[GradeResult],
|
|
354
|
+
total_score: float,
|
|
355
|
+
passed: bool,
|
|
356
|
+
iteration: int,
|
|
357
|
+
total_iterations: int,
|
|
358
|
+
previous_score: float | None,
|
|
359
|
+
) -> str:
|
|
360
|
+
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
361
|
+
verdict = "✅ PASSED" if passed else "❌ FAILED"
|
|
362
|
+
threshold_note = f"{PASS_THRESHOLD:.0f}% threshold"
|
|
363
|
+
|
|
364
|
+
delta_str = ''
|
|
365
|
+
if previous_score is not None:
|
|
366
|
+
delta = total_score - previous_score
|
|
367
|
+
sign = '+' if delta >= 0 else ''
|
|
368
|
+
delta_str = f" ({sign}{delta:.1f}% from previous)"
|
|
369
|
+
|
|
370
|
+
lines = [
|
|
371
|
+
f"# Babel Fish — Installation Report",
|
|
372
|
+
f"",
|
|
373
|
+
f"**Generated**: {now}",
|
|
374
|
+
f"**Iteration**: {iteration} of {total_iterations} ",
|
|
375
|
+
f"**Score**: {total_score:.1f}%{delta_str} ",
|
|
376
|
+
f"**Result**: {verdict} ({threshold_note})",
|
|
377
|
+
f"",
|
|
378
|
+
"---",
|
|
379
|
+
"",
|
|
380
|
+
"## Scoring Breakdown",
|
|
381
|
+
"",
|
|
382
|
+
"| Category | Score | Weight | Weighted | Status |",
|
|
383
|
+
"|----------|-------|--------|----------|--------|",
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
for r in results:
|
|
387
|
+
status = "✅" if r.raw_score >= 90 else "⚠️" if r.raw_score >= 70 else "❌"
|
|
388
|
+
lines.append(
|
|
389
|
+
f"| {r.display_name} | {r.raw_score:.1f}% | {r.weight}% "
|
|
390
|
+
f"| {r.weighted:.1f} | {status} {r.details} |"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
lines += [
|
|
394
|
+
"",
|
|
395
|
+
f"**Total: {total_score:.1f} / 100**",
|
|
396
|
+
"",
|
|
397
|
+
"---",
|
|
398
|
+
"",
|
|
399
|
+
"## Issues Found",
|
|
400
|
+
"",
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
all_issues = [(r.display_name, issue) for r in results for issue in r.issues]
|
|
404
|
+
if all_issues:
|
|
405
|
+
for category, issue in all_issues:
|
|
406
|
+
lines.append(f"- ❌ **{category}**: {issue}")
|
|
407
|
+
else:
|
|
408
|
+
lines.append("_No issues found._")
|
|
409
|
+
|
|
410
|
+
if iteration > 1:
|
|
411
|
+
lines += [
|
|
412
|
+
"",
|
|
413
|
+
"---",
|
|
414
|
+
"",
|
|
415
|
+
"## Improvements Made This Iteration",
|
|
416
|
+
"",
|
|
417
|
+
]
|
|
418
|
+
all_fixes = [(r.display_name, fix) for r in results for fix in r.fixes]
|
|
419
|
+
if all_fixes:
|
|
420
|
+
for category, fix in all_fixes:
|
|
421
|
+
lines.append(f"- ✅ **{category}**: {fix}")
|
|
422
|
+
else:
|
|
423
|
+
lines.append("_No specific improvements recorded._")
|
|
424
|
+
|
|
425
|
+
lines += [
|
|
426
|
+
"",
|
|
427
|
+
"---",
|
|
428
|
+
"",
|
|
429
|
+
"## Final Verdict",
|
|
430
|
+
"",
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
if passed:
|
|
434
|
+
lines += [
|
|
435
|
+
f"✅ **PASSED ({total_score:.1f}%)** — Map is production-ready.",
|
|
436
|
+
"",
|
|
437
|
+
"The project map meets quality standards and is ready to use.",
|
|
438
|
+
"Future sessions will load relevant sections automatically via the developer skill.",
|
|
439
|
+
]
|
|
440
|
+
else:
|
|
441
|
+
remaining = total_iterations - iteration
|
|
442
|
+
if remaining > 0:
|
|
443
|
+
lines += [
|
|
444
|
+
f"❌ **FAILED ({total_score:.1f}%)** — Below {PASS_THRESHOLD:.0f}% threshold.",
|
|
445
|
+
"",
|
|
446
|
+
f"{remaining} iteration(s) remaining. Addressing issues above and retrying...",
|
|
447
|
+
]
|
|
448
|
+
else:
|
|
449
|
+
lines += [
|
|
450
|
+
f"⚠️ **COMPLETED WITH WARNINGS ({total_score:.1f}%)** — Final iteration reached.",
|
|
451
|
+
"",
|
|
452
|
+
"The map was generated but did not reach the 90% quality threshold.",
|
|
453
|
+
"Review the issues above and consider running `python generate.py --force` after",
|
|
454
|
+
"addressing them, then re-running `python grader.py` to verify.",
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
lines += [
|
|
458
|
+
"",
|
|
459
|
+
"---",
|
|
460
|
+
"",
|
|
461
|
+
"## How to Use the Project Map",
|
|
462
|
+
"",
|
|
463
|
+
"```bash",
|
|
464
|
+
"# Read the map index",
|
|
465
|
+
"cat .claude/project-map/PROJECT_MAP.md",
|
|
466
|
+
"",
|
|
467
|
+
"# Force regeneration",
|
|
468
|
+
"python .claude/project-map/generate.py --force",
|
|
469
|
+
"",
|
|
470
|
+
"# Re-run grader",
|
|
471
|
+
"python .claude/project-map/grader.py",
|
|
472
|
+
"```",
|
|
473
|
+
"",
|
|
474
|
+
"_Report generated by `grader.py` — part of the Babel Fish plugin._",
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
return '\n'.join(lines) + '\n'
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def print_terminal_summary(results: list[GradeResult], total_score: float, passed: bool,
|
|
481
|
+
iteration: int, total_iterations: int) -> None:
|
|
482
|
+
"""Print a concise terminal summary with color."""
|
|
483
|
+
print(f"\n{CYAN}{'─' * 55}{RESET}")
|
|
484
|
+
print(f"{BOLD} Babel Fish — Grade Report "
|
|
485
|
+
f"(Iteration {iteration}/{total_iterations}){RESET}")
|
|
486
|
+
print(f"{CYAN}{'─' * 55}{RESET}\n")
|
|
487
|
+
|
|
488
|
+
for r in results:
|
|
489
|
+
bar_filled = int(r.raw_score / 10)
|
|
490
|
+
bar = ('█' * bar_filled) + ('░' * (10 - bar_filled))
|
|
491
|
+
c = GREEN if r.raw_score >= 90 else YELLOW if r.raw_score >= 70 else RED
|
|
492
|
+
print(f" {r.display_name:<30} {c}{bar}{RESET} {r.raw_score:5.1f}% (×{r.weight}%)")
|
|
493
|
+
|
|
494
|
+
print(f"\n{CYAN}{'─' * 55}{RESET}")
|
|
495
|
+
score_color = GREEN if passed else (YELLOW if total_score >= 70 else RED)
|
|
496
|
+
verdict = f"{GREEN}PASS ✓{RESET}" if passed else f"{RED}FAIL ✗{RESET}"
|
|
497
|
+
print(f" {'TOTAL SCORE':<30} {score_color}{total_score:5.1f}%{RESET} → {verdict}")
|
|
498
|
+
print(f"{CYAN}{'─' * 55}{RESET}\n")
|
|
499
|
+
|
|
500
|
+
all_issues = [(r.display_name, i) for r in results for i in r.issues]
|
|
501
|
+
if all_issues:
|
|
502
|
+
print(f"{YELLOW} Issues:{RESET}")
|
|
503
|
+
for cat, issue in all_issues[:8]:
|
|
504
|
+
print(f" ✗ {cat}: {issue}")
|
|
505
|
+
if len(all_issues) > 8:
|
|
506
|
+
print(f" ... and {len(all_issues) - 8} more (see report)")
|
|
507
|
+
print()
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# ╔══════════════════════════════════════════════════════════════════════════╗
|
|
511
|
+
# ║ MAIN ║
|
|
512
|
+
# ╚══════════════════════════════════════════════════════════════════════════╝
|
|
513
|
+
|
|
514
|
+
def main() -> None:
|
|
515
|
+
parser = argparse.ArgumentParser(description='Grade babel-fish output')
|
|
516
|
+
parser.add_argument('--project-root', type=Path, default=None)
|
|
517
|
+
parser.add_argument('--iteration', type=int, default=1)
|
|
518
|
+
parser.add_argument('--total', type=int, default=3, help='Total iterations planned')
|
|
519
|
+
parser.add_argument('--previous-score', type=float, default=None)
|
|
520
|
+
parser.add_argument('--report-path', type=Path, default=None,
|
|
521
|
+
help='Override report output path')
|
|
522
|
+
args = parser.parse_args()
|
|
523
|
+
|
|
524
|
+
global PROJECT_ROOT
|
|
525
|
+
if args.project_root:
|
|
526
|
+
PROJECT_ROOT = args.project_root.resolve()
|
|
527
|
+
|
|
528
|
+
print(f"[grader] Grading iteration {args.iteration}/{args.total}...")
|
|
529
|
+
|
|
530
|
+
results = [
|
|
531
|
+
grade_section_completeness(),
|
|
532
|
+
grade_vocabulary_accuracy(),
|
|
533
|
+
grade_import_chains(),
|
|
534
|
+
grade_secret_safety(),
|
|
535
|
+
grade_section_sizes(),
|
|
536
|
+
grade_structural_integrity(),
|
|
537
|
+
grade_checksum_functionality(),
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
total_score = sum(r.weighted for r in results)
|
|
541
|
+
passed = total_score >= PASS_THRESHOLD
|
|
542
|
+
|
|
543
|
+
print_terminal_summary(results, total_score, passed, args.iteration, args.total)
|
|
544
|
+
|
|
545
|
+
# Write report
|
|
546
|
+
report_content = build_report(results, total_score, passed,
|
|
547
|
+
args.iteration, args.total, args.previous_score)
|
|
548
|
+
|
|
549
|
+
report_path = args.report_path or (REPORTS_DIR / f"iteration-{args.iteration:02d}-report.md")
|
|
550
|
+
report_path.write_text(report_content, encoding='utf-8')
|
|
551
|
+
|
|
552
|
+
# Also write/update the main install report
|
|
553
|
+
install_report = REPORTS_DIR / "install-report.md"
|
|
554
|
+
if args.iteration == 1:
|
|
555
|
+
install_report.write_text(report_content, encoding='utf-8')
|
|
556
|
+
else:
|
|
557
|
+
# Append this iteration's report
|
|
558
|
+
existing = install_report.read_text(encoding='utf-8') if install_report.exists() else ''
|
|
559
|
+
separator = f"\n\n{'='*60}\n\n"
|
|
560
|
+
install_report.write_text(existing + separator + report_content, encoding='utf-8')
|
|
561
|
+
|
|
562
|
+
print(f"[grader] Report written → {report_path}")
|
|
563
|
+
print(f"[grader] Install report → {install_report}")
|
|
564
|
+
|
|
565
|
+
# Output score as JSON for installer to read
|
|
566
|
+
score_data = {
|
|
567
|
+
'score': round(total_score, 2),
|
|
568
|
+
'passed': passed,
|
|
569
|
+
'threshold': PASS_THRESHOLD,
|
|
570
|
+
'iteration': args.iteration,
|
|
571
|
+
'issues': [
|
|
572
|
+
{'category': r.category, 'issue': i}
|
|
573
|
+
for r in results for i in r.issues
|
|
574
|
+
],
|
|
575
|
+
}
|
|
576
|
+
score_json = REPORTS_DIR / f"iteration-{args.iteration:02d}-score.json"
|
|
577
|
+
score_json.write_text(json.dumps(score_data, indent=2), encoding='utf-8')
|
|
578
|
+
|
|
579
|
+
sys.exit(0 if passed else 1)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
if __name__ == '__main__':
|
|
583
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|