compass-kb-validation 3.2.1__tar.gz → 3.4.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.1 → compass_kb_validation-3.4.0}/PKG-INFO +15 -1
  2. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/README.md +14 -0
  3. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/__init__.py +1 -1
  4. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/citation.py +50 -1
  5. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/cli.py +158 -0
  6. compass_kb_validation-3.4.0/compass/eval_enrich.py +380 -0
  7. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/grounding.py +5 -0
  8. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/PKG-INFO +15 -1
  9. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/SOURCES.txt +2 -0
  10. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_citation.py +29 -0
  11. compass_kb_validation-3.4.0/tests/test_eval_enrich.py +135 -0
  12. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/LICENSE +0 -0
  13. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/__main__.py +0 -0
  14. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/ask.py +0 -0
  15. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/baseline.py +0 -0
  16. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/chunk_coherence.py +0 -0
  17. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/config/__init__.py +0 -0
  18. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/config_lint.py +0 -0
  19. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/content_inspect.py +0 -0
  20. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/corpus.py +0 -0
  21. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/corpus_health.py +0 -0
  22. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/diagnostics.py +0 -0
  23. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/embedding_drift.py +0 -0
  24. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/eval_interop.py +0 -0
  25. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/evidence.py +0 -0
  26. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/init_wizard.py +0 -0
  27. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/live_dashboard.py +0 -0
  28. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/mcp_server.py +0 -0
  29. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/models/__init__.py +0 -0
  30. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/notifications.py +0 -0
  31. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/pipeline/__init__.py +0 -0
  32. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/profiler.py +0 -0
  33. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/provenance.py +0 -0
  34. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/__init__.py +0 -0
  35. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/embedding.py +0 -0
  36. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/graph.py +0 -0
  37. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/source.py +0 -0
  38. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/storage.py +0 -0
  39. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/vectorstore.py +0 -0
  40. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/remediation.py +0 -0
  41. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/__init__.py +0 -0
  42. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/_report_css.py +0 -0
  43. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/_report_js.py +0 -0
  44. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/analysis.py +0 -0
  45. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/audit.py +0 -0
  46. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/export.py +0 -0
  47. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/shell.py +0 -0
  48. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/retrieval_analysis.py +0 -0
  49. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/text_probes.py +0 -0
  50. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/trend_tracker.py +0 -0
  51. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/utils/__init__.py +0 -0
  52. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/__init__.py +0 -0
  53. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/api_upload.py +0 -0
  54. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/base.py +0 -0
  55. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/base_sparql.py +0 -0
  56. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/cross_stage.py +0 -0
  57. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/graphdb_metadata.py +0 -0
  58. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/neptune_metadata.py +0 -0
  59. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/redis_chunks.py +0 -0
  60. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/registry.py +0 -0
  61. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/retrieval_quality.py +0 -0
  62. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/s3_storage.py +0 -0
  63. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
  64. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
  65. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/requires.txt +0 -0
  66. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
  67. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/discover_and_validate.py +0 -0
  68. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/ingestion_validation/__init__.py +0 -0
  69. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/profile_kb.py +0 -0
  70. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/pyproject.toml +0 -0
  71. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/run_validation.py +0 -0
  72. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/setup.cfg +0 -0
  73. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_alt_adapters.py +0 -0
  74. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_analysis_report.py +0 -0
  75. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_api_upload.py +0 -0
  76. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_ask.py +0 -0
  77. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_baseline.py +0 -0
  78. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_benchmark.py +0 -0
  79. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_check_catalog.py +0 -0
  80. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_cli.py +0 -0
  81. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_compat_shim.py +0 -0
  82. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_config.py +0 -0
  83. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_config_lint.py +0 -0
  84. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_content_inspect.py +0 -0
  85. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_corpus.py +0 -0
  86. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_corpus_health.py +0 -0
  87. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_coverage.py +0 -0
  88. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_cross_stage.py +0 -0
  89. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_diagnostics.py +0 -0
  90. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_discover_cli.py +0 -0
  91. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_discovery_parallel.py +0 -0
  92. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_embedding_drift.py +0 -0
  93. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_eval_interop.py +0 -0
  94. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_evidence.py +0 -0
  95. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_gate.py +0 -0
  96. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_graph_transport.py +0 -0
  97. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_graph_validators.py +0 -0
  98. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_grounding.py +0 -0
  99. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_init_wizard.py +0 -0
  100. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_manifest.py +0 -0
  101. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_mcp.py +0 -0
  102. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_migration.py +0 -0
  103. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_models.py +0 -0
  104. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_neptune_bugfixes.py +0 -0
  105. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_neptune_bugfixes2.py +0 -0
  106. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_notifications.py +0 -0
  107. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_pipeline.py +0 -0
  108. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_profiler.py +0 -0
  109. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_providers.py +0 -0
  110. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_redis_chunks.py +0 -0
  111. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_registry.py +0 -0
  112. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_remediation.py +0 -0
  113. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_reporting.py +0 -0
  114. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_retrieval_assertions.py +0 -0
  115. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_retrieval_quality.py +0 -0
  116. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_review_hardening.py +0 -0
  117. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_run_cli.py +0 -0
  118. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_sources.py +0 -0
  119. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_storage_validator.py +0 -0
  120. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_trends.py +0 -0
  121. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_universal_concerns.py +0 -0
  122. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_utils.py +0 -0
  123. {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_vectorstore_filters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compass-kb-validation
3
- Version: 3.2.1
3
+ Version: 3.4.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.1"
8
+ __version__ = "3.4.0"
@@ -377,7 +377,19 @@ def verify_level1(citations: list[Citation], eval_chunks: list[dict], cfg: Any)
377
377
  v.terms_missing.append(cv.raw)
378
378
  v._missing.append(cv)
379
379
  if not cit.values:
380
- v.l1_verdict, v.severity = "UNVERIFIABLE", "INFO" # no quantitative claim to check
380
+ # No numeric claim. If the claim carries distinctive entities (proper nouns / quoted
381
+ # phrases) and they are ALL present in the cited chunk, it is TERM_SUPPORTED - a
382
+ # weaker, lexical tier counted in a SEPARATE term-grounding rate, never the strict
383
+ # numeric one. Missing entities stay UNVERIFIABLE (lexical absence can be paraphrase).
384
+ ents = [] if cit.negated else _extract_entities(cit.claim)
385
+ if ents:
386
+ hay = _squash(_haystack(chunk, text_field))
387
+ v.entity_missing = [e for e in ents if _squash(e) not in hay]
388
+ v.entities_checked = True
389
+ v.l1_verdict, v.severity = (("TERM_SUPPORTED", "INFO") if not v.entity_missing
390
+ else ("UNVERIFIABLE", "INFO"))
391
+ else:
392
+ v.l1_verdict, v.severity = "UNVERIFIABLE", "INFO" # no quantitative claim to check
381
393
  if cit.negated:
382
394
  v.note = "claim is negated - lexical verification not applicable"
383
395
  elif not v.terms_missing:
@@ -539,6 +551,7 @@ _ATTRIB = {
539
551
  "citation_error": ("citation_error", "MEDIUM"), # right fact, wrong cited location
540
552
  "fabricated_source": ("fabricated_source", "HIGH"),
541
553
  "none": ("none", "INFO"),
554
+ "term_supported": ("term_supported", "INFO"), # distinctive entities present in cited chunk
542
555
  "unverifiable": ("unverifiable", "INFO"), # no quantitative term; lexical check N/A
543
556
  "unknown": ("unknown", "MEDIUM"), # a miss we could not attribute (no live KB)
544
557
  }
@@ -558,6 +571,8 @@ def attribute(verdicts: list[CitationVerdict], l1_ran: bool) -> None:
558
571
  key = "none"
559
572
  elif l2 == "FABRICATED_SOURCE":
560
573
  key = "fabricated_source" # cited a document that isn't in the KB
574
+ elif l1 == "TERM_SUPPORTED":
575
+ key = "term_supported" # distinctive entities present; weaker than numeric
561
576
  elif l2 in _L2_UNVERIFIABLE:
562
577
  key = "unverifiable" # source unresolved / too large / no numeric claim
563
578
  elif l1 == "UNVERIFIABLE" and not l2:
@@ -650,6 +665,7 @@ def audit_citations(response: str, eval_chunks: Optional[list[dict]], adapter: A
650
665
  summary = summarize(verdicts, response, l1_ran, l2_ran)
651
666
  if l1_ran:
652
667
  summary.update(context_utilization(citations, eval_chunks or [], cfg))
668
+ summary.update(text_utilization(response, eval_chunks or [], cfg))
653
669
  # DEFECT-003 safety net: if the response clearly carries citation-like patterns but none
654
670
  # were extracted, say so rather than reporting a silent total_citations=0 that reads as
655
671
  # "no citations exist" (which was the DEFECT-001 failure mode).
@@ -680,6 +696,31 @@ def context_utilization(citations: list[Citation], eval_chunks: list[dict], cfg:
680
696
  "context_utilization_pct": round(100.0 * used / total, 1)}
681
697
 
682
698
 
699
+ def _ngrams(text: str, n: int = 3) -> set:
700
+ toks = re.findall(r"[a-z0-9]+", (text or "").lower())
701
+ return {tuple(toks[i:i + n]) for i in range(len(toks) - n + 1)} if len(toks) >= n else set()
702
+
703
+
704
+ def text_utilization(response: str, eval_chunks: list[dict], cfg: Any,
705
+ n: int = 3, min_overlap: float = 0.1) -> dict:
706
+ """How much of the retrieved CONTENT actually appears in the response, by n-gram overlap -
707
+ distinct from ``context_utilization_pct`` (which measures whether a chunk's PAGE was cited).
708
+ A chunk counts as used when at least ``min_overlap`` of its n-grams recur in the response."""
709
+ text_field = getattr(cfg, "gt_chunk_text_field", "text")
710
+ total = len(eval_chunks or [])
711
+ if not total:
712
+ return {}
713
+ resp = _ngrams(response, n)
714
+ if not resp:
715
+ return {"text_utilization_pct": 0.0}
716
+ used = 0
717
+ for ch in eval_chunks:
718
+ cg = _ngrams(_haystack(ch, text_field), n)
719
+ if cg and len(cg & resp) / len(cg) >= min_overlap:
720
+ used += 1
721
+ return {"text_utilization_pct": round(100.0 * used / total, 1)}
722
+
723
+
683
724
  def summarize(verdicts: list[CitationVerdict], response: str, l1_ran: bool, l2_ran: bool) -> dict:
684
725
  """Aggregate signal for the audit: how many citations are sound, and the fault mix."""
685
726
  total = len(verdicts)
@@ -693,6 +734,11 @@ def summarize(verdicts: list[CitationVerdict], response: str, l1_ran: bool, l2_r
693
734
  checkable = sound + fault_total
694
735
  # Accuracy is over CHECKABLE citations only; unverifiable/unknown are excluded, not scored.
695
736
  accuracy = round(100.0 * sound / checkable, 1) if checkable else None
737
+ # Term grounding: a SEPARATE, weaker tier for qualitative citations whose distinctive entities
738
+ # are present in the cited chunk. Never folded into the strict numeric rate above.
739
+ term_supported = sum(1 for v in verdicts if v.attribution == "term_supported")
740
+ term_checkable = sum(1 for v in verdicts if v.entities_checked)
741
+ term_rate = round(100.0 * term_supported / term_checkable, 1) if term_checkable else None
696
742
  # Citation coverage: sentences with a checkable quantitative claim that carry NO citation.
697
743
  sentences = [s.strip() for s in re.split(r"(?<=[.!?])\s+", response or "") if s.strip()]
698
744
  numeric = [s for s in sentences if extract_terms(s)]
@@ -704,6 +750,9 @@ def summarize(verdicts: list[CitationVerdict], response: str, l1_ran: bool, l2_r
704
750
  "checkable": checkable,
705
751
  "unverifiable": unverifiable,
706
752
  "accuracy_pct": accuracy,
753
+ "term_supported": term_supported,
754
+ "term_checkable": term_checkable,
755
+ "term_grounding_rate_pct": term_rate,
707
756
  "faults": faults,
708
757
  "coverage_pct": coverage,
709
758
  "uncited_numeric_claims": len(uncited),
@@ -1701,6 +1701,158 @@ def cmd_selftest(argv: list[str]) -> int:
1701
1701
  # --------------------------------------------------------------------------- #
1702
1702
  # Dispatcher
1703
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
+
1704
1856
  _HELP = """compass - knowledge-base readiness validation
1705
1857
 
1706
1858
  Usage: compass <command> [options]
@@ -1716,6 +1868,8 @@ Commands:
1716
1868
  freshness flag stale sources + orphan documents (source object gone)
1717
1869
  evidence produce live, reproducible PROOF of flagged issues (chunks + queries) - no LLM
1718
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
1719
1873
  demo generate a sample report from synthetic data (no backend needed)
1720
1874
  init [name] scaffold a project (config + .env.example + marker). -i/--interactive wizard; --graph graph block; --ci workflow
1721
1875
  lint statically check a project config for silent-failure traps (no backend)
@@ -1764,6 +1918,10 @@ def main(argv: list[str] | None = None) -> int:
1764
1918
  return cmd_evidence(rest)
1765
1919
  if cmd == "grounding-audit":
1766
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)
1767
1925
  if cmd == "freshness":
1768
1926
  return cmd_freshness(rest)
1769
1927
  if cmd == "selftest":