#!/usr/bin/env python3 """Create a uniquely numbered, generic Code4History research card. Stdlib only.""" from __future__ import annotations import argparse import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] KINDS = { 'primary': ('SRC-P', '01_sources/records/primary', 'primary.md'), 'secondary': ('SRC-S', '01_sources/records/secondary', 'secondary.md'), 'evidence': ('EV', '02_evidence', 'evidence.md'), 'lead': ('L', '03_inquiry/leads', 'lead.md'), 'question': ('Q', '03_inquiry/questions', 'question.md'), 'hypothesis': ('H', '03_inquiry/hypotheses', 'hypothesis.md'), 'claim': ('C', '06_arguments/claims', 'claim.md'), 'synthesis': ('SYN', '06_arguments/syntheses', 'synthesis.md'), 'conclusion': ('CON', '06_arguments/conclusions', 'conclusion.md'), 'decision': ('D', '00_governance', 'decision.md'), 'work': ('W', '05_historiography/works', 'work.md'), } def create(root: Path, kind: str, slug: str, dry_run: bool = False) -> tuple[str, Path]: if kind not in KINDS: raise ValueError(f'Unknown kind: {kind}') if not re.fullmatch(r'[a-z0-9]+(?:-[a-z0-9]+)*', slug): raise ValueError('Slug must be lowercase ASCII kebab-case (e.g. archive-letter).') prefix, folder, template = KINDS[kind] folder_path = root / folder folder_path.mkdir(parents=True, exist_ok=True) # Identify all allocated IDs, including cards no longer in their expected folder. occupied = set() id_pattern = re.compile(rf'^{re.escape(prefix)}-(\d{{4}})(?:-|\.md)') for p in root.rglob('*.md'): if '10_templates' in p.parts or '.agents' in p.parts: continue m = id_pattern.match(p.name) if m: occupied.add(int(m.group(1))) n = max(occupied, default=0) + 1 ident = f'{prefix}-{n:04d}' output = folder_path / f'{ident}-{slug}.md' if output.exists(): raise FileExistsError(output) if not dry_run: template_path = root / '10_templates' / template if not template_path.is_file(): raise FileNotFoundError(template_path) text = template_path.read_text(encoding='utf-8').replace('__ID__', ident) output.write_text(text, encoding='utf-8') return ident, output def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--kind', required=True, choices=sorted(KINDS)) parser.add_argument('--slug', required=True, help='ASCII kebab-case; not a historical claim') parser.add_argument('--dry-run', action='store_true') args = parser.parse_args() try: ident, path = create(ROOT, args.kind, args.slug, args.dry_run) except (ValueError, FileNotFoundError, FileExistsError) as exc: parser.error(str(exc)) print(f'{"WOULD CREATE" if args.dry_run else "CREATED"}: {ident} → {path.relative_to(ROOT)}') if __name__ == '__main__': main()