1"""jof250 command-line interface (Typer)."""2from __future__ import annotations3import json, typer4from pathlib import Path5from typing import Optional6app = typer.Typer(help="JoF-250 Writing Engine — UQO WP45", no_args_is_help=True)7corpus_app = typer.Typer(help="Corpus inventory and parsing"); app.add_typer(corpus_app, name="corpus")8profiles_app = typer.Typer(help="Writing profiles"); app.add_typer(profiles_app, name="profiles")910@corpus_app.command("audit")11def corpus_audit(verify: bool = typer.Option(True, help="Verify DOIs against Crossref (network)")):12 """Phase 1: inventory, validate, deduplicate, verify and freeze the corpus manifest."""13 from . import inventory14 df, s = inventory.build(verify=verify)15 typer.echo(json.dumps(s, indent=1))1617@corpus_app.command("parse")18def corpus_parse(limit: Optional[int] = None, force: bool = False, doc: Optional[str] = None):19 """Phase 2: structured extraction of every included PDF (PyMuPDF)."""20 from . import parsing21 typer.echo(json.dumps(parsing.parse_corpus(limit=limit, force=force, only=doc), indent=1))2223@corpus_app.command("annotate")24def corpus_annotate(limit: Optional[int] = None, force: bool = False, doc: Optional[str] = None, workers: int = 4):25 """Phase 3: schema-validated LLM annotation of rhetorical structure (uses API; budget-capped)."""26 from . import rhetoric27 typer.echo(json.dumps(rhetoric.annotate_corpus(limit=limit, force=force, only=doc, workers=workers), indent=1))2829@profiles_app.command("build")30def profiles_build():31 """Phase 4: compute measured writing profiles (YAML) + figures from parsed/annotated corpus."""32 from . import features33 typer.echo(json.dumps(features.build_profiles(), indent=1, default=str))3435@app.command()36def index():37 """Phase 5: build the hybrid retrieval index (BM25 + local embeddings)."""38 from . import embeddings39 typer.echo(json.dumps(embeddings.build_index(), indent=1))4041@app.command()42def retrieve(topic: str = typer.Option(..., "--topic"), k: int = 8, section: Optional[str] = None, year: Optional[int] = None, unit: str = "annotation"):43 """Hybrid retrieval with provenance."""44 from . import retrieval45 for r in retrieval.search(topic, k=k, section=section, year=year, unit=unit):46 typer.echo(json.dumps(r, ensure_ascii=False))4748@app.command()49def write(paper: Path = typer.Option(..., "--paper"), section: str = typer.Option("introduction", "--section"), output: Optional[Path] = None, mode: str = typer.Option("write", help="outline|write|upgrade"), profile: Optional[str] = None, condition: str = "E_full_engine"):50 """Draft / outline / upgrade a manuscript section using the engine (proposes a new file, never edits the manuscript)."""51 from . import generation52 res = generation.run(paper=paper, section=section, mode=mode, output=output, profile=profile, condition=condition)53 typer.echo(json.dumps({k: v for k, v in res.items() if k != "text"}, indent=1, default=str))54 if not output: typer.echo(res["text"])5556@app.command()57def review(paper: Path = typer.Option(..., "--paper"), output: Optional[Path] = None, full: bool = True, role: str = "referee"):58 """Referee-style review of a manuscript (role: referee|editor|table_editor|integrity)."""59 from . import generation60 res = generation.review(paper=paper, output=output, role=role)61 typer.echo(json.dumps({k: v for k, v in res.items() if k != "text"}, indent=1, default=str))62 if not output: typer.echo(res["text"])6364@app.command()65def audit(paper: Path = typer.Option(..., "--paper"), tables: bool = True, equations: bool = True, citations: bool = True, overlap: bool = True, output: Optional[Path] = None):66 """Deterministic manuscript audit: tables, equations, citations/DOIs, numeric consistency, overlap with corpus."""67 from . import auditing68 rep = auditing.audit_manuscript(paper, tables=tables, equations=equations, citations=citations, overlap=overlap)69 txt = auditing.render_report(rep)70 if output: Path(output).write_text(txt); typer.echo(f"written {output}")71 else: typer.echo(txt)7273@app.command()74def evidence(paper: Path = typer.Option(..., "--paper"), claim: str = typer.Option(..., "--claim")):75 """Locate evidence for a claim inside the manuscript (tables, numbers, sections) and report support status."""76 from . import auditing77 typer.echo(json.dumps(auditing.evidence_for_claim(paper, claim), indent=1, ensure_ascii=False))7879@app.command()80def benchmark(config_path: Path = typer.Option(Path("config/evaluation.yaml"), "--config"), protocol: Optional[str] = None, dry_run: bool = False, tasks: Optional[str] = None, conditions: Optional[str] = None, reps: Optional[int] = None):81 """Run the preregistered evaluation protocol (A–E conditions) — logs cost, tokens, latency; never fabricates."""82 from . import evaluation83 typer.echo(json.dumps(evaluation.run_benchmark(config_path, protocol=protocol, dry_run=dry_run, tasks=tasks, conditions=conditions, reps=reps), indent=1, default=str))8485@app.command()86def status():87 """Show pipeline status (manifest, parsed, annotated, profiles, index, experiments)."""88 from . import reporting89 typer.echo(reporting.status_text())9091@app.command("install-claude-code")92def install_claude_code(scope: str = typer.Option("project", help="project|user"), force: bool = False):93 """Write Claude Code commands/skills/agents (project scope) or copy them to ~/.claude (user scope, explicit)."""94 from .integrations import claude_code95 typer.echo(json.dumps(claude_code.install(scope=scope, force=force), indent=1))9697if __name__ == "__main__":98 app()99