compass-kb-validation 2.6.0__tar.gz → 2.6.1__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.6.0 → compass_kb_validation-2.6.1}/PKG-INFO +1 -1
  2. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/compass_kb_validation.egg-info/PKG-INFO +1 -1
  3. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/discover_and_validate.py +7 -7
  4. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/__init__.py +1 -1
  5. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/cli.py +23 -12
  6. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/grounding.py +8 -0
  7. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/profiler.py +7 -3
  8. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/remediation.py +11 -6
  9. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/report/__init__.py +11 -2
  10. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_grounding.py +15 -0
  11. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_neptune_bugfixes2.py +13 -1
  12. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_profiler.py +2 -2
  13. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_reporting.py +26 -2
  14. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/LICENSE +0 -0
  15. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/README.md +0 -0
  16. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/compass_kb_validation.egg-info/SOURCES.txt +0 -0
  17. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
  18. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/compass_kb_validation.egg-info/entry_points.txt +0 -0
  19. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/compass_kb_validation.egg-info/requires.txt +0 -0
  20. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/compass_kb_validation.egg-info/top_level.txt +0 -0
  21. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/__main__.py +0 -0
  22. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/ask.py +0 -0
  23. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/baseline.py +0 -0
  24. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/chunk_coherence.py +0 -0
  25. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/config/__init__.py +0 -0
  26. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/config_lint.py +0 -0
  27. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/content_inspect.py +0 -0
  28. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/corpus.py +0 -0
  29. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/corpus_health.py +0 -0
  30. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/diagnostics.py +0 -0
  31. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/embedding_drift.py +0 -0
  32. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/eval_interop.py +0 -0
  33. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/init_wizard.py +0 -0
  34. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/live_dashboard.py +0 -0
  35. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/mcp_server.py +0 -0
  36. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/models/__init__.py +0 -0
  37. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/notifications.py +0 -0
  38. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/pipeline/__init__.py +0 -0
  39. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/provenance.py +0 -0
  40. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/providers/__init__.py +0 -0
  41. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/providers/embedding.py +0 -0
  42. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/providers/graph.py +0 -0
  43. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/providers/source.py +0 -0
  44. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/providers/storage.py +0 -0
  45. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/providers/vectorstore.py +0 -0
  46. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/report/_report_css.py +0 -0
  47. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/report/_report_js.py +0 -0
  48. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/report/analysis.py +0 -0
  49. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/report/export.py +0 -0
  50. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/report/shell.py +0 -0
  51. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/retrieval_analysis.py +0 -0
  52. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/trend_tracker.py +0 -0
  53. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/utils/__init__.py +0 -0
  54. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/__init__.py +0 -0
  55. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/api_upload.py +0 -0
  56. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/base.py +0 -0
  57. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/base_sparql.py +0 -0
  58. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/cross_stage.py +0 -0
  59. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/graphdb_metadata.py +0 -0
  60. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/neptune_metadata.py +0 -0
  61. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/redis_chunks.py +0 -0
  62. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/registry.py +0 -0
  63. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/retrieval_quality.py +0 -0
  64. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/ingestion_validation/validators/s3_storage.py +0 -0
  65. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/profile_kb.py +0 -0
  66. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/pyproject.toml +0 -0
  67. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/run_validation.py +0 -0
  68. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/setup.cfg +0 -0
  69. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_alt_adapters.py +0 -0
  70. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_analysis_report.py +0 -0
  71. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_api_upload.py +0 -0
  72. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_ask.py +0 -0
  73. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_baseline.py +0 -0
  74. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_benchmark.py +0 -0
  75. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_check_catalog.py +0 -0
  76. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_cli.py +0 -0
  77. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_config.py +0 -0
  78. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_config_lint.py +0 -0
  79. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_content_inspect.py +0 -0
  80. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_corpus.py +0 -0
  81. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_corpus_health.py +0 -0
  82. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_coverage.py +0 -0
  83. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_cross_stage.py +0 -0
  84. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_diagnostics.py +0 -0
  85. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_discover_cli.py +0 -0
  86. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_discovery_parallel.py +0 -0
  87. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_embedding_drift.py +0 -0
  88. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_eval_interop.py +0 -0
  89. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_gate.py +0 -0
  90. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_graph_transport.py +0 -0
  91. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_graph_validators.py +0 -0
  92. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_init_wizard.py +0 -0
  93. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_manifest.py +0 -0
  94. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_mcp.py +0 -0
  95. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_migration.py +0 -0
  96. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_models.py +0 -0
  97. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_neptune_bugfixes.py +0 -0
  98. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_notifications.py +0 -0
  99. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_pipeline.py +0 -0
  100. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_providers.py +0 -0
  101. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_redis_chunks.py +0 -0
  102. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_registry.py +0 -0
  103. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_remediation.py +0 -0
  104. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_retrieval_assertions.py +0 -0
  105. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_retrieval_quality.py +0 -0
  106. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_run_cli.py +0 -0
  107. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_sources.py +0 -0
  108. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_storage_validator.py +0 -0
  109. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_trends.py +0 -0
  110. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_universal_concerns.py +0 -0
  111. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/tests/test_utils.py +0 -0
  112. {compass_kb_validation-2.6.0 → compass_kb_validation-2.6.1}/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: 2.6.0
3
+ Version: 2.6.1
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compass-kb-validation
3
- Version: 2.6.0
3
+ Version: 2.6.1
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
@@ -138,14 +138,14 @@ def _resolve_skip_steps(settings, *, include_graphdb: bool, include_retrieval: b
138
138
  (BUG-22). ``--keep s3_storage`` overrides the auto-skip.
139
139
  """
140
140
  skip = ["API Upload"]
141
- # Graph validation runs by default when a graph block is actually configured (a
142
- # sparql_query or an endpoint) - a configured neptune:/graphdb: block should not be
143
- # silently ignored just because --include-graphdb was not passed. Opt out with
144
- # --skip neptune_metadata (or graphdb_metadata). --include-graphdb still forces it on.
141
+ # Graph validation runs by default when THIS PROJECT declares a per-document graph
142
+ # query (sparql_query) - the signal that graph validation is actually intended. It must
143
+ # NOT key off `neptune.endpoint`, which resolves from a shared NEPTUNE_ENDPOINT env var:
144
+ # doing so turned graph on for every project whenever that env var was set, so a project
145
+ # with no graph block inherited another project's Neptune and failed all 19 graph checks
146
+ # (regression). Opt out with --skip neptune_metadata; --include-graphdb still forces on.
145
147
  nep, gdb = settings.neptune, settings.graphdb
146
- graph_configured = bool(
147
- getattr(nep, "sparql_query", "") or getattr(nep, "endpoint", "")
148
- or getattr(gdb, "sparql_query", "") or getattr(gdb, "base_url", ""))
148
+ graph_configured = bool(getattr(nep, "sparql_query", "") or getattr(gdb, "sparql_query", ""))
149
149
  if not (include_graphdb or graph_configured):
150
150
  skip += ["GraphDB Metadata", "Neptune Metadata"]
151
151
  if not include_retrieval:
@@ -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.6.0"
8
+ __version__ = "2.6.1"
@@ -1318,7 +1318,7 @@ def cmd_grounding_audit(argv: list[str]) -> int:
1318
1318
  g = p.add_mutually_exclusive_group(required=True)
1319
1319
  g.add_argument("--eval-result", metavar="FILE", help="One eval record as a single JSON object.")
1320
1320
  g.add_argument("--eval-results", metavar="FILE",
1321
- help="Batch: a JSONL file (one JSON object per LINE, not a JSON array). "
1321
+ help="Batch: JSONL (one object per line) or a JSON array - both accepted. "
1322
1322
  "`compass ask --format eval` emits a compatible file.")
1323
1323
  p.add_argument("--format", choices=["markdown", "json"], default="markdown")
1324
1324
  p.add_argument("--output", help="Write the report to a file (default: stdout).")
@@ -1352,15 +1352,23 @@ def cmd_grounding_audit(argv: list[str]) -> int:
1352
1352
  return 2
1353
1353
  records = [one]
1354
1354
  else:
1355
- for n, line in enumerate(Path(args.eval_results).read_text(encoding="utf-8").splitlines(), 1):
1356
- if not line.strip():
1357
- continue
1358
- try:
1359
- records.append(_json.loads(line))
1360
- except Exception: # noqa: BLE001 - point at the offending line, not char 1
1361
- print(f" --eval-results is JSONL (one JSON object per line); line {n} is not valid JSON. "
1362
- "A JSON array is not accepted here - see `compass ask --format eval`.")
1363
- return 2
1355
+ raw = Path(args.eval_results).read_text(encoding="utf-8")
1356
+ # Accept BOTH a JSON array and JSONL (one object per line) - a whole-file array
1357
+ # is a natural thing to pass, so handle it instead of erroring.
1358
+ stripped = raw.lstrip()
1359
+ if stripped.startswith("["):
1360
+ arr = _json.loads(raw)
1361
+ records = [r for r in arr if isinstance(r, dict)]
1362
+ else:
1363
+ for n, line in enumerate(raw.splitlines(), 1):
1364
+ if not line.strip():
1365
+ continue
1366
+ try:
1367
+ records.append(_json.loads(line))
1368
+ except Exception: # noqa: BLE001 - point at the offending line, not char 1
1369
+ print(f" --eval-results: line {n} is not valid JSON. Expected JSONL "
1370
+ "(one object per line) or a single JSON array.")
1371
+ return 2
1364
1372
  except Exception as exc: # noqa: BLE001 - clean message, never a traceback
1365
1373
  print(f" Could not read eval input: {exc}")
1366
1374
  return 2
@@ -1381,9 +1389,12 @@ def cmd_grounding_audit(argv: list[str]) -> int:
1381
1389
  else:
1382
1390
  print(body)
1383
1391
 
1384
- fabricated = sum(1 for r in results if r.wrong_claims)
1392
+ # Gate on fabrications in EITHER path: a count that disagrees with the KB, or a cited
1393
+ # document absent from it. (Previously only structured count mismatches counted, so a
1394
+ # semantic fabricated-source case exited 0 despite being flagged.)
1395
+ fabricated = sum(1 for r in results if r.has_fabrication)
1385
1396
  if fabricated:
1386
- print(f"\n {fabricated}/{len(results)} case(s) contain claims that disagree with the KB.")
1397
+ print(f"\n {fabricated}/{len(results)} case(s) contain a claim or citation not backed by the KB.")
1387
1398
  if args.fail_on_fabrication:
1388
1399
  return 1
1389
1400
  return 0
@@ -309,6 +309,14 @@ class GroundingResult:
309
309
  def verified_claims(self) -> list[ClaimVerdict]:
310
310
  return [v for v in self.claim_verdicts if v.verifiable]
311
311
 
312
+ @property
313
+ def has_fabrication(self) -> bool:
314
+ """A count that disagrees with the KB, OR a cited document absent from the KB -
315
+ the two deterministic 'the model asserted something not in the KB' signals that
316
+ --fail-on-fabrication gates on (semantic fabrications were previously missed)."""
317
+ return bool(self.wrong_claims) or any(
318
+ "not found in KB" in s.get("label", "") for s in self.signals)
319
+
312
320
  def layer_diagnosis(self) -> str:
313
321
  """Deterministic layer attribution - which stage is implicated, no quality score."""
314
322
  if self.query_type == "structured" and self.wrong_claims:
@@ -242,11 +242,15 @@ def _configured_graph(settings: Any) -> tuple[Any, str]:
242
242
  configured Neptune endpoint. Returns ``(None, "")`` when neither is set.
243
243
  """
244
244
  gdb = getattr(settings, "graphdb", None)
245
- if gdb is not None and (getattr(gdb, "endpoint", "")
246
- or (getattr(gdb, "base_url", "") and getattr(gdb, "repository", ""))):
245
+ if gdb is not None and getattr(gdb, "sparql_query", "") and (
246
+ getattr(gdb, "endpoint", "") or (getattr(gdb, "base_url", "") and getattr(gdb, "repository", ""))):
247
247
  return gdb, gdb.provider
248
248
  nep = getattr(settings, "neptune", None)
249
- if nep is not None and getattr(nep, "endpoint", ""):
249
+ # Require the project to declare a per-document sparql_query, NOT just a reachable
250
+ # endpoint: neptune.endpoint resolves from the shared NEPTUNE_ENDPOINT env var, so keying
251
+ # only off it made an unrelated project profile (and then validate) another project's
252
+ # graph when the env var was set.
253
+ if nep is not None and getattr(nep, "sparql_query", "") and getattr(nep, "endpoint", ""):
250
254
  return nep, "neptune"
251
255
  return None, ""
252
256
 
@@ -116,14 +116,19 @@ _HINTS: list[tuple[str, str]] = [
116
116
  "Stored vectors were embedded with a different model than the one declared/serving: retrieval is "
117
117
  "silently wrong. Re-embed the corpus with the declared model, or correct the declared model."),
118
118
  (r"exact-term retrieval|keyword-index completeness|findable lexically",
119
- "Chunks are not findable by exact keyword: the text field is not indexed for lexical search. Enable "
120
- "hybrid (keyword + vector) retrieval, or index the text field for full-text search."),
119
+ "Exact-keyword retrieval is weak. Either the text field is not indexed for lexical search, OR - in a "
120
+ "large corpus of similar documents - a common term returns many near-identical hits and the right one "
121
+ "does not rank first. Confirm the text field is indexed; if it is, add hybrid (keyword+vector) or a "
122
+ "metadata filter, or raise the similarity threshold."),
121
123
  (r"negative retrieval|off-topic",
122
- "An off-topic query still scored high: embeddings are too generic or the index is polluted by "
123
- "boilerplate. Improve chunk quality and strip boilerplate so unrelated queries do not match."),
124
+ "An off-topic query still scored high: embeddings may be too generic, or the corpus is dense with "
125
+ "similar/boilerplate content so unrelated queries still match. Strip boilerplate and consider a "
126
+ "higher similarity threshold or metadata pre-filtering."),
124
127
  (r"recall|mrr|paraphrase|boundary|empty query",
125
- "The document is not retrievable as expected for real queries: embeddings may not match the query "
126
- "path, or the filter field is not indexed. Verify the retrieval/query configuration."),
128
+ "Retrieval is weak for real queries. This can be a config problem (embeddings not matching the query "
129
+ "path, or an unindexed filter field) OR corpus-specific: a large set of highly similar documents makes "
130
+ "paraphrase/boundary disambiguation genuinely hard. Verify the retrieval config first; if it is sound, "
131
+ "adjust the similarity threshold or filter by metadata."),
127
132
 
128
133
  # ---- Ingestion API / storage / connectivity --------------------------------------
129
134
  (r"http status|graphql|success status|valid json|api ",
@@ -46,6 +46,10 @@ def _step_icons() -> dict:
46
46
  _BAND = {"PASSED": "ok", "FAILED": "bad", "ERROR": "bad", "PARTIAL": "warn", "SKIPPED": "na", "EMPTY": "na"}
47
47
  # Cap RDF triples embedded per step for the knowledge-graph view (bounds payload size).
48
48
  _KG_TRIPLE_CAP = 80
49
+ # At scale, a passed document keeps its chunk previews if it has at most this many - small
50
+ # docs are cheap and their preview is what you drill into. Only large docs (heavy previews)
51
+ # are trimmed, per document, rather than a corpus-wide blanket that emptied even 1-chunk docs.
52
+ _CHUNK_ITEMS_KEEP = 10
49
53
  # Max folder/category rows in the readiness roll-up (worst pass-rate first); keeps it
50
54
  # a signal rather than one row per document when folders are fine-grained.
51
55
  _FOLDER_DISPLAY_CAP = 20
@@ -1875,8 +1879,13 @@ def generate_batch_dashboard(
1875
1879
  ]
1876
1880
  if not drillable:
1877
1881
  trimmed += 1
1878
- if c.get("chunks"):
1879
- c["chunks"] = {**c["chunks"], "items": []}
1882
+ # Drop chunk previews PER DOCUMENT by its own size, not a corpus-wide
1883
+ # blanket: a doc with a handful of chunks costs almost nothing and its
1884
+ # preview is exactly what you drill into, so keep it. Only large docs
1885
+ # (where the previews are actually heavy) are trimmed.
1886
+ ch = c.get("chunks")
1887
+ if ch and len(ch.get("items") or []) > _CHUNK_ITEMS_KEEP:
1888
+ c["chunks"] = {**ch, "items": []}
1880
1889
  trim_note = (
1881
1890
  f'<div class="meta-item"><span class="meta-key">Detail:</span> triples + verbose fields '
1882
1891
  f'trimmed for {trimmed} passed doc(s); check names + status kept</div>'
@@ -248,3 +248,18 @@ def test_emit_config_suggests_aggregatable_fields():
248
248
  out = emit_project_config(profiles, project="demo")
249
249
  assert "aggregatable_fields:" in out
250
250
  assert "field: document_type" in out and "count_by: file_name" in out
251
+
252
+
253
+ def test_fabrication_gate_covers_semantic_fabricated_source():
254
+ # --fail-on-fabrication must fire on a cited document absent from the KB (semantic),
255
+ # not only structured count mismatches - previously it exited 0 on a fabricated source.
256
+ class KB:
257
+ supports_aggregation = True
258
+ def text_search(self, f, v, limit=1): return [] if "fake" in v.lower() else [{"x": 1}]
259
+ r = run_audit({"input": "summary?", "response": "Per **Fake Report.pdf**, x happened.",
260
+ "context.retrieved": "c", "scores": {}}, KB(), _cfg(gt_context_path="context.retrieved"))
261
+ assert r.has_fabrication is True
262
+ # a clean case does not trip the gate
263
+ r2 = run_audit({"input": "outlook?", "response": "Cautious.", "context.retrieved": "cautious given rates",
264
+ "scores": {"Groundedness": {"score": 0.9, "target": 0.7}}}, KB(), _cfg(gt_context_path="context.retrieved"))
265
+ assert r2.has_fabrication is False
@@ -51,6 +51,7 @@ def test_configured_graph_prefers_graphdb():
51
51
  s = Settings()
52
52
  s.graphdb.base_url = "http://gdb"
53
53
  s.graphdb.repository = "repo"
54
+ s.graphdb.sparql_query = "SELECT ?s WHERE { ?s ?p ?o {filter_clause} }"
54
55
  cfg, name = _configured_graph(s)
55
56
  assert cfg is s.graphdb and name == s.graphdb.provider
56
57
 
@@ -58,6 +59,7 @@ def test_configured_graph_prefers_graphdb():
58
59
  def test_configured_graph_falls_back_to_neptune():
59
60
  s = Settings()
60
61
  s.neptune.endpoint_default = "cluster.example.neptune.amazonaws.com"
62
+ s.neptune.sparql_query = "SELECT ?s WHERE { ?s ?p ?o {filter_clause} }"
61
63
  cfg, name = _configured_graph(s)
62
64
  assert cfg is s.neptune and name == "neptune"
63
65
 
@@ -67,17 +69,27 @@ def test_configured_graph_none_when_unset():
67
69
  assert cfg is None and name == ""
68
70
 
69
71
 
72
+ def test_configured_graph_ignores_env_only_endpoint(monkeypatch):
73
+ # Regression (#18): neptune.endpoint resolves from the shared NEPTUNE_ENDPOINT env var,
74
+ # so a project with NO graph query (no graph block declared) must NOT be treated as
75
+ # graph-configured just because that env var is set for another project.
76
+ monkeypatch.setenv("NEPTUNE_ENDPOINT", "https://someone-elses-cluster.example:8182")
77
+ cfg, name = _configured_graph(Settings()) # no sparql_query declared
78
+ assert cfg is None and name == ""
79
+
80
+
70
81
  def test_profile_graph_reports_neptune_not_missing(monkeypatch):
71
82
  from ingestion_validation import profiler
72
83
  s = Settings()
73
84
  s.neptune.endpoint_default = "cluster.example.neptune.amazonaws.com"
85
+ s.neptune.sparql_query = "SELECT ?s WHERE { ?s ?p ?o {filter_clause} }"
74
86
 
75
87
  class _FakeGraph:
76
88
  def reachable(self):
77
89
  return False
78
90
  monkeypatch.setattr(profiler, "get_adapter", lambda *a, **k: _FakeGraph())
79
91
  prof = profiler.profile_graph(s)
80
- # It must not claim "no graph configured" - it now sees the neptune block.
92
+ # It must not claim "no graph configured" - it now sees the neptune block (with a query).
81
93
  assert prof["provider"] == "neptune"
82
94
  assert not any("No graph endpoint" in n for n in prof["notes"])
83
95
 
@@ -172,7 +172,7 @@ def test_profile_storage_no_bucket_is_neutral():
172
172
  def test_profile_graph_collects_predicates(monkeypatch):
173
173
  rows = [{"p": "http://ex/eTag"}, {"p": "http://ex/name"}]
174
174
  _patch(monkeypatch, FakeGraphAdapter(rows=rows, reachable=True))
175
- s = Settings(graphdb=GraphDBConfig(base_url="http://g", repository="repo"))
175
+ s = Settings(graphdb=GraphDBConfig(base_url="http://g", repository="repo", sparql_query="SELECT ?s WHERE { ?s ?p ?o {filter_clause} }"))
176
176
  p = profiler.profile_graph(s)
177
177
  assert p["reachable"] is True
178
178
  assert "http://ex/eTag" in p["predicates"]
@@ -190,7 +190,7 @@ def test_profile_graph_predicate_coverage_suggests_required(monkeypatch):
190
190
  # can be computed. eTag 10/10, name 9/10 -> required; rare 1/10 -> optional.
191
191
  rows = [{"p": "http://ex/eTag", "c": "10"}, {"p": "http://ex/name", "c": "9"}, {"p": "http://ex/rare", "c": "1"}]
192
192
  _patch(monkeypatch, FakeGraphAdapter(rows=rows, reachable=True))
193
- s = Settings(graphdb=GraphDBConfig(base_url="http://g", repository="repo"))
193
+ s = Settings(graphdb=GraphDBConfig(base_url="http://g", repository="repo", sparql_query="SELECT ?s WHERE { ?s ?p ?o {filter_clause} }"))
194
194
  p = profiler.profile_graph(s)
195
195
  assert p["subjects"] == 10
196
196
  assert len(p["coverage"]) == 3
@@ -265,8 +265,9 @@ def test_scale_guard_trims_passed_docs(tmp_path):
265
265
  r.add_step(StepResult(
266
266
  "Redis Chunks", status, "x",
267
267
  checks=[{"label": "Chunks exist", "passed": status is StepStatus.PASSED, "detail": "9 chunks, verbose"}],
268
- details={"chunk_view": {"count": 9, "total_chars": 100, "avg_chars": 11,
269
- "items": [{"key": "k", "length": 11, "preview": "p", "fields": {}}]}},
268
+ details={"chunk_view": {"count": 15, "total_chars": 100, "avg_chars": 11,
269
+ "items": [{"key": f"k{j}", "length": 11, "preview": "p", "fields": {}}
270
+ for j in range(15)]}}, # >_CHUNK_ITEMS_KEEP -> trimmed at scale
270
271
  ))
271
272
  r.add_step(StepResult(
272
273
  "Neptune Metadata", StepStatus.PASSED, "graph",
@@ -685,3 +686,26 @@ def test_recommendations_section_only_with_fixable_issues():
685
686
  # clean run -> nothing rendered (follows "only when issues exist")
686
687
  clean_agg = report_mod._aggregate([case(i, clean=True) for i in range(10)])
687
688
  assert report_mod._recommendations_section(clean_agg) == ""
689
+
690
+
691
+ def test_chunk_previews_kept_per_document_not_blanket_at_scale():
692
+ # A small doc's chunk previews are cheap and are what you drill into; only large docs
693
+ # are trimmed at scale - previously the whole corpus was blanked, emptying 1-chunk docs.
694
+ def case(name, n_chunks):
695
+ r = PipelineResult(document_id=name, file_name=f"{name}.pdf")
696
+ items = [{"key": f"c{j}", "length": 10, "preview": "text", "fields": {}} for j in range(n_chunks)]
697
+ r.add_step(StepResult("Redis Chunks", StepStatus.PASSED, "ok",
698
+ checks=[{"label": "Chunks exist", "passed": True}],
699
+ details={"chunk_view": {"count": n_chunks, "total_chars": 10 * n_chunks,
700
+ "avg_chars": 10, "items": items}}))
701
+ r.finalize()
702
+ return {"name": name, "result": r}
703
+ cases = [case("small", 2)] + [case(f"big{i}", 20) for i in range(250)] # > embed_detail_cap
704
+ html = open(generate_batch_dashboard(cases, __import__("tempfile").mktemp(suffix=".html"),
705
+ embed_detail_cap=200), encoding="utf-8").read()
706
+ m = re.search(r"const REPORT = (\{.*?\});", html, re.S)
707
+ assert m is not None
708
+ payload = json.loads(m.group(1))
709
+ by = {c["name"]: c for c in payload["cases"]}
710
+ assert by["small"]["chunks"]["items"] # 2-chunk doc keeps its previews
711
+ assert by["big0"]["chunks"]["items"] == [] # 20-chunk doc is trimmed