"""jof250 command-line interface (Typer).""" from __future__ import annotations import json, typer from pathlib import Path from typing import Optional app = typer.Typer(help="JoF-250 Writing Engine — UQO WP45", no_args_is_help=True) corpus_app = typer.Typer(help="Corpus inventory and parsing"); app.add_typer(corpus_app, name="corpus") profiles_app = typer.Typer(help="Writing profiles"); app.add_typer(profiles_app, name="profiles") @corpus_app.command("audit") def corpus_audit(verify: bool = typer.Option(True, help="Verify DOIs against Crossref (network)")): """Phase 1: inventory, validate, deduplicate, verify and freeze the corpus manifest.""" from . import inventory df, s = inventory.build(verify=verify) typer.echo(json.dumps(s, indent=1)) @corpus_app.command("parse") def corpus_parse(limit: Optional[int] = None, force: bool = False, doc: Optional[str] = None): """Phase 2: structured extraction of every included PDF (PyMuPDF).""" from . import parsing typer.echo(json.dumps(parsing.parse_corpus(limit=limit, force=force, only=doc), indent=1)) @corpus_app.command("annotate") def corpus_annotate(limit: Optional[int] = None, force: bool = False, doc: Optional[str] = None, workers: int = 4): """Phase 3: schema-validated LLM annotation of rhetorical structure (uses API; budget-capped).""" from . import rhetoric typer.echo(json.dumps(rhetoric.annotate_corpus(limit=limit, force=force, only=doc, workers=workers), indent=1)) @profiles_app.command("build") def profiles_build(): """Phase 4: compute measured writing profiles (YAML) + figures from parsed/annotated corpus.""" from . import features typer.echo(json.dumps(features.build_profiles(), indent=1, default=str)) @app.command() def index(): """Phase 5: build the hybrid retrieval index (BM25 + local embeddings).""" from . import embeddings typer.echo(json.dumps(embeddings.build_index(), indent=1)) @app.command() def retrieve(topic: str = typer.Option(..., "--topic"), k: int = 8, section: Optional[str] = None, year: Optional[int] = None, unit: str = "annotation"): """Hybrid retrieval with provenance.""" from . import retrieval for r in retrieval.search(topic, k=k, section=section, year=year, unit=unit): typer.echo(json.dumps(r, ensure_ascii=False)) @app.command() def 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"): """Draft / outline / upgrade a manuscript section using the engine (proposes a new file, never edits the manuscript).""" from . import generation res = generation.run(paper=paper, section=section, mode=mode, output=output, profile=profile, condition=condition) typer.echo(json.dumps({k: v for k, v in res.items() if k != "text"}, indent=1, default=str)) if not output: typer.echo(res["text"]) @app.command() def review(paper: Path = typer.Option(..., "--paper"), output: Optional[Path] = None, full: bool = True, role: str = "referee"): """Referee-style review of a manuscript (role: referee|editor|table_editor|integrity).""" from . import generation res = generation.review(paper=paper, output=output, role=role) typer.echo(json.dumps({k: v for k, v in res.items() if k != "text"}, indent=1, default=str)) if not output: typer.echo(res["text"]) @app.command() def audit(paper: Path = typer.Option(..., "--paper"), tables: bool = True, equations: bool = True, citations: bool = True, overlap: bool = True, output: Optional[Path] = None): """Deterministic manuscript audit: tables, equations, citations/DOIs, numeric consistency, overlap with corpus.""" from . import auditing rep = auditing.audit_manuscript(paper, tables=tables, equations=equations, citations=citations, overlap=overlap) txt = auditing.render_report(rep) if output: Path(output).write_text(txt); typer.echo(f"written {output}") else: typer.echo(txt) @app.command() def evidence(paper: Path = typer.Option(..., "--paper"), claim: str = typer.Option(..., "--claim")): """Locate evidence for a claim inside the manuscript (tables, numbers, sections) and report support status.""" from . import auditing typer.echo(json.dumps(auditing.evidence_for_claim(paper, claim), indent=1, ensure_ascii=False)) @app.command() def 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, workers: int = 8): """Run the preregistered evaluation protocol (A–E conditions) — logs cost, tokens, latency; never fabricates.""" from . import evaluation typer.echo(json.dumps(evaluation.run_benchmark(config_path, protocol=protocol, dry_run=dry_run, tasks=tasks, conditions=conditions, reps=reps, workers=workers), indent=1, default=str)) @app.command() def status(): """Show pipeline status (manifest, parsed, annotated, profiles, index, experiments).""" from . import reporting typer.echo(reporting.status_text()) @app.command("install-claude-code") def install_claude_code(scope: str = typer.Option("project", help="project|user"), force: bool = False): """Write Claude Code commands/skills/agents (project scope) or copy them to ~/.claude (user scope, explicit).""" from .integrations import claude_code typer.echo(json.dumps(claude_code.install(scope=scope, force=force), indent=1)) if __name__ == "__main__": app()