compass-kb-validation 2.4.0__tar.gz → 2.5.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.4.0 → compass_kb_validation-2.5.0}/PKG-INFO +1 -1
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/compass_kb_validation.egg-info/PKG-INFO +1 -1
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/__init__.py +1 -1
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/__init__.py +154 -19
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/_report_css.py +5 -3
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/trend_tracker.py +16 -4
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_reporting.py +104 -1
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/LICENSE +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/README.md +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/compass_kb_validation.egg-info/SOURCES.txt +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/compass_kb_validation.egg-info/requires.txt +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/discover_and_validate.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/__main__.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/ask.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/baseline.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/chunk_coherence.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/cli.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/config/__init__.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/config_lint.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/content_inspect.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/corpus.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/corpus_health.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/diagnostics.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/embedding_drift.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/eval_interop.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/grounding.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/init_wizard.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/live_dashboard.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/mcp_server.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/models/__init__.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/notifications.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/pipeline/__init__.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/profiler.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/provenance.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/providers/__init__.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/providers/embedding.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/providers/graph.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/providers/source.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/providers/storage.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/providers/vectorstore.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/remediation.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/_report_js.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/analysis.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/export.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/shell.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/retrieval_analysis.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/utils/__init__.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/__init__.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/api_upload.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/base.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/base_sparql.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/cross_stage.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/graphdb_metadata.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/neptune_metadata.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/redis_chunks.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/registry.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/retrieval_quality.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/s3_storage.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/profile_kb.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/pyproject.toml +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/run_validation.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/setup.cfg +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_alt_adapters.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_analysis_report.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_api_upload.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_ask.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_baseline.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_benchmark.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_check_catalog.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_cli.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_config.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_config_lint.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_content_inspect.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_corpus.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_corpus_health.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_coverage.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_cross_stage.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_diagnostics.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_discover_cli.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_discovery_parallel.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_embedding_drift.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_eval_interop.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_gate.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_graph_transport.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_graph_validators.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_grounding.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_init_wizard.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_manifest.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_mcp.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_migration.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_models.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_neptune_bugfixes.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_neptune_bugfixes2.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_notifications.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_pipeline.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_profiler.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_providers.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_redis_chunks.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_registry.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_remediation.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_retrieval_assertions.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_retrieval_quality.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_run_cli.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_sources.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_storage_validator.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_trends.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_universal_concerns.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_utils.py +0 -0
- {compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_vectorstore_filters.py +0 -0
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/__init__.py
RENAMED
|
@@ -58,8 +58,23 @@ _CAT_ORDER = [
|
|
|
58
58
|
("Content Preservation", "Fidelity, coverage, contamination"),
|
|
59
59
|
("Index & Schema", "Index fields, types, vector config"),
|
|
60
60
|
("Retrieval & Query", "Semantic search, filters, resilience"),
|
|
61
|
-
|
|
61
|
+
# Stage-derived buckets: catch a stage's checks that don't match a semantic pattern
|
|
62
|
+
# above, so they get a named home instead of swelling an opaque "Other".
|
|
63
|
+
("Knowledge Graph", "Graph existence, typing, links, namespaces"),
|
|
64
|
+
("Object Storage", "Source object existence, type, size, integrity"),
|
|
65
|
+
("Cross-Stage Consistency", "Do the stores agree about a document?"),
|
|
66
|
+
("Ingestion API", "Upload response, document id, status"),
|
|
67
|
+
("Other", "Uncategorised checks"),
|
|
62
68
|
]
|
|
69
|
+
# When a check's label matches no semantic pattern, fall back to the category of the STEP
|
|
70
|
+
# it came from (so 'Other' stays small and honest instead of holding 54% of the checks).
|
|
71
|
+
_STEP_CATEGORY = {
|
|
72
|
+
"Neptune Metadata": "Knowledge Graph",
|
|
73
|
+
"GraphDB Metadata": "Knowledge Graph",
|
|
74
|
+
"S3 Storage": "Object Storage",
|
|
75
|
+
"Cross-Stage Consistency": "Cross-Stage Consistency",
|
|
76
|
+
"API Upload": "Ingestion API",
|
|
77
|
+
}
|
|
63
78
|
_CAT_PATTERNS = [
|
|
64
79
|
("Metadata & Consistency", re.compile(r"e_tag|required field|metadata consist|positional|section|page metadata|directory|predicate|date format|email|field rule", re.I)),
|
|
65
80
|
("Chunk Structure", re.compile(r"boundaries|chunk count|size within|ordering|overlap", re.I)),
|
|
@@ -92,6 +107,15 @@ li .check-hint{margin:4px 0 0 0;display:block;}
|
|
|
92
107
|
.metric-chip{background:var(--chip-bg);border:1px solid var(--line);border-radius:8px;padding:6px 11px;font-size:12px;color:var(--muted);}
|
|
93
108
|
.metric-chip b{color:var(--ink);font-variant-numeric:tabular-nums;}
|
|
94
109
|
.exec-interp-title{font-size:12px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);font-weight:700;margin-bottom:6px;}
|
|
110
|
+
.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);}
|
|
111
|
+
.subsys-rollup{padding:14px 16px 18px;display:flex;flex-direction:column;gap:8px;border-bottom:1px solid var(--line);}
|
|
112
|
+
.subsys-head{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:700;margin-bottom:4px;}
|
|
113
|
+
.subsys-row{display:grid;grid-template-columns:160px 1fr auto;align-items:center;gap:12px;font-size:12px;}
|
|
114
|
+
.subsys-name{font-weight:700;color:var(--ink);}
|
|
115
|
+
.subsys-bar{height:10px;background:var(--line);border-radius:5px;overflow:hidden;}
|
|
116
|
+
.subsys-bar i{display:block;height:100%;border-radius:5px;}
|
|
117
|
+
.subsys-meta{color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums;}
|
|
118
|
+
@media(max-width:600px){.subsys-row{grid-template-columns:1fr;gap:3px;}}
|
|
95
119
|
.exec-bullets{margin:0 0 4px;padding-left:18px;font-size:13.5px;line-height:1.75;color:var(--ink);}
|
|
96
120
|
.exec-bullets li{margin:0;}
|
|
97
121
|
.exec-bullets li::marker{color:var(--muted);}
|
|
@@ -103,6 +127,8 @@ li .check-hint{margin:4px 0 0 0;display:block;}
|
|
|
103
127
|
.hygiene-score{font-size:15px;font-weight:800;margin-top:2px;}
|
|
104
128
|
.hygiene-score.ok{color:var(--ok);}.hygiene-score.warn{color:var(--warn);}.hygiene-score.bad{color:var(--bad);}
|
|
105
129
|
.hygiene-worst{font-size:11px;color:var(--muted);margin-top:2px;}
|
|
130
|
+
.hygiene-delta{font-size:11px;font-weight:700;margin-left:6px;}
|
|
131
|
+
.hygiene-delta.ok{color:var(--ok);}.hygiene-delta.bad{color:var(--bad);}
|
|
106
132
|
.hero-chips{display:flex;flex-wrap:wrap;gap:5px;}
|
|
107
133
|
.hero-chip{font-size:10.5px;font-weight:700;padding:2px 7px;border-radius:9px;border:1px solid var(--line);color:var(--muted);white-space:nowrap;}
|
|
108
134
|
.hero-chip.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent);}
|
|
@@ -198,11 +224,12 @@ def _band(status: str) -> str:
|
|
|
198
224
|
return _BAND.get(status, "na")
|
|
199
225
|
|
|
200
226
|
|
|
201
|
-
def _categorise(label: str) -> str:
|
|
227
|
+
def _categorise(label: str, step: str = "") -> str:
|
|
202
228
|
for name, pat in _CAT_PATTERNS:
|
|
203
229
|
if pat.search(label or ""):
|
|
204
230
|
return name
|
|
205
|
-
|
|
231
|
+
# No semantic match: give it the step's own category so it isn't dumped into "Other".
|
|
232
|
+
return _STEP_CATEGORY.get(step, "Other")
|
|
206
233
|
|
|
207
234
|
|
|
208
235
|
def _esc(s) -> str:
|
|
@@ -293,7 +320,7 @@ def _aggregate(cases: list[dict], hide_skipped: bool = True) -> dict:
|
|
|
293
320
|
rec["ran"] += 1
|
|
294
321
|
docs_incomplete += incomplete
|
|
295
322
|
for c in cases:
|
|
296
|
-
for st in c["steps"].
|
|
323
|
+
for step_name, st in c["steps"].items():
|
|
297
324
|
# A step that did not run has nothing to say about the data. Its checks are
|
|
298
325
|
# neutral, exactly like an unevaluable one - never hard failures. Without
|
|
299
326
|
# this, a SKIPPED step (e.g. an unreachable graph, or a document absent from
|
|
@@ -310,7 +337,7 @@ def _aggregate(cases: list[dict], hide_skipped: bool = True) -> dict:
|
|
|
310
337
|
if ch.get("supported", True) is False:
|
|
311
338
|
checks_neutral += 1 # neutral — backend can't evaluate this check
|
|
312
339
|
continue
|
|
313
|
-
k = _categorise(ch.get("label", ""))
|
|
340
|
+
k = _categorise(ch.get("label", ""), step_name)
|
|
314
341
|
cat[k]["total"] += 1
|
|
315
342
|
ch_passed = bool(ch.get("passed"))
|
|
316
343
|
sev = "warn" if str(ch.get("severity", "hard")).lower() == "warn" else "hard"
|
|
@@ -329,7 +356,7 @@ def _aggregate(cases: list[dict], hide_skipped: bool = True) -> dict:
|
|
|
329
356
|
else:
|
|
330
357
|
checks_hard += 1
|
|
331
358
|
label = ch.get("label", "")
|
|
332
|
-
rec = offenders.setdefault(label, {"label": label, "docs": 0, "severity": sev})
|
|
359
|
+
rec = offenders.setdefault(label, {"label": label, "docs": 0, "severity": sev, "step": step_name})
|
|
333
360
|
rec["docs"] += 1
|
|
334
361
|
# a label is HARD overall if it ever gates
|
|
335
362
|
if sev == "hard":
|
|
@@ -444,6 +471,53 @@ _SKIP_REASON_TEXT = {
|
|
|
444
471
|
}
|
|
445
472
|
|
|
446
473
|
|
|
474
|
+
def _narrative(agg: dict) -> str:
|
|
475
|
+
"""A 2-3 sentence deterministic reading of the run - the story the bullets support.
|
|
476
|
+
|
|
477
|
+
Purely template-selected from the aggregate (no LLM, no inference beyond arithmetic):
|
|
478
|
+
integrity verdict, then the dominant hygiene issue if any, then a coverage caveat if
|
|
479
|
+
the run did not look at everything. Turns "Chunk Structure 47%" (data) into "chunking
|
|
480
|
+
is the dominant issue, typically configuration not corruption" (reading).
|
|
481
|
+
"""
|
|
482
|
+
total = max(agg.get("total", 0), 1)
|
|
483
|
+
gated = agg.get("failed", 0) + agg.get("errors", 0)
|
|
484
|
+
sents: list[str] = []
|
|
485
|
+
|
|
486
|
+
# 1. Integrity verdict.
|
|
487
|
+
if gated == 0:
|
|
488
|
+
sents.append(f"All {agg['total']} document(s) passed every integrity gate")
|
|
489
|
+
else:
|
|
490
|
+
sents.append(f"{gated} of {agg['total']} document(s) failed an integrity gate")
|
|
491
|
+
|
|
492
|
+
# 2. Dominant hygiene issue (advisory quality beyond the gate).
|
|
493
|
+
hy = agg.get("hygiene")
|
|
494
|
+
worst = hy.get("worst") if hy else None
|
|
495
|
+
if hy and worst and worst["rate"] < 100:
|
|
496
|
+
tier = hy["tier"].lower()
|
|
497
|
+
if gated == 0:
|
|
498
|
+
sents[-1] += f", but data-plane hygiene is {tier} at {hy['score']:g}%"
|
|
499
|
+
else:
|
|
500
|
+
sents[-1] += f"; data-plane hygiene is {tier} at {hy['score']:g}%"
|
|
501
|
+
if worst["rate"] < 50:
|
|
502
|
+
sents.append(f"The {worst['name']} checks are the dominant issue "
|
|
503
|
+
f"({worst['rate']:g}% pass) - a pattern that usually points to "
|
|
504
|
+
f"ingestion configuration rather than data corruption")
|
|
505
|
+
else:
|
|
506
|
+
sents.append(f"The weakest area is {worst['name']} ({worst['rate']:g}% pass)")
|
|
507
|
+
elif hy:
|
|
508
|
+
sents[-1] += f", and data-plane hygiene is {hy['tier'].lower()} at {hy['score']:g}%"
|
|
509
|
+
|
|
510
|
+
# 3. Coverage caveat - never let the reading imply full coverage it did not have.
|
|
511
|
+
cov = agg.get("coverage") or []
|
|
512
|
+
if cov:
|
|
513
|
+
worst_cov = cov[0]
|
|
514
|
+
pct = round(100 * worst_cov["skipped"] / total)
|
|
515
|
+
sents.append(f"Note: {worst_cov['name']} did not run on {pct}% of documents, so this "
|
|
516
|
+
"reading covers less than the whole pipeline")
|
|
517
|
+
|
|
518
|
+
return ". ".join(s.rstrip(".") for s in sents) + "."
|
|
519
|
+
|
|
520
|
+
|
|
447
521
|
def _exec_bullets(agg: dict) -> str:
|
|
448
522
|
"""The executive summary: verdict, what was NOT validated, and the top offender.
|
|
449
523
|
|
|
@@ -508,11 +582,18 @@ def _hygiene_gauge(h: dict | None) -> str:
|
|
|
508
582
|
worst_html = (f'<div class="hygiene-worst" data-tip="Lowest-scoring dimension - fix this in '
|
|
509
583
|
f'ingestion first">worst: {_esc(worst["name"])} {worst["rate"]:g}%</div>'
|
|
510
584
|
if worst and worst["rate"] < 100 else "")
|
|
585
|
+
# Direction vs the previous run, so the trend is legible without opening the panel.
|
|
586
|
+
delta = h.get("delta")
|
|
587
|
+
delta_html = ""
|
|
588
|
+
if isinstance(delta, (int, float)) and delta != 0:
|
|
589
|
+
arrow, cls = ("↑", "ok") if delta > 0 else ("↓", "bad")
|
|
590
|
+
delta_html = (f'<span class="hygiene-delta {cls}" data-tip="vs previous run">'
|
|
591
|
+
f'{arrow} {abs(delta):g}</span>')
|
|
511
592
|
return (
|
|
512
593
|
f'<div class="hygiene-gauge" data-tip="Share of advisory (WARN) data-quality checks that '
|
|
513
594
|
f'passed - data-plane hygiene, not answer quality. Separate from the READY gate.">'
|
|
514
595
|
f'<div class="hygiene-cap">KB Hygiene</div>'
|
|
515
|
-
f'<div class="hygiene-score {h["band"]}">{h["score"]:g}% · {h["tier"]}</div>'
|
|
596
|
+
f'<div class="hygiene-score {h["band"]}">{h["score"]:g}% · {h["tier"]}{delta_html}</div>'
|
|
516
597
|
f'{worst_html}</div>'
|
|
517
598
|
)
|
|
518
599
|
|
|
@@ -559,6 +640,7 @@ def _exec_summary(agg: dict, corpus: dict | None = None, cases: list[dict] | Non
|
|
|
559
640
|
"not-applicable checks could not be evaluated." if (adv or neu) else ".")
|
|
560
641
|
)
|
|
561
642
|
bullets = _exec_bullets(agg)
|
|
643
|
+
narrative = _narrative(agg)
|
|
562
644
|
# Lead with the verdict, not the pass rate: the segment bar below already carries the
|
|
563
645
|
# pass/partial/fail split, so a donut of the same number spends the most prominent
|
|
564
646
|
# slot restating it. The corpus verdict is the real headline when discovery supplies
|
|
@@ -602,6 +684,7 @@ def _exec_summary(agg: dict, corpus: dict | None = None, cases: list[dict] | Non
|
|
|
602
684
|
</div>
|
|
603
685
|
<div class="exec-right" style="flex:1">
|
|
604
686
|
<div class="exec-interp-title">Executive Summary</div>
|
|
687
|
+
<div class="exec-narrative">{_esc(narrative)}</div>
|
|
605
688
|
<ul class="exec-bullets" data-tip="{_esc(interp)}">{bullets}</ul>
|
|
606
689
|
<div class="seg-bar">{seg}</div>
|
|
607
690
|
<div class="seg-legend">
|
|
@@ -755,7 +838,7 @@ def _top_issues_table(agg: dict) -> str:
|
|
|
755
838
|
pct = round(100 * o["docs"] / total)
|
|
756
839
|
sev = o.get("severity", "hard")
|
|
757
840
|
sev_cls, sev_txt = ("bad", "Gating") if sev == "hard" else ("warn", "Advisory")
|
|
758
|
-
cat = _categorise(o["label"])
|
|
841
|
+
cat = _categorise(o["label"], o.get("step", ""))
|
|
759
842
|
bar_cls = "bad" if sev == "hard" else "warn"
|
|
760
843
|
rows += (
|
|
761
844
|
f'<tr><td><b>{_esc(o["label"])}</b></td><td class="check-detail">{_esc(cat)}</td>'
|
|
@@ -770,13 +853,52 @@ def _top_issues_table(agg: dict) -> str:
|
|
|
770
853
|
return (
|
|
771
854
|
'<div class="table-card" style="margin-top:18px"><div class="table-cap">Most Frequent Issues '
|
|
772
855
|
'<span class="cat-badge">ranked by documents affected</span></div>'
|
|
773
|
-
|
|
856
|
+
+ _subsystem_rollup(agg)
|
|
857
|
+
+ '<div class="subsys-head" style="padding:18px 16px 0">By check</div>'
|
|
858
|
+
+ '<div class="table-wrap"><table>'
|
|
774
859
|
'<thead><tr><th>Check</th><th>Category</th><th class="num">Documents</th>'
|
|
775
860
|
'<th class="num">Share</th><th>Spread</th><th>Severity</th></tr></thead>'
|
|
776
861
|
f'<tbody>{rows}</tbody></table></div></div>'
|
|
777
862
|
)
|
|
778
863
|
|
|
779
864
|
|
|
865
|
+
def _subsystem_rollup(agg: dict) -> str:
|
|
866
|
+
"""Group offending checks by subsystem so one root cause reads as one problem.
|
|
867
|
+
|
|
868
|
+
Five failing chunk checks are not five problems - they are one chunking issue seen
|
|
869
|
+
five ways. This rolls the offenders up by category: how many distinct checks, and the
|
|
870
|
+
widest document spread within it, so the story ("chunking is the problem") is visible
|
|
871
|
+
above the per-check detail.
|
|
872
|
+
"""
|
|
873
|
+
offenders = agg.get("offenders", [])
|
|
874
|
+
if not offenders:
|
|
875
|
+
return ""
|
|
876
|
+
total = max(agg.get("total", 0), 1)
|
|
877
|
+
groups: dict[str, dict] = {}
|
|
878
|
+
for o in offenders:
|
|
879
|
+
cat = _categorise(o["label"], o.get("step", ""))
|
|
880
|
+
g = groups.setdefault(cat, {"checks": 0, "max_docs": 0, "severity": "warn"})
|
|
881
|
+
g["checks"] += 1
|
|
882
|
+
g["max_docs"] = max(g["max_docs"], o["docs"])
|
|
883
|
+
if o.get("severity") == "hard":
|
|
884
|
+
g["severity"] = "hard"
|
|
885
|
+
ranked = sorted(groups.items(), key=lambda kv: (-kv[1]["max_docs"], kv[0]))
|
|
886
|
+
bars = ""
|
|
887
|
+
for cat, g in ranked:
|
|
888
|
+
pct = round(100 * g["max_docs"] / total)
|
|
889
|
+
cls = "bad" if g["severity"] == "hard" else "warn"
|
|
890
|
+
bars += (
|
|
891
|
+
f'<div class="subsys-row"><div class="subsys-name">{_esc(cat)}</div>'
|
|
892
|
+
f'<div class="subsys-bar"><i style="width:{pct}%;background:var(--{cls})"></i></div>'
|
|
893
|
+
f'<div class="subsys-meta">{g["checks"]} check(s) · up to {g["max_docs"]}/{total} docs</div></div>'
|
|
894
|
+
)
|
|
895
|
+
return (
|
|
896
|
+
'<div class="subsys-rollup">'
|
|
897
|
+
'<div class="subsys-head">By subsystem <span class="cat-badge">one root cause, grouped</span></div>'
|
|
898
|
+
f'{bars}</div>'
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
|
|
780
902
|
def _findings_section(cases: list[dict], agg: dict | None = None) -> str:
|
|
781
903
|
failed_items, skipped_items = [], []
|
|
782
904
|
advisory: Counter = Counter() # warn-severity check label -> count, across the corpus
|
|
@@ -1166,7 +1288,9 @@ def _trends_section(trends: list[dict]) -> str:
|
|
|
1166
1288
|
charts_block = ""
|
|
1167
1289
|
if runs >= 2:
|
|
1168
1290
|
series = sorted(trends, key=lambda t: str(t.get("timestamp", "")))
|
|
1169
|
-
|
|
1291
|
+
# Prefer the run DATE on the x-axis (a timeline reads temporally); fall back to the
|
|
1292
|
+
# run id only when no timestamp was recorded. The run-id table below keeps the ids.
|
|
1293
|
+
labels = [str(str(t.get("timestamp", ""))[:16].replace("T", " ") or t.get("run_id") or f"run {i+1}")
|
|
1170
1294
|
for i, t in enumerate(series)]
|
|
1171
1295
|
def _rate(num, den):
|
|
1172
1296
|
return [round(100 * t.get(num, 0) / t[den], 1) if t.get(den) else 0 for t in series]
|
|
@@ -1359,7 +1483,8 @@ function _insights(d){
|
|
|
1359
1483
|
}
|
|
1360
1484
|
return out;
|
|
1361
1485
|
}
|
|
1362
|
-
|
|
1486
|
+
var _STEP_CATEGORY={"Neptune Metadata":"Knowledge Graph","GraphDB Metadata":"Knowledge Graph","S3 Storage":"Object Storage","Cross-Stage Consistency":"Cross-Stage Consistency","API Upload":"Ingestion API"};
|
|
1487
|
+
function _categorise(label, step){
|
|
1363
1488
|
var l=(label||'').toLowerCase();
|
|
1364
1489
|
if(/e_tag|required field|metadata consist|positional|section|page metadata|directory|predicate|date format|email|field rule/.test(l)) return 'Metadata & Consistency';
|
|
1365
1490
|
if(/boundaries|chunk count|size within|ordering|overlap/.test(l)) return 'Chunk Structure';
|
|
@@ -1367,9 +1492,9 @@ function _categorise(label){
|
|
|
1367
1492
|
if(/fidelity|coverage|contaminat|duplicate|header|footer|table\/figure/.test(l)) return 'Content Preservation';
|
|
1368
1493
|
if(/index|schema/.test(l)) return 'Index & Schema';
|
|
1369
1494
|
if(/retriev|semantic|filter|hybrid|wildcard|empty query|recall|mrr|paraphrase|boundary-span|exact-term/.test(l)) return 'Retrieval & Query';
|
|
1370
|
-
return 'Other';
|
|
1495
|
+
return _STEP_CATEGORY[step]||'Other';
|
|
1371
1496
|
}
|
|
1372
|
-
function _buildCategorisedChecks(checks){
|
|
1497
|
+
function _buildCategorisedChecks(checks, step){
|
|
1373
1498
|
function _sup(c){ return c.supported !== false; } // unsupported = neutral
|
|
1374
1499
|
var evaluable=checks.filter(_sup);
|
|
1375
1500
|
var total=evaluable.length, passed=evaluable.filter(function(c){return c.passed;}).length;
|
|
@@ -1379,7 +1504,7 @@ function _buildCategorisedChecks(checks){
|
|
|
1379
1504
|
var hard=evaluable.filter(function(c){return !c.passed && (c.severity||'hard')!=='warn';}).length;
|
|
1380
1505
|
var advisory=evaluable.filter(function(c){return !c.passed && c.severity==='warn';}).length;
|
|
1381
1506
|
var groups={};
|
|
1382
|
-
checks.forEach(function(c){ var cat=_categorise(c.label); (groups[cat]=groups[cat]||[]).push(c); });
|
|
1507
|
+
checks.forEach(function(c){ var cat=_categorise(c.label, step); (groups[cat]=groups[cat]||[]).push(c); });
|
|
1383
1508
|
var cats=_CAT_ORDER.filter(function(co){return groups[co[0]];});
|
|
1384
1509
|
var note=(advisory?(' · '+advisory+' advisory'):'')+(unsupported?(' · '+unsupported+' not applicable'):'');
|
|
1385
1510
|
var html='<div class="check-detail" style="margin-bottom:6px"><b style="color:var(--ink)">'+passed+'/'+total+'</b> checks passed across '+cats.length+' categor'+(cats.length===1?'y':'ies')+note+(hard?'':'. Only hard failures gate; advisory is informational.')+'</div>';
|
|
@@ -1412,14 +1537,17 @@ function showStepDetail(ci, name){
|
|
|
1412
1537
|
var band=_bandOf(d.status);
|
|
1413
1538
|
document.getElementById('fbModalTitle').textContent=name+' — '+c.name;
|
|
1414
1539
|
var st=document.getElementById('fbModalStatus'); st.textContent=d.status; st.className='status-pill '+band;
|
|
1415
|
-
|
|
1540
|
+
// A step that reuses earlier steps' artifacts (e.g. Cross-Stage Consistency) does almost
|
|
1541
|
+
// no I/O and rounds to 0.0s, which reads as "not measured". Show <0.1s so near-instant is
|
|
1542
|
+
// legible as fast, not broken.
|
|
1543
|
+
document.getElementById('fbModalDur').textContent=(d.duration!=null?(d.duration<0.05?'<0.1s':d.duration+'s'):'');
|
|
1416
1544
|
document.getElementById('fbModalMsg').textContent=d.message||'—';
|
|
1417
1545
|
var keys=d.metrics?Object.keys(d.metrics):[];
|
|
1418
1546
|
var mRow=document.getElementById('fbModalMetricsRow');
|
|
1419
1547
|
if(keys.length){ document.getElementById('fbModalMetrics').innerHTML=keys.map(function(k){return '<span class="metric-chip">'+_escHtml(k)+': <b>'+_escHtml(d.metrics[k])+'</b></span>';}).join(''); mRow.style.display=''; }
|
|
1420
1548
|
else { mRow.style.display='none'; }
|
|
1421
1549
|
var box=document.getElementById('fbModalChecks');
|
|
1422
|
-
var inner=(!d.checks||!d.checks.length)?'<div class="check-detail">No individual checks for this step.</div>':_buildCategorisedChecks(d.checks);
|
|
1550
|
+
var inner=(!d.checks||!d.checks.length)?'<div class="check-detail">No individual checks for this step.</div>':_buildCategorisedChecks(d.checks, name);
|
|
1423
1551
|
// Chunks belong to the vector step — show them here (collapsed) rather than at document level.
|
|
1424
1552
|
if(name===c.chunksStep && c.chunks && c.chunks.items && c.chunks.items.length){
|
|
1425
1553
|
inner+=_buildChunksDisclosure(c.chunks);
|
|
@@ -1547,7 +1675,7 @@ function renderDoc(idx){
|
|
|
1547
1675
|
}
|
|
1548
1676
|
// Per-document Checks by Category — actionable at the document level.
|
|
1549
1677
|
var cc={}; _CAT_ORDER.forEach(function(co){cc[co[0]]={t:0,p:0};});
|
|
1550
|
-
d.order.forEach(function(name){ var s=d.steps[name]; if(!s) return; (s.checks||[]).forEach(function(ch){ if(ch.supported===false) return; var k=_categorise(ch.label); cc[k].t++; if(ch.passed) cc[k].p++; }); });
|
|
1678
|
+
d.order.forEach(function(name){ var s=d.steps[name]; if(!s) return; (s.checks||[]).forEach(function(ch){ if(ch.supported===false) return; var k=_categorise(ch.label, name); cc[k].t++; if(ch.passed) cc[k].p++; }); });
|
|
1551
1679
|
var catRows=_CAT_ORDER.filter(function(co){return cc[co[0]].t;}).map(function(co){
|
|
1552
1680
|
var x=cc[co[0]], rate=x.t?Math.round(100*x.p/x.t):0, rb=rate===100?'ok':(rate>=50?'warn':'bad');
|
|
1553
1681
|
var color=rb==='ok'?'#16a34a':rb==='warn'?'#d97706':'#dc2626';
|
|
@@ -1575,14 +1703,14 @@ function renderDoc(idx){
|
|
|
1575
1703
|
datasets:[{label:'Passed',data:s.map(function(x){return x.passed;}),backgroundColor:OK,stack:'a'},
|
|
1576
1704
|
{label:'Failed',data:s.map(function(x){return x.failed+x.error;}),backgroundColor:BAD,stack:'a'},
|
|
1577
1705
|
{label:'Skipped',data:s.map(function(x){return x.skipped;}),backgroundColor:NA,stack:'a'}]},
|
|
1578
|
-
options:{plugins:{legend:{labels:{color:c.text,font:{size:11}}}},
|
|
1706
|
+
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:c.text,font:{size:11}}}},
|
|
1579
1707
|
scales:{x:{stacked:true,ticks:{color:c.text,font:{size:10}},grid:{color:c.grid}},y:{stacked:true,ticks:{color:c.text,precision:0},grid:{color:c.grid}}}}});
|
|
1580
1708
|
var cr=REPORT.agg.cat_rows||[];
|
|
1581
1709
|
var catRate=cr.map(function(x){return x.total?Math.round(100*x.passed/x.total):0;});
|
|
1582
1710
|
var cat=new Chart(document.getElementById('catChart'),{type:'bar',
|
|
1583
1711
|
data:{labels:cr.map(function(x){return x.name.replace(' & ','/').replace(' Quality','').replace(' Preservation','').replace(' Consistency','').replace(' Structure','').replace(' Query','');}),
|
|
1584
1712
|
datasets:[{label:'Pass rate %',data:catRate,backgroundColor:catRate.map(function(r){return r===100?OK:(r>=50?WARN:BAD);}),borderWidth:0,borderRadius:4}]},
|
|
1585
|
-
options:{indexAxis:'y',plugins:{legend:{display:false}},
|
|
1713
|
+
options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},
|
|
1586
1714
|
scales:{x:{max:100,ticks:{color:c.text,callback:function(v){return v+'%';}},grid:{color:c.grid}},y:{ticks:{color:c.text,font:{size:10}},grid:{display:false}}}}});
|
|
1587
1715
|
if(typeof allCharts!=='undefined'){allCharts.push(donut,step,cat);}
|
|
1588
1716
|
})();
|
|
@@ -1646,6 +1774,13 @@ def generate_batch_dashboard(
|
|
|
1646
1774
|
agg["wall_seconds"] = round(wall_seconds, 1)
|
|
1647
1775
|
if concurrency is not None:
|
|
1648
1776
|
agg["concurrency"] = concurrency
|
|
1777
|
+
# Hygiene delta vs the previous run: trends are newest-first and already include THIS
|
|
1778
|
+
# run (recorded before the report), so the prior run is trends[1]. Lets the gauge show
|
|
1779
|
+
# direction ("62% POOR +3 vs previous") without opening the trends panel.
|
|
1780
|
+
if agg.get("hygiene") and trends and len(trends) > 1:
|
|
1781
|
+
prev = trends[1].get("hygiene_score")
|
|
1782
|
+
if isinstance(prev, (int, float)):
|
|
1783
|
+
agg["hygiene"]["delta"] = round(agg["hygiene"]["score"] - prev, 1)
|
|
1649
1784
|
# Shape + coverage are computed on FULL check data, before the scale-trim below
|
|
1650
1785
|
# drops per-check detail for passed documents.
|
|
1651
1786
|
shape_inner = _profile_section(profile) + _coverage_section(cases)
|
|
@@ -215,20 +215,22 @@ main { max-width:1800px; margin:0 auto; padding:24px 32px; }
|
|
|
215
215
|
|
|
216
216
|
/* Charts */
|
|
217
217
|
.charts-row {
|
|
218
|
-
display:grid; grid-template-columns:
|
|
218
|
+
display:grid; grid-template-columns:repeat(3, 1fr); gap:16px; margin-top:24px;
|
|
219
219
|
}
|
|
220
220
|
@media(max-width:1100px) { .charts-row { grid-template-columns:repeat(2,1fr); } }
|
|
221
221
|
@media(max-width:700px) { .charts-row { grid-template-columns:1fr; } }
|
|
222
222
|
.chart-box {
|
|
223
223
|
background:var(--panel); border:1px solid var(--line); border-radius:var(--radius);
|
|
224
224
|
padding:16px; box-shadow:var(--box-shadow); position:relative;
|
|
225
|
+
/* flex column so the canvas fills the box height (grid stretches all boxes equal),
|
|
226
|
+
removing the empty real-estate the aspect-ratio bar charts used to leave. */
|
|
227
|
+
display:flex; flex-direction:column;
|
|
225
228
|
}
|
|
226
229
|
.chart-box h3 {
|
|
227
230
|
font-size:13px; font-weight:600; color:var(--muted);
|
|
228
231
|
text-transform:uppercase; letter-spacing:.06em; margin:0 0 12px 0;
|
|
229
232
|
}
|
|
230
|
-
.chart-box canvas { width:100%!important; max-height:
|
|
231
|
-
#donutChart { max-height:320px!important; }
|
|
233
|
+
.chart-box canvas { width:100%!important; flex:1 1 auto; min-height:230px; max-height:320px; }
|
|
232
234
|
|
|
233
235
|
/* Tables */
|
|
234
236
|
.table-card { background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--box-shadow); overflow:hidden; }
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/trend_tracker.py
RENAMED
|
@@ -56,7 +56,8 @@ class TrendTracker:
|
|
|
56
56
|
|
|
57
57
|
# Columns added after the original schema; absent in older DBs.
|
|
58
58
|
_ADDED_COLUMNS = {"project": "TEXT", "steps_total": "INTEGER",
|
|
59
|
-
"steps_passed": "INTEGER", "steps_failed": "INTEGER"
|
|
59
|
+
"steps_passed": "INTEGER", "steps_failed": "INTEGER",
|
|
60
|
+
"hygiene_score": "REAL"}
|
|
60
61
|
|
|
61
62
|
def _ensure_schema(self) -> None:
|
|
62
63
|
with self._connect() as conn:
|
|
@@ -98,12 +99,23 @@ class TrendTracker:
|
|
|
98
99
|
)
|
|
99
100
|
)
|
|
100
101
|
|
|
102
|
+
# KB Hygiene score for this run, so trends can show its direction over time. Computed
|
|
103
|
+
# from the same deterministic aggregation the report uses; None when there are no
|
|
104
|
+
# advisory checks to score.
|
|
105
|
+
hygiene_score = None
|
|
106
|
+
try:
|
|
107
|
+
from ingestion_validation.report import hygiene_from_results
|
|
108
|
+
hy = hygiene_from_results(results)
|
|
109
|
+
hygiene_score = hy["score"] if hy else None
|
|
110
|
+
except Exception: # noqa: BLE001 - hygiene is advisory context; never break recording
|
|
111
|
+
hygiene_score = None
|
|
112
|
+
|
|
101
113
|
with self._connect() as conn:
|
|
102
114
|
conn.execute(
|
|
103
|
-
"INSERT INTO runs (run_id,project,timestamp,environment,total_cases,passed,failed,errors,skipped,total_duration_seconds,mode,steps_total,steps_passed,steps_failed)"
|
|
104
|
-
" VALUES (
|
|
115
|
+
"INSERT INTO runs (run_id,project,timestamp,environment,total_cases,passed,failed,errors,skipped,total_duration_seconds,mode,steps_total,steps_passed,steps_failed,hygiene_score)"
|
|
116
|
+
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
105
117
|
(run_id, project, ts, environment, len(results), agg["passed"], agg["failed"], agg["errors"], agg["skipped"], round(agg["duration"], 3), mode,
|
|
106
|
-
agg["steps_total"], agg["steps_passed"], agg["steps_failed"]),
|
|
118
|
+
agg["steps_total"], agg["steps_passed"], agg["steps_failed"], hygiene_score),
|
|
107
119
|
)
|
|
108
120
|
conn.executemany(
|
|
109
121
|
"INSERT INTO run_cases (run_id,case_name,file_name,document_id,overall,duration_seconds,step_details)"
|
|
@@ -479,7 +479,8 @@ def test_trends_render_tabbed_time_series_with_multiple_runs():
|
|
|
479
479
|
m = re.search(r"window\.TREND_SERIES=(\{.*?\});", html, re.S)
|
|
480
480
|
assert m is not None
|
|
481
481
|
s = json.loads(m.group(1))
|
|
482
|
-
|
|
482
|
+
# x-axis labels are the run DATE (a timeline reads temporally), oldest -> newest
|
|
483
|
+
assert s["labels"] == ["2026-07-12 09:00", "2026-07-14 09:00", "2026-07-17 09:00"]
|
|
483
484
|
# Pass Rate carries two lines - documents and steps; the gap is the failures.
|
|
484
485
|
doc_line = next(sr for sr in s["metrics"]["passRate"]["series"] if sr["name"] == "Documents")
|
|
485
486
|
assert doc_line["data"] == [84.0, 59.8, 100.0]
|
|
@@ -560,3 +561,105 @@ def test_embedded_triples_keep_only_rendered_fields():
|
|
|
560
561
|
triples = report_mod._result_payload("d.pdf", r)["steps"]["Neptune Metadata"]["triples"]
|
|
561
562
|
assert triples == [{"predicate": "documentId", "value": "DOC-1"}]
|
|
562
563
|
assert "subject" not in triples[0] and "predicate_uri" not in triples[0]
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def test_stage_checks_get_named_categories_not_other():
|
|
567
|
+
# Graph/storage/cross-stage checks with no semantic-pattern match were all dumped into
|
|
568
|
+
# "Other" (54% of checks on a multi-stage run). They now fall back to their stage's
|
|
569
|
+
# category so the breakdown is transparent.
|
|
570
|
+
r = PipelineResult(document_id="d", file_name="d.pdf")
|
|
571
|
+
r.add_step(StepResult("S3 Storage", StepStatus.PASSED, "ok",
|
|
572
|
+
checks=[{"label": "File exists in storage", "passed": True}]))
|
|
573
|
+
r.add_step(StepResult("Neptune Metadata", StepStatus.PASSED, "ok",
|
|
574
|
+
checks=[{"label": "Document found in graph", "passed": True},
|
|
575
|
+
{"label": "Orphan nodes absent", "passed": True}]))
|
|
576
|
+
r.add_step(StepResult("Cross-Stage Consistency", StepStatus.PASSED, "ok",
|
|
577
|
+
checks=[{"label": "Stores agree on document", "passed": True}]))
|
|
578
|
+
agg = report_mod._aggregate([report_mod._result_payload("d.pdf", r)])
|
|
579
|
+
cats = {row["name"]: row["total"] for row in agg["cat_rows"]}
|
|
580
|
+
assert cats.get("Knowledge Graph") == 2 # graph checks, not "Other"
|
|
581
|
+
assert cats.get("Object Storage") == 1
|
|
582
|
+
assert cats.get("Cross-Stage Consistency") == 1
|
|
583
|
+
assert "Other" not in cats # nothing uncategorised here
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def test_categorise_semantic_pattern_still_wins_over_step():
|
|
587
|
+
# a chunk-structure check on the vector step keeps its semantic category
|
|
588
|
+
assert report_mod._categorise("Overlap correctness", "Redis Chunks") == "Chunk Structure"
|
|
589
|
+
# a graph check with no semantic match takes the stage category
|
|
590
|
+
assert report_mod._categorise("Document found in graph", "Neptune Metadata") == "Knowledge Graph"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_narrative_reads_the_run_deterministically():
|
|
594
|
+
def case(i, overlap_ok):
|
|
595
|
+
r = PipelineResult(document_id=f"d{i}", file_name=f"d{i}.pdf")
|
|
596
|
+
r.add_step(StepResult("Redis Chunks", StepStatus.PASSED, "ok", checks=[
|
|
597
|
+
{"label": "Chunks exist", "passed": True},
|
|
598
|
+
{"label": "Overlap correctness", "passed": overlap_ok, "severity": "warn"}]))
|
|
599
|
+
r.finalize()
|
|
600
|
+
return report_mod._result_payload(f"d{i}.pdf", r)
|
|
601
|
+
# chunking poor -> names it as the dominant issue with the config-not-corruption reading
|
|
602
|
+
agg = report_mod._aggregate([case(i, overlap_ok=(i >= 8)) for i in range(10)])
|
|
603
|
+
text = report_mod._narrative(agg)
|
|
604
|
+
assert "passed every integrity gate" in text
|
|
605
|
+
assert "Chunk Structure" in text and "dominant issue" in text
|
|
606
|
+
assert "configuration rather than data corruption" in text
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def test_narrative_flags_incomplete_coverage():
|
|
610
|
+
def case(i):
|
|
611
|
+
r = PipelineResult(document_id=f"d{i}", file_name=f"d{i}.pdf")
|
|
612
|
+
r.add_step(StepResult("Redis Chunks", StepStatus.PASSED, "ok",
|
|
613
|
+
checks=[{"label": "Chunks exist", "passed": True}]))
|
|
614
|
+
r.add_step(StepResult("Neptune Metadata", StepStatus.SKIPPED, "unreachable",
|
|
615
|
+
details={"skip_reason": "unreachable", "unreachable": True},
|
|
616
|
+
checks=[{"label": "reachable", "passed": False}]))
|
|
617
|
+
r.finalize()
|
|
618
|
+
return report_mod._result_payload(f"d{i}.pdf", r)
|
|
619
|
+
text = report_mod._narrative(report_mod._aggregate([case(i) for i in range(4)]))
|
|
620
|
+
assert "did not run on" in text and "less than the whole pipeline" in text
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def test_subsystem_rollup_groups_offenders_by_category():
|
|
624
|
+
def case(i):
|
|
625
|
+
r = PipelineResult(document_id=f"d{i}", file_name=f"d{i}.pdf")
|
|
626
|
+
r.add_step(StepResult("Redis Chunks", StepStatus.PASSED, "ok", checks=[
|
|
627
|
+
{"label": "Overlap correctness", "passed": i >= 8, "severity": "warn"},
|
|
628
|
+
{"label": "Chunk boundaries clean", "passed": i >= 7, "severity": "warn"},
|
|
629
|
+
{"label": "No near-duplicate embeddings", "passed": i >= 4, "severity": "warn"}]))
|
|
630
|
+
r.finalize()
|
|
631
|
+
return report_mod._result_payload(f"d{i}.pdf", r)
|
|
632
|
+
agg = report_mod._aggregate([case(i) for i in range(10)])
|
|
633
|
+
html = report_mod._subsystem_rollup(agg)
|
|
634
|
+
# two chunk checks roll up into one Chunk Structure row (one root cause, not two)
|
|
635
|
+
assert "Chunk Structure" in html and "2 check(s)" in html
|
|
636
|
+
assert "Embedding Quality" in html and "1 check(s)" in html
|
|
637
|
+
# Chunk Structure (worst spread 8/10) ranks above Embedding Quality
|
|
638
|
+
assert html.index("Chunk Structure") < html.index("Embedding Quality")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def test_hygiene_gauge_shows_delta_direction():
|
|
642
|
+
up = report_mod._hygiene_gauge({"score": 62, "tier": "POOR", "band": "bad", "worst": None, "delta": 3.0})
|
|
643
|
+
assert "↑" in up and "3" in up and "hygiene-delta ok" in up
|
|
644
|
+
down = report_mod._hygiene_gauge({"score": 58, "tier": "POOR", "band": "bad", "worst": None, "delta": -4.0})
|
|
645
|
+
assert "↓" in down and "hygiene-delta bad" in down
|
|
646
|
+
flat = report_mod._hygiene_gauge({"score": 62, "tier": "POOR", "band": "bad", "worst": None, "delta": 0})
|
|
647
|
+
assert "hygiene-delta" not in flat # no change -> no arrow
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def test_trend_tracker_persists_hygiene_score(tmp_path):
|
|
651
|
+
from ingestion_validation.trend_tracker import TrendTracker
|
|
652
|
+
|
|
653
|
+
def results(overlap_ok):
|
|
654
|
+
r = PipelineResult(document_id="d", file_name="d.pdf")
|
|
655
|
+
r.add_step(StepResult("Redis Chunks", StepStatus.PASSED, "ok", checks=[
|
|
656
|
+
{"label": "Chunks exist", "passed": True},
|
|
657
|
+
{"label": "Overlap correctness", "passed": overlap_ok, "severity": "warn"}]))
|
|
658
|
+
r.finalize()
|
|
659
|
+
return [{"name": "d.pdf", "result": r}]
|
|
660
|
+
|
|
661
|
+
tt = TrendTracker(str(tmp_path / "t.db"))
|
|
662
|
+
tt.record(results(False), run_id="r1", project="p") # hygiene 0%
|
|
663
|
+
tt.record(results(True), run_id="r2", project="p") # hygiene 100%
|
|
664
|
+
scores = [r.get("hygiene_score") for r in tt.summary(project="p")]
|
|
665
|
+
assert scores == [100.0, 0.0] # newest-first, both persisted
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/baseline.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/chunk_coherence.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/config/__init__.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/config_lint.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/content_inspect.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/corpus_health.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/diagnostics.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/embedding_drift.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/eval_interop.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/grounding.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/init_wizard.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/live_dashboard.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/mcp_server.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/models/__init__.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/notifications.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/profiler.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/provenance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/providers/graph.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/providers/source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/remediation.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/analysis.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/export.py
RENAMED
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/report/shell.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/ingestion_validation/validators/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_discovery_parallel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_retrieval_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_universal_concerns.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-2.4.0 → compass_kb_validation-2.5.0}/tests/test_vectorstore_filters.py
RENAMED
|
File without changes
|