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.
Files changed (123) hide show
  1. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/PKG-INFO +15 -1
  2. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/README.md +14 -0
  3. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/__init__.py +1 -1
  4. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/citation.py +17 -0
  5. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/cli.py +183 -4
  6. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/corpus_health.py +11 -0
  7. compass_kb_validation-3.3.0/compass/eval_enrich.py +305 -0
  8. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/eval_interop.py +36 -0
  9. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/grounding.py +19 -2
  10. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/vectorstore.py +12 -1
  11. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/PKG-INFO +15 -1
  12. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/SOURCES.txt +2 -0
  13. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/discover_and_validate.py +2 -1
  14. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/profile_kb.py +5 -1
  15. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/run_validation.py +2 -1
  16. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_citation.py +29 -0
  17. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_corpus_health.py +12 -0
  18. compass_kb_validation-3.3.0/tests/test_eval_enrich.py +109 -0
  19. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_eval_interop.py +16 -0
  20. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_grounding.py +19 -0
  21. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_vectorstore_filters.py +14 -0
  22. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/LICENSE +0 -0
  23. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/__main__.py +0 -0
  24. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/ask.py +0 -0
  25. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/baseline.py +0 -0
  26. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/chunk_coherence.py +0 -0
  27. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/config/__init__.py +0 -0
  28. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/config_lint.py +0 -0
  29. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/content_inspect.py +0 -0
  30. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/corpus.py +0 -0
  31. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/diagnostics.py +0 -0
  32. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/embedding_drift.py +0 -0
  33. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/evidence.py +0 -0
  34. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/init_wizard.py +0 -0
  35. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/live_dashboard.py +0 -0
  36. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/mcp_server.py +0 -0
  37. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/models/__init__.py +0 -0
  38. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/notifications.py +0 -0
  39. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/pipeline/__init__.py +0 -0
  40. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/profiler.py +0 -0
  41. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/provenance.py +0 -0
  42. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/__init__.py +0 -0
  43. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/embedding.py +0 -0
  44. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/graph.py +0 -0
  45. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/source.py +0 -0
  46. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/providers/storage.py +0 -0
  47. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/remediation.py +0 -0
  48. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/__init__.py +0 -0
  49. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/_report_css.py +0 -0
  50. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/_report_js.py +0 -0
  51. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/analysis.py +0 -0
  52. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/audit.py +0 -0
  53. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/export.py +0 -0
  54. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/report/shell.py +0 -0
  55. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/retrieval_analysis.py +0 -0
  56. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/text_probes.py +0 -0
  57. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/trend_tracker.py +0 -0
  58. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/utils/__init__.py +0 -0
  59. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/__init__.py +0 -0
  60. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/api_upload.py +0 -0
  61. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/base.py +0 -0
  62. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/base_sparql.py +0 -0
  63. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/cross_stage.py +0 -0
  64. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/graphdb_metadata.py +0 -0
  65. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/neptune_metadata.py +0 -0
  66. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/redis_chunks.py +0 -0
  67. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/registry.py +0 -0
  68. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/retrieval_quality.py +0 -0
  69. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass/validators/s3_storage.py +0 -0
  70. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
  71. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
  72. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/requires.txt +0 -0
  73. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
  74. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/ingestion_validation/__init__.py +0 -0
  75. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/pyproject.toml +0 -0
  76. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/setup.cfg +0 -0
  77. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_alt_adapters.py +0 -0
  78. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_analysis_report.py +0 -0
  79. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_api_upload.py +0 -0
  80. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_ask.py +0 -0
  81. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_baseline.py +0 -0
  82. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_benchmark.py +0 -0
  83. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_check_catalog.py +0 -0
  84. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_cli.py +0 -0
  85. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_compat_shim.py +0 -0
  86. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_config.py +0 -0
  87. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_config_lint.py +0 -0
  88. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_content_inspect.py +0 -0
  89. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_corpus.py +0 -0
  90. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_coverage.py +0 -0
  91. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_cross_stage.py +0 -0
  92. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_diagnostics.py +0 -0
  93. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_discover_cli.py +0 -0
  94. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_discovery_parallel.py +0 -0
  95. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_embedding_drift.py +0 -0
  96. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_evidence.py +0 -0
  97. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_gate.py +0 -0
  98. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_graph_transport.py +0 -0
  99. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_graph_validators.py +0 -0
  100. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_init_wizard.py +0 -0
  101. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_manifest.py +0 -0
  102. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_mcp.py +0 -0
  103. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_migration.py +0 -0
  104. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_models.py +0 -0
  105. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_neptune_bugfixes.py +0 -0
  106. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_neptune_bugfixes2.py +0 -0
  107. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_notifications.py +0 -0
  108. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_pipeline.py +0 -0
  109. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_profiler.py +0 -0
  110. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_providers.py +0 -0
  111. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_redis_chunks.py +0 -0
  112. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_registry.py +0 -0
  113. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_remediation.py +0 -0
  114. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_reporting.py +0 -0
  115. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_retrieval_assertions.py +0 -0
  116. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_retrieval_quality.py +0 -0
  117. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_review_hardening.py +0 -0
  118. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_run_cli.py +0 -0
  119. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_sources.py +0 -0
  120. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_storage_validator.py +0 -0
  121. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_trends.py +0 -0
  122. {compass_kb_validation-3.2.0 → compass_kb_validation-3.3.0}/tests/test_universal_concerns.py +0 -0
  123. {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.2.0
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.
@@ -5,4 +5,4 @@ end-to-end: API upload -> object storage -> knowledge graph -> vector store ->
5
5
  retrieval quality, plus cross-store consistency.
6
6
  """
7
7
 
8
- __version__ = "3.2.0"
8
+ __version__ = "3.3.0"
@@ -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
- print(f" Comparing compass retrieval against {len(cases)} ground-truth case(s){vnote} ...")
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