Guide — full enrichment pass: 19 tense guides + 36 grammar notes
Brings every tense guide and grammar note in the app up to teacher-
handout depth via parallel research subagents drafting against a
shared "thorough" checklist (TL;DR, usages, conjugation table, common
irregulars, mnemonic, top pitfalls, contrast with neighbour topic,
real-world dialogue example).
Tense guides — Conjuga/conjuga_data.json (tenseGuides[].body)
All 19 remaining guides rewritten. The 20th (subj_presente) was
enriched in the prior commit. Each body now ~4-5.5K chars (vs the
500-1500 chars of the pre-pass reference cards), covering:
- All five indicative tenses, both conditionals, both imperatives.
- Full subjunctive set including the archaic futuro / futuro
perfecto, framed honestly with "recognise, don't produce" guidance.
- Per-tense conjugation patterns and the top 5-15 irregular verbs.
- Tense-vs-tense contrasts (preterite↔imperfect, future↔ir-a,
-ra↔-se past subjunctive, etc.).
- Pitfalls that English speakers actually make.
Grammar notes — Conjuga/Conjuga/Models/GrammarNote.swift
All 36 notes audited and rewritten where the existing body was
missing one of: explicit mnemonic, contrast pair, pitfalls section,
or coverage of a key sub-topic. None copied verbatim — every note
got at least one of those slotted in. Notable additions:
- DOCTOR/PLACE, WEIRDO, ESCAPA, RID, PRODDS, BANGS, RRPIA mnemonics
where missing.
- commands-imperative: nosotros + vosotros forms were entirely
absent; both added with the -d/-os and present-subjunctive rules.
- relative-pronouns: el que/el cual distinction, cuyo, lo que/lo
cual, donde/adonde.
- se-constructions: all 6 uses including the le→se substitution.
- irregular-yo-verbs: impact on subjunctive and negative tú command.
- Plus 5-item pitfalls sections on every note that lacked one.
Tooling — Conjuga/Scripts/guide-enrichment/
- PLAN.md (prior commit) — the audit, checklist, and priority order
that drove this pass.
- apply_drafts.py (new) — reads drafts/out/*.md, swaps tense guides
into the JSON and grammar notes into the Swift source via regex on
the GrammarNote(...) declarations. Handles multi-block `#` comment
headers some agents emitted. drafts/in/ and drafts/out/ are
gitignored — regeneratable from current state.
DataLoader.swift — courseDataVersion 8 → 9 so existing installs re-
seed all guides on next launch.
Verification:
- `swift -frontend -parse` on GrammarNote.swift succeeds (exit 0).
- JSON validates (python3 json.load round-trip).
- Triple-quote count is even (72 = 36 pairs, matching 36 notes).
- Full xcodebuild verify deferred — local SDK install was disrupted
by an Xcode update; will retest as part of the next ad-hoc deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply enriched bodies from drafts/out/ back into the live source files.
|
||||
|
||||
Tense guides → Conjuga/Conjuga/conjuga_data.json (tenseGuides[].body)
|
||||
Grammar notes → Conjuga/Conjuga/Models/GrammarNote.swift (body: \"\"\"...\"\"\")
|
||||
|
||||
Filename conventions in drafts/out/:
|
||||
tense__<tenseId>.md — body to drop into the matching tenseGuide
|
||||
note__<noteId>.md — body to drop into the matching GrammarNote(...) declaration
|
||||
|
||||
Run from anywhere; uses absolute paths anchored at the repo root.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path('/Users/m4mini/Desktop/code/Spanish')
|
||||
OUT_DIR = REPO / 'Conjuga/Scripts/guide-enrichment/out'
|
||||
TENSE_JSON = REPO / 'Conjuga/Conjuga/conjuga_data.json'
|
||||
NOTES_SWIFT = REPO / 'Conjuga/Conjuga/Models/GrammarNote.swift'
|
||||
|
||||
|
||||
def read_draft(path: Path) -> str:
|
||||
"""Drafts may start with comment blocks like `# Title: ...`, `# Category:
|
||||
...`, and `# ENRICHED BODY` separated by blank lines. Strip every leading
|
||||
line that is blank or starts with `#` until we reach the actual body."""
|
||||
raw = path.read_text(encoding='utf-8')
|
||||
lines = raw.splitlines()
|
||||
start = 0
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped == '' or stripped.startswith('#'):
|
||||
start = i + 1
|
||||
continue
|
||||
# Hit the first real content line — keep everything from here.
|
||||
start = i
|
||||
break
|
||||
body = '\n'.join(lines[start:]).strip()
|
||||
if not body:
|
||||
raise ValueError(f'Empty body after stripping header in {path}')
|
||||
return body
|
||||
|
||||
|
||||
def apply_tense_guides() -> int:
|
||||
data = json.loads(TENSE_JSON.read_text(encoding='utf-8'))
|
||||
drafts = sorted(OUT_DIR.glob('tense__*.md'))
|
||||
by_id = {g['tenseId']: g for g in data['tenseGuides']}
|
||||
applied = 0
|
||||
for path in drafts:
|
||||
tense_id = path.stem.removeprefix('tense__')
|
||||
if tense_id not in by_id:
|
||||
print(f' SKIP {tense_id}: not in tenseGuides', file=sys.stderr)
|
||||
continue
|
||||
body = read_draft(path)
|
||||
by_id[tense_id]['body'] = body
|
||||
applied += 1
|
||||
print(f' applied tense: {tense_id} ({len(body)} chars)')
|
||||
TENSE_JSON.write_text(
|
||||
json.dumps(data, ensure_ascii=False, separators=(',', ':')),
|
||||
encoding='utf-8'
|
||||
)
|
||||
return applied
|
||||
|
||||
|
||||
# Match each GrammarNote(...) declaration. Body uses """...""" — may contain
|
||||
# anything except a triple-quote.
|
||||
NOTE_PATTERN = re.compile(
|
||||
r'(GrammarNote\(\s*id:\s*"([^"]+)",\s*'
|
||||
r'title:\s*"(?:[^"\\]|\\.)*",\s*'
|
||||
r'category:\s*"[^"]+",\s*'
|
||||
r'body:\s*""")(.*?)(""")',
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def apply_grammar_notes() -> int:
|
||||
src = NOTES_SWIFT.read_text(encoding='utf-8')
|
||||
drafts = sorted(OUT_DIR.glob('note__*.md'))
|
||||
by_id = {p.stem.removeprefix('note__'): p for p in drafts}
|
||||
applied = [0]
|
||||
|
||||
def replace_match(m):
|
||||
prefix, note_id, _, suffix = m.group(1), m.group(2), m.group(3), m.group(4) if m.lastindex and m.lastindex >= 4 else m.group(3)
|
||||
return m.group(0) # placeholder, see real callback below
|
||||
|
||||
def real_replace(m):
|
||||
prefix = m.group(1)
|
||||
note_id = m.group(2)
|
||||
suffix = m.group(4)
|
||||
if note_id not in by_id:
|
||||
return m.group(0)
|
||||
body = read_draft(by_id[note_id])
|
||||
if '"""' in body:
|
||||
raise ValueError(f'Body for {note_id} contains triple-quote — would break Swift parser')
|
||||
# Re-indent to match the existing Swift block. The existing format uses
|
||||
# 8 spaces of leading indent inside body lines. We don't enforce that —
|
||||
# the Swift compiler handles multiline string indentation by stripping
|
||||
# the leading whitespace common to all lines based on the closing """.
|
||||
# Just write the body verbatim.
|
||||
applied[0] += 1
|
||||
print(f' applied note: {note_id} ({len(body)} chars)')
|
||||
return f'{prefix}\n{body}\n{suffix}'
|
||||
|
||||
new_src = NOTE_PATTERN.sub(real_replace, src)
|
||||
NOTES_SWIFT.write_text(new_src, encoding='utf-8')
|
||||
return applied[0]
|
||||
|
||||
|
||||
def main():
|
||||
if not OUT_DIR.exists():
|
||||
print(f'No drafts/out directory at {OUT_DIR}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f'=== Tense guides ===')
|
||||
tense_count = apply_tense_guides()
|
||||
print(f'\n=== Grammar notes ===')
|
||||
note_count = apply_grammar_notes()
|
||||
|
||||
print(f'\nTotal applied: {tense_count} tense guides + {note_count} grammar notes')
|
||||
if tense_count == 0 and note_count == 0:
|
||||
print('Nothing applied — drafts/out/ was empty.', file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user