compass-kb-validation 2.7.0__tar.gz → 2.8.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.
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/PKG-INFO +1 -1
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/compass_kb_validation.egg-info/PKG-INFO +1 -1
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/compass_kb_validation.egg-info/SOURCES.txt +3 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/__init__.py +1 -1
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/cli.py +133 -15
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/config/__init__.py +20 -1
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/corpus_health.py +54 -4
- compass_kb_validation-2.8.0/ingestion_validation/evidence.py +358 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/grounding.py +50 -2
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/report/__init__.py +148 -125
- compass_kb_validation-2.8.0/ingestion_validation/text_probes.py +57 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/retrieval_quality.py +4 -28
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_citation.py +28 -0
- compass_kb_validation-2.8.0/tests/test_evidence.py +167 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_reporting.py +16 -16
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_review_hardening.py +34 -1
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/LICENSE +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/README.md +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/compass_kb_validation.egg-info/requires.txt +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/discover_and_validate.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/__main__.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/ask.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/baseline.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/chunk_coherence.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/citation.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/config_lint.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/content_inspect.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/corpus.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/diagnostics.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/embedding_drift.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/eval_interop.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/init_wizard.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/live_dashboard.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/mcp_server.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/models/__init__.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/notifications.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/pipeline/__init__.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/profiler.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/provenance.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/providers/__init__.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/providers/embedding.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/providers/graph.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/providers/source.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/providers/storage.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/providers/vectorstore.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/remediation.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/report/_report_css.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/report/_report_js.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/report/analysis.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/report/export.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/report/shell.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/retrieval_analysis.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/trend_tracker.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/utils/__init__.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/__init__.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/api_upload.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/base.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/base_sparql.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/cross_stage.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/graphdb_metadata.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/neptune_metadata.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/redis_chunks.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/registry.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/validators/s3_storage.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/profile_kb.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/pyproject.toml +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/run_validation.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/setup.cfg +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_alt_adapters.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_analysis_report.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_api_upload.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_ask.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_baseline.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_benchmark.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_check_catalog.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_cli.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_config.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_config_lint.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_content_inspect.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_corpus.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_corpus_health.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_coverage.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_cross_stage.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_diagnostics.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_discover_cli.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_discovery_parallel.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_embedding_drift.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_eval_interop.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_gate.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_graph_transport.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_graph_validators.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_grounding.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_init_wizard.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_manifest.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_mcp.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_migration.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_models.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_neptune_bugfixes.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_neptune_bugfixes2.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_notifications.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_pipeline.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_profiler.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_providers.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_redis_chunks.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_registry.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_remediation.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_retrieval_assertions.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_retrieval_quality.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_run_cli.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_sources.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_storage_validator.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_trends.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_universal_concerns.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_utils.py +0 -0
- {compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/tests/test_vectorstore_filters.py +0 -0
|
@@ -24,6 +24,7 @@ ingestion_validation/corpus_health.py
|
|
|
24
24
|
ingestion_validation/diagnostics.py
|
|
25
25
|
ingestion_validation/embedding_drift.py
|
|
26
26
|
ingestion_validation/eval_interop.py
|
|
27
|
+
ingestion_validation/evidence.py
|
|
27
28
|
ingestion_validation/grounding.py
|
|
28
29
|
ingestion_validation/init_wizard.py
|
|
29
30
|
ingestion_validation/live_dashboard.py
|
|
@@ -33,6 +34,7 @@ ingestion_validation/profiler.py
|
|
|
33
34
|
ingestion_validation/provenance.py
|
|
34
35
|
ingestion_validation/remediation.py
|
|
35
36
|
ingestion_validation/retrieval_analysis.py
|
|
37
|
+
ingestion_validation/text_probes.py
|
|
36
38
|
ingestion_validation/trend_tracker.py
|
|
37
39
|
ingestion_validation/config/__init__.py
|
|
38
40
|
ingestion_validation/models/__init__.py
|
|
@@ -82,6 +84,7 @@ tests/test_discover_cli.py
|
|
|
82
84
|
tests/test_discovery_parallel.py
|
|
83
85
|
tests/test_embedding_drift.py
|
|
84
86
|
tests/test_eval_interop.py
|
|
87
|
+
tests/test_evidence.py
|
|
85
88
|
tests/test_gate.py
|
|
86
89
|
tests/test_graph_transport.py
|
|
87
90
|
tests/test_graph_validators.py
|
|
@@ -1251,6 +1251,91 @@ def _health_env(args) -> None:
|
|
|
1251
1251
|
pass
|
|
1252
1252
|
|
|
1253
1253
|
|
|
1254
|
+
def cmd_evidence(argv: list[str]) -> int:
|
|
1255
|
+
p = argparse.ArgumentParser(
|
|
1256
|
+
prog="compass evidence",
|
|
1257
|
+
description="Produce live, reproducible PROOF of the issues a discovery report flags - the "
|
|
1258
|
+
"actual chunks, queries and scores to attach to a ticket. Read-only, no LLM, no "
|
|
1259
|
+
"live embedding: probes are built from the KB's own chunks and run the same "
|
|
1260
|
+
"checks as validation, so a CONFIRMED verdict is proof of what the report found.")
|
|
1261
|
+
p.add_argument("--project")
|
|
1262
|
+
p.add_argument("--env", default="")
|
|
1263
|
+
p.add_argument("--checks", "-c", default=",".join(evidence_mod_all()),
|
|
1264
|
+
help="Comma-separated: paraphrase,boundary_spanning,exact_term,duplicate_chunks,"
|
|
1265
|
+
"near_duplicate,chunk_boundaries (default: all).")
|
|
1266
|
+
p.add_argument("--document", "-d", default="", help="Only probe documents whose name contains this.")
|
|
1267
|
+
p.add_argument("--limit", "-l", type=int, default=0, help="Max documents to probe (default: config sample_size).")
|
|
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).")
|
|
1271
|
+
args = p.parse_args(argv)
|
|
1272
|
+
|
|
1273
|
+
try:
|
|
1274
|
+
from dotenv import load_dotenv
|
|
1275
|
+
for fn in (".env", f".env.{args.env}" if args.env else None):
|
|
1276
|
+
if fn and Path(fn).exists():
|
|
1277
|
+
load_dotenv(fn, override=True)
|
|
1278
|
+
except Exception: # noqa: BLE001 - dotenv optional / file absent
|
|
1279
|
+
pass
|
|
1280
|
+
|
|
1281
|
+
from ingestion_validation import evidence as ev
|
|
1282
|
+
from ingestion_validation.config import load_settings, project_config_file
|
|
1283
|
+
|
|
1284
|
+
if args.project and project_config_file(args.project) is None:
|
|
1285
|
+
print(f" Project '{args.project}' not found (config/projects/{args.project}.yaml).")
|
|
1286
|
+
return 2
|
|
1287
|
+
try:
|
|
1288
|
+
settings = load_settings(project=args.project, env=args.env or None)
|
|
1289
|
+
except Exception as exc: # noqa: BLE001
|
|
1290
|
+
print(f" Config error: {exc}")
|
|
1291
|
+
return 2
|
|
1292
|
+
|
|
1293
|
+
valid = set(ev.ALL_CHECKS)
|
|
1294
|
+
checks = [c.strip() for c in args.checks.split(",") if c.strip() in valid]
|
|
1295
|
+
bad = [c.strip() for c in args.checks.split(",") if c.strip() and c.strip() not in valid]
|
|
1296
|
+
if bad:
|
|
1297
|
+
print(f" Unknown check(s): {', '.join(bad)}. Valid: {', '.join(ev.ALL_CHECKS)}.")
|
|
1298
|
+
return 2
|
|
1299
|
+
if not checks:
|
|
1300
|
+
print(" No valid checks selected.")
|
|
1301
|
+
return 2
|
|
1302
|
+
if args.limit < 0:
|
|
1303
|
+
print(" --limit must be >= 0 (0 uses the configured sample_size).")
|
|
1304
|
+
return 2
|
|
1305
|
+
limit = 1 if args.quick else (args.limit or settings.evidence.sample_size)
|
|
1306
|
+
|
|
1307
|
+
print(f" compass evidence - project={args.project or '(none)'} checks={checks} limit={limit}")
|
|
1308
|
+
try:
|
|
1309
|
+
payload = ev.gather_evidence(settings, checks, document_filter=args.document, limit=limit)
|
|
1310
|
+
except Exception as exc: # noqa: BLE001 - unreachable/misconfigured backend -> clean message
|
|
1311
|
+
print(f" Could not gather evidence: {exc}")
|
|
1312
|
+
return 1
|
|
1313
|
+
if payload.get("error"):
|
|
1314
|
+
print(f" {payload['error']}")
|
|
1315
|
+
return 1
|
|
1316
|
+
|
|
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}")
|
|
1323
|
+
else:
|
|
1324
|
+
print(body)
|
|
1325
|
+
counts: dict[str, int] = {}
|
|
1326
|
+
for r in payload["results"]:
|
|
1327
|
+
counts[r.verdict] = counts.get(r.verdict, 0) + 1
|
|
1328
|
+
print(f" {payload['docs_probed']} doc(s) probed - "
|
|
1329
|
+
f"CONFIRMED {counts.get('CONFIRMED', 0)}, WEAK {counts.get('WEAK', 0)}, "
|
|
1330
|
+
f"not reproduced {counts.get('NOT_REPRODUCED', 0)}, errors {counts.get('ERROR', 0)}")
|
|
1331
|
+
return 0
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def evidence_mod_all() -> tuple:
|
|
1335
|
+
from ingestion_validation.evidence import ALL_CHECKS
|
|
1336
|
+
return ALL_CHECKS
|
|
1337
|
+
|
|
1338
|
+
|
|
1254
1339
|
def cmd_quality(argv: list[str]) -> int:
|
|
1255
1340
|
p = argparse.ArgumentParser(
|
|
1256
1341
|
prog="compass quality",
|
|
@@ -1475,25 +1560,55 @@ def cmd_grounding_audit(argv: list[str]) -> int:
|
|
|
1475
1560
|
else:
|
|
1476
1561
|
print(body)
|
|
1477
1562
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1563
|
+
if not results:
|
|
1564
|
+
if args.format != "json":
|
|
1565
|
+
print("\n No eval records parsed from the input (empty file or no JSON objects).")
|
|
1566
|
+
return 0
|
|
1567
|
+
|
|
1568
|
+
# Human-readable summary lines go to stdout only for the markdown format - in --format json
|
|
1569
|
+
# they would corrupt the JSON body a downstream parser is reading.
|
|
1570
|
+
if args.format != "json":
|
|
1571
|
+
# If the answer field (or others) had to be auto-detected, say so once - a silent field
|
|
1572
|
+
# mismatch is the #1 reason grounding-audit "finds nothing".
|
|
1573
|
+
auto = {}
|
|
1486
1574
|
for r in results:
|
|
1487
|
-
for
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1575
|
+
for kind, info in (r.field_resolution or {}).items():
|
|
1576
|
+
auto.setdefault(kind, info["used"])
|
|
1577
|
+
if auto:
|
|
1578
|
+
mapping = ", ".join(f"{k} <- '{p}'" for k, p in sorted(auto.items()))
|
|
1579
|
+
print(f"\n NOTE: auto-detected eval fields ({mapping}). "
|
|
1580
|
+
"Set eval.gt_question_path / gt_response_path / gt_context_path to pin them.")
|
|
1581
|
+
|
|
1582
|
+
# Citation audit rollup across cases (deterministic; only counts checkable citations).
|
|
1583
|
+
total_cits = sum(r.citations.get("summary", {}).get("total_citations", 0) for r in results)
|
|
1584
|
+
cit_sound = sum(r.citations.get("summary", {}).get("sound", 0) for r in results)
|
|
1585
|
+
cit_checkable = sum(r.citations.get("summary", {}).get("checkable", 0) for r in results)
|
|
1586
|
+
if cit_checkable:
|
|
1587
|
+
faultmix: dict[str, int] = {}
|
|
1588
|
+
for r in results:
|
|
1589
|
+
for k, n in r.citations.get("summary", {}).get("faults", {}).items():
|
|
1590
|
+
faultmix[k] = faultmix.get(k, 0) + n
|
|
1591
|
+
acc = round(100.0 * cit_sound / cit_checkable, 1)
|
|
1592
|
+
print(f"\n Citation audit: {cit_sound}/{cit_checkable} checkable citations grounded ({acc}%).")
|
|
1593
|
+
if faultmix:
|
|
1594
|
+
print(" faults: " + ", ".join(f"{k} x{n}" for k, n in sorted(faultmix.items())))
|
|
1595
|
+
elif total_cits == 0:
|
|
1596
|
+
# 0 citations is either a retrieval-only export (nothing to audit) or, far more often,
|
|
1597
|
+
# the answer field is not mapped - say which, loudly, instead of a silent INFO.
|
|
1598
|
+
if all(r.retrieval_only for r in results):
|
|
1599
|
+
print("\n NOTE: these are retrieval-only cases (no generated answer) - nothing to cite-audit.")
|
|
1600
|
+
else:
|
|
1601
|
+
print("\n WARNING: 0 citations found across "
|
|
1602
|
+
f"{len(results)} case(s). If the answers DO cite sources, the answer field is "
|
|
1603
|
+
"probably not mapped - set eval.gt_response_path to your answer field (e.g. "
|
|
1604
|
+
"'output'/'answer'), and eval.citation_patterns if your citation style is custom.")
|
|
1493
1605
|
|
|
1606
|
+
# The fabrication gate drives the exit code regardless of output format (the message is
|
|
1607
|
+
# print-gated above; only the return value matters for --format json).
|
|
1494
1608
|
fabricated = sum(1 for r in results if r.has_fabrication)
|
|
1495
1609
|
if fabricated:
|
|
1496
|
-
|
|
1610
|
+
if args.format != "json":
|
|
1611
|
+
print(f"\n {fabricated}/{len(results)} case(s) contain a claim or citation not backed by the KB.")
|
|
1497
1612
|
if args.fail_on_fabrication:
|
|
1498
1613
|
return 1
|
|
1499
1614
|
return 0
|
|
@@ -1552,6 +1667,7 @@ Commands:
|
|
|
1552
1667
|
compare compare retrieval against a ground-truth file (precision/recall/Jaccard)
|
|
1553
1668
|
quality score chunk quality (density, self-containedness, boilerplate) - no LLM
|
|
1554
1669
|
freshness flag stale sources + orphan documents (source object gone)
|
|
1670
|
+
evidence produce live, reproducible PROOF of flagged issues (chunks + queries) - no LLM
|
|
1555
1671
|
grounding-audit check an eval's grounding finding against the KB (count claims) - no LLM
|
|
1556
1672
|
demo generate a sample report from synthetic data (no backend needed)
|
|
1557
1673
|
init [name] scaffold a project (config + .env.example + marker). -i/--interactive wizard; --graph graph block; --ci workflow
|
|
@@ -1597,6 +1713,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1597
1713
|
return cmd_compare(rest)
|
|
1598
1714
|
if cmd == "quality":
|
|
1599
1715
|
return cmd_quality(rest)
|
|
1716
|
+
if cmd == "evidence":
|
|
1717
|
+
return cmd_evidence(rest)
|
|
1600
1718
|
if cmd == "grounding-audit":
|
|
1601
1719
|
return cmd_grounding_audit(rest)
|
|
1602
1720
|
if cmd == "freshness":
|
{compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/config/__init__.py
RENAMED
|
@@ -569,6 +569,22 @@ class CorpusConfig:
|
|
|
569
569
|
extra: dict[str, Any] = field(default_factory=dict)
|
|
570
570
|
|
|
571
571
|
|
|
572
|
+
@dataclass
|
|
573
|
+
class EvidenceConfig:
|
|
574
|
+
"""`compass evidence`: how many documents to probe and how strict the confirm thresholds are.
|
|
575
|
+
|
|
576
|
+
Evidence is a read-only proof surface; these only tune sampling/sensitivity, never a
|
|
577
|
+
pass/fail. All deterministic - no LLM, no live embedding.
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
sample_size: int = 10 # documents probed per run (--limit overrides)
|
|
581
|
+
output_dir: str = "reports/evidence"
|
|
582
|
+
# Below this MEAN overlap = paraphrase weakness CONFIRMED. Matches the retrieval-quality
|
|
583
|
+
# validator's own paraphrase threshold, so evidence reproduces what validation flags.
|
|
584
|
+
paraphrase_overlap_threshold: float = 0.3
|
|
585
|
+
near_duplicate_cosine: float = 0.97 # at/above this chunk-embedding cosine = near-duplicate
|
|
586
|
+
|
|
587
|
+
|
|
572
588
|
@dataclass
|
|
573
589
|
class EmbeddingConfig:
|
|
574
590
|
"""How to turn a natural-language query into a vector for `compass ask`.
|
|
@@ -728,6 +744,7 @@ class Settings:
|
|
|
728
744
|
eval: EvalConfig = field(default_factory=EvalConfig)
|
|
729
745
|
consistency: ConsistencyConfig = field(default_factory=ConsistencyConfig)
|
|
730
746
|
corpus: CorpusConfig = field(default_factory=CorpusConfig)
|
|
747
|
+
evidence: EvidenceConfig = field(default_factory=EvidenceConfig)
|
|
731
748
|
# Non-service metadata carried through from config files.
|
|
732
749
|
project_name: str = ""
|
|
733
750
|
display_name: str = ""
|
|
@@ -749,7 +766,8 @@ _META_KEYS = frozenset({"default_env", "display_name", "environments", "skip_ste
|
|
|
749
766
|
# new block can never be added to one and forgotten in the other (which is exactly how
|
|
750
767
|
# `consistency` came to be silently dropped at the top level).
|
|
751
768
|
_SERVICE_BLOCKS = ("api", "s3", "graphdb", "redis", "neptune", "pipeline",
|
|
752
|
-
"retrieval", "discovery", "embedding", "eval", "consistency", "corpus"
|
|
769
|
+
"retrieval", "discovery", "embedding", "eval", "consistency", "corpus",
|
|
770
|
+
"evidence")
|
|
753
771
|
|
|
754
772
|
|
|
755
773
|
def _deep_merge(base: dict, overlay: dict) -> dict:
|
|
@@ -880,6 +898,7 @@ def load_settings(
|
|
|
880
898
|
eval=_hydrate(EvalConfig, flat.get("eval")),
|
|
881
899
|
consistency=_hydrate(ConsistencyConfig, flat.get("consistency")),
|
|
882
900
|
corpus=_hydrate(CorpusConfig, flat.get("corpus")),
|
|
901
|
+
evidence=_hydrate(EvidenceConfig, flat.get("evidence")),
|
|
883
902
|
project_name=project or "",
|
|
884
903
|
display_name=merged.get("display_name", ""),
|
|
885
904
|
environment=resolved_env,
|
{compass_kb_validation-2.7.0 → compass_kb_validation-2.8.0}/ingestion_validation/corpus_health.py
RENAMED
|
@@ -23,10 +23,24 @@ _SENTENCE_END = tuple(".!?\"')]}")
|
|
|
23
23
|
# --------------------------------------------------------------------------- #
|
|
24
24
|
# Chunk quality (ENH-6)
|
|
25
25
|
# --------------------------------------------------------------------------- #
|
|
26
|
-
def
|
|
26
|
+
def _is_tabular(t: str, digit_ratio: float = 0.4) -> bool:
|
|
27
|
+
"""Heuristic: the chunk is a table / structured data, where low lexical uniqueness and
|
|
28
|
+
repetition are EXPECTED (repeated column labels, codes, numbers) - not a quality problem.
|
|
29
|
+
``digit_ratio`` is config-driven (redis.table_digit_ratio_threshold) so a numeric-but-prose
|
|
30
|
+
corpus is not misflagged; prose runs ~2-5% digits, tables far higher."""
|
|
31
|
+
lines = [ln for ln in t.splitlines() if ln.strip()]
|
|
32
|
+
if sum(1 for ln in lines if ln.count("|") >= 2) >= 2:
|
|
33
|
+
return True
|
|
34
|
+
digits = sum(c.isdigit() for c in t)
|
|
35
|
+
return len(t) > 60 and digits / len(t) > digit_ratio
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _chunk_quality(text: str, min_chars: int, boilerplate_lines: set[str],
|
|
39
|
+
digit_ratio: float = 0.4) -> dict:
|
|
27
40
|
t = (text or "").strip()
|
|
28
41
|
toks = _WORD.findall(t.lower())
|
|
29
42
|
unique_ratio = len(set(toks)) / len(toks) if toks else 0.0
|
|
43
|
+
tabular = _is_tabular(t, digit_ratio)
|
|
30
44
|
too_short = len(t) < min_chars
|
|
31
45
|
starts_mid = bool(t[:1]) and t[:1].islower() # begins mid-sentence
|
|
32
46
|
ends_clean = bool(t) and t[-1] in _SENTENCE_END
|
|
@@ -43,7 +57,7 @@ def _chunk_quality(text: str, min_chars: int, boilerplate_lines: set[str]) -> di
|
|
|
43
57
|
score = round(sum(signals) / len(signals), 3)
|
|
44
58
|
return {"length": len(t), "unique_ratio": round(unique_ratio, 3), "too_short": too_short,
|
|
45
59
|
"self_contained": self_contained, "boilerplate_ratio": round(boiler_ratio, 3),
|
|
46
|
-
"score": score}
|
|
60
|
+
"tabular": tabular, "score": score}
|
|
47
61
|
|
|
48
62
|
|
|
49
63
|
def quality_report(settings: Settings, *, sample: int = 500, min_chars: int = 50,
|
|
@@ -62,9 +76,10 @@ def quality_report(settings: Settings, *, sample: int = 500, min_chars: int = 50
|
|
|
62
76
|
floor = max(3, int(0.3 * len(chunks)))
|
|
63
77
|
boilerplate_lines = {ln for ln, n in line_counts.items() if n >= floor}
|
|
64
78
|
|
|
79
|
+
digit_ratio = float(getattr(settings.redis, "table_digit_ratio_threshold", 0.4) or 0.4)
|
|
65
80
|
per: list[dict] = []
|
|
66
81
|
for c in chunks:
|
|
67
|
-
q = _chunk_quality(c.get("text", ""), min_chars, boilerplate_lines)
|
|
82
|
+
q = _chunk_quality(c.get("text", ""), min_chars, boilerplate_lines, digit_ratio)
|
|
68
83
|
q["key"] = c.get("key", "")
|
|
69
84
|
q["text"] = (c.get("text", "") or "")[:80]
|
|
70
85
|
per.append(q)
|
|
@@ -77,13 +92,25 @@ def quality_report(settings: Settings, *, sample: int = 500, min_chars: int = 50
|
|
|
77
92
|
verdict = ("HEALTHY - chunks are dense and self-contained" if mean_score >= 0.75
|
|
78
93
|
else "MIXED - a meaningful share of chunks are short/boilerplate/fragmentary" if mean_score >= 0.5
|
|
79
94
|
else "POOR - many chunks are low-value (short, boilerplate, or mid-sentence)")
|
|
95
|
+
pct_tabular = round(sum(p.get("tabular", False) for p in per) / n, 3)
|
|
96
|
+
# Content-type context: the density signal (unique-ratio) is naturally low for tables and
|
|
97
|
+
# structured data, so a MIXED verdict on a heavily tabular corpus can be expected, not a
|
|
98
|
+
# defect. Say so rather than letting the number read as poor quality.
|
|
99
|
+
content_note = ""
|
|
100
|
+
if pct_tabular >= 0.15:
|
|
101
|
+
content_note = (f"{pct_tabular:.0%} of sampled chunks look tabular/structured; low "
|
|
102
|
+
"lexical uniqueness (information density) is expected for these, so the "
|
|
103
|
+
"density component of the score understates their quality. Judge tabular "
|
|
104
|
+
"content on retrievability, not density.")
|
|
80
105
|
return {
|
|
81
106
|
"sample": n, "min_chars": min_chars,
|
|
82
107
|
"mean_score": mean_score, "mean_unique_ratio": _mean("unique_ratio"),
|
|
83
108
|
"pct_too_short": round(sum(p["too_short"] for p in per) / n, 3),
|
|
84
109
|
"pct_not_self_contained": round(sum(not p["self_contained"] for p in per) / n, 3),
|
|
110
|
+
"pct_tabular": pct_tabular,
|
|
85
111
|
"boilerplate_lines": len(boilerplate_lines),
|
|
86
112
|
"verdict": verdict,
|
|
113
|
+
"content_note": content_note,
|
|
87
114
|
"worst": per[: min(15, n)],
|
|
88
115
|
"per_chunk": per,
|
|
89
116
|
}
|
|
@@ -98,8 +125,10 @@ def format_quality(payload: dict) -> str:
|
|
|
98
125
|
f"too-short: {payload['pct_too_short']:.0%} not self-contained: {payload['pct_not_self_contained']:.0%}",
|
|
99
126
|
f" recurring boilerplate lines: {payload['boilerplate_lines']}",
|
|
100
127
|
f" VERDICT: {payload['verdict']}",
|
|
101
|
-
" lowest-quality chunks:",
|
|
102
128
|
]
|
|
129
|
+
if payload.get("content_note"):
|
|
130
|
+
lines.append(f" NOTE: {payload['content_note']}")
|
|
131
|
+
lines.append(" lowest-quality chunks:")
|
|
103
132
|
for p in payload["worst"]:
|
|
104
133
|
flags = ",".join(f for f, on in (("short", p["too_short"]),
|
|
105
134
|
("mid-sentence", not p["self_contained"]),
|
|
@@ -201,6 +230,22 @@ def freshness_report(settings: Settings, *, sample: int = 500, stale_after_days:
|
|
|
201
230
|
if orphans:
|
|
202
231
|
parts.append(f"{len(orphans)} orphan(s) (source object gone)")
|
|
203
232
|
verdict = "STALE - " + ", ".join(parts)
|
|
233
|
+
|
|
234
|
+
# Actionable next steps, not just a count: "36/36 STALE" is alarming without a fix path.
|
|
235
|
+
remediation: list[str] = []
|
|
236
|
+
if drift:
|
|
237
|
+
remediation.append(
|
|
238
|
+
f"Re-ingest the {len(drift)} drifted document(s) so the KB reflects the current source "
|
|
239
|
+
"(the source changed after it was embedded).")
|
|
240
|
+
if stale:
|
|
241
|
+
remediation.append(
|
|
242
|
+
f"Refresh the {len(stale)} stale source(s): re-run the ingestion pipeline for their "
|
|
243
|
+
"prefix/bucket to re-embed the latest content.")
|
|
244
|
+
if orphans:
|
|
245
|
+
remediation.append(
|
|
246
|
+
f"Reconcile the {len(orphans)} orphan(s) (source object gone): if the deletions were "
|
|
247
|
+
"intentional, delete the corresponding chunks from the index; if not, restore the "
|
|
248
|
+
"source objects and re-ingest.")
|
|
204
249
|
result = {
|
|
205
250
|
"documents": checked, "sampled_documents": len(seen_docs),
|
|
206
251
|
"without_source_key": without_key, "stale_after_days": stale_after_days,
|
|
@@ -209,6 +254,7 @@ def freshness_report(settings: Settings, *, sample: int = 500, stale_after_days:
|
|
|
209
254
|
"stale": stale[: min(20, len(stale))], "orphans": orphans[: min(20, len(orphans))],
|
|
210
255
|
"drift": drift[: min(20, len(drift))],
|
|
211
256
|
"verdict": verdict,
|
|
257
|
+
"remediation": remediation,
|
|
212
258
|
}
|
|
213
259
|
if without_key:
|
|
214
260
|
result["note"] = (
|
|
@@ -259,4 +305,8 @@ def format_freshness(payload: dict) -> str:
|
|
|
259
305
|
lines.append(" stalest sources:")
|
|
260
306
|
for s in payload["stale"]:
|
|
261
307
|
lines.append(f" {s['age_days']}d {s['doc']} ({s['s3_key']})")
|
|
308
|
+
if payload.get("remediation"):
|
|
309
|
+
lines.append(" REMEDIATION:")
|
|
310
|
+
for step in payload["remediation"]:
|
|
311
|
+
lines.append(f" - {step}")
|
|
262
312
|
return "\n".join(lines)
|