eval-doctor 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sneha Upadhyaula
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: eval-doctor
3
+ Version: 0.1.0
4
+ Summary: Know what you are not testing before you ship - an eval coverage auditor for RAG and agent systems.
5
+ Author: Sneha Upadhyaula
6
+ License: MIT
7
+ Keywords: evals,llm,rag,agents,testing,evaluation
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Quality Assurance
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: typer>=0.9
16
+ Requires-Dist: pydantic>=2.0
17
+ Requires-Dist: rich>=13.0
18
+ Requires-Dist: pyyaml>=6.0
19
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
20
+ Provides-Extra: anthropic
21
+ Requires-Dist: anthropic>=0.40; extra == "anthropic"
22
+ Provides-Extra: openai
23
+ Requires-Dist: openai>=1.60; extra == "openai"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Requires-Dist: ruff>=0.15; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # Eval Doctor
30
+
31
+ > Know what you are not testing before you ship.
32
+
33
+ Eval Doctor is a CLI that helps AI builders find blind spots in their eval suite — a linter for AI evals.
34
+
35
+ Most AI projects start with architecture and implementation. Evals come later, once the system already "kind of works." But for RAG systems and agents, the hardest part is not writing test cases — it's knowing **what failure modes matter**. Eval Doctor scans your repo, detects your AI system's capabilities, maps them to expected evaluation dimensions, and tells you what your current eval suite is missing.
36
+
37
+ ```text
38
+ You are testing faithfulness and answer relevance.
39
+
40
+ You are not testing:
41
+ - empty retrieval
42
+ - stale documents
43
+ - tool timeout
44
+ - hallucinated tool arguments
45
+ - prompt injection
46
+ - ambiguous user requests
47
+ ```
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install eval-doctor # static analysis, no API keys needed
53
+ pip install "eval-doctor[anthropic]" # + optional LLM-powered capability detection (Claude API)
54
+ pip install "eval-doctor[openai]" # + optional LLM-powered capability detection (OpenAI API)
55
+ ```
56
+
57
+ Already have [Claude Code](https://claude.com/claude-code) installed? No extra or API key needed — `eval-doctor audit --llm` uses your Claude subscription through the `claude` CLI.
58
+
59
+ ## Usage
60
+
61
+ ```bash
62
+ cd your-ai-project
63
+ eval-doctor audit
64
+ ```
65
+
66
+ You get a console report plus two files:
67
+
68
+ - `eval_doctor_report.md` — human-readable coverage report
69
+ - `eval_doctor_findings.json` — machine-readable findings for automation
70
+
71
+ Options:
72
+
73
+ ```bash
74
+ eval-doctor audit --repo path/to/repo # audit a different directory
75
+ eval-doctor audit --evals path/to/evals # eval files living outside the repo
76
+ eval-doctor audit --output-dir reports/ # where to write report files
77
+ eval-doctor audit --llm # force LLM enrichment (also enables the claude CLI path)
78
+ eval-doctor audit --no-llm # force pure static analysis
79
+ eval-doctor audit --force # overwrite existing report files
80
+ eval-doctor interview # short risk interview; audit picks up the answers
81
+ eval-doctor show-taxonomy # print the built-in taxonomy
82
+ ```
83
+
84
+ ### Risk interview
85
+
86
+ Severity defaults are generic; your risks are not. `eval-doctor interview` asks a few multiple-choice questions keyed to what the scanner actually found in your repo — retrieval, web grounding, tool calling, SQL, memory — plus where the system will run, and writes `eval_doctor_risk.json`. The next `eval-doctor audit` picks it up automatically (or point at one with `--risk path/to/eval_doctor_risk.json`) and elevates the severity of the dimensions you said would hurt most, re-ordering the report. Answers only ever raise severity — the interview never argues a risk away.
87
+
88
+ ## How it works
89
+
90
+ ```text
91
+ Repo Scanner ──► Capability Extractor ──► Coverage Engine ──► Report
92
+ (deterministic) (static rules, (expected vs
93
+ optional LLM enrich) observed dimensions)
94
+ ```
95
+
96
+ 1. **Scan** — a deterministic scanner walks your repo and detects signals: frameworks (LangChain, LlamaIndex, CrewAI, ...), vector DBs, embeddings, chunking, reranking, tool definitions, SQL, browser automation, memory, prompt templates, and existing eval/test files. Nothing leaves your machine.
97
+ 2. **Extract** — signals are mapped to a capability profile (`rag`, `tool_calling_agent`, `sql_agent`, ...).
98
+ 3. **Compare** — the profile selects applicable dimensions from a built-in taxonomy of 40 eval dimensions across RAG, tool calling, SQL agents, memory, and general AI risks. Your existing eval files are checked for evidence of each.
99
+ 4. **Report** — every applicable dimension is rated `covered`, `weak`, or `missing`, with a severity and a concrete recommendation.
100
+
101
+ ### Optional LLM enhancement
102
+
103
+ Static analysis sees imports and patterns, not intent. With an LLM available, Eval Doctor sends its collected evidence snippets (never your whole repo) to a model to correct and complete the capability profile — catching things like web-search-grounded generation that deserves faithfulness evals, hand-rolled RAG with no framework, and pruning false positives like an sqlite cache being flagged as a SQL agent. Two ways to enable it:
104
+
105
+ - **API key** — set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` and install the matching extra. Used automatically when present.
106
+ - **Claude subscription** — have [Claude Code](https://claude.com/claude-code) installed and signed in, then pass `--llm`. Eval Doctor shells out to `claude` on your machine; no API key or extra needed. This path is opt-in only — an installed CLI is never used unless you ask.
107
+
108
+ Without either, everything still works; the report just notes it ran in static-only mode.
109
+
110
+ Additional providers are welcome contributions: implement the two-method `LLMProvider` protocol in [`eval_doctor/llm.py`](eval_doctor/llm.py) — the existing providers are small, self-contained classes.
111
+
112
+ ## Example
113
+
114
+ See [`examples/sample_report.md`](examples/sample_report.md) for the report produced by auditing a small RAG + tool-calling fixture project.
115
+
116
+ ## Roadmap
117
+
118
+ - **Candidate eval generation** — generate golden eval cases for your missing dimensions
119
+ - **Coverage diff** — `eval-doctor diff` to score an eval suite against recommended coverage
120
+ - **CI / PR mode** — comment on PRs that add AI capabilities without corresponding evals
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ pip install -e ".[dev]"
126
+ pytest
127
+ ```
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,103 @@
1
+ # Eval Doctor
2
+
3
+ > Know what you are not testing before you ship.
4
+
5
+ Eval Doctor is a CLI that helps AI builders find blind spots in their eval suite — a linter for AI evals.
6
+
7
+ Most AI projects start with architecture and implementation. Evals come later, once the system already "kind of works." But for RAG systems and agents, the hardest part is not writing test cases — it's knowing **what failure modes matter**. Eval Doctor scans your repo, detects your AI system's capabilities, maps them to expected evaluation dimensions, and tells you what your current eval suite is missing.
8
+
9
+ ```text
10
+ You are testing faithfulness and answer relevance.
11
+
12
+ You are not testing:
13
+ - empty retrieval
14
+ - stale documents
15
+ - tool timeout
16
+ - hallucinated tool arguments
17
+ - prompt injection
18
+ - ambiguous user requests
19
+ ```
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install eval-doctor # static analysis, no API keys needed
25
+ pip install "eval-doctor[anthropic]" # + optional LLM-powered capability detection (Claude API)
26
+ pip install "eval-doctor[openai]" # + optional LLM-powered capability detection (OpenAI API)
27
+ ```
28
+
29
+ Already have [Claude Code](https://claude.com/claude-code) installed? No extra or API key needed — `eval-doctor audit --llm` uses your Claude subscription through the `claude` CLI.
30
+
31
+ ## Usage
32
+
33
+ ```bash
34
+ cd your-ai-project
35
+ eval-doctor audit
36
+ ```
37
+
38
+ You get a console report plus two files:
39
+
40
+ - `eval_doctor_report.md` — human-readable coverage report
41
+ - `eval_doctor_findings.json` — machine-readable findings for automation
42
+
43
+ Options:
44
+
45
+ ```bash
46
+ eval-doctor audit --repo path/to/repo # audit a different directory
47
+ eval-doctor audit --evals path/to/evals # eval files living outside the repo
48
+ eval-doctor audit --output-dir reports/ # where to write report files
49
+ eval-doctor audit --llm # force LLM enrichment (also enables the claude CLI path)
50
+ eval-doctor audit --no-llm # force pure static analysis
51
+ eval-doctor audit --force # overwrite existing report files
52
+ eval-doctor interview # short risk interview; audit picks up the answers
53
+ eval-doctor show-taxonomy # print the built-in taxonomy
54
+ ```
55
+
56
+ ### Risk interview
57
+
58
+ Severity defaults are generic; your risks are not. `eval-doctor interview` asks a few multiple-choice questions keyed to what the scanner actually found in your repo — retrieval, web grounding, tool calling, SQL, memory — plus where the system will run, and writes `eval_doctor_risk.json`. The next `eval-doctor audit` picks it up automatically (or point at one with `--risk path/to/eval_doctor_risk.json`) and elevates the severity of the dimensions you said would hurt most, re-ordering the report. Answers only ever raise severity — the interview never argues a risk away.
59
+
60
+ ## How it works
61
+
62
+ ```text
63
+ Repo Scanner ──► Capability Extractor ──► Coverage Engine ──► Report
64
+ (deterministic) (static rules, (expected vs
65
+ optional LLM enrich) observed dimensions)
66
+ ```
67
+
68
+ 1. **Scan** — a deterministic scanner walks your repo and detects signals: frameworks (LangChain, LlamaIndex, CrewAI, ...), vector DBs, embeddings, chunking, reranking, tool definitions, SQL, browser automation, memory, prompt templates, and existing eval/test files. Nothing leaves your machine.
69
+ 2. **Extract** — signals are mapped to a capability profile (`rag`, `tool_calling_agent`, `sql_agent`, ...).
70
+ 3. **Compare** — the profile selects applicable dimensions from a built-in taxonomy of 40 eval dimensions across RAG, tool calling, SQL agents, memory, and general AI risks. Your existing eval files are checked for evidence of each.
71
+ 4. **Report** — every applicable dimension is rated `covered`, `weak`, or `missing`, with a severity and a concrete recommendation.
72
+
73
+ ### Optional LLM enhancement
74
+
75
+ Static analysis sees imports and patterns, not intent. With an LLM available, Eval Doctor sends its collected evidence snippets (never your whole repo) to a model to correct and complete the capability profile — catching things like web-search-grounded generation that deserves faithfulness evals, hand-rolled RAG with no framework, and pruning false positives like an sqlite cache being flagged as a SQL agent. Two ways to enable it:
76
+
77
+ - **API key** — set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` and install the matching extra. Used automatically when present.
78
+ - **Claude subscription** — have [Claude Code](https://claude.com/claude-code) installed and signed in, then pass `--llm`. Eval Doctor shells out to `claude` on your machine; no API key or extra needed. This path is opt-in only — an installed CLI is never used unless you ask.
79
+
80
+ Without either, everything still works; the report just notes it ran in static-only mode.
81
+
82
+ Additional providers are welcome contributions: implement the two-method `LLMProvider` protocol in [`eval_doctor/llm.py`](eval_doctor/llm.py) — the existing providers are small, self-contained classes.
83
+
84
+ ## Example
85
+
86
+ See [`examples/sample_report.md`](examples/sample_report.md) for the report produced by auditing a small RAG + tool-calling fixture project.
87
+
88
+ ## Roadmap
89
+
90
+ - **Candidate eval generation** — generate golden eval cases for your missing dimensions
91
+ - **Coverage diff** — `eval-doctor diff` to score an eval suite against recommended coverage
92
+ - **CI / PR mode** — comment on PRs that add AI capabilities without corresponding evals
93
+
94
+ ## Development
95
+
96
+ ```bash
97
+ pip install -e ".[dev]"
98
+ pytest
99
+ ```
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,8 @@
1
+ """Eval Doctor - an eval coverage auditor for RAG and agent systems."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("eval-doctor")
7
+ except PackageNotFoundError: # running from a source tree without install
8
+ __version__ = "0.0.0.dev0"
@@ -0,0 +1,120 @@
1
+ """Map scanner signals to a CapabilityProfile.
2
+
3
+ The static pass always runs and needs no API key. An optional LLM provider
4
+ (see llm.py) can enrich the static profile using the collected evidence.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .schemas import CapabilityProfile, Evidence, ScanResult
10
+
11
+ _RETRIEVAL_SIGNALS = {
12
+ "retrieval",
13
+ "embedding",
14
+ "chunking",
15
+ "query_rewriting",
16
+ "reranking",
17
+ }
18
+ _TOOL_SIGNALS = {"tool_calling", "http_tool"}
19
+
20
+ # Which scanner signals substantiate each capability tag (used to attach
21
+ # repo evidence to the taxonomy dimensions selected by that tag).
22
+ _TAG_SIGNALS: dict[str, set[str]] = {
23
+ "retrieval": _RETRIEVAL_SIGNALS,
24
+ "tool_calling": _TOOL_SIGNALS,
25
+ "sql": {"sql", "sql_query_text"},
26
+ "browser": {"browser"},
27
+ "web_grounding": {"web_search"},
28
+ "memory": {"memory"},
29
+ "embedding": {"embedding"},
30
+ "chunking": {"chunking"},
31
+ "query_rewriting": {"query_rewriting"},
32
+ "reranking": {"reranking"},
33
+ "prompting": {"prompt_template"},
34
+ }
35
+ _TAG_SIGNAL_PREFIXES: dict[str, tuple[str, ...]] = {"retrieval": ("vector_db:",)}
36
+
37
+
38
+ def _has_prefix(signals: list[str], prefix: str) -> bool:
39
+ return any(s.startswith(prefix) for s in signals)
40
+
41
+
42
+ def extract_static(scan: ScanResult) -> CapabilityProfile:
43
+ """Rule-based capability extraction from scanner signals."""
44
+ signals = scan.signals
45
+ profile = CapabilityProfile(source="static")
46
+
47
+ profile.retrieval = bool(_RETRIEVAL_SIGNALS & set(signals)) or _has_prefix(
48
+ signals, "vector_db:"
49
+ )
50
+ profile.browser = "browser" in signals
51
+ profile.memory = "memory" in signals
52
+ # Only real tool-wiring patterns (@tool, tools=[, input_schema, ...) make a
53
+ # tool_calling agent. http_tool alone is not enough: shikhu's requests.post
54
+ # calls its LLM provider (the model call itself, not a tool the model
55
+ # invokes), yet it profiled as a tool_calling_agent with 9 tool dims.
56
+ tool_calling = "tool_calling" in signals
57
+ # SQL counts as a capability only when it can be model-facing (wired as a
58
+ # tool). Standalone sqlite/SQL is app storage: shikhu's coverage.db got 8
59
+ # SQL-agent dims (4 critical) that hijacked the top recommendation.
60
+ profile.sql = "sql" in signals and tool_calling
61
+
62
+ if profile.retrieval:
63
+ profile.capabilities.append("retrieval")
64
+ profile.system_type.append("rag")
65
+ for cap in ("query_rewriting", "reranking", "chunking", "embedding"):
66
+ if cap in signals:
67
+ profile.capabilities.append(cap)
68
+ if tool_calling:
69
+ profile.capabilities.append("tool_calling")
70
+ profile.system_type.append("tool_calling_agent")
71
+ if profile.sql:
72
+ profile.capabilities.append("sql")
73
+ profile.tools.append("sql_tool")
74
+ if profile.browser:
75
+ profile.capabilities.append("browser")
76
+ profile.tools.append("browser_tool")
77
+ if "web_search" in signals:
78
+ profile.capabilities.append("web_grounding")
79
+ profile.tools.append("web_search_tool")
80
+ if tool_calling and "http_tool" in signals:
81
+ profile.tools.append("http_tool")
82
+ if profile.memory:
83
+ profile.capabilities.append("memory")
84
+ if "prompt_template" in signals:
85
+ profile.capabilities.append("prompting")
86
+ if not profile.system_type and (
87
+ _has_prefix(signals, "sdk:") or _has_prefix(signals, "framework:")
88
+ ):
89
+ profile.system_type.append("llm_pipeline")
90
+
91
+ profile.eval_files_detected = list(scan.eval_files)
92
+ return profile
93
+
94
+
95
+ def evidence_for_tags(evidence: list[Evidence], tags: list[str]) -> list[Evidence]:
96
+ """Scanner evidence that substantiates any of the given capability tags."""
97
+ signals: set[str] = set()
98
+ prefixes: tuple[str, ...] = ()
99
+ for tag in tags:
100
+ signals |= _TAG_SIGNALS.get(tag, set())
101
+ prefixes += _TAG_SIGNAL_PREFIXES.get(tag, ())
102
+ return [
103
+ e
104
+ for e in evidence
105
+ if e.snippet and (e.signal in signals or e.signal.startswith(prefixes))
106
+ ]
107
+
108
+
109
+ def capability_tags(profile: CapabilityProfile) -> set[str]:
110
+ """Tags used to select applicable taxonomy dimensions."""
111
+ tags = set(profile.capabilities)
112
+ if profile.retrieval:
113
+ tags.add("retrieval")
114
+ if profile.sql:
115
+ tags.add("sql")
116
+ if profile.browser:
117
+ tags.add("browser")
118
+ if profile.memory:
119
+ tags.add("memory")
120
+ return tags
@@ -0,0 +1,249 @@
1
+ """Eval Doctor CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from . import __version__
12
+ from .capability_extractor import capability_tags, evidence_for_tags, extract_static
13
+ from .coverage_engine import analyze_coverage, apply_coverage_judgments
14
+ from .interview import RISK_JSON, build_risk_profile, questions_for
15
+ from .llm import enrich_profile, get_default_provider
16
+ from .report_generator import print_console_report, write_reports
17
+ from .scanner import scan_repo
18
+ from .schemas import RiskProfile
19
+ from .taxonomy import TAXONOMY
20
+
21
+ app = typer.Typer(
22
+ name="eval-doctor",
23
+ help="Know what you are not testing before you ship - an eval coverage auditor for RAG and agent systems.",
24
+ no_args_is_help=True,
25
+ )
26
+ console = Console()
27
+
28
+
29
+ @app.command()
30
+ def audit(
31
+ repo: Path = typer.Option(Path("."), "--repo", help="Path to the repo to audit."),
32
+ evals: Path | None = typer.Option(
33
+ None, "--evals", help="Extra path to eval files outside the repo."
34
+ ),
35
+ output_dir: Path = typer.Option(
36
+ Path("."), "--output-dir", help="Where to write report files."
37
+ ),
38
+ llm: bool | None = typer.Option(
39
+ None,
40
+ "--llm/--no-llm",
41
+ help="Force LLM enrichment on/off. Default: use it when an API key is available.",
42
+ ),
43
+ risk: Path | None = typer.Option(
44
+ None,
45
+ "--risk",
46
+ help=f"Risk profile from `eval-doctor interview`. Default: {RISK_JSON} in --output-dir, if present.",
47
+ ),
48
+ force: bool = typer.Option(
49
+ False, "--force", help="Overwrite existing report files."
50
+ ),
51
+ ):
52
+ """Scan a repo and produce an eval coverage report."""
53
+ try:
54
+ scan = scan_repo(repo, evals_path=evals)
55
+ except FileNotFoundError as exc:
56
+ console.print(f"[red]error:[/red] {exc}")
57
+ raise typer.Exit(code=1) from None
58
+
59
+ risk_path = risk if risk is not None else output_dir / RISK_JSON
60
+ risk_profile = None
61
+ if risk_path.exists():
62
+ try:
63
+ risk_profile = RiskProfile.model_validate_json(
64
+ risk_path.read_text(encoding="utf-8")
65
+ )
66
+ except ValueError as exc:
67
+ console.print(f"[red]error:[/red] invalid risk profile {risk_path}: {exc}")
68
+ raise typer.Exit(code=1) from None
69
+ elif risk is not None:
70
+ console.print(f"[red]error:[/red] risk profile not found: {risk}")
71
+ raise typer.Exit(code=1)
72
+
73
+ profile = extract_static(scan)
74
+
75
+ provider = None
76
+ if llm is not False:
77
+ # The claude CLI (subscription, no keys) is opt-in via --llm only.
78
+ provider = get_default_provider(include_claude_cli=llm is True)
79
+ if llm is True and provider is None:
80
+ console.print(
81
+ "[red]error:[/red] --llm requested but no provider is available. "
82
+ "Set ANTHROPIC_API_KEY or OPENAI_API_KEY (and install "
83
+ "eval-doctor\\[anthropic] or eval-doctor\\[openai]), or install "
84
+ "Claude Code so the `claude` CLI can be used."
85
+ )
86
+ raise typer.Exit(code=1)
87
+ if provider is not None:
88
+ try:
89
+ with console.status(f"Enriching capability profile via {provider.name}..."):
90
+ profile = enrich_profile(provider, scan.evidence, profile)
91
+ except Exception as exc: # degrade, don't crash the audit
92
+ console.print(
93
+ f"[yellow]warning:[/yellow] LLM enrichment failed ({exc}); using static analysis only."
94
+ )
95
+ elif llm is None:
96
+ console.print(
97
+ "[dim]Static analysis only - set ANTHROPIC_API_KEY or OPENAI_API_KEY "
98
+ "(and pip install 'eval-doctor\\[anthropic]' or 'eval-doctor\\[openai]'), "
99
+ "or pass --llm to use an installed Claude Code (`claude` CLI), for "
100
+ "deeper capability detection and eval-quality judgment.[/dim]"
101
+ )
102
+
103
+ report = analyze_coverage(scan, profile, risk=risk_profile)
104
+
105
+ if not report.findings and not profile.system_type:
106
+ console.print(
107
+ f"No AI system detected in {scan.repo_path} "
108
+ f"({scan.files_scanned} files scanned) - nothing to audit."
109
+ )
110
+ raise typer.Exit()
111
+
112
+ if provider is not None and scan.eval_files:
113
+ try:
114
+ with console.status(f"Judging eval coverage via {provider.name}..."):
115
+ judgments = provider.judge_coverage(
116
+ scan.eval_file_contents, report.findings
117
+ )
118
+ apply_coverage_judgments(report, judgments)
119
+ except Exception as exc: # degrade, don't crash the audit
120
+ console.print(
121
+ f"[yellow]warning:[/yellow] LLM coverage judgment failed ({exc}); "
122
+ "keeping keyword-based coverage."
123
+ )
124
+
125
+ print_console_report(report, console)
126
+
127
+ try:
128
+ paths = write_reports(report, output_dir, force=force)
129
+ except FileExistsError as exc:
130
+ console.print(f"[red]error:[/red] {exc}")
131
+ raise typer.Exit(code=1) from None
132
+ console.print(f"Wrote {', '.join(str(p) for p in paths)}")
133
+
134
+
135
+ @app.command()
136
+ def interview(
137
+ repo: Path = typer.Option(
138
+ Path("."), "--repo", help="Path to the repo the interview is about."
139
+ ),
140
+ output_dir: Path = typer.Option(
141
+ Path("."), "--output-dir", help=f"Where to write {RISK_JSON}."
142
+ ),
143
+ force: bool = typer.Option(
144
+ False, "--force", help="Overwrite an existing risk profile."
145
+ ),
146
+ ):
147
+ """Short risk interview based on what the scanner found; feeds `audit`."""
148
+ try:
149
+ scan = scan_repo(repo)
150
+ except FileNotFoundError as exc:
151
+ console.print(f"[red]error:[/red] {exc}")
152
+ raise typer.Exit(code=1) from None
153
+
154
+ profile = extract_static(scan)
155
+ tags = capability_tags(profile)
156
+ if not profile.system_type and not tags:
157
+ console.print(
158
+ f"No AI system detected in {scan.repo_path} "
159
+ f"({scan.files_scanned} files scanned) - nothing to interview."
160
+ )
161
+ raise typer.Exit()
162
+
163
+ risk_path = Path(output_dir) / RISK_JSON
164
+ if risk_path.exists() and not force:
165
+ console.print(
166
+ f"[red]error:[/red] Refusing to overwrite {risk_path} (use --force)."
167
+ )
168
+ raise typer.Exit(code=1)
169
+
170
+ questions = questions_for(tags)
171
+ console.print("\n[bold]Eval Doctor Risk Interview[/bold]")
172
+ console.print(f"[dim]{scan.repo_path}[/dim]")
173
+ console.print(
174
+ f"Detected system type: [bold]{' + '.join(profile.system_type)}[/bold]\n"
175
+ )
176
+
177
+ answered = []
178
+ for number, question in enumerate(questions, 1):
179
+ anchors = evidence_for_tags(scan.evidence, question.applies_when)
180
+ location = f" [dim]({anchors[0].path})[/dim]" if anchors else ""
181
+ console.print(
182
+ f"[bold]\\[{number}/{len(questions)}][/bold] {question.context}{location}"
183
+ )
184
+ console.print(question.prompt)
185
+ for n, option in enumerate(question.options, 1):
186
+ console.print(f" {n}. {option.label}")
187
+ while True:
188
+ choice = typer.prompt("Answer", type=int)
189
+ if 1 <= choice <= len(question.options):
190
+ break
191
+ console.print(f"Enter a number between 1 and {len(question.options)}.")
192
+ answered.append((question, choice - 1))
193
+ console.print()
194
+
195
+ risk = build_risk_profile(scan.repo_path, answered)
196
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
197
+ risk_path.write_text(risk.model_dump_json(indent=2), encoding="utf-8")
198
+
199
+ names = {d.id: d.name for d in TAXONOMY}
200
+ if risk.elevated_dimensions:
201
+ elevated = ", ".join(names.get(d, d) for d in risk.elevated_dimensions)
202
+ console.print(f"Elevated severity for: [bold]{elevated}[/bold]")
203
+ else:
204
+ console.print("No dimensions elevated - default severities stand.")
205
+ console.print(
206
+ f"Wrote {risk_path}. Run [bold]eval-doctor audit --repo {repo}[/bold] "
207
+ "to see the re-prioritized report."
208
+ )
209
+
210
+
211
+ @app.command("show-taxonomy")
212
+ def show_taxonomy():
213
+ """Print the built-in eval coverage taxonomy."""
214
+ table = Table(
215
+ title=f"Eval Doctor taxonomy ({len(TAXONOMY)} dimensions)", show_lines=False
216
+ )
217
+ table.add_column("Category", style="bold")
218
+ table.add_column("Dimension")
219
+ table.add_column("Applies when")
220
+ table.add_column("Severity")
221
+ table.add_column("Description", max_width=60)
222
+ for dim in TAXONOMY:
223
+ table.add_row(
224
+ dim.category,
225
+ dim.name,
226
+ ", ".join(dim.applies_when),
227
+ dim.severity_default,
228
+ dim.description,
229
+ )
230
+ console.print(table)
231
+
232
+
233
+ def _print_version(value: bool):
234
+ if value:
235
+ console.print(f"eval-doctor {__version__}")
236
+ raise typer.Exit()
237
+
238
+
239
+ @app.callback()
240
+ def _main(
241
+ version: bool = typer.Option(
242
+ False,
243
+ "--version",
244
+ help="Show version and exit.",
245
+ callback=_print_version,
246
+ is_eager=True,
247
+ ),
248
+ ):
249
+ pass