compass-kb-validation 2.2.0__tar.gz → 2.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 (112) hide show
  1. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/PKG-INFO +5 -1
  2. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/README.md +4 -0
  3. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/PKG-INFO +5 -1
  4. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/SOURCES.txt +2 -0
  5. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/discover_and_validate.py +63 -8
  6. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/__init__.py +1 -1
  7. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/cli.py +69 -0
  8. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/config/__init__.py +34 -1
  9. compass_kb_validation-2.4.0/ingestion_validation/grounding.py +341 -0
  10. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/profiler.py +72 -1
  11. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/vectorstore.py +56 -0
  12. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/__init__.py +7 -2
  13. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_config.py +25 -0
  14. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_discover_cli.py +57 -0
  15. compass_kb_validation-2.4.0/tests/test_grounding.py +150 -0
  16. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_reporting.py +15 -0
  17. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_vectorstore_filters.py +35 -0
  18. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/LICENSE +0 -0
  19. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
  20. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
  21. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/requires.txt +0 -0
  22. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
  23. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/__main__.py +0 -0
  24. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/ask.py +0 -0
  25. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/baseline.py +0 -0
  26. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/chunk_coherence.py +0 -0
  27. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/config_lint.py +0 -0
  28. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/content_inspect.py +0 -0
  29. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/corpus.py +0 -0
  30. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/corpus_health.py +0 -0
  31. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/diagnostics.py +0 -0
  32. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/embedding_drift.py +0 -0
  33. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/eval_interop.py +0 -0
  34. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/init_wizard.py +0 -0
  35. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/live_dashboard.py +0 -0
  36. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/mcp_server.py +0 -0
  37. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/models/__init__.py +0 -0
  38. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/notifications.py +0 -0
  39. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/pipeline/__init__.py +0 -0
  40. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/provenance.py +0 -0
  41. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/__init__.py +0 -0
  42. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/embedding.py +0 -0
  43. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/graph.py +0 -0
  44. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/source.py +0 -0
  45. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/storage.py +0 -0
  46. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/remediation.py +0 -0
  47. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/_report_css.py +0 -0
  48. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/_report_js.py +0 -0
  49. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/analysis.py +0 -0
  50. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/export.py +0 -0
  51. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/shell.py +0 -0
  52. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/retrieval_analysis.py +0 -0
  53. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/trend_tracker.py +0 -0
  54. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/utils/__init__.py +0 -0
  55. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/__init__.py +0 -0
  56. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/api_upload.py +0 -0
  57. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/base.py +0 -0
  58. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/base_sparql.py +0 -0
  59. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/cross_stage.py +0 -0
  60. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/graphdb_metadata.py +0 -0
  61. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/neptune_metadata.py +0 -0
  62. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/redis_chunks.py +0 -0
  63. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/registry.py +0 -0
  64. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/retrieval_quality.py +0 -0
  65. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/s3_storage.py +0 -0
  66. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/profile_kb.py +0 -0
  67. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/pyproject.toml +0 -0
  68. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/run_validation.py +0 -0
  69. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/setup.cfg +0 -0
  70. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_alt_adapters.py +0 -0
  71. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_analysis_report.py +0 -0
  72. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_api_upload.py +0 -0
  73. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_ask.py +0 -0
  74. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_baseline.py +0 -0
  75. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_benchmark.py +0 -0
  76. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_check_catalog.py +0 -0
  77. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_cli.py +0 -0
  78. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_config_lint.py +0 -0
  79. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_content_inspect.py +0 -0
  80. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_corpus.py +0 -0
  81. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_corpus_health.py +0 -0
  82. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_coverage.py +0 -0
  83. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_cross_stage.py +0 -0
  84. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_diagnostics.py +0 -0
  85. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_discovery_parallel.py +0 -0
  86. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_embedding_drift.py +0 -0
  87. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_eval_interop.py +0 -0
  88. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_gate.py +0 -0
  89. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_graph_transport.py +0 -0
  90. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_graph_validators.py +0 -0
  91. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_init_wizard.py +0 -0
  92. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_manifest.py +0 -0
  93. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_mcp.py +0 -0
  94. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_migration.py +0 -0
  95. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_models.py +0 -0
  96. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_neptune_bugfixes.py +0 -0
  97. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_neptune_bugfixes2.py +0 -0
  98. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_notifications.py +0 -0
  99. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_pipeline.py +0 -0
  100. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_profiler.py +0 -0
  101. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_providers.py +0 -0
  102. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_redis_chunks.py +0 -0
  103. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_registry.py +0 -0
  104. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_remediation.py +0 -0
  105. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_retrieval_assertions.py +0 -0
  106. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_retrieval_quality.py +0 -0
  107. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_run_cli.py +0 -0
  108. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_sources.py +0 -0
  109. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_storage_validator.py +0 -0
  110. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_trends.py +0 -0
  111. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.0}/tests/test_universal_concerns.py +0 -0
  112. {compass_kb_validation-2.2.0 → compass_kb_validation-2.4.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: 2.2.0
3
+ Version: 2.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
@@ -228,6 +228,10 @@ compass ask --project my-kb --batch questions.jsonl --format eval --output eval_
228
228
  # Corpus health (deterministic, no LLM): score chunk quality / find stale + orphan sources
229
229
  compass quality --project my-kb
230
230
  compass freshness --project my-kb --stale-after 30
231
+
232
+ # Adjudicate an eval's grounding finding against the KB: did the bot's counts match reality?
233
+ # Deterministic - attributes a failure to a layer (retrieval vs the model), never scores answer quality.
234
+ compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
231
235
  ```
232
236
 
233
237
  `migrate` and `coverage` also render a **self-contained HTML report** (same look as the
@@ -161,6 +161,10 @@ compass ask --project my-kb --batch questions.jsonl --format eval --output eval_
161
161
  # Corpus health (deterministic, no LLM): score chunk quality / find stale + orphan sources
162
162
  compass quality --project my-kb
163
163
  compass freshness --project my-kb --stale-after 30
164
+
165
+ # Adjudicate an eval's grounding finding against the KB: did the bot's counts match reality?
166
+ # Deterministic - attributes a failure to a layer (retrieval vs the model), never scores answer quality.
167
+ compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
164
168
  ```
165
169
 
166
170
  `migrate` and `coverage` also render a **self-contained HTML report** (same look as the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compass-kb-validation
3
- Version: 2.2.0
3
+ Version: 2.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
@@ -228,6 +228,10 @@ compass ask --project my-kb --batch questions.jsonl --format eval --output eval_
228
228
  # Corpus health (deterministic, no LLM): score chunk quality / find stale + orphan sources
229
229
  compass quality --project my-kb
230
230
  compass freshness --project my-kb --stale-after 30
231
+
232
+ # Adjudicate an eval's grounding finding against the KB: did the bot's counts match reality?
233
+ # Deterministic - attributes a failure to a layer (retrieval vs the model), never scores answer quality.
234
+ compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
231
235
  ```
232
236
 
233
237
  `migrate` and `coverage` also render a **self-contained HTML report** (same look as the
@@ -23,6 +23,7 @@ ingestion_validation/corpus_health.py
23
23
  ingestion_validation/diagnostics.py
24
24
  ingestion_validation/embedding_drift.py
25
25
  ingestion_validation/eval_interop.py
26
+ ingestion_validation/grounding.py
26
27
  ingestion_validation/init_wizard.py
27
28
  ingestion_validation/live_dashboard.py
28
29
  ingestion_validation/mcp_server.py
@@ -82,6 +83,7 @@ tests/test_eval_interop.py
82
83
  tests/test_gate.py
83
84
  tests/test_graph_transport.py
84
85
  tests/test_graph_validators.py
86
+ tests/test_grounding.py
85
87
  tests/test_init_wizard.py
86
88
  tests/test_manifest.py
87
89
  tests/test_mcp.py
@@ -67,6 +67,12 @@ def parse_args(argv=None) -> argparse.Namespace:
67
67
  help="Also fail unless the KB Hygiene score is >= PCT (0-100). Hygiene is "
68
68
  "the share of advisory data-quality checks that passed - data-plane "
69
69
  "cleanliness, separate from the READY integrity gate.")
70
+ p.add_argument("--suggest-config", nargs="?", const="-", metavar="PATH",
71
+ help="After the run, emit config refinements from what it observed "
72
+ "(document count, chunk-size distribution, checks that never applied). "
73
+ "Prints to stdout, or writes to PATH. Review before merging.")
74
+ p.add_argument("--no-tips", action="store_true",
75
+ help="Suppress the end-of-run tips about relevant unused flags.")
70
76
  # --- config overrides (run scenarios without editing YAML; CLI wins over config) ---
71
77
  p.add_argument("--readiness-threshold", type=float, metavar="FRAC",
72
78
  help="Override corpus readiness cutoff (0-1). READY when pass-rate >= this.")
@@ -136,8 +142,8 @@ def _resolve_skip_steps(settings, *, include_graphdb: bool, include_retrieval: b
136
142
  skip += ["GraphDB Metadata", "Neptune Metadata"]
137
143
  if not include_retrieval:
138
144
  skip.append("Retrieval Quality")
139
- skip += _normalize_skips(list(getattr(settings, "skip_steps", None) or []))
140
- skip += _normalize_skips(cli_skip)
145
+ skip += _normalize_skips(list(getattr(settings, "skip_steps", None) or []), source="config")
146
+ skip += _normalize_skips(cli_skip, source="cli")
141
147
 
142
148
  s3_auto_skipped = False
143
149
  if settings.discovery.provider in ("vectorstore", "index", "manifest"):
@@ -149,8 +155,14 @@ def _resolve_skip_steps(settings, *, include_graphdb: bool, include_retrieval: b
149
155
  return list(dict.fromkeys(skip)), s3_auto_skipped
150
156
 
151
157
 
152
- def _normalize_skips(tokens: list[str]) -> list[str]:
153
- """Map --skip tokens (step keys or names) to the step names the pipeline skips by."""
158
+ def _normalize_skips(tokens: list[str], source: str = "cli") -> list[str]:
159
+ """Map --skip tokens (step keys or names) to the step names the pipeline skips by.
160
+
161
+ An unknown entry from an interactive ``--skip`` is worth flagging (the user just typed
162
+ it and expects it to work). An unknown entry from the project's ``skip_steps`` is
163
+ config drift (e.g. a step removed in an upgrade) - silent at runtime because it is
164
+ harmless there and firing every run is noise; ``compass lint`` surfaces it once.
165
+ """
154
166
  from ingestion_validation.validators.registry import registered_steps
155
167
  specs = registered_steps()
156
168
  by_key = {s.key: s.name for s in specs}
@@ -161,7 +173,7 @@ def _normalize_skips(tokens: list[str]) -> list[str]:
161
173
  out.append(by_key[t])
162
174
  elif t in names:
163
175
  out.append(t)
164
- else:
176
+ elif source == "cli":
165
177
  print(f" (skip: unknown step '{t}'; valid keys: {', '.join(sorted(by_key))})")
166
178
  return out
167
179
 
@@ -240,6 +252,32 @@ _AUTH_HINTS = ("403", "forbidden", "expired", "credential", "token", "signatured
240
252
  "invalidclienttokenid", "accessdenied", "unauthor")
241
253
 
242
254
 
255
+ def _print_tips(args, hygiene: dict | None, saved_baseline: bool) -> None:
256
+ """End-of-run hints about the most relevant unused flags.
257
+
258
+ Discoverability, not noise: at most three, only when the flag was NOT already used and
259
+ the run's own outcome makes it relevant. Suppress with --no-tips. This is why a partner
260
+ can run for weeks without finding --save-baseline / --unreachable-as / --suggest-config.
261
+ """
262
+ tips: list[str] = []
263
+ if not saved_baseline:
264
+ tips.append("Save this run as a baseline to catch regressions next time: "
265
+ "--save-baseline baselines/{}.json".format(args.project or "kb"))
266
+ if hygiene and hygiene.get("score", 100) < 90 and args.quality_threshold is None:
267
+ tips.append("Gate CI on data-plane hygiene (not just integrity): "
268
+ "--quality-threshold {}".format(int(hygiene["score"])))
269
+ if not getattr(args, "include_retrieval", False):
270
+ tips.append("Add retrieval-quality checks (can a stored vector be found via KNN?): "
271
+ "--include-retrieval")
272
+ if not args.suggest_config:
273
+ tips.append("Refine your config from what this run observed: --suggest-config")
274
+ if not tips:
275
+ return
276
+ print("\n Tips (--no-tips to silence):")
277
+ for t in tips[:3]:
278
+ print(f" * {t}")
279
+
280
+
243
281
  def _resolve_expected_documents(raw, project: str):
244
282
  """Resolve the configured expected_documents, supporting ``auto``.
245
283
 
@@ -476,13 +514,18 @@ def main(argv=None) -> int:
476
514
  # Corpus-level knowledge-base readiness (completeness, dedup, coverage).
477
515
  from ingestion_validation.corpus import analyze_corpus
478
516
 
517
+ # Read corpus expectations from the canonical `corpus:` block, falling back to the
518
+ # legacy `discovery.extra` location so older configs keep working.
519
+ cc = settings.corpus
520
+ dx = settings.discovery.extra
479
521
  expected_documents = _resolve_expected_documents(
480
- settings.discovery.extra.get("expected_documents"), args.project)
522
+ cc.expected_documents if cc.expected_documents is not None else dx.get("expected_documents"),
523
+ args.project)
481
524
  corpus = analyze_corpus(
482
525
  documents, results,
483
526
  expected_documents=expected_documents,
484
- expected_folders=settings.discovery.extra.get("expected_folders"),
485
- expected_document_names=settings.discovery.extra.get("expected_document_names"),
527
+ expected_folders=cc.expected_folders or dx.get("expected_folders"),
528
+ expected_document_names=cc.expected_document_names or dx.get("expected_document_names"),
486
529
  readiness_threshold=settings.pipeline.readiness_threshold,
487
530
  )
488
531
  print(f" KB readiness: {corpus['verdict']} ({corpus['passed']}/{corpus['total']} passed, "
@@ -502,6 +545,16 @@ def main(argv=None) -> int:
502
545
  if args.output_format in ("junit", "all"):
503
546
  print(f" JUnit report: {export_junit(results, base + '.xml')}")
504
547
 
548
+ if args.suggest_config:
549
+ from ingestion_validation.profiler import suggest_config_from_run
550
+ snippet = suggest_config_from_run(results, documents)
551
+ if args.suggest_config == "-":
552
+ print("\n" + snippet)
553
+ else:
554
+ from pathlib import Path as _P
555
+ _P(args.suggest_config).write_text(snippet, encoding="utf-8")
556
+ print(f" Config suggestions written: {args.suggest_config} (review before merging)")
557
+
505
558
  # KB Hygiene: data-plane cleanliness (advisory checks), separate from the integrity
506
559
  # gate. Printed for every run; gates the build only when --quality-threshold is set.
507
560
  # Best-effort context - a failure to score it must not crash the gate decision.
@@ -539,6 +592,8 @@ def main(argv=None) -> int:
539
592
  reason = f"{len(documents)} documents < s3.min_documents ({min_docs})"
540
593
  failed_gate = True
541
594
  code = EXIT_GATE_FAILED if (failed_gate or (regression and args.fail_on_regression)) else EXIT_OK
595
+ if not args.no_tips:
596
+ _print_tips(args, hygiene, saved_baseline=args.save_baseline is not None or bool(args.against))
542
597
  print("=" * 60)
543
598
  print(f" Results: {passed} passed, {failed} failed ({len(results)} total)")
544
599
  print(f" Gate: {'FAILED' if failed_gate else 'PASSED'} - {reason}")
@@ -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__ = "2.2.0"
8
+ __version__ = "2.4.0"
@@ -1302,6 +1302,72 @@ def cmd_freshness(argv: list[str]) -> int:
1302
1302
  return 0
1303
1303
 
1304
1304
 
1305
+ def cmd_grounding_audit(argv: list[str]) -> int:
1306
+ p = argparse.ArgumentParser(
1307
+ prog="compass grounding-audit",
1308
+ description="Adjudicate an eval's grounding finding against the live KB. Deterministic: "
1309
+ "verifies count/aggregation claims exactly against the KB and attributes the "
1310
+ "failure to a layer (retrieval vs the model). Does not judge answer quality.")
1311
+ p.add_argument("--project")
1312
+ p.add_argument("--env", default="")
1313
+ g = p.add_mutually_exclusive_group(required=True)
1314
+ g.add_argument("--eval-result", metavar="FILE", help="One eval record as JSON.")
1315
+ g.add_argument("--eval-results", metavar="FILE", help="JSONL of eval records (one per line).")
1316
+ p.add_argument("--format", choices=["markdown", "json"], default="markdown")
1317
+ p.add_argument("--output", help="Write the report to a file (default: stdout).")
1318
+ p.add_argument("--fail-on-fabrication", action="store_true",
1319
+ help="Exit non-zero if any verifiable claim disagrees with the KB.")
1320
+ args = p.parse_args(argv)
1321
+ _health_env(args)
1322
+
1323
+ import json as _json
1324
+
1325
+ from ingestion_validation.config import load_settings, project_config_file
1326
+ from ingestion_validation.grounding import render_report, run_audit
1327
+ from ingestion_validation.providers import get_adapter
1328
+
1329
+ if args.project and project_config_file(args.project) is None:
1330
+ print(f" Project '{args.project}' not found (config/projects/{args.project}.yaml).")
1331
+ return 2
1332
+ settings = load_settings(project=args.project, env=args.env or None)
1333
+
1334
+ # Load the eval record(s).
1335
+ records: list[dict] = []
1336
+ try:
1337
+ if args.eval_result:
1338
+ records = [_json.loads(Path(args.eval_result).read_text(encoding="utf-8"))]
1339
+ else:
1340
+ for line in Path(args.eval_results).read_text(encoding="utf-8").splitlines():
1341
+ if line.strip():
1342
+ records.append(_json.loads(line))
1343
+ except Exception as exc: # noqa: BLE001 - clean message, never a traceback
1344
+ print(f" Could not read eval input: {exc}")
1345
+ return 2
1346
+
1347
+ adapter = get_adapter("vectorstore", settings.redis.provider, config=settings.redis)
1348
+ results = [run_audit(rec, adapter, settings.eval) for rec in records]
1349
+
1350
+ if args.format == "json":
1351
+ from dataclasses import asdict
1352
+ body = _json.dumps([asdict(r) for r in results], indent=2, default=str)
1353
+ else:
1354
+ body = ("\n\n".join(render_report(r, project=args.project or "") for r in results))
1355
+
1356
+ if args.output:
1357
+ Path(args.output).parent.mkdir(parents=True, exist_ok=True)
1358
+ Path(args.output).write_text(body + "\n", encoding="utf-8")
1359
+ print(f" Wrote {args.output}")
1360
+ else:
1361
+ print(body)
1362
+
1363
+ fabricated = sum(1 for r in results if r.wrong_claims)
1364
+ if fabricated:
1365
+ print(f"\n {fabricated}/{len(results)} case(s) contain claims that disagree with the KB.")
1366
+ if args.fail_on_fabrication:
1367
+ return 1
1368
+ return 0
1369
+
1370
+
1305
1371
  # --------------------------------------------------------------------------- #
1306
1372
  # compass selftest - prove exit-code propagation in THIS shell (BUG-14/15 diagnosis)
1307
1373
  # --------------------------------------------------------------------------- #
@@ -1355,6 +1421,7 @@ Commands:
1355
1421
  compare compare retrieval against a ground-truth file (precision/recall/Jaccard)
1356
1422
  quality score chunk quality (density, self-containedness, boilerplate) - no LLM
1357
1423
  freshness flag stale sources + orphan documents (source object gone)
1424
+ grounding-audit check an eval's grounding finding against the KB (count claims) - no LLM
1358
1425
  demo generate a sample report from synthetic data (no backend needed)
1359
1426
  init [name] scaffold a project (config + .env.example + marker). -i/--interactive wizard; --graph graph block; --ci workflow
1360
1427
  lint statically check a project config for silent-failure traps (no backend)
@@ -1399,6 +1466,8 @@ def main(argv: list[str] | None = None) -> int:
1399
1466
  return cmd_compare(rest)
1400
1467
  if cmd == "quality":
1401
1468
  return cmd_quality(rest)
1469
+ if cmd == "grounding-audit":
1470
+ return cmd_grounding_audit(rest)
1402
1471
  if cmd == "freshness":
1403
1472
  return cmd_freshness(rest)
1404
1473
  if cmd == "selftest":
@@ -534,6 +534,22 @@ class DiscoveryConfig:
534
534
  extra: dict[str, Any] = field(default_factory=dict)
535
535
 
536
536
 
537
+ @dataclass
538
+ class CorpusConfig:
539
+ """Corpus-level expectations: what SHOULD be in the knowledge base.
540
+
541
+ The natural home for the expected document set, so completeness alerts on drift. This
542
+ is the block users reach for (`corpus:`); previously it was neither recognized nor read
543
+ (the values had to live under `discovery.extra`, which few discover), so a
544
+ `corpus.expected_documents` was silently ignored. Now first-class and read directly.
545
+ """
546
+
547
+ expected_documents: Any = None # int, or "auto" (baseline from last run)
548
+ expected_folders: list[str] = field(default_factory=list)
549
+ expected_document_names: list[str] = field(default_factory=list)
550
+ extra: dict[str, Any] = field(default_factory=dict)
551
+
552
+
537
553
  @dataclass
538
554
  class EmbeddingConfig:
539
555
  """How to turn a natural-language query into a vector for `compass ask`.
@@ -582,6 +598,21 @@ class EvalConfig:
582
598
  gt_variant_path: str = ""
583
599
  gt_variant_filter: list[str] = field(default_factory=list)
584
600
 
601
+ # --- Grounding audit (compass grounding-audit) ------------------------------------
602
+ # Verify an eval's grounding finding against the live KB. Every field name here comes
603
+ # from config, never assumed - a KB whose ingestion named things differently only
604
+ # changes these values, not code.
605
+ gt_response_path: str = "response" # the answer text in the eval record
606
+ gt_context_path: str = "context.knowledge_retrieval" # the retrieved context the eval saw
607
+ gt_scores_path: str = "scores" # eval scores (shown in the report)
608
+ # Structured-query verification: which fields the KB can aggregate to check a count
609
+ # claim. Each entry: {field: <group-by field>, distinct_by: <doc-identity field>,
610
+ # label: <optional human label>}. Empty = count claims are not verified.
611
+ aggregatable_fields: list[dict] = field(default_factory=list)
612
+ # Extra query-intent regexes, merged with the built-in defaults (how many / count / by
613
+ # type / list all / most recent ...). Lets a domain add its own phrasings.
614
+ structured_query_patterns: list[str] = field(default_factory=list)
615
+
585
616
 
586
617
  @dataclass
587
618
  class ConsistencyConfig:
@@ -640,6 +671,7 @@ class Settings:
640
671
  embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig)
641
672
  eval: EvalConfig = field(default_factory=EvalConfig)
642
673
  consistency: ConsistencyConfig = field(default_factory=ConsistencyConfig)
674
+ corpus: CorpusConfig = field(default_factory=CorpusConfig)
643
675
  # Non-service metadata carried through from config files.
644
676
  project_name: str = ""
645
677
  display_name: str = ""
@@ -661,7 +693,7 @@ _META_KEYS = frozenset({"default_env", "display_name", "environments", "skip_ste
661
693
  # new block can never be added to one and forgotten in the other (which is exactly how
662
694
  # `consistency` came to be silently dropped at the top level).
663
695
  _SERVICE_BLOCKS = ("api", "s3", "graphdb", "redis", "neptune", "pipeline",
664
- "retrieval", "discovery", "embedding", "eval", "consistency")
696
+ "retrieval", "discovery", "embedding", "eval", "consistency", "corpus")
665
697
 
666
698
 
667
699
  def _deep_merge(base: dict, overlay: dict) -> dict:
@@ -787,6 +819,7 @@ def load_settings(
787
819
  embedding=_hydrate(EmbeddingConfig, flat.get("embedding")),
788
820
  eval=_hydrate(EvalConfig, flat.get("eval")),
789
821
  consistency=_hydrate(ConsistencyConfig, flat.get("consistency")),
822
+ corpus=_hydrate(CorpusConfig, flat.get("corpus")),
790
823
  project_name=project or "",
791
824
  display_name=merged.get("display_name", ""),
792
825
  environment=resolved_env,