compass-kb-validation 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/PKG-INFO +1 -1
  2. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/compass_kb_validation.egg-info/PKG-INFO +1 -1
  3. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/discover_and_validate.py +44 -0
  4. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/__init__.py +1 -1
  5. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/profiler.py +73 -1
  6. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/report/__init__.py +7 -2
  7. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_discover_cli.py +46 -0
  8. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_reporting.py +15 -0
  9. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/LICENSE +0 -0
  10. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/README.md +0 -0
  11. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/compass_kb_validation.egg-info/SOURCES.txt +0 -0
  12. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
  13. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
  14. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/compass_kb_validation.egg-info/requires.txt +0 -0
  15. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
  16. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/__main__.py +0 -0
  17. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/ask.py +0 -0
  18. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/baseline.py +0 -0
  19. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/chunk_coherence.py +0 -0
  20. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/cli.py +0 -0
  21. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/config/__init__.py +0 -0
  22. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/config_lint.py +0 -0
  23. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/content_inspect.py +0 -0
  24. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/corpus.py +0 -0
  25. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/corpus_health.py +0 -0
  26. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/diagnostics.py +0 -0
  27. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/embedding_drift.py +0 -0
  28. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/eval_interop.py +0 -0
  29. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/init_wizard.py +0 -0
  30. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/live_dashboard.py +0 -0
  31. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/mcp_server.py +0 -0
  32. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/models/__init__.py +0 -0
  33. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/notifications.py +0 -0
  34. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/pipeline/__init__.py +0 -0
  35. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/provenance.py +0 -0
  36. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/providers/__init__.py +0 -0
  37. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/providers/embedding.py +0 -0
  38. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/providers/graph.py +0 -0
  39. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/providers/source.py +0 -0
  40. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/providers/storage.py +0 -0
  41. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/providers/vectorstore.py +0 -0
  42. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/remediation.py +0 -0
  43. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/report/_report_css.py +0 -0
  44. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/report/_report_js.py +0 -0
  45. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/report/analysis.py +0 -0
  46. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/report/export.py +0 -0
  47. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/report/shell.py +0 -0
  48. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/retrieval_analysis.py +0 -0
  49. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/trend_tracker.py +0 -0
  50. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/utils/__init__.py +0 -0
  51. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/__init__.py +0 -0
  52. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/api_upload.py +0 -0
  53. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/base.py +0 -0
  54. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/base_sparql.py +0 -0
  55. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/cross_stage.py +0 -0
  56. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/graphdb_metadata.py +0 -0
  57. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/neptune_metadata.py +0 -0
  58. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/redis_chunks.py +0 -0
  59. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/registry.py +0 -0
  60. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/retrieval_quality.py +0 -0
  61. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/ingestion_validation/validators/s3_storage.py +0 -0
  62. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/profile_kb.py +0 -0
  63. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/pyproject.toml +0 -0
  64. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/run_validation.py +0 -0
  65. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/setup.cfg +0 -0
  66. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_alt_adapters.py +0 -0
  67. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_analysis_report.py +0 -0
  68. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_api_upload.py +0 -0
  69. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_ask.py +0 -0
  70. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_baseline.py +0 -0
  71. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_benchmark.py +0 -0
  72. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_check_catalog.py +0 -0
  73. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_cli.py +0 -0
  74. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_config.py +0 -0
  75. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_config_lint.py +0 -0
  76. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_content_inspect.py +0 -0
  77. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_corpus.py +0 -0
  78. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_corpus_health.py +0 -0
  79. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_coverage.py +0 -0
  80. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_cross_stage.py +0 -0
  81. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_diagnostics.py +0 -0
  82. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_discovery_parallel.py +0 -0
  83. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_embedding_drift.py +0 -0
  84. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_eval_interop.py +0 -0
  85. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_gate.py +0 -0
  86. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_graph_transport.py +0 -0
  87. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_graph_validators.py +0 -0
  88. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_init_wizard.py +0 -0
  89. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_manifest.py +0 -0
  90. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_mcp.py +0 -0
  91. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_migration.py +0 -0
  92. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_models.py +0 -0
  93. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_neptune_bugfixes.py +0 -0
  94. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_neptune_bugfixes2.py +0 -0
  95. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_notifications.py +0 -0
  96. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_pipeline.py +0 -0
  97. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_profiler.py +0 -0
  98. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_providers.py +0 -0
  99. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_redis_chunks.py +0 -0
  100. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_registry.py +0 -0
  101. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_remediation.py +0 -0
  102. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_retrieval_assertions.py +0 -0
  103. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_retrieval_quality.py +0 -0
  104. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_run_cli.py +0 -0
  105. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_sources.py +0 -0
  106. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_storage_validator.py +0 -0
  107. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_trends.py +0 -0
  108. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_universal_concerns.py +0 -0
  109. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.0}/tests/test_utils.py +0 -0
  110. {compass_kb_validation-2.2.0 → compass_kb_validation-2.3.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.2.0
3
+ Version: 2.3.0
4
4
  Summary: Knowledge-base readiness validation for RAG & AI agents — config-driven, deterministic pipeline integrity checks.
5
5
  Author: COMPASS
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compass-kb-validation
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Knowledge-base readiness validation for RAG & AI agents — config-driven, deterministic pipeline integrity checks.
5
5
  Author: COMPASS
6
6
  License: MIT
@@ -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.")
@@ -240,6 +246,32 @@ _AUTH_HINTS = ("403", "forbidden", "expired", "credential", "token", "signatured
240
246
  "invalidclienttokenid", "accessdenied", "unauthor")
241
247
 
242
248
 
249
+ def _print_tips(args, hygiene: dict | None, saved_baseline: bool) -> None:
250
+ """End-of-run hints about the most relevant unused flags.
251
+
252
+ Discoverability, not noise: at most three, only when the flag was NOT already used and
253
+ the run's own outcome makes it relevant. Suppress with --no-tips. This is why a partner
254
+ can run for weeks without finding --save-baseline / --unreachable-as / --suggest-config.
255
+ """
256
+ tips: list[str] = []
257
+ if not saved_baseline:
258
+ tips.append("Save this run as a baseline to catch regressions next time: "
259
+ "--save-baseline baselines/{}.json".format(args.project or "kb"))
260
+ if hygiene and hygiene.get("score", 100) < 90 and args.quality_threshold is None:
261
+ tips.append("Gate CI on data-plane hygiene (not just integrity): "
262
+ "--quality-threshold {}".format(int(hygiene["score"])))
263
+ if not getattr(args, "include_retrieval", False):
264
+ tips.append("Add retrieval-quality checks (can a stored vector be found via KNN?): "
265
+ "--include-retrieval")
266
+ if not args.suggest_config:
267
+ tips.append("Refine your config from what this run observed: --suggest-config")
268
+ if not tips:
269
+ return
270
+ print("\n Tips (--no-tips to silence):")
271
+ for t in tips[:3]:
272
+ print(f" * {t}")
273
+
274
+
243
275
  def _resolve_expected_documents(raw, project: str):
244
276
  """Resolve the configured expected_documents, supporting ``auto``.
245
277
 
@@ -502,6 +534,16 @@ def main(argv=None) -> int:
502
534
  if args.output_format in ("junit", "all"):
503
535
  print(f" JUnit report: {export_junit(results, base + '.xml')}")
504
536
 
537
+ if args.suggest_config:
538
+ from ingestion_validation.profiler import suggest_config_from_run
539
+ snippet = suggest_config_from_run(results, documents)
540
+ if args.suggest_config == "-":
541
+ print("\n" + snippet)
542
+ else:
543
+ from pathlib import Path as _P
544
+ _P(args.suggest_config).write_text(snippet, encoding="utf-8")
545
+ print(f" Config suggestions written: {args.suggest_config} (review before merging)")
546
+
505
547
  # KB Hygiene: data-plane cleanliness (advisory checks), separate from the integrity
506
548
  # gate. Printed for every run; gates the build only when --quality-threshold is set.
507
549
  # Best-effort context - a failure to score it must not crash the gate decision.
@@ -539,6 +581,8 @@ def main(argv=None) -> int:
539
581
  reason = f"{len(documents)} documents < s3.min_documents ({min_docs})"
540
582
  failed_gate = True
541
583
  code = EXIT_GATE_FAILED if (failed_gate or (regression and args.fail_on_regression)) else EXIT_OK
584
+ if not args.no_tips:
585
+ _print_tips(args, hygiene, saved_baseline=args.save_baseline is not None or bool(args.against))
542
586
  print("=" * 60)
543
587
  print(f" Results: {passed} passed, {failed} failed ({len(results)} total)")
544
588
  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.3.0"
@@ -537,7 +537,79 @@ def apply_profile_to_settings(settings: Any, profiles: dict) -> list[str]:
537
537
  return applied
538
538
 
539
539
 
540
+ def suggest_config_from_run(results: list, documents: list) -> str:
541
+ """Suggest config refinements from what a validation RUN actually observed.
542
+
543
+ Complements ``emit_project_config`` (pre-run backend introspection) with the things
544
+ only a run reveals: the real document count, the observed chunk-size distribution, and
545
+ the checks that were NEUTRAL on every document - checks that never apply to this KB and
546
+ are pure noise (e.g. section_metadata when nothing carries sections). Returns a review-
547
+ and-merge snippet, never a file to apply blind - a wrong config key would be exactly the
548
+ silent-failure trap COMPASS exists to catch, so these are suggestions with the evidence.
549
+ """
550
+ import statistics
551
+
552
+ n_docs = len(documents)
553
+ avg_chars: list[int] = []
554
+ # label -> [evaluated_count, neutral_count]; a check neutral everywhere never applies.
555
+ support: dict[str, list[int]] = {}
556
+ for item in results:
557
+ for st in getattr(item.get("result"), "steps", []) or []:
558
+ details = getattr(st, "details", {}) or {}
559
+ cv = details.get("chunk_view")
560
+ if isinstance(cv, dict) and cv.get("avg_chars"):
561
+ avg_chars.append(int(cv["avg_chars"]))
562
+ status = getattr(getattr(st, "status", None), "value", "")
563
+ if status == "SKIPPED":
564
+ continue # a step that did not run says nothing about applicability
565
+ for ch in getattr(st, "checks", []) or []:
566
+ lbl = ch.get("label", "")
567
+ if not lbl:
568
+ continue
569
+ rec = support.setdefault(lbl, [0, 0])
570
+ if ch.get("supported", True) is False:
571
+ rec[1] += 1
572
+ else:
573
+ rec[0] += 1
574
+
575
+ never_applies = sorted(lbl for lbl, (ev, neu) in support.items() if ev == 0 and neu > 0)
576
+
577
+ lines = [
578
+ "# --- compass discover --suggest-config -------------------------------------",
579
+ "# Refinements from THIS run's observations. Review each, then merge into your",
580
+ "# project YAML - nothing here is applied automatically.",
581
+ "",
582
+ "# Documents discovered this run. Pin it to alert on drift (or use 'auto' to",
583
+ "# baseline against the previous run automatically):",
584
+ "discovery:",
585
+ " extra:",
586
+ f" expected_documents: {n_docs}",
587
+ ]
588
+ if avg_chars:
589
+ median_size = int(statistics.median(avg_chars))
590
+ lines += [
591
+ "",
592
+ f"# Observed chunk size: median {median_size} chars across {len(avg_chars)} document(s)",
593
+ f"# (range {min(avg_chars)}-{max(avg_chars)}). Set expected bounds if you gate on size:",
594
+ "# redis:",
595
+ f"# chunk_size: {median_size}",
596
+ ]
597
+ if never_applies:
598
+ lines += [
599
+ "",
600
+ "# These checks were NOT APPLICABLE to any document this run (neutral every time) -",
601
+ "# the backend/data cannot answer them. Disabling removes them from the tally and",
602
+ "# the report noise. Verify each is genuinely irrelevant before disabling:",
603
+ ]
604
+ for lbl in never_applies:
605
+ lines.append(f"# - {lbl}")
606
+ else:
607
+ lines += ["", "# Every check applied to at least one document - nothing to disable."]
608
+ return "\n".join(lines) + "\n"
609
+
610
+
540
611
  __all__ = [
541
612
  "profile_vectorstore", "profile_storage", "profile_graph", "profile_kb",
542
- "apply_profile_to_settings", "suggest_config",
613
+ "apply_profile_to_settings", "suggest_config", "emit_project_config",
614
+ "suggest_config_from_run",
543
615
  ]
@@ -224,9 +224,14 @@ def _result_payload(name: str, r: PipelineResult) -> dict:
224
224
  continue
225
225
  order.append(s.step_name)
226
226
  metrics = {k: v for k, v in s.details.items() if k not in ("screenshot", "chunk_view", "rdf_triples") and isinstance(v, (int, float, str))}
227
- # Graph metadata steps export RDF triples for the knowledge-graph view.
227
+ # Graph metadata steps export RDF triples for the knowledge-graph view, which
228
+ # renders ONLY predicate + value. The raw triples also carry `subject` and
229
+ # `predicate_uri` - both full IRIs, repeated on every triple - that nothing reads;
230
+ # they were ~83% of a graph-heavy report's payload. Keep only the rendered fields.
228
231
  raw_triples = s.details.get("rdf_triples")
229
- triples = raw_triples[:_KG_TRIPLE_CAP] if isinstance(raw_triples, list) and raw_triples else None
232
+ triples = ([{"predicate": t.get("predicate", ""), "value": t.get("value", "")}
233
+ for t in raw_triples[:_KG_TRIPLE_CAP]]
234
+ if isinstance(raw_triples, list) and raw_triples else None)
230
235
  # Attach a remediation hint to each failed, evaluable check (advisory only).
231
236
  checks_out = [
232
237
  ({**c, "hint": hint_for(c.get("label", ""))}
@@ -144,3 +144,49 @@ def test_quality_threshold_gates_on_hygiene():
144
144
  assert not ok
145
145
  # no threshold set -> hygiene never gates
146
146
  assert not dav.gate_failed(10, 10, "READY", None, False, hygiene_score=10.0)[0]
147
+
148
+
149
+ def _run_case(name, avg_chars, section_supported):
150
+ from ingestion_validation.models import PipelineResult, StepResult, StepStatus
151
+ r = PipelineResult(document_id=name, file_name=f"{name}.pdf")
152
+ checks = [{"label": "Chunks exist", "passed": True},
153
+ {"label": "Section metadata present", "passed": False, "supported": section_supported}]
154
+ r.add_step(StepResult("Redis Chunks", StepStatus.PASSED, "ok", checks=checks,
155
+ details={"chunk_view": {"count": 10, "total_chars": avg_chars * 10, "avg_chars": avg_chars}}))
156
+ r.finalize()
157
+ return {"name": name, "result": r}
158
+
159
+
160
+ def test_suggest_config_from_run_reports_count_size_and_dead_checks():
161
+ from ingestion_validation.profiler import suggest_config_from_run
162
+ docs = [{"file_name": f"d{i}.pdf"} for i in range(12)]
163
+ # section metadata is neutral (unsupported) on every doc -> flagged as never-applicable
164
+ results = [_run_case(f"d{i}", 800 + i * 20, section_supported=False) for i in range(12)]
165
+ out = suggest_config_from_run(results, docs)
166
+ assert "expected_documents: 12" in out
167
+ assert "median 910 chars" in out # observed size distribution
168
+ assert "Section metadata present" in out # dead check surfaced for disabling
169
+
170
+
171
+ def test_suggest_config_does_not_flag_a_check_that_applied_somewhere():
172
+ from ingestion_validation.profiler import suggest_config_from_run
173
+ docs = [{"file_name": "d0.pdf"}, {"file_name": "d1.pdf"}]
174
+ # section metadata is supported (applies) on d1 -> must NOT be suggested for disabling
175
+ results = [_run_case("d0", 900, section_supported=False),
176
+ _run_case("d1", 900, section_supported=True)]
177
+ out = suggest_config_from_run(results, docs)
178
+ assert "Section metadata present" not in out
179
+ assert "nothing to disable" in out
180
+
181
+
182
+ def test_post_run_tips_are_contextual_and_suppressible(capsys):
183
+ class _A:
184
+ project = "wp-kb"; quality_threshold = None; include_retrieval = False; suggest_config = None
185
+ dav._print_tips(_A(), {"score": 56.1, "tier": "POOR"}, saved_baseline=False)
186
+ out = capsys.readouterr().out
187
+ assert "--save-baseline" in out and "--quality-threshold 56" in out and "--include-retrieval" in out
188
+
189
+ class _B:
190
+ project = "wp-kb"; quality_threshold = 80; include_retrieval = True; suggest_config = "-"
191
+ dav._print_tips(_B(), {"score": 98}, saved_baseline=True) # everything used, clean run
192
+ assert capsys.readouterr().out == "" # -> no tips
@@ -545,3 +545,18 @@ def test_hygiene_gauge_shows_score_and_worst_beside_the_verdict():
545
545
  assert "KB Hygiene" in gauge and "POOR" in gauge
546
546
  assert "worst:" in gauge and "Chunk Structure" in gauge
547
547
  assert report_mod._hygiene_gauge(None) == "" # no advisory -> no gauge
548
+
549
+
550
+ def test_embedded_triples_keep_only_rendered_fields():
551
+ # The KG view renders predicate + value only; subject and predicate_uri (full IRIs,
552
+ # repeated per triple) were ~83% of a graph-heavy payload and are never read.
553
+ r = PipelineResult(document_id="d", file_name="d.pdf")
554
+ r.add_step(StepResult("Neptune Metadata", StepStatus.PASSED, "ok",
555
+ checks=[{"label": "Found", "passed": True}],
556
+ details={"rdf_triples": [
557
+ {"subject": "http://ns/doc/1", "predicate": "documentId",
558
+ "predicate_uri": "http://ns/documentId", "value": "DOC-1"}]}))
559
+ r.finalize()
560
+ triples = report_mod._result_payload("d.pdf", r)["steps"]["Neptune Metadata"]["triples"]
561
+ assert triples == [{"predicate": "documentId", "value": "DOC-1"}]
562
+ assert "subject" not in triples[0] and "predicate_uri" not in triples[0]