compass-kb-validation 2.8.0__tar.gz → 2.9.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 (119) hide show
  1. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/PKG-INFO +1 -1
  2. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/compass_kb_validation.egg-info/PKG-INFO +1 -1
  3. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/compass_kb_validation.egg-info/SOURCES.txt +1 -0
  4. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/discover_and_validate.py +25 -1
  5. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/__init__.py +1 -1
  6. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/cli.py +41 -22
  7. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/report/__init__.py +19 -12
  8. compass_kb_validation-2.9.0/ingestion_validation/report/audit.py +186 -0
  9. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_citation.py +11 -0
  10. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_evidence.py +11 -0
  11. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_reporting.py +38 -0
  12. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/LICENSE +0 -0
  13. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/README.md +0 -0
  14. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
  15. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
  16. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/compass_kb_validation.egg-info/requires.txt +0 -0
  17. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
  18. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/__main__.py +0 -0
  19. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/ask.py +0 -0
  20. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/baseline.py +0 -0
  21. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/chunk_coherence.py +0 -0
  22. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/citation.py +0 -0
  23. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/config/__init__.py +0 -0
  24. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/config_lint.py +0 -0
  25. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/content_inspect.py +0 -0
  26. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/corpus.py +0 -0
  27. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/corpus_health.py +0 -0
  28. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/diagnostics.py +0 -0
  29. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/embedding_drift.py +0 -0
  30. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/eval_interop.py +0 -0
  31. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/evidence.py +0 -0
  32. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/grounding.py +0 -0
  33. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/init_wizard.py +0 -0
  34. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/live_dashboard.py +0 -0
  35. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/mcp_server.py +0 -0
  36. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/models/__init__.py +0 -0
  37. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/notifications.py +0 -0
  38. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/pipeline/__init__.py +0 -0
  39. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/profiler.py +0 -0
  40. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/provenance.py +0 -0
  41. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/providers/__init__.py +0 -0
  42. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/providers/embedding.py +0 -0
  43. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/providers/graph.py +0 -0
  44. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/providers/source.py +0 -0
  45. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/providers/storage.py +0 -0
  46. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/providers/vectorstore.py +0 -0
  47. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/remediation.py +0 -0
  48. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/report/_report_css.py +0 -0
  49. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/report/_report_js.py +0 -0
  50. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/report/analysis.py +0 -0
  51. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/report/export.py +0 -0
  52. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/report/shell.py +0 -0
  53. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/retrieval_analysis.py +0 -0
  54. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/text_probes.py +0 -0
  55. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/trend_tracker.py +0 -0
  56. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/utils/__init__.py +0 -0
  57. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/__init__.py +0 -0
  58. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/api_upload.py +0 -0
  59. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/base.py +0 -0
  60. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/base_sparql.py +0 -0
  61. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/cross_stage.py +0 -0
  62. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/graphdb_metadata.py +0 -0
  63. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/neptune_metadata.py +0 -0
  64. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/redis_chunks.py +0 -0
  65. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/registry.py +0 -0
  66. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/retrieval_quality.py +0 -0
  67. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/ingestion_validation/validators/s3_storage.py +0 -0
  68. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/profile_kb.py +0 -0
  69. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/pyproject.toml +0 -0
  70. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/run_validation.py +0 -0
  71. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/setup.cfg +0 -0
  72. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_alt_adapters.py +0 -0
  73. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_analysis_report.py +0 -0
  74. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_api_upload.py +0 -0
  75. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_ask.py +0 -0
  76. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_baseline.py +0 -0
  77. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_benchmark.py +0 -0
  78. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_check_catalog.py +0 -0
  79. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_cli.py +0 -0
  80. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_config.py +0 -0
  81. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_config_lint.py +0 -0
  82. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_content_inspect.py +0 -0
  83. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_corpus.py +0 -0
  84. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_corpus_health.py +0 -0
  85. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_coverage.py +0 -0
  86. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_cross_stage.py +0 -0
  87. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_diagnostics.py +0 -0
  88. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_discover_cli.py +0 -0
  89. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_discovery_parallel.py +0 -0
  90. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_embedding_drift.py +0 -0
  91. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_eval_interop.py +0 -0
  92. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_gate.py +0 -0
  93. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_graph_transport.py +0 -0
  94. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_graph_validators.py +0 -0
  95. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_grounding.py +0 -0
  96. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_init_wizard.py +0 -0
  97. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_manifest.py +0 -0
  98. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_mcp.py +0 -0
  99. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_migration.py +0 -0
  100. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_models.py +0 -0
  101. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_neptune_bugfixes.py +0 -0
  102. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_neptune_bugfixes2.py +0 -0
  103. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_notifications.py +0 -0
  104. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_pipeline.py +0 -0
  105. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_profiler.py +0 -0
  106. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_providers.py +0 -0
  107. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_redis_chunks.py +0 -0
  108. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_registry.py +0 -0
  109. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_remediation.py +0 -0
  110. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_retrieval_assertions.py +0 -0
  111. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_retrieval_quality.py +0 -0
  112. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_review_hardening.py +0 -0
  113. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_run_cli.py +0 -0
  114. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_sources.py +0 -0
  115. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_storage_validator.py +0 -0
  116. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_trends.py +0 -0
  117. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_universal_concerns.py +0 -0
  118. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.0}/tests/test_utils.py +0 -0
  119. {compass_kb_validation-2.8.0 → compass_kb_validation-2.9.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: 2.8.0
3
+ Version: 2.9.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compass-kb-validation
3
- Version: 2.8.0
3
+ Version: 2.9.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
@@ -49,6 +49,7 @@ ingestion_validation/report/__init__.py
49
49
  ingestion_validation/report/_report_css.py
50
50
  ingestion_validation/report/_report_js.py
51
51
  ingestion_validation/report/analysis.py
52
+ ingestion_validation/report/audit.py
52
53
  ingestion_validation/report/export.py
53
54
  ingestion_validation/report/shell.py
54
55
  ingestion_validation/utils/__init__.py
@@ -33,6 +33,9 @@ def parse_args(argv=None) -> argparse.Namespace:
33
33
  p.add_argument("--env", default="dev", help="Environment")
34
34
  p.add_argument("--folder", help="Filter to a specific storage subfolder")
35
35
  p.add_argument("--limit", type=int, help="Max documents to validate")
36
+ p.add_argument("--with-evidence", nargs="?", type=int, const=0, default=None, metavar="N",
37
+ help="Also run `compass evidence` and embed live proof in each document's modal "
38
+ "(extra KB queries). Optional N caps the documents probed (default: config).")
36
39
  p.add_argument("--dry-run", action="store_true", help="List documents without validating")
37
40
  p.add_argument("--output-format", choices=["html", "json", "junit", "all"], default="html")
38
41
  p.add_argument("--include-graphdb", action="store_true", help="Also validate graph metadata")
@@ -550,8 +553,29 @@ def main(argv=None) -> int:
550
553
  passed = sum(1 for r in results if r["result"].all_passed)
551
554
  failed = len(results) - passed
552
555
  base = f"reports/{args.project}-discovery-report"
556
+
557
+ # Optional: run `compass evidence` and embed live proof per document (query surface; extra KB
558
+ # queries). Keyed by document name so the doc modal renders the proof. Never fails the run.
559
+ evidence_by_doc = None
560
+ if args.with_evidence is not None:
561
+ try:
562
+ from ingestion_validation import evidence as _ev
563
+ from dataclasses import asdict as _asdict
564
+ ev_limit = args.with_evidence or settings.evidence.sample_size
565
+ print(f"\n Gathering evidence (up to {ev_limit} document(s))...")
566
+ payload = _ev.gather_evidence(settings, list(_ev.ALL_CHECKS), limit=ev_limit)
567
+ if payload.get("error"):
568
+ print(f" evidence skipped: {payload['error']}")
569
+ else:
570
+ evidence_by_doc = {}
571
+ for r in payload.get("results", []):
572
+ evidence_by_doc.setdefault(r.document, []).append(_asdict(r))
573
+ print(f" embedded proof for {len(evidence_by_doc)} document(s).")
574
+ except Exception as exc: # noqa: BLE001 - evidence must never break the validation run
575
+ print(f" evidence skipped: {exc}")
576
+
553
577
  if args.output_format in ("html", "all"):
554
- print(f"\n HTML report: {generate_batch_dashboard(results, base + '.html', project=args.project, environment=args.env, run_id=run_id, trends=trend_rows, corpus=corpus, mode='Discovery', profile=profile_data, wall_seconds=wall_seconds, concurrency=concurrency)}")
578
+ print(f"\n HTML report: {generate_batch_dashboard(results, base + '.html', project=args.project, environment=args.env, run_id=run_id, trends=trend_rows, corpus=corpus, mode='Discovery', profile=profile_data, wall_seconds=wall_seconds, concurrency=concurrency, evidence=evidence_by_doc)}")
555
579
  if args.output_format in ("json", "all"):
556
580
  print(f" JSON report: {export_json(results, base + '.json')}")
557
581
  if args.output_format in ("junit", "all"):
@@ -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.8.0"
8
+ __version__ = "2.9.0"
@@ -1266,8 +1266,9 @@ def cmd_evidence(argv: list[str]) -> int:
1266
1266
  p.add_argument("--document", "-d", default="", help="Only probe documents whose name contains this.")
1267
1267
  p.add_argument("--limit", "-l", type=int, default=0, help="Max documents to probe (default: config sample_size).")
1268
1268
  p.add_argument("--quick", action="store_true", help="Probe a single document.")
1269
- p.add_argument("--format", "-f", choices=["markdown", "json"], default="markdown")
1270
- p.add_argument("--output", "-o", help="Write to a file (default: stdout).")
1269
+ p.add_argument("--format", "-f", choices=["markdown", "json", "html"], default="markdown")
1270
+ p.add_argument("--output", "-o", help="Write to a file (default: stdout for md/json; reports/ for html).")
1271
+ p.add_argument("--open", action="store_true", help="Open the HTML report in a browser.")
1271
1272
  args = p.parse_args(argv)
1272
1273
 
1273
1274
  try:
@@ -1314,14 +1315,23 @@ def cmd_evidence(argv: list[str]) -> int:
1314
1315
  print(f" {payload['error']}")
1315
1316
  return 1
1316
1317
 
1317
- body = ev.format_json(payload, args.project or "") if args.format == "json" \
1318
- else ev.format_markdown(payload, args.project or "")
1319
- if args.output:
1320
- Path(args.output).parent.mkdir(parents=True, exist_ok=True)
1321
- Path(args.output).write_text(body + "\n", encoding="utf-8")
1322
- print(f" Wrote {args.output}")
1318
+ if args.format == "html":
1319
+ from ingestion_validation.report.audit import generate_evidence_report
1320
+ out = generate_evidence_report(payload, args.output or f"reports/evidence-{args.project or 'kb'}.html",
1321
+ project=args.project or "")
1322
+ print(f" HTML report: {out}")
1323
+ if args.open:
1324
+ import webbrowser
1325
+ webbrowser.open(Path(out).resolve().as_uri())
1323
1326
  else:
1324
- print(body)
1327
+ body = ev.format_json(payload, args.project or "") if args.format == "json" \
1328
+ else ev.format_markdown(payload, args.project or "")
1329
+ if args.output:
1330
+ Path(args.output).parent.mkdir(parents=True, exist_ok=True)
1331
+ Path(args.output).write_text(body + "\n", encoding="utf-8")
1332
+ print(f" Wrote {args.output}")
1333
+ else:
1334
+ print(body)
1325
1335
  counts: dict[str, int] = {}
1326
1336
  for r in payload["results"]:
1327
1337
  counts[r.verdict] = counts.get(r.verdict, 0) + 1
@@ -1469,8 +1479,9 @@ def cmd_grounding_audit(argv: list[str]) -> int:
1469
1479
  g.add_argument("--eval-results", metavar="FILE",
1470
1480
  help="Batch: JSONL (one object per line) or a JSON array - both accepted. "
1471
1481
  "`compass ask --format eval` emits a compatible file.")
1472
- p.add_argument("--format", choices=["markdown", "json"], default="markdown")
1473
- p.add_argument("--output", help="Write the report to a file (default: stdout).")
1482
+ p.add_argument("--format", choices=["markdown", "json", "html"], default="markdown")
1483
+ p.add_argument("--output", help="Write the report to a file (default: stdout for md/json; reports/ for html).")
1484
+ p.add_argument("--open", action="store_true", help="Open the HTML report in a browser.")
1474
1485
  p.add_argument("--fail-on-fabrication", action="store_true",
1475
1486
  help="Exit non-zero if any verifiable claim disagrees with the KB.")
1476
1487
  args = p.parse_args(argv)
@@ -1547,18 +1558,26 @@ def cmd_grounding_audit(argv: list[str]) -> int:
1547
1558
  doc_exists = _graph_doc_exists(settings) if getattr(settings.eval, "citation_graph_ask", "") else None
1548
1559
  results = [run_audit(rec, adapter, settings.eval, doc_exists=doc_exists) for rec in records]
1549
1560
 
1550
- if args.format == "json":
1551
- from dataclasses import asdict
1552
- body = _json.dumps([asdict(r) for r in results], indent=2, default=str)
1553
- else:
1554
- body = ("\n\n".join(render_report(r, project=args.project or "") for r in results))
1555
-
1556
- if args.output:
1557
- Path(args.output).parent.mkdir(parents=True, exist_ok=True)
1558
- Path(args.output).write_text(body + "\n", encoding="utf-8")
1559
- print(f" Wrote {args.output}")
1561
+ if args.format == "html":
1562
+ from ingestion_validation.report.audit import generate_grounding_report
1563
+ out = generate_grounding_report(results, args.output or f"reports/grounding-{args.project or 'kb'}.html",
1564
+ project=args.project or "")
1565
+ print(f" HTML report: {out}")
1566
+ if args.open:
1567
+ import webbrowser
1568
+ webbrowser.open(Path(out).resolve().as_uri())
1560
1569
  else:
1561
- print(body)
1570
+ if args.format == "json":
1571
+ from dataclasses import asdict
1572
+ body = _json.dumps([asdict(r) for r in results], indent=2, default=str)
1573
+ else:
1574
+ body = ("\n\n".join(render_report(r, project=args.project or "") for r in results))
1575
+ if args.output:
1576
+ Path(args.output).parent.mkdir(parents=True, exist_ok=True)
1577
+ Path(args.output).write_text(body + "\n", encoding="utf-8")
1578
+ print(f" Wrote {args.output}")
1579
+ else:
1580
+ print(body)
1562
1581
 
1563
1582
  if not results:
1564
1583
  if args.format != "json":
@@ -112,20 +112,9 @@ li .check-hint{margin:4px 0 0 0;display:block;}
112
112
  .metric-chip b{color:var(--ink);font-variant-numeric:tabular-nums;}
113
113
  .exec-interp-title{font-size:12px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);font-weight:700;margin-bottom:6px;}
114
114
  .exec-narrative{font-size:13.5px;line-height:1.6;color:var(--ink);margin:0 0 10px;padding:9px 12px;background:var(--chip-bg);border-radius:8px;border-left:3px solid var(--accent);}
115
- .subsys-rollup{padding:14px 16px 18px;display:flex;flex-direction:column;gap:8px;border-bottom:1px solid var(--line);}
116
- .subsys-head{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:700;margin-bottom:4px;}
117
115
  .rec-list{padding:14px 16px 16px;display:flex;flex-direction:column;gap:10px;}
118
- .rec-card{border:1px solid var(--line);border-left:3px solid var(--muted);border-radius:8px;padding:10px 14px;background:var(--chip-bg);}
119
- .rec-card.bad{border-left-color:var(--bad);}.rec-card.warn{border-left-color:var(--warn);}
120
- .rec-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:13px;margin-bottom:4px;}
121
- .rec-impact{margin-left:auto;font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums;}
122
116
  .rec-fix{font-size:12.5px;line-height:1.55;color:var(--ink);}
123
117
  .rec-more{font-size:12px;color:var(--muted);padding:4px 2px;}
124
- .subsys-row{display:grid;grid-template-columns:160px 1fr auto;align-items:center;gap:12px;font-size:12px;}
125
- .subsys-name{font-weight:700;color:var(--ink);}
126
- .subsys-bar{height:10px;background:var(--line);border-radius:5px;overflow:hidden;}
127
- .subsys-bar i{display:block;height:100%;border-radius:5px;}
128
- .subsys-meta{color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums;}
129
118
  @media(max-width:600px){.subsys-row{grid-template-columns:1fr;gap:3px;}}
130
119
  .exec-bullets{margin:0 0 4px;padding-left:18px;font-size:13.5px;line-height:1.75;color:var(--ink);}
131
120
  .exec-bullets li{margin:0;}
@@ -1773,6 +1762,19 @@ function renderDoc(idx){
1773
1762
  html+='<div class="section-title" style="margin-top:24px">Checks by Category</div><div class="table-card"><div class="table-wrap"><table>'+
1774
1763
  '<thead><tr><th>Category</th><th class="num">Total</th><th class="num">Passed</th><th class="num">Failed</th><th class="num">Pass Rate</th><th>Coverage</th></tr></thead><tbody>'+catRows+'</tbody></table></div></div>';
1775
1764
  }
1765
+ // Evidence (present only with `discover --with-evidence`): concrete live proof for THIS doc.
1766
+ var evAll=(typeof REPORT!=='undefined'&&REPORT.evidence)?REPORT.evidence:null;
1767
+ var ev=evAll?(evAll[d.file]||evAll[d.documentId]||null):null;
1768
+ if(ev&&ev.length){
1769
+ var _vc={CONFIRMED:['bad','Confirmed'],WEAK:['warn','Weak'],NOT_REPRODUCED:['ok','Not reproduced'],ERROR:['warn','Error']};
1770
+ var _pre='white-space:pre-wrap;font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.5;color:var(--muted);margin:0;max-height:260px;overflow:auto;';
1771
+ var cards=ev.map(function(r){
1772
+ var vc=_vc[r.verdict]||['warn',r.verdict];
1773
+ var raw=(r.details&&Object.keys(r.details).length)?'<details class="cov-row"><summary class="cov-sum"><span class="cov-name">raw evidence</span></summary><div class="cov-body"><pre style="'+_pre+'">'+_escHtml(JSON.stringify(r.details,null,2))+'</pre></div></details>':'';
1774
+ return '<div class="rca-pattern" style="border-left-color:var(--'+vc[0]+')"><div class="rca-pattern-header"><span class="tag '+vc[0]+'">'+vc[1]+'</span><span class="rca-pattern-title">'+_escHtml((r.check||'').replace(/_/g,' '))+'</span></div><div class="rca-cause">'+_escHtml(r.summary||'')+'</div>'+raw+'</div>';
1775
+ }).join('');
1776
+ html+='<div class="section-title" style="margin-top:24px">Evidence <span class="cat-badge">live proof from the KB</span></div><div class="table-card"><div class="rec-list">'+cards+'</div></div>';
1777
+ }
1776
1778
  box.innerHTML=html;
1777
1779
  }
1778
1780
  // ---- Charts ----
@@ -1832,6 +1834,7 @@ def generate_batch_dashboard(
1832
1834
  embed_detail_cap: int = 200,
1833
1835
  wall_seconds: float | None = None,
1834
1836
  concurrency: int | None = None,
1837
+ evidence: dict | None = None,
1835
1838
  ) -> str:
1836
1839
  """Generate a self-contained, interactive HTML dashboard. Returns abs path.
1837
1840
 
@@ -1915,7 +1918,11 @@ def generate_batch_dashboard(
1915
1918
  if trimmed else ""
1916
1919
  )
1917
1920
 
1918
- report_json = json.dumps({"agg": agg, "cases": cases}, default=str).replace("</", "<\\/")
1921
+ # Optional live-proof payload (from `discover --with-evidence`), keyed by document name, so
1922
+ # the per-document modal can show the concrete evidence for THIS doc. Purely additive: the
1923
+ # report builder stays a pure function of data - evidence is computed upstream (query surface).
1924
+ report_json = json.dumps({"agg": agg, "cases": cases, "evidence": evidence or {}},
1925
+ default=str).replace("</", "<\\/")
1919
1926
  trends_html = _trends_section(trends or [])
1920
1927
  trends_pill = '<button class="nav-pill" data-section="section-trends" onclick="navScrollTo(\'section-trends\')">Trends</button>' if trends else ""
1921
1928
  action_plan_html = _action_plan_section(agg)
@@ -0,0 +1,186 @@
1
+ """Self-contained HTML reports for ``compass evidence`` and ``compass grounding-audit``.
2
+
3
+ Same chrome as the validation dashboard and the coverage/migration analysis reports (via
4
+ ``report.shell.render_page``): header, nav pills, dark/light, theme variables, the shared
5
+ card/tile/badge classes. Read-only PRESENTATION of already-computed, deterministic results -
6
+ no LLM and no pass/fail decision happens here. Values are HTML-escaped through ``esc``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from ingestion_validation.report.shell import esc, render_page
16
+
17
+ _PRE = ("white-space:pre-wrap;font-family:ui-monospace,Menlo,monospace;font-size:11.5px;"
18
+ "line-height:1.5;color:var(--muted);margin:0;max-height:320px;overflow:auto;")
19
+
20
+
21
+ def _abspath(path: str) -> str:
22
+ p = Path(path)
23
+ p.parent.mkdir(parents=True, exist_ok=True)
24
+ return str(p.resolve())
25
+
26
+
27
+ def _tiles(items: list[tuple[str, object, str]]) -> str:
28
+ cells = "".join(
29
+ f'<div class="tile"><div class="label">{esc(label)}</div>'
30
+ f'<div class="value {cls}">{esc(val)}</div></div>'
31
+ for label, val, cls in items)
32
+ return f'<div class="kpi-grid">{cells}</div>'
33
+
34
+
35
+ def _error_page(title: str, heading: str, project: str, run_id: str, message: str, path: str) -> str:
36
+ section = (f'<section class="section" id="section-summary"><div class="section-title">Summary</div>'
37
+ f'<div class="card"><div class="card-inner"><b>{esc(message)}</b></div></div></section>')
38
+ html = render_page(title=title, heading=heading, subtitle=project,
39
+ meta={"Project": project or "-", "Run": run_id or "-"},
40
+ nav=[("section-summary", "Summary")], sections=section)
41
+ Path(path).write_text(html, encoding="utf-8")
42
+ return path
43
+
44
+
45
+ # --------------------------------------------------------------------------- #
46
+ # compass evidence
47
+ # --------------------------------------------------------------------------- #
48
+ _VERDICT = {"CONFIRMED": ("bad", "Confirmed"), "WEAK": ("warn", "Weak"),
49
+ "NOT_REPRODUCED": ("ok", "Not reproduced"), "ERROR": ("warn", "Error")}
50
+
51
+
52
+ def generate_evidence_report(payload: dict, output_path: str = "reports/evidence-report.html",
53
+ project: str = "", run_id: str = "") -> str:
54
+ """Render `compass evidence` results: per-check proof cards with the raw evidence."""
55
+ path = _abspath(output_path)
56
+ if payload.get("error"):
57
+ return _error_page("COMPASS Evidence", "Evidence", project, run_id,
58
+ f"Evidence: {payload['error']}", path)
59
+ results = payload.get("results", [])
60
+ counts: dict[str, int] = {}
61
+ for r in results:
62
+ counts[r.verdict] = counts.get(r.verdict, 0) + 1
63
+ tiles = _tiles([
64
+ ("Documents probed", payload.get("docs_probed", 0), ""),
65
+ ("Confirmed", counts.get("CONFIRMED", 0), "bad"),
66
+ ("Weak", counts.get("WEAK", 0), "warn"),
67
+ ("Not reproduced", counts.get("NOT_REPRODUCED", 0), "ok"),
68
+ ("Errors", counts.get("ERROR", 0), "warn"),
69
+ ])
70
+ summary = (f'<section class="section" id="section-summary"><div class="section-title">Summary</div>'
71
+ f'{tiles}<div class="muted" style="margin-top:12px">Deterministic proof built from the '
72
+ "KB's own chunks - no LLM, no live embedding. A CONFIRMED verdict reproduces exactly "
73
+ 'what validation flagged.</div></section>')
74
+
75
+ by_check: dict[str, list] = {}
76
+ for r in results:
77
+ by_check.setdefault(r.check, []).append(r)
78
+ nav = [("section-summary", "Summary")]
79
+ sections = summary
80
+ for check, items in by_check.items():
81
+ confirmed = sum(1 for r in items if r.verdict == "CONFIRMED")
82
+ sid = "section-" + "".join(ch for ch in check if ch.isalnum())
83
+ label = check.replace("_", " ").title()
84
+ nav.append((sid, label))
85
+ cards = ""
86
+ for r in items:
87
+ cls, txt = _VERDICT.get(r.verdict, ("warn", r.verdict))
88
+ details = ""
89
+ if r.details:
90
+ details = ('<details class="cov-row"><summary class="cov-sum">'
91
+ '<span class="cov-name">raw evidence</span></summary>'
92
+ f'<div class="cov-body"><pre style="{_PRE}">'
93
+ f'{esc(json.dumps(r.details, indent=2, default=str))}</pre></div></details>')
94
+ cards += (f'<div class="rca-pattern" style="border-left-color:var(--{cls})">'
95
+ '<div class="rca-pattern-header">'
96
+ f'<span class="tag {cls}">{esc(txt)}</span>'
97
+ f'<span class="rca-pattern-title">{esc(r.document)}</span></div>'
98
+ f'<div class="rca-cause">{esc(r.summary)}</div>{details}</div>')
99
+ sections += (f'<section class="section" id="{sid}"><div class="section-title">{esc(label)} '
100
+ f'<span class="cat-badge">{confirmed}/{len(items)} confirmed</span></div>'
101
+ f'<div class="table-card"><div class="rec-list">{cards}</div></div></section>')
102
+
103
+ html = render_page(
104
+ title=f"COMPASS Evidence - {project or 'KB'}",
105
+ heading=f"Evidence - {project or 'KB'}", subtitle=project,
106
+ meta={"Project": project or "-", "Run": run_id or "-",
107
+ "Documents probed": payload.get("docs_probed", 0)},
108
+ nav=nav, sections=sections)
109
+ Path(path).write_text(html, encoding="utf-8")
110
+ return path
111
+
112
+
113
+ # --------------------------------------------------------------------------- #
114
+ # compass grounding-audit
115
+ # --------------------------------------------------------------------------- #
116
+ _ATTR = {
117
+ "none": ("ok", "Grounded"),
118
+ "generation": ("bad", "Generation hallucination"),
119
+ "fabricated_source": ("bad", "Fabricated source"),
120
+ "retrieval": ("warn", "Retrieval gap"),
121
+ "citation_error": ("warn", "Citation error"),
122
+ "unverifiable": ("", "Unverifiable"),
123
+ "unknown": ("", "Unattributed"),
124
+ }
125
+ _SEV = {"CRITICAL": "bad", "HIGH": "bad", "MEDIUM": "warn", "LOW": "warn", "INFO": "ok"}
126
+
127
+
128
+ def generate_grounding_report(results: list[Any], output_path: str = "reports/grounding-report.html",
129
+ project: str = "", run_id: str = "") -> str:
130
+ """Render `compass grounding-audit` results: per-case verdict + per-claim citation table."""
131
+ path = _abspath(output_path)
132
+ if not results:
133
+ return _error_page("COMPASS Grounding Audit", "Grounding Audit", project, run_id,
134
+ "No eval records to report.", path)
135
+ sound = sum(r.citations.get("summary", {}).get("sound", 0) for r in results)
136
+ checkable = sum(r.citations.get("summary", {}).get("checkable", 0) for r in results)
137
+ acc = round(100.0 * sound / checkable, 1) if checkable else None
138
+ fabr = sum(1 for r in results if r.has_fabrication)
139
+ tiles = _tiles([
140
+ ("Cases", len(results), ""),
141
+ ("Citations grounded", f"{sound}/{checkable}", ""),
142
+ ("Accuracy", f"{acc}%" if acc is not None else "-", "ok" if (acc or 0) >= 80 else "warn"),
143
+ ("Cases with fabrication", fabr, "bad" if fabr else "ok"),
144
+ ])
145
+ summary = (f'<section class="section" id="section-summary"><div class="section-title">Summary</div>'
146
+ f'{tiles}<div class="muted" style="margin-top:12px">Deterministic adjudication of the '
147
+ "eval's claims against the KB - no LLM in any verdict; it does not judge answer "
148
+ 'quality.</div></section>')
149
+ nav = [("section-summary", "Summary")]
150
+ sections = summary
151
+ for i, r in enumerate(results):
152
+ sid = f"section-case{i}"
153
+ sev = r.severity()
154
+ nav.append((sid, (r.test_id or f"case {i + 1}")[:18]))
155
+ verdicts = r.citation_verdicts
156
+ if verdicts:
157
+ rows = "".join(
158
+ f'<tr><td>{esc((v.claim[:90] + "...") if len(v.claim) > 90 else v.claim)}</td>'
159
+ f'<td>{esc(v.doc)}</td><td>{esc(v.page or "-")}</td>'
160
+ f'<td><span class="tag {_ATTR.get(v.attribution, ("", v.attribution))[0]}">'
161
+ f'{esc(_ATTR.get(v.attribution, ("", v.attribution))[1])}</span></td></tr>'
162
+ for v in verdicts)
163
+ table = ('<div class="table-wrap"><table><thead><tr><th>Cited claim</th><th>Source</th>'
164
+ f'<th>Page</th><th>Attribution</th></tr></thead><tbody>{rows}</tbody></table></div>')
165
+ else:
166
+ table = '<div class="muted">No citations in this answer.</div>'
167
+ signals = "".join(
168
+ f'<li><span class="tag {_SEV.get(s.get("severity", "INFO"), "warn")}">{esc(s.get("severity", ""))}'
169
+ f'</span> {esc(s.get("label", ""))} <span class="check-detail">{esc(s.get("detail", ""))}</span></li>'
170
+ for s in (r.signals or []))
171
+ signals_html = f'<ul>{signals}</ul>' if signals else ""
172
+ sections += (
173
+ f'<section class="section" id="{sid}"><div class="section-title">'
174
+ f'{esc(r.test_id or f"Case {i + 1}")} <span class="tag {_SEV.get(sev, "warn")}">{esc(sev)}</span></div>'
175
+ '<div class="card"><div class="card-inner">'
176
+ f'<div class="muted" style="margin-bottom:8px">Q: {esc(r.query)}</div>'
177
+ f'<div class="rca-cause"><b>Verdict:</b> {esc(r.layer_diagnosis())}</div>'
178
+ f'{signals_html}{table}</div></div></section>')
179
+
180
+ html = render_page(
181
+ title=f"COMPASS Grounding Audit - {project or 'KB'}",
182
+ heading=f"Grounding Audit - {project or 'KB'}", subtitle=project,
183
+ meta={"Project": project or "-", "Run": run_id or "-", "Cases": len(results)},
184
+ nav=nav, sections=sections)
185
+ Path(path).write_text(html, encoding="utf-8")
186
+ return path
@@ -425,6 +425,17 @@ def test_nonstring_answer_field_is_not_stringified():
425
425
  assert "{" not in r.query
426
426
 
427
427
 
428
+ def test_grounding_html_report_renders(tmp_path):
429
+ from ingestion_validation.report.audit import generate_grounding_report
430
+ rec = {"input": "Earnings?", "output": "Earnings rose 40% (MIS, May 2026, p. 1).",
431
+ "context.retrieved": "c", "scores": {}}
432
+ results = [run_audit(rec, _kb(), _cfg(gt_response_path="response"))]
433
+ out = generate_grounding_report(results, str(tmp_path / "gr.html"), project="wp")
434
+ html = open(out, encoding="utf-8").read()
435
+ assert "Grounding Audit" in html
436
+ assert "Generation hallucination" in html and "HIGH" in html # per-claim attribution + severity
437
+
438
+
428
439
  def test_report_renders_citation_section():
429
440
  rec = {"input": "Earnings?", "response": "Earnings rose 40% (MIS, May 2026, p. 1).",
430
441
  "context.retrieved": "c", "scores": {}}
@@ -165,3 +165,14 @@ def test_probe_reproduces_validator_via_shared_text_probes():
165
165
  from ingestion_validation import text_probes
166
166
  from ingestion_validation.validators import retrieval_quality
167
167
  assert retrieval_quality._shared_paraphrases is text_probes.paraphrases
168
+
169
+
170
+ def test_evidence_html_report_renders_and_escapes(tmp_path):
171
+ from ingestion_validation.report.audit import generate_evidence_report
172
+ payload = {"docs_probed": 1, "results": [
173
+ ev.EvidenceResult("duplicate_chunks", "<img onerror=x>.pdf", "CONFIRMED",
174
+ "1 exact-duplicate pair", {"pairs": []})]}
175
+ out = generate_evidence_report(payload, str(tmp_path / "ev.html"), project="wp")
176
+ html = open(out, encoding="utf-8").read()
177
+ assert "COMPASS Evidence" in html and "Confirmed" in html
178
+ assert "<img onerror=x>" not in html and "&lt;img" in html # untrusted doc name escaped
@@ -709,3 +709,41 @@ def test_chunk_previews_kept_per_document_not_blanket_at_scale():
709
709
  by = {c["name"]: c for c in payload["cases"]}
710
710
  assert by["small"]["chunks"]["items"] # 2-chunk doc keeps its previews
711
711
  assert by["big0"]["chunks"]["items"] == [] # 20-chunk doc is trimmed
712
+
713
+
714
+ def _dash_item(name, status):
715
+ r = PipelineResult(document_id=name, file_name=name)
716
+ r.add_step(StepResult("Redis Chunks", status, "ok" if status == StepStatus.PASSED else "x",
717
+ checks=[{"label": "Chunk boundaries clean", "passed": status == StepStatus.PASSED,
718
+ "severity": "warn"}]))
719
+ r.finalize()
720
+ return {"name": name, "result": r}
721
+
722
+
723
+ def _embedded_report(html_path):
724
+ import json as _json
725
+ import pathlib
726
+ import re as _re
727
+ doc = pathlib.Path(html_path).read_text(encoding="utf-8")
728
+ m = _re.search(r"const REPORT = (\{.*?\});", doc, _re.S)
729
+ assert m is not None
730
+ pathlib.Path(html_path).unlink()
731
+ return _json.loads(m.group(1)), doc
732
+
733
+
734
+ def test_evidence_embeds_in_report_keyed_by_document():
735
+ # `discover --with-evidence` embeds live proof keyed by doc; the modal renders it.
736
+ items = [_dash_item("Guide.pdf", StepStatus.FAILED)]
737
+ evidence = {"Guide.pdf": [{"check": "duplicate_chunks", "document": "Guide.pdf",
738
+ "verdict": "CONFIRMED", "summary": "1 dup pair", "details": {"n": 1}}]}
739
+ html = report_mod.generate_batch_dashboard(items, "reports/_t_ev.html", project="p", evidence=evidence)
740
+ data, doc = _embedded_report(html)
741
+ assert data["evidence"]["Guide.pdf"][0]["verdict"] == "CONFIRMED"
742
+ assert "live proof from the KB" in doc # the modal evidence section renders
743
+
744
+
745
+ def test_report_without_evidence_stays_backward_compatible():
746
+ items = [_dash_item("Guide.pdf", StepStatus.PASSED)]
747
+ html = report_mod.generate_batch_dashboard(items, "reports/_t_noev.html", project="p")
748
+ data, _ = _embedded_report(html)
749
+ assert data["evidence"] == {}