compass-kb-validation 3.2.0__tar.gz → 3.3.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.
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/PKG-INFO +15 -1
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/README.md +14 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/__init__.py +1 -1
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/citation.py +17 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/cli.py +183 -4
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/corpus_health.py +11 -0
- compass_kb_validation-3.3.0/compass/eval_enrich.py +305 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/eval_interop.py +36 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/grounding.py +19 -2
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/vectorstore.py +12 -1
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/PKG-INFO +15 -1
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/SOURCES.txt +2 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/discover_and_validate.py +2 -1
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/profile_kb.py +5 -1
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/run_validation.py +2 -1
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_citation.py +29 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_corpus_health.py +12 -0
- compass_kb_validation-3.3.0/tests/test_eval_enrich.py +109 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_eval_interop.py +16 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_grounding.py +19 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_vectorstore_filters.py +14 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/LICENSE +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/__main__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/ask.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/baseline.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/chunk_coherence.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/config/__init__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/config_lint.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/content_inspect.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/corpus.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/diagnostics.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/embedding_drift.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/evidence.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/init_wizard.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/live_dashboard.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/mcp_server.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/models/__init__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/notifications.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/pipeline/__init__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/profiler.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/provenance.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/__init__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/embedding.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/graph.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/source.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/storage.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/remediation.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/__init__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/_report_css.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/_report_js.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/analysis.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/audit.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/export.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/shell.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/retrieval_analysis.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/text_probes.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/trend_tracker.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/utils/__init__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/__init__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/api_upload.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/base.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/base_sparql.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/cross_stage.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/graphdb_metadata.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/neptune_metadata.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/redis_chunks.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/registry.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/retrieval_quality.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/s3_storage.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/requires.txt +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/ingestion_validation/__init__.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/pyproject.toml +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/setup.cfg +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_alt_adapters.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_analysis_report.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_api_upload.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_ask.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_baseline.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_benchmark.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_check_catalog.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_cli.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_compat_shim.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_config.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_config_lint.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_content_inspect.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_corpus.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_coverage.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_cross_stage.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_diagnostics.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_discover_cli.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_discovery_parallel.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_embedding_drift.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_evidence.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_gate.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_graph_transport.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_graph_validators.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_init_wizard.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_manifest.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_mcp.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_migration.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_models.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_neptune_bugfixes.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_neptune_bugfixes2.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_notifications.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_pipeline.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_profiler.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_providers.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_redis_chunks.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_registry.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_remediation.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_reporting.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_retrieval_assertions.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_retrieval_quality.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_review_hardening.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_run_cli.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_sources.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_storage_validator.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_trends.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_universal_concerns.py +0 -0
- {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compass-kb-validation
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Knowledge-base readiness validation for RAG & AI agents — config-driven, deterministic pipeline integrity checks.
|
|
5
5
|
Author: COMPASS
|
|
6
6
|
License: MIT
|
|
@@ -147,6 +147,7 @@ Two modes: **live** validation as documents are ingested, and **post-hoc discove
|
|
|
147
147
|
✅ **Pluggable steps** — register custom validators in a step registry; the report adapts automatically
|
|
148
148
|
✅ **Deterministic & cheap** — structural/byte/schema checks, no LLM, CI-gateable
|
|
149
149
|
✅ **Query & analyze the KB** — ask it questions, expose it to agents over MCP, and preview embedding-model migrations / map retrieval coverage — all deterministic, read-only (see *Beyond validation*)
|
|
150
|
+
✅ **Eval oracle** — enrich any eval with deterministic KB-grounding signals, then measure how accurate its LLM judges are against that ground truth (`compass enrich` / `compass calibrate-judges`) — the eval framework guesses, COMPASS checks the KB
|
|
150
151
|
✅ **Interactive reporting** — self-contained HTML dashboard + JSON + JUnit XML
|
|
151
152
|
✅ **Trend Tracking** — SQLite historical performance analysis
|
|
152
153
|
✅ **Notifications** — Slack/Teams webhooks for alerts
|
|
@@ -238,8 +239,21 @@ compass freshness --project my-kb --stale-after 30
|
|
|
238
239
|
# chunks when present (Level 1) and the live KB (Level 2); the answer text + a reachable KB alone
|
|
239
240
|
# still catches fabricated figures and phantom citations.
|
|
240
241
|
compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
|
|
242
|
+
|
|
243
|
+
# Eval oracle (deterministic, no LLM): enrich an eval with KB-grounded signals, then measure how
|
|
244
|
+
# accurate the eval's OWN LLM judges are against that deterministic truth. LLM judges GUESS whether
|
|
245
|
+
# a response is grounded; COMPASS CHECKS it against the KB - so it can tell you which judge is right.
|
|
246
|
+
compass enrich --project my-kb --input eval.json --output enriched.json
|
|
247
|
+
compass calibrate-judges --input enriched.json --metric groundedness
|
|
241
248
|
```
|
|
242
249
|
|
|
250
|
+
`enrich` adds a `compass` block per case (a **citation grounding rate** over the verifiable
|
|
251
|
+
citation+numeric subset, citation fidelity, chunk quality, context utilization, `COMPASS_*` signal
|
|
252
|
+
codes). It is **schema-agnostic** — every field resolves through your `eval.gt_*` config, so it
|
|
253
|
+
works whatever shape your eval JSON takes. `calibrate-judges` then reports each judge's error,
|
|
254
|
+
bias, accuracy, and an evidence-based **recommended weight** — something no eval framework can
|
|
255
|
+
compute, because it needs KB access.
|
|
256
|
+
|
|
243
257
|
`migrate` and `coverage` also render a **self-contained HTML report** (same look as the
|
|
244
258
|
validation dashboard) with `--html [path] --open` — a coverage heatmap, a migration
|
|
245
259
|
stability distribution, verdicts, and the most-changed / dead-zone tables.
|
|
@@ -80,6 +80,7 @@ Two modes: **live** validation as documents are ingested, and **post-hoc discove
|
|
|
80
80
|
✅ **Pluggable steps** — register custom validators in a step registry; the report adapts automatically
|
|
81
81
|
✅ **Deterministic & cheap** — structural/byte/schema checks, no LLM, CI-gateable
|
|
82
82
|
✅ **Query & analyze the KB** — ask it questions, expose it to agents over MCP, and preview embedding-model migrations / map retrieval coverage — all deterministic, read-only (see *Beyond validation*)
|
|
83
|
+
✅ **Eval oracle** — enrich any eval with deterministic KB-grounding signals, then measure how accurate its LLM judges are against that ground truth (`compass enrich` / `compass calibrate-judges`) — the eval framework guesses, COMPASS checks the KB
|
|
83
84
|
✅ **Interactive reporting** — self-contained HTML dashboard + JSON + JUnit XML
|
|
84
85
|
✅ **Trend Tracking** — SQLite historical performance analysis
|
|
85
86
|
✅ **Notifications** — Slack/Teams webhooks for alerts
|
|
@@ -171,8 +172,21 @@ compass freshness --project my-kb --stale-after 30
|
|
|
171
172
|
# chunks when present (Level 1) and the live KB (Level 2); the answer text + a reachable KB alone
|
|
172
173
|
# still catches fabricated figures and phantom citations.
|
|
173
174
|
compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
|
|
175
|
+
|
|
176
|
+
# Eval oracle (deterministic, no LLM): enrich an eval with KB-grounded signals, then measure how
|
|
177
|
+
# accurate the eval's OWN LLM judges are against that deterministic truth. LLM judges GUESS whether
|
|
178
|
+
# a response is grounded; COMPASS CHECKS it against the KB - so it can tell you which judge is right.
|
|
179
|
+
compass enrich --project my-kb --input eval.json --output enriched.json
|
|
180
|
+
compass calibrate-judges --input enriched.json --metric groundedness
|
|
174
181
|
```
|
|
175
182
|
|
|
183
|
+
`enrich` adds a `compass` block per case (a **citation grounding rate** over the verifiable
|
|
184
|
+
citation+numeric subset, citation fidelity, chunk quality, context utilization, `COMPASS_*` signal
|
|
185
|
+
codes). It is **schema-agnostic** — every field resolves through your `eval.gt_*` config, so it
|
|
186
|
+
works whatever shape your eval JSON takes. `calibrate-judges` then reports each judge's error,
|
|
187
|
+
bias, accuracy, and an evidence-based **recommended weight** — something no eval framework can
|
|
188
|
+
compute, because it needs KB access.
|
|
189
|
+
|
|
176
190
|
`migrate` and `coverage` also render a **self-contained HTML report** (same look as the
|
|
177
191
|
validation dashboard) with `--html [path] --open` — a coverage heatmap, a migration
|
|
178
192
|
stability distribution, verdicts, and the most-changed / dead-zone tables.
|
|
@@ -245,8 +245,16 @@ def _claim_before(text: str, idx: int) -> str:
|
|
|
245
245
|
"""The statement a citation supports: the text preceding it, back to the last real
|
|
246
246
|
sentence break. Excludes the citation itself and anything after it."""
|
|
247
247
|
start = 0
|
|
248
|
+
prev_start = 0
|
|
248
249
|
for m in _SENT_BOUND.finditer(text[:idx]):
|
|
250
|
+
prev_start = start
|
|
249
251
|
start = m.end()
|
|
252
|
+
# If the last sentence boundary sits right where the citation begins, that period is the
|
|
253
|
+
# TERMINATOR of the claim sentence (the standard "...claim. (SOURCE, p. N)" format), not a
|
|
254
|
+
# separator inside the claim - so text[start:idx] would be empty and the citation would be
|
|
255
|
+
# silently dropped. Fall back one boundary to capture the claim sentence itself.
|
|
256
|
+
if start >= idx - 2:
|
|
257
|
+
start = prev_start
|
|
250
258
|
nl = text.rfind("\n", 0, idx)
|
|
251
259
|
if nl + 1 > start:
|
|
252
260
|
start = nl + 1
|
|
@@ -642,6 +650,15 @@ def audit_citations(response: str, eval_chunks: Optional[list[dict]], adapter: A
|
|
|
642
650
|
summary = summarize(verdicts, response, l1_ran, l2_ran)
|
|
643
651
|
if l1_ran:
|
|
644
652
|
summary.update(context_utilization(citations, eval_chunks or [], cfg))
|
|
653
|
+
# DEFECT-003 safety net: if the response clearly carries citation-like patterns but none
|
|
654
|
+
# were extracted, say so rather than reporting a silent total_citations=0 that reads as
|
|
655
|
+
# "no citations exist" (which was the DEFECT-001 failure mode).
|
|
656
|
+
if not citations and response:
|
|
657
|
+
raw = len(_PAREN_CITE.findall(response))
|
|
658
|
+
if raw:
|
|
659
|
+
summary["diagnostic"] = (
|
|
660
|
+
f"{raw} citation-like pattern(s) matched by regex but 0 were extracted - "
|
|
661
|
+
"likely a claim-boundary parsing issue, not an absence of citations.")
|
|
645
662
|
return {"verdicts": verdicts, "l1_ran": l1_ran, "l2_ran": l2_ran, "summary": summary}
|
|
646
663
|
|
|
647
664
|
|
|
@@ -1203,6 +1203,22 @@ def cmd_compare(argv: list[str]) -> int:
|
|
|
1203
1203
|
if not cases:
|
|
1204
1204
|
print(f" No usable cases in {args.ground_truth} "
|
|
1205
1205
|
f"(check the eval.gt_* field paths / variant filter in your config).")
|
|
1206
|
+
# Say WHICH paths were tried and what resolved on the first record, so the user can see
|
|
1207
|
+
# the actual mismatch instead of guessing (DEFECT-007).
|
|
1208
|
+
from compass.eval_interop import diagnose_paths
|
|
1209
|
+
diag = diagnose_paths(args.ground_truth, {
|
|
1210
|
+
"gt_question_path": ev.gt_question_path,
|
|
1211
|
+
"gt_chunks_path": ev.gt_chunks_path,
|
|
1212
|
+
"gt_filters_path": ev.gt_filters_path,
|
|
1213
|
+
"gt_variant_path": ev.gt_variant_path,
|
|
1214
|
+
})
|
|
1215
|
+
if diag:
|
|
1216
|
+
print(" Configured paths on the first record:")
|
|
1217
|
+
for line in diag:
|
|
1218
|
+
print(line)
|
|
1219
|
+
if variant_filter:
|
|
1220
|
+
print(f" variant filter: {', '.join(variant_filter)} "
|
|
1221
|
+
f"(via gt_variant_path=\"{ev.gt_variant_path or '(unset)'}\")")
|
|
1206
1222
|
return 2
|
|
1207
1223
|
|
|
1208
1224
|
# Ground-truth cases carry production's own metadata filters. If the backend cannot
|
|
@@ -1214,7 +1230,9 @@ def cmd_compare(argv: list[str]) -> int:
|
|
|
1214
1230
|
file=sys.stderr)
|
|
1215
1231
|
|
|
1216
1232
|
vnote = f" (variant filter: {', '.join(variant_filter)})" if variant_filter else ""
|
|
1217
|
-
|
|
1233
|
+
# Progress goes to stderr under --json so stdout stays a clean, pipeable JSON document.
|
|
1234
|
+
print(f" Comparing compass retrieval against {len(cases)} ground-truth case(s){vnote} ...",
|
|
1235
|
+
file=sys.stderr if args.json else sys.stdout)
|
|
1218
1236
|
try:
|
|
1219
1237
|
payload = comparison_report(settings, cases, top_k=args.top_k or None,
|
|
1220
1238
|
skip_empty=not args.include_empty)
|
|
@@ -1312,7 +1330,8 @@ def cmd_evidence(argv: list[str]) -> int:
|
|
|
1312
1330
|
return 2
|
|
1313
1331
|
limit = 1 if args.quick else (args.limit or settings.evidence.sample_size)
|
|
1314
1332
|
|
|
1315
|
-
print(f" compass evidence - project={args.project or '(none)'} checks={checks} limit={limit}"
|
|
1333
|
+
print(f" compass evidence - project={args.project or '(none)'} checks={checks} limit={limit}",
|
|
1334
|
+
file=sys.stderr if args.format == "json" else sys.stdout)
|
|
1316
1335
|
try:
|
|
1317
1336
|
payload = ev.gather_evidence(settings, checks, document_filter=args.document, limit=limit)
|
|
1318
1337
|
except Exception as exc: # noqa: BLE001 - unreachable/misconfigured backend -> clean message
|
|
@@ -1383,7 +1402,8 @@ def cmd_quality(argv: list[str]) -> int:
|
|
|
1383
1402
|
except Exception as exc: # noqa: BLE001 - malformed/empty project YAML -> clean message, not a traceback
|
|
1384
1403
|
print(f" Config error: {exc}")
|
|
1385
1404
|
return 2
|
|
1386
|
-
print(f" Sampling up to {args.sample} chunks for quality signals ..."
|
|
1405
|
+
print(f" Sampling up to {args.sample} chunks for quality signals ...",
|
|
1406
|
+
file=sys.stderr if args.json else sys.stdout)
|
|
1387
1407
|
try:
|
|
1388
1408
|
payload = quality_report(settings, sample=args.sample, min_chars=args.min_chars)
|
|
1389
1409
|
except Exception as exc: # noqa: BLE001 - clean message, never a traceback
|
|
@@ -1428,7 +1448,8 @@ def cmd_freshness(argv: list[str]) -> int:
|
|
|
1428
1448
|
except Exception as exc: # noqa: BLE001 - malformed/empty project YAML -> clean message, not a traceback
|
|
1429
1449
|
print(f" Config error: {exc}")
|
|
1430
1450
|
return 2
|
|
1431
|
-
print(f" Checking source freshness for up to {args.sample} document(s) ..."
|
|
1451
|
+
print(f" Checking source freshness for up to {args.sample} document(s) ...",
|
|
1452
|
+
file=sys.stderr if args.json else sys.stdout)
|
|
1432
1453
|
try:
|
|
1433
1454
|
payload = freshness_report(settings, sample=args.sample, stale_after_days=args.stale_after)
|
|
1434
1455
|
except Exception as exc: # noqa: BLE001 - clean message, never a traceback
|
|
@@ -1680,6 +1701,158 @@ def cmd_selftest(argv: list[str]) -> int:
|
|
|
1680
1701
|
# --------------------------------------------------------------------------- #
|
|
1681
1702
|
# Dispatcher
|
|
1682
1703
|
# --------------------------------------------------------------------------- #
|
|
1704
|
+
def cmd_enrich(argv: list[str]) -> int:
|
|
1705
|
+
p = argparse.ArgumentParser(
|
|
1706
|
+
prog="compass enrich",
|
|
1707
|
+
description="Add deterministic, KB-derived signals to an eval file: a citation grounding "
|
|
1708
|
+
"rate (over the verifiable citation+numeric subset), citation fidelity, chunk "
|
|
1709
|
+
"quality, context utilization, and COMPASS_* signal codes. Read-only, no LLM. "
|
|
1710
|
+
"Schema-agnostic - field locations come from your eval.gt_* config.")
|
|
1711
|
+
p.add_argument("--project")
|
|
1712
|
+
p.add_argument("--env", default="")
|
|
1713
|
+
p.add_argument("--input", required=True, metavar="FILE",
|
|
1714
|
+
help="Eval file: a JSON array of cases, or a JSON object carrying a case array")
|
|
1715
|
+
p.add_argument("--output", metavar="FILE", help="Write enriched JSON here (default: stdout)")
|
|
1716
|
+
p.add_argument("--no-retrieval", action="store_true",
|
|
1717
|
+
help="Skip live-KB (Level 2) verification; use only the eval's own chunks "
|
|
1718
|
+
"(Level 1). No backend needed.")
|
|
1719
|
+
p.add_argument("--json", action="store_true", help="(default) JSON output")
|
|
1720
|
+
args = p.parse_args(argv)
|
|
1721
|
+
_health_env(args)
|
|
1722
|
+
|
|
1723
|
+
import json as _json
|
|
1724
|
+
|
|
1725
|
+
from compass.config import load_settings, project_config_file
|
|
1726
|
+
from compass.eval_enrich import discover_cases, enrich_cases
|
|
1727
|
+
|
|
1728
|
+
if args.project and project_config_file(args.project) is None:
|
|
1729
|
+
print(f" Project '{args.project}' not found (config/projects/{args.project}.yaml).", file=sys.stderr)
|
|
1730
|
+
return 2
|
|
1731
|
+
try:
|
|
1732
|
+
settings = load_settings(project=args.project, env=args.env or None)
|
|
1733
|
+
except Exception as exc: # noqa: BLE001 - clean message, not a traceback
|
|
1734
|
+
print(f" Config error: {exc}", file=sys.stderr)
|
|
1735
|
+
return 2
|
|
1736
|
+
try:
|
|
1737
|
+
with open(args.input, encoding="utf-8") as f:
|
|
1738
|
+
data = _json.load(f)
|
|
1739
|
+
except (OSError, ValueError) as exc:
|
|
1740
|
+
print(f" Cannot read {args.input}: {exc}", file=sys.stderr)
|
|
1741
|
+
return 2
|
|
1742
|
+
cases = discover_cases(data)
|
|
1743
|
+
if not cases:
|
|
1744
|
+
print(f" No case array found in {args.input}.", file=sys.stderr)
|
|
1745
|
+
return 2
|
|
1746
|
+
|
|
1747
|
+
# A reachable KB enables Level 2 (KB-verified) attribution; without it, Level 1 signals from
|
|
1748
|
+
# the eval's own chunks still compute. Never fail the command over an unreachable KB.
|
|
1749
|
+
adapter = None
|
|
1750
|
+
if args.project and not args.no_retrieval:
|
|
1751
|
+
try:
|
|
1752
|
+
from compass.providers import get_adapter
|
|
1753
|
+
adapter = get_adapter("vectorstore", settings.redis.provider, config=settings.redis)
|
|
1754
|
+
if not getattr(adapter, "ping", lambda: True)():
|
|
1755
|
+
adapter = None
|
|
1756
|
+
except Exception: # noqa: BLE001 - degrade to Level 1
|
|
1757
|
+
adapter = None
|
|
1758
|
+
|
|
1759
|
+
print(f" Enriching {len(cases)} case(s) with deterministic KB signals "
|
|
1760
|
+
f"(level {'1+2' if adapter else '1'}) ...", file=sys.stderr)
|
|
1761
|
+
enriched, summary = enrich_cases(cases, settings.eval, adapter)
|
|
1762
|
+
if isinstance(data, dict):
|
|
1763
|
+
data["compass_summary"] = summary
|
|
1764
|
+
out_obj = data
|
|
1765
|
+
else:
|
|
1766
|
+
out_obj = enriched # a list: each case now carries a "compass" block; shape preserved
|
|
1767
|
+
body = _json.dumps(out_obj, indent=2, ensure_ascii=False)
|
|
1768
|
+
mean = summary["mean_citation_grounding_rate"]
|
|
1769
|
+
note = (f"{summary['cases_enriched']} enriched, {summary['cases_with_verifiable_claims']} "
|
|
1770
|
+
f"with verifiable claims" + (f", mean grounding rate {mean:.0%}" if mean is not None else ""))
|
|
1771
|
+
if args.output:
|
|
1772
|
+
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
|
1773
|
+
Path(args.output).write_text(body + "\n", encoding="utf-8")
|
|
1774
|
+
print(f" Wrote {args.output} ({note})", file=sys.stderr)
|
|
1775
|
+
else:
|
|
1776
|
+
print(body)
|
|
1777
|
+
print(f" {note}", file=sys.stderr)
|
|
1778
|
+
return 0
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _format_calibration(report: dict, metric: str) -> str:
|
|
1782
|
+
lines = [f"Judge calibration for '{metric}' vs COMPASS deterministic truth "
|
|
1783
|
+
f"({report['cases_scored']} case(s), tolerance +/-{report['tolerance']}):", ""]
|
|
1784
|
+
rows = sorted(report["judges"].items(),
|
|
1785
|
+
key=lambda kv: (kv[1]["avg_error"] is None, kv[1]["avg_error"] or 0))
|
|
1786
|
+
lines.append(f" {'judge':<18} {'n':>4} {'error':>8} {'bias':>8} {'acc':>7} {'weight':>8}")
|
|
1787
|
+
for name, v in rows:
|
|
1788
|
+
lean = "" if v["bias"] is None else (" (lenient)" if v["bias"] > 0.03 else
|
|
1789
|
+
" (strict)" if v["bias"] < -0.03 else "")
|
|
1790
|
+
lines.append(f" {name:<18} {v['n']:>4} {v['avg_error']:>8.3f} {v['bias']:>+8.3f} "
|
|
1791
|
+
f"{v['accuracy']:>6.0%} {v['recommended_weight']:>8.3f}{lean}")
|
|
1792
|
+
if report.get("most_accurate"):
|
|
1793
|
+
lines += ["", f" Most accurate: {report['most_accurate']}. Recommended weights are "
|
|
1794
|
+
"inverse-error (a more accurate judge is trusted more than equal weighting)."]
|
|
1795
|
+
lines += ["", " " + report["note"]]
|
|
1796
|
+
return "\n".join(lines)
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
def cmd_calibrate_judges(argv: list[str]) -> int:
|
|
1800
|
+
p = argparse.ArgumentParser(
|
|
1801
|
+
prog="compass calibrate-judges",
|
|
1802
|
+
description="Measure how accurate each LLM judge is against COMPASS's deterministic "
|
|
1803
|
+
"grounding truth: per-judge error, bias, accuracy, and an inverse-error "
|
|
1804
|
+
"recommended weight. Input cases must carry both COMPASS truth (run "
|
|
1805
|
+
"'compass enrich' first) and the eval's per-judge scores. Schema-agnostic.")
|
|
1806
|
+
p.add_argument("--input", required=True, metavar="FILE")
|
|
1807
|
+
p.add_argument("--metric", required=True,
|
|
1808
|
+
help="Judge metric to calibrate (e.g. 'groundedness'); matched case-insensitively")
|
|
1809
|
+
p.add_argument("--truth-path", default="compass.grounding.citation_grounding_rate",
|
|
1810
|
+
help="Dotted path to COMPASS truth per case (default: the 'enrich' output field)")
|
|
1811
|
+
p.add_argument("--scores-path", default="",
|
|
1812
|
+
help="Dotted path to the per-case scores block (default: auto-discover)")
|
|
1813
|
+
p.add_argument("--tolerance", type=float, default=0.2,
|
|
1814
|
+
help="A judge score within this of the truth counts as accurate (default 0.2)")
|
|
1815
|
+
p.add_argument("--json", action="store_true")
|
|
1816
|
+
args = p.parse_args(argv)
|
|
1817
|
+
|
|
1818
|
+
import json as _json
|
|
1819
|
+
|
|
1820
|
+
from compass.eval_enrich import calibrate_judges, discover_cases, extract_judge_scores
|
|
1821
|
+
from compass.eval_interop import get_path
|
|
1822
|
+
|
|
1823
|
+
try:
|
|
1824
|
+
with open(args.input, encoding="utf-8") as f:
|
|
1825
|
+
data = _json.load(f)
|
|
1826
|
+
except (OSError, ValueError) as exc:
|
|
1827
|
+
print(f" Cannot read {args.input}: {exc}", file=sys.stderr)
|
|
1828
|
+
return 2
|
|
1829
|
+
cases = discover_cases(data)
|
|
1830
|
+
if not cases:
|
|
1831
|
+
print(f" No case array found in {args.input}.", file=sys.stderr)
|
|
1832
|
+
return 2
|
|
1833
|
+
|
|
1834
|
+
pairs, truth_found, judges_found = [], 0, 0
|
|
1835
|
+
for c in cases:
|
|
1836
|
+
truth = get_path(c, args.truth_path)
|
|
1837
|
+
judges = extract_judge_scores(c, args.metric, args.scores_path or None)
|
|
1838
|
+
if isinstance(truth, (int, float)) and not isinstance(truth, bool):
|
|
1839
|
+
truth_found += 1
|
|
1840
|
+
if judges:
|
|
1841
|
+
judges_found += 1
|
|
1842
|
+
if isinstance(truth, (int, float)) and not isinstance(truth, bool) and judges:
|
|
1843
|
+
pairs.append({"truth": float(truth), "judges": judges})
|
|
1844
|
+
if not pairs:
|
|
1845
|
+
print(f" No comparable cases. COMPASS truth found in {truth_found}/{len(cases)} case(s) "
|
|
1846
|
+
f"(path '{args.truth_path}'); judge scores for metric '{args.metric}' in "
|
|
1847
|
+
f"{judges_found}/{len(cases)}. Run 'compass enrich' first, and check --metric, "
|
|
1848
|
+
f"--scores-path, and --truth-path.", file=sys.stderr)
|
|
1849
|
+
return 2
|
|
1850
|
+
report = calibrate_judges(pairs, tolerance=args.tolerance)
|
|
1851
|
+
print(_json.dumps(report, indent=2, ensure_ascii=False) if args.json
|
|
1852
|
+
else _format_calibration(report, args.metric))
|
|
1853
|
+
return 0
|
|
1854
|
+
|
|
1855
|
+
|
|
1683
1856
|
_HELP = """compass - knowledge-base readiness validation
|
|
1684
1857
|
|
|
1685
1858
|
Usage: compass <command> [options]
|
|
@@ -1695,6 +1868,8 @@ Commands:
|
|
|
1695
1868
|
freshness flag stale sources + orphan documents (source object gone)
|
|
1696
1869
|
evidence produce live, reproducible PROOF of flagged issues (chunks + queries) - no LLM
|
|
1697
1870
|
grounding-audit check an eval's grounding finding against the KB (count claims) - no LLM
|
|
1871
|
+
enrich add deterministic KB signals (grounding rate, chunk quality) to an eval file
|
|
1872
|
+
calibrate-judges measure each LLM judge's accuracy/bias vs COMPASS's deterministic truth
|
|
1698
1873
|
demo generate a sample report from synthetic data (no backend needed)
|
|
1699
1874
|
init [name] scaffold a project (config + .env.example + marker). -i/--interactive wizard; --graph graph block; --ci workflow
|
|
1700
1875
|
lint statically check a project config for silent-failure traps (no backend)
|
|
@@ -1743,6 +1918,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1743
1918
|
return cmd_evidence(rest)
|
|
1744
1919
|
if cmd == "grounding-audit":
|
|
1745
1920
|
return cmd_grounding_audit(rest)
|
|
1921
|
+
if cmd == "enrich":
|
|
1922
|
+
return cmd_enrich(rest)
|
|
1923
|
+
if cmd == "calibrate-judges":
|
|
1924
|
+
return cmd_calibrate_judges(rest)
|
|
1746
1925
|
if cmd == "freshness":
|
|
1747
1926
|
return cmd_freshness(rest)
|
|
1748
1927
|
if cmd == "selftest":
|
|
@@ -261,6 +261,17 @@ def freshness_report(settings: Settings, *, sample: int = 500, stale_after_days:
|
|
|
261
261
|
f"{without_key} of {len(seen_docs)} sampled document(s) carry no source key, so "
|
|
262
262
|
f"their source could not be checked - likely a missing s3_key field (prefix/field "
|
|
263
263
|
f"mismatch) or sources in a different store, not necessarily orphans.")
|
|
264
|
+
# A near-total orphan rate almost never means every source was deleted - it points at a
|
|
265
|
+
# systemic mismatch (the chunk s3_key does not match where objects actually live: wrong
|
|
266
|
+
# bucket, or a prefix baked into the key that the store does not use). Flag it so the user
|
|
267
|
+
# checks configuration before "reconciling" hundreds of false orphans (DEFECT-006).
|
|
268
|
+
if checked and len(orphans) / checked > 0.8:
|
|
269
|
+
result["warning"] = (
|
|
270
|
+
f"{len(orphans)}/{checked} sampled sources ({round(100 * len(orphans) / checked)}%) "
|
|
271
|
+
"look orphaned - a rate this high usually means a key mismatch (wrong bucket, or a "
|
|
272
|
+
"prefix in the chunk s3_key that the object store does not expect), not that the "
|
|
273
|
+
"sources are actually gone. Verify s3.bucket_name and the s3_key values before "
|
|
274
|
+
"reconciling.")
|
|
264
275
|
return result
|
|
265
276
|
|
|
266
277
|
|