compass-kb-validation 3.0.0__tar.gz → 3.2.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-3.0.0 → compass_kb_validation-3.2.0}/PKG-INFO +1 -1
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/__init__.py +1 -1
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/cli.py +7 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/config/__init__.py +8 -1
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/report/__init__.py +341 -65
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass_kb_validation.egg-info/PKG-INFO +1 -1
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/discover_and_validate.py +34 -3
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/ingestion_validation/__init__.py +28 -5
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/run_validation.py +1 -1
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_compat_shim.py +10 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_config.py +16 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_discover_cli.py +17 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_reporting.py +129 -5
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/LICENSE +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/README.md +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/__main__.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/ask.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/baseline.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/chunk_coherence.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/citation.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/config_lint.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/content_inspect.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/corpus.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/corpus_health.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/diagnostics.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/embedding_drift.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/eval_interop.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/evidence.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/grounding.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/init_wizard.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/live_dashboard.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/mcp_server.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/models/__init__.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/notifications.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/pipeline/__init__.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/profiler.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/provenance.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/providers/__init__.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/providers/embedding.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/providers/graph.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/providers/source.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/providers/storage.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/providers/vectorstore.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/remediation.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/report/_report_css.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/report/_report_js.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/report/analysis.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/report/audit.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/report/export.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/report/shell.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/retrieval_analysis.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/text_probes.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/trend_tracker.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/utils/__init__.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/__init__.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/api_upload.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/base.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/base_sparql.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/cross_stage.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/graphdb_metadata.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/neptune_metadata.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/redis_chunks.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/registry.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/retrieval_quality.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass/validators/s3_storage.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass_kb_validation.egg-info/SOURCES.txt +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass_kb_validation.egg-info/requires.txt +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/profile_kb.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/pyproject.toml +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/setup.cfg +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_alt_adapters.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_analysis_report.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_api_upload.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_ask.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_baseline.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_benchmark.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_check_catalog.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_citation.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_cli.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_config_lint.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_content_inspect.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_corpus.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_corpus_health.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_coverage.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_cross_stage.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_diagnostics.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_discovery_parallel.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_embedding_drift.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_eval_interop.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_evidence.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_gate.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_graph_transport.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_graph_validators.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_grounding.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_init_wizard.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_manifest.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_mcp.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_migration.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_models.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_neptune_bugfixes.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_neptune_bugfixes2.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_notifications.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_pipeline.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_profiler.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_providers.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_redis_chunks.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_registry.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_remediation.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_retrieval_assertions.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_retrieval_quality.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_review_hardening.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_run_cli.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_sources.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_storage_validator.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_trends.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_universal_concerns.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_utils.py +0 -0
- {compass_kb_validation-3.0.0 → compass_kb_validation-3.2.0}/tests/test_vectorstore_filters.py +0 -0
|
@@ -36,6 +36,13 @@ pipeline:
|
|
|
36
36
|
critical_steps: []
|
|
37
37
|
readiness_threshold: 1.0 # READY when document pass-rate >= this (e.g. 0.99)
|
|
38
38
|
|
|
39
|
+
# Advisory checks you have ACCEPTED as expected for this corpus (e.g. an insurance KB where
|
|
40
|
+
# near-duplicate boilerplate is normal). They drop out of the hygiene / Platform Health scores
|
|
41
|
+
# but are still counted and shown ("N suppressed"). Only advisory checks - a hard gating failure
|
|
42
|
+
# is never suppressed. Ad-hoc equivalent: `compass discover --suppress "Near-duplicate embeddings"`.
|
|
43
|
+
# suppressions:
|
|
44
|
+
# - "Near-duplicate embeddings"
|
|
45
|
+
|
|
39
46
|
environments:
|
|
40
47
|
dev:
|
|
41
48
|
redis:
|
|
@@ -750,6 +750,12 @@ class Settings:
|
|
|
750
750
|
display_name: str = ""
|
|
751
751
|
environment: str = "dev"
|
|
752
752
|
skip_steps: list[str] = field(default_factory=list)
|
|
753
|
+
# Advisory check labels the project has ACCEPTED as expected for this corpus (e.g. an
|
|
754
|
+
# insurance KB where near-duplicate boilerplate is normal). Suppressed advisories are
|
|
755
|
+
# excluded from the hygiene / Platform Health scores but still counted and shown ("N
|
|
756
|
+
# suppressed"). Only advisory (warn) checks can be suppressed - a hard gating failure
|
|
757
|
+
# always counts, so a known-issue can never mask a real failure.
|
|
758
|
+
suppressions: list[str] = field(default_factory=list)
|
|
753
759
|
# Advisories raised while loading (e.g. an unrecognized top-level block that was
|
|
754
760
|
# ignored). Surfaced by validate_config so a silently-dropped key is never invisible.
|
|
755
761
|
config_warnings: list[str] = field(default_factory=list)
|
|
@@ -760,7 +766,7 @@ class Settings:
|
|
|
760
766
|
# --------------------------------------------------------------------------- #
|
|
761
767
|
# Non-service top-level keys that are legitimately not a service block.
|
|
762
768
|
_META_KEYS = frozenset({"default_env", "display_name", "environments", "skip_steps",
|
|
763
|
-
"pipeline_dependencies"})
|
|
769
|
+
"pipeline_dependencies", "suppressions"})
|
|
764
770
|
# The service-config blocks that may appear at the top level or under an environment.
|
|
765
771
|
# Single source of truth: the flatten loop AND the unknown-key check both read this, so a
|
|
766
772
|
# new block can never be added to one and forgotten in the other (which is exactly how
|
|
@@ -903,6 +909,7 @@ def load_settings(
|
|
|
903
909
|
display_name=merged.get("display_name", ""),
|
|
904
910
|
environment=resolved_env,
|
|
905
911
|
skip_steps=list(merged.get("skip_steps", [])),
|
|
912
|
+
suppressions=list(merged.get("suppressions", [])),
|
|
906
913
|
)
|
|
907
914
|
# The embedding block's declared dimension IS the index's expected dimension. Cross-fill
|
|
908
915
|
# it into the vector-store check when that check has no explicit value, so a project
|
|
@@ -80,8 +80,11 @@ _STEP_CATEGORY = {
|
|
|
80
80
|
"API Upload": "Ingestion API",
|
|
81
81
|
}
|
|
82
82
|
_CAT_PATTERNS = [
|
|
83
|
-
("Metadata & Consistency", re.compile(r"e_tag|required field|metadata consist|
|
|
84
|
-
|
|
83
|
+
("Metadata & Consistency", re.compile(r"e_tag|required field|metadata consist|page metadata|directory|predicate|date format|email|field rule", re.I)),
|
|
84
|
+
# 'positional correctness', 'chunk structure coherence' and 'all chunks validated' are
|
|
85
|
+
# STRUCTURAL (chunk ordering / integrity), not metadata - keep them out of the metadata
|
|
86
|
+
# bucket and out of an opaque "Other".
|
|
87
|
+
("Chunk Structure", re.compile(r"boundaries|chunk count|chunk structure|all chunks|coheren|positional|section split|size within|ordering|overlap", re.I)),
|
|
85
88
|
("Embedding Quality", re.compile(r"embedding|normaliz|near-duplicate|dimension|distance metric", re.I)),
|
|
86
89
|
("Content Preservation", re.compile(r"fidelity|coverage|contaminat|duplicate|header|footer|table/figure", re.I)),
|
|
87
90
|
("Index & Schema", re.compile(r"index|schema", re.I)),
|
|
@@ -115,6 +118,35 @@ li .check-hint{margin:4px 0 0 0;display:block;}
|
|
|
115
118
|
.rec-list{padding:14px 16px 16px;display:flex;flex-direction:column;gap:10px;}
|
|
116
119
|
.rec-fix{font-size:12.5px;line-height:1.55;color:var(--ink);}
|
|
117
120
|
.rec-more{font-size:12px;color:var(--muted);padding:4px 2px;}
|
|
121
|
+
/* Action Plan confidence tiers: Observed (fact) / Likely (primary) / Possible (alternates). */
|
|
122
|
+
.rca-line{display:flex;gap:8px;align-items:baseline;font-size:12.5px;line-height:1.55;color:var(--ink);margin-top:3px;}
|
|
123
|
+
.rca-line:first-child{margin-top:0;}
|
|
124
|
+
.conf-tag{flex:none;font-size:9.5px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;padding:1px 7px;border-radius:999px;line-height:1.5;}
|
|
125
|
+
.conf-tag.observed{background:rgba(37,99,235,.13);color:var(--accent);}
|
|
126
|
+
.conf-tag.likely{background:rgba(217,119,6,.14);color:var(--warn);}
|
|
127
|
+
.conf-tag.possible{background:var(--chip-bg);color:var(--muted);}
|
|
128
|
+
/* Platform Health: a compact per-subsystem pass-rate rollup (dimensions are dynamic). */
|
|
129
|
+
.phz{margin-top:12px;padding-top:11px;border-top:1px solid var(--line);}
|
|
130
|
+
.phz-head{display:flex;align-items:baseline;gap:6px;margin-bottom:8px;}
|
|
131
|
+
.phz-cap{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:700;}
|
|
132
|
+
.phz-overall{font-size:17px;font-weight:800;margin-left:auto;font-variant-numeric:tabular-nums;}
|
|
133
|
+
.phz-overall.ok{color:var(--ok);}.phz-overall.warn{color:var(--warn);}.phz-overall.bad{color:var(--bad);}
|
|
134
|
+
.phz-max{font-size:11px;color:var(--muted);font-weight:600;}
|
|
135
|
+
.phz-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:8px 16px;}
|
|
136
|
+
.phz-top{display:flex;justify-content:space-between;align-items:baseline;font-size:11px;margin-bottom:2px;gap:8px;}
|
|
137
|
+
.phz-name{color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
138
|
+
.phz-score{flex:none;}
|
|
139
|
+
.phz-score{font-weight:800;font-variant-numeric:tabular-nums;}
|
|
140
|
+
.phz-score.ok{color:var(--ok);}.phz-score.warn{color:var(--warn);}.phz-score.bad{color:var(--bad);}
|
|
141
|
+
.phz-bar{height:4px;border-radius:999px;background:var(--chip-bg);overflow:hidden;}
|
|
142
|
+
.phz-bar i{display:block;height:100%;border-radius:999px;}
|
|
143
|
+
.phz-bar i.ok{background:var(--ok);}.phz-bar i.warn{background:var(--warn);}.phz-bar i.bad{background:var(--bad);}
|
|
144
|
+
/* Engineering Wins: the healthy counterpart to the Action Plan. */
|
|
145
|
+
.wins-block{margin-top:16px;padding:12px 14px;background:rgba(22,163,74,.06);border:1px solid rgba(22,163,74,.2);border-radius:10px;}
|
|
146
|
+
.wins-cap{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--ok);font-weight:800;margin-bottom:8px;}
|
|
147
|
+
.wins-grid{display:flex;flex-wrap:wrap;gap:7px 16px;}
|
|
148
|
+
.win-item{display:flex;align-items:center;gap:6px;font-size:12.5px;color:var(--ink);}
|
|
149
|
+
.win-tick{color:var(--ok);font-weight:800;}
|
|
118
150
|
@media(max-width:600px){.subsys-row{grid-template-columns:1fr;gap:3px;}}
|
|
119
151
|
.exec-bullets{margin:0 0 4px;padding-left:18px;font-size:13.5px;line-height:1.75;color:var(--ink);}
|
|
120
152
|
.exec-bullets li{margin:0;}
|
|
@@ -285,7 +317,8 @@ def _result_payload(name: str, r: PipelineResult) -> dict:
|
|
|
285
317
|
}
|
|
286
318
|
|
|
287
319
|
|
|
288
|
-
def _aggregate(cases: list[dict], hide_skipped: bool = True
|
|
320
|
+
def _aggregate(cases: list[dict], hide_skipped: bool = True,
|
|
321
|
+
suppress: set[str] | None = None) -> dict:
|
|
289
322
|
total = len(cases)
|
|
290
323
|
passed = sum(1 for c in cases if c["overall"] == "PASSED")
|
|
291
324
|
failed = sum(1 for c in cases if c["overall"] == "FAILED")
|
|
@@ -298,6 +331,12 @@ def _aggregate(cases: list[dict], hide_skipped: bool = True) -> dict:
|
|
|
298
331
|
# check is either a HARD failure (fails the step) or an ADVISORY warning (does not).
|
|
299
332
|
# Neutral checks (the backend/config can't evaluate them) are counted separately.
|
|
300
333
|
checks_hard = checks_advisory = checks_neutral = 0
|
|
334
|
+
# Advisory checks a project has explicitly accepted (config `suppressions`). They are
|
|
335
|
+
# excluded from scoring - like neutral checks - but tallied and shown, never hidden. HARD
|
|
336
|
+
# (gating) checks can NEVER be suppressed, so an accepted known-issue can't mask a failure.
|
|
337
|
+
checks_suppressed = 0
|
|
338
|
+
suppressed: Counter = Counter()
|
|
339
|
+
suppress = suppress or set()
|
|
301
340
|
cat = {name: {"total": 0, "passed": 0, "adv_total": 0, "adv_passed": 0} for name, _ in _CAT_ORDER}
|
|
302
341
|
# Which checks fail across the MOST documents. One systemic cause usually shows up as
|
|
303
342
|
# one label repeated over many documents, so ranking by document count points at the
|
|
@@ -337,10 +376,19 @@ def _aggregate(cases: list[dict], hide_skipped: bool = True) -> dict:
|
|
|
337
376
|
if ch.get("supported", True) is False:
|
|
338
377
|
checks_neutral += 1 # neutral — backend can't evaluate this check
|
|
339
378
|
continue
|
|
340
|
-
|
|
379
|
+
_label = ch.get("label", "")
|
|
380
|
+
_sev = "warn" if str(ch.get("severity", "hard")).lower() == "warn" else "hard"
|
|
381
|
+
# Suppressed = an ADVISORY check the project accepted as expected (e.g. known
|
|
382
|
+
# boilerplate near-duplicates). Excluded from scoring but counted + shown.
|
|
383
|
+
# Only advisory can be suppressed - a hard/gating check ignores the list.
|
|
384
|
+
if _sev == "warn" and _label in suppress:
|
|
385
|
+
checks_suppressed += 1
|
|
386
|
+
suppressed[_label] += 1
|
|
387
|
+
continue
|
|
388
|
+
k = _categorise(_label, step_name)
|
|
341
389
|
cat[k]["total"] += 1
|
|
342
390
|
ch_passed = bool(ch.get("passed"))
|
|
343
|
-
sev =
|
|
391
|
+
sev = _sev
|
|
344
392
|
# Hygiene tracks ADVISORY (WARN) checks only - the quality BEYOND the
|
|
345
393
|
# integrity gate. HARD checks passing is what READY already reports, so
|
|
346
394
|
# counting them would just pull hygiene toward 100 and hide the signal.
|
|
@@ -355,9 +403,17 @@ def _aggregate(cases: list[dict], hide_skipped: bool = True) -> dict:
|
|
|
355
403
|
checks_advisory += 1
|
|
356
404
|
else:
|
|
357
405
|
checks_hard += 1
|
|
358
|
-
label =
|
|
359
|
-
rec = offenders.setdefault(
|
|
406
|
+
label = _label
|
|
407
|
+
rec = offenders.setdefault(
|
|
408
|
+
label, {"label": label, "docs": 0, "severity": sev, "step": step_name, "detail": ""})
|
|
360
409
|
rec["docs"] += 1
|
|
410
|
+
# Keep one concrete, measured example (the check's own detail, e.g.
|
|
411
|
+
# "4200 (~3000 expected)") so the Action Plan can show a specific number
|
|
412
|
+
# instead of a generic cause. First non-empty wins - deterministic.
|
|
413
|
+
if not rec["detail"]:
|
|
414
|
+
_dv = str(ch.get("detail") or "").strip()
|
|
415
|
+
if _dv:
|
|
416
|
+
rec["detail"] = _dv
|
|
361
417
|
# a label is HARD overall if it ever gates
|
|
362
418
|
if sev == "hard":
|
|
363
419
|
rec["severity"] = "hard"
|
|
@@ -401,6 +457,11 @@ def _aggregate(cases: list[dict], hide_skipped: bool = True) -> dict:
|
|
|
401
457
|
# rests on less than the full pipeline.
|
|
402
458
|
"docs_incomplete": docs_incomplete,
|
|
403
459
|
"hygiene": hygiene,
|
|
460
|
+
# Accepted advisory issues (config `suppressions`): count + per-label tally, shown
|
|
461
|
+
# so a suppressed warning is disclosed, never silently dropped.
|
|
462
|
+
"checks_suppressed": checks_suppressed,
|
|
463
|
+
"suppressed": [{"label": lbl, "docs": n} for lbl, n in
|
|
464
|
+
sorted(suppressed.items(), key=lambda kv: (-kv[1], kv[0]))],
|
|
404
465
|
}
|
|
405
466
|
|
|
406
467
|
|
|
@@ -412,15 +473,16 @@ _HYGIENE_TIERS = (
|
|
|
412
473
|
)
|
|
413
474
|
|
|
414
475
|
|
|
415
|
-
def hygiene_from_results(results: list[dict]) -> dict | None:
|
|
476
|
+
def hygiene_from_results(results: list[dict], suppress: list[str] | None = None) -> dict | None:
|
|
416
477
|
"""Public: compute the KB Hygiene score from a list of ``{name, result}`` items.
|
|
417
478
|
|
|
418
479
|
Same deterministic scoring the report uses, exposed so the CLI can gate on it
|
|
419
|
-
(``--quality-threshold``) without rendering HTML.
|
|
420
|
-
advisory
|
|
480
|
+
(``--quality-threshold``) without rendering HTML. ``suppress`` accepts the same accepted
|
|
481
|
+
advisory labels as the report, so the gate and the report agree. Returns None if there
|
|
482
|
+
are no advisory checks to score.
|
|
421
483
|
"""
|
|
422
484
|
cases = [_result_payload(item.get("name", item["result"].file_name), item["result"]) for item in results]
|
|
423
|
-
return _aggregate(cases).get("hygiene")
|
|
485
|
+
return _aggregate(cases, suppress=set(suppress or [])).get("hygiene")
|
|
424
486
|
|
|
425
487
|
|
|
426
488
|
def _hygiene_score(cat: dict) -> dict | None:
|
|
@@ -661,6 +723,12 @@ def _exec_summary(agg: dict, corpus: dict | None = None, cases: list[dict] | Non
|
|
|
661
723
|
f'these documents - see Coverage">{agg["docs_incomplete"]} not fully validated</span>')
|
|
662
724
|
if agg.get("checks_advisory"):
|
|
663
725
|
chips.append(f'<span class="hero-chip">{agg["checks_advisory"]} advisory</span>')
|
|
726
|
+
if agg.get("checks_suppressed"):
|
|
727
|
+
# Accepted known-issues: disclosed, never hidden. Tooltip names what was suppressed.
|
|
728
|
+
acc = ", ".join(f'{s["label"]} (x{s["docs"]})' for s in agg.get("suppressed", [])[:8])
|
|
729
|
+
chips.append(f'<span class="hero-chip" data-tip="Accepted advisory issues (config '
|
|
730
|
+
f'suppressions) - excluded from hygiene & health, never from the gate: '
|
|
731
|
+
f'{_esc(acc)}">{agg["checks_suppressed"]} suppressed</span>')
|
|
664
732
|
chips_html = f'<div class="hero-chips">{"".join(chips)}</div>' if chips else ""
|
|
665
733
|
t = max(agg["total"], 1)
|
|
666
734
|
seg = ""
|
|
@@ -692,8 +760,10 @@ def _exec_summary(agg: dict, corpus: dict | None = None, cases: list[dict] | Non
|
|
|
692
760
|
<span><i class="warn"></i> {agg['partial']} partial</span>
|
|
693
761
|
<span><i class="bad"></i> {agg['failed']} failed</span>
|
|
694
762
|
</div>
|
|
763
|
+
{_health_strip(_platform_health(agg.get('cat_rows', [])))}
|
|
695
764
|
</div>
|
|
696
765
|
</div>
|
|
766
|
+
{_wins_block(agg)}
|
|
697
767
|
<div class="exec-stats summary-stats-foot">
|
|
698
768
|
<div class="exec-stat"><div class="v">{agg['total']}</div><div class="l">Documents</div></div>
|
|
699
769
|
<div class="exec-stat"><div class="v">{agg['steps_passed']}/{agg['steps_total']}</div><div class="l">Steps Passed</div></div>
|
|
@@ -848,6 +918,135 @@ _EFFORT = {
|
|
|
848
918
|
_EFFORT_RANK = {"LOW": 0, "MEDIUM": 1, "HIGH": 2}
|
|
849
919
|
_ACTION_CAP = 6 # max cards in the always-open Action Plan; the rest are in the collapsible list
|
|
850
920
|
|
|
921
|
+
# A category-specific, ACTIONABLE fix (an instruction), distinct from _ROOT_CAUSE (a cause).
|
|
922
|
+
# Keeping one per subsystem stops two different cards printing the same remediation verbatim.
|
|
923
|
+
_FIX_GUIDANCE = {
|
|
924
|
+
"Chunk Structure": "Re-chunk with a size/overlap that fits these documents and keep splits on sentence or paragraph boundaries.",
|
|
925
|
+
"Embedding Quality": "Re-embed after de-duplicating and stripping boilerplate; confirm the model and dimension match the index.",
|
|
926
|
+
"Content Preservation": "Strip headers/footers before chunking and verify ingestion drops or duplicates no source text.",
|
|
927
|
+
"Metadata & Consistency": "Backfill the missing metadata field and reconcile its value across the stores that disagree.",
|
|
928
|
+
"Index & Schema": "Align the index field types and vector config with what the pipeline actually writes.",
|
|
929
|
+
"Retrieval & Query": "Verify the query-embedding path and metadata filters, then re-tune the similarity threshold.",
|
|
930
|
+
"Knowledge Graph": "Re-ingest the graph so every document has its expected nodes, links, and typing.",
|
|
931
|
+
"Object Storage": "Restore the missing or altered source objects and re-run the storage stage.",
|
|
932
|
+
"Cross-Stage Consistency": "Re-run ingestion so every store agrees on id, hash, and content for each document.",
|
|
933
|
+
"Ingestion API": "Inspect the upload response and retry ingestion for documents without a success id.",
|
|
934
|
+
"Other": "Review the grouped checks below and address the shared cause.",
|
|
935
|
+
}
|
|
936
|
+
# Secondary hypotheses. Rendered under "Possible" so the report separates what it MEASURED
|
|
937
|
+
# (Observed) from its primary guess (Likely) and the alternates it cannot rule out (Possible).
|
|
938
|
+
_POSSIBLE_CAUSES = {
|
|
939
|
+
"Chunk Structure": ["Source documents vary widely in length", "Overlap set too low for cross-boundary context"],
|
|
940
|
+
"Embedding Quality": ["Repetitive/boilerplate text inflating near-duplicates", "A model or dimension mismatch at write time"],
|
|
941
|
+
"Content Preservation": ["Tables or figures dropped by the extractor", "Headers/footers folded into chunk bodies"],
|
|
942
|
+
"Metadata & Consistency": ["A field renamed between stores", "Ingestion wrote before metadata was attached"],
|
|
943
|
+
"Index & Schema": ["Schema drifted after an upgrade", "A field indexed with the wrong type"],
|
|
944
|
+
"Retrieval & Query": ["A metadata filter excluding valid chunks", "Similarity threshold set too high", "Chunk overlap starving boundary recall"],
|
|
945
|
+
"Knowledge Graph": ["A namespace or prefix mismatch", "Links written under an unexpected predicate"],
|
|
946
|
+
"Object Storage": ["An object expired or was moved", "Wrong content-type set on upload"],
|
|
947
|
+
"Cross-Stage Consistency": ["A store lagged during ingestion", "A partial re-ingest left stores out of sync"],
|
|
948
|
+
"Ingestion API": ["A transient upload error went unretried", "The ingestion response schema changed"],
|
|
949
|
+
"Other": [],
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _platform_health(cat_rows: list[dict]) -> dict | None:
|
|
954
|
+
"""A compact KB-health rollup: one dimension PER CATEGORY actually present in this run.
|
|
955
|
+
|
|
956
|
+
Dynamic by design - the dimensions are whatever the pipeline's steps produced (via
|
|
957
|
+
``cat_rows``), not a hardcoded list. A project with no graph step simply has no graph
|
|
958
|
+
dimension. Each dimension scores the pass rate of its evaluable checks (hard + advisory),
|
|
959
|
+
so it reads as overall subsystem health, and the composite is check-weighted across them.
|
|
960
|
+
Worst-first, so the weakest subsystem is the signal that leads. None when nothing ran.
|
|
961
|
+
"""
|
|
962
|
+
dims, tp, tt = [], 0, 0
|
|
963
|
+
for r in cat_rows:
|
|
964
|
+
if not r.get("total"):
|
|
965
|
+
continue
|
|
966
|
+
dims.append({"name": r["name"], "score": round(100 * r["passed"] / r["total"]),
|
|
967
|
+
"passed": r["passed"], "total": r["total"]})
|
|
968
|
+
tp += r["passed"]
|
|
969
|
+
tt += r["total"]
|
|
970
|
+
if not dims:
|
|
971
|
+
return None
|
|
972
|
+
dims.sort(key=lambda d: (d["score"], d["name"]))
|
|
973
|
+
return {"score": round(100 * tp / tt), "dimensions": dims}
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _health_band(score: int) -> str:
|
|
977
|
+
return "ok" if score >= 90 else "warn" if score >= 70 else "bad"
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def _health_strip(h: dict | None) -> str:
|
|
981
|
+
"""A subtle five-second read of subsystem health: a composite plus a mini-bar per
|
|
982
|
+
dimension. Deliberately compact - a signal, not another table."""
|
|
983
|
+
if not h:
|
|
984
|
+
return ""
|
|
985
|
+
bars = ""
|
|
986
|
+
for d in h["dimensions"]:
|
|
987
|
+
cls = _health_band(d["score"])
|
|
988
|
+
bars += (
|
|
989
|
+
f'<div class="phz-dim" data-tip="{d["passed"]}/{d["total"]} checks passed">'
|
|
990
|
+
f'<div class="phz-top"><span class="phz-name">{_esc(d["name"])}</span>'
|
|
991
|
+
f'<span class="phz-score {cls}">{d["score"]}</span></div>'
|
|
992
|
+
f'<div class="phz-bar"><i class="{cls}" style="width:{d["score"]}%"></i></div></div>'
|
|
993
|
+
)
|
|
994
|
+
ocls = _health_band(h["score"])
|
|
995
|
+
return (
|
|
996
|
+
'<div class="phz" data-tip="Check pass-rate grouped by KB subsystem present in this run. '
|
|
997
|
+
'Deterministic; overall integrity/quality still live in the verdict and hygiene above.">'
|
|
998
|
+
'<div class="phz-head"><span class="phz-cap">Platform Health</span>'
|
|
999
|
+
f'<span class="phz-overall {ocls}">{h["score"]}</span><span class="phz-max">/100</span></div>'
|
|
1000
|
+
f'<div class="phz-grid">{bars}</div></div>'
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
# A per-category "win" phrased as an outcome, shown only when that category is 100% clean.
|
|
1005
|
+
_WIN_LABEL = {
|
|
1006
|
+
"Object Storage": "No storage failures",
|
|
1007
|
+
"Knowledge Graph": "No graph inconsistencies",
|
|
1008
|
+
"Cross-Stage Consistency": "Stores agree on every document",
|
|
1009
|
+
"Ingestion API": "Every document ingested cleanly",
|
|
1010
|
+
"Embedding Quality": "Embeddings clean across the corpus",
|
|
1011
|
+
"Chunk Structure": "Chunk structure clean across the corpus",
|
|
1012
|
+
"Content Preservation": "Source content fully preserved",
|
|
1013
|
+
"Metadata & Consistency": "Metadata complete and consistent",
|
|
1014
|
+
"Index & Schema": "Index schema matches expectations",
|
|
1015
|
+
"Retrieval & Query": "Retrieval clean across the corpus",
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _engineering_wins(agg: dict) -> list[str]:
|
|
1020
|
+
"""What is demonstrably HEALTHY - so a good KB is not represented only by its problems.
|
|
1021
|
+
|
|
1022
|
+
Deterministic and honest: every item is a check-backed fact (a category that passed
|
|
1023
|
+
100%, zero errors, a full-pass gate), never a platitude. Empty list -> no section.
|
|
1024
|
+
"""
|
|
1025
|
+
wins: list[str] = []
|
|
1026
|
+
total = agg.get("total", 0)
|
|
1027
|
+
if total and agg.get("failed", 0) == 0 and agg.get("errors", 0) == 0:
|
|
1028
|
+
wins.append(f"All {total} document(s) validated - no integrity failures")
|
|
1029
|
+
elif agg.get("errors", 0) == 0 and agg.get("checks_total", 0):
|
|
1030
|
+
wins.append("No runtime errors")
|
|
1031
|
+
for row in agg.get("cat_rows", []):
|
|
1032
|
+
if row.get("total") and row["passed"] == row["total"]:
|
|
1033
|
+
lbl = _WIN_LABEL.get(row["name"])
|
|
1034
|
+
if lbl and lbl not in wins:
|
|
1035
|
+
wins.append(lbl)
|
|
1036
|
+
return wins[:6]
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _wins_block(agg: dict) -> str:
|
|
1040
|
+
wins = _engineering_wins(agg)
|
|
1041
|
+
if not wins:
|
|
1042
|
+
return ""
|
|
1043
|
+
items = "".join(f'<span class="win-item"><span class="win-tick">✓</span>{_esc(w)}</span>'
|
|
1044
|
+
for w in wins)
|
|
1045
|
+
return ('<div class="wins-block" data-tip="Check-backed strengths in this run - '
|
|
1046
|
+
'the healthy counterpart to the Action Plan.">'
|
|
1047
|
+
'<div class="wins-cap">Engineering Wins</div>'
|
|
1048
|
+
f'<div class="wins-grid">{items}</div></div>')
|
|
1049
|
+
|
|
851
1050
|
|
|
852
1051
|
def _impact_cls(pct: int) -> str:
|
|
853
1052
|
return "bad" if pct >= 50 else "warn" if pct >= 20 else "ok"
|
|
@@ -901,9 +1100,22 @@ def _action_plan_section(agg: dict) -> str:
|
|
|
901
1100
|
# cards below; a pointer here makes the remainder explicit rather than silently dropped.
|
|
902
1101
|
cards = ""
|
|
903
1102
|
for i, g in enumerate(ranked[:_ACTION_CAP], 1):
|
|
1103
|
+
ranked_checks = sorted(g["checks"], key=lambda o: -o["docs"])
|
|
904
1104
|
evidence = ", ".join(f'{_esc(o["label"])} {round(100 * o["docs"] / g["total"])}%'
|
|
905
|
-
for o in
|
|
906
|
-
|
|
1105
|
+
for o in ranked_checks[:3])
|
|
1106
|
+
# A concrete measured example from the worst check (e.g. "4200 (~3000 expected)"),
|
|
1107
|
+
# so Observed carries a specific number - the "why" - not just which check failed.
|
|
1108
|
+
example = next((o.get("detail") for o in ranked_checks if o.get("detail")), "")
|
|
1109
|
+
if example:
|
|
1110
|
+
evidence += f' <span class="check-detail">e.g. {_esc(example)}</span>'
|
|
1111
|
+
# A concise, category-specific INSTRUCTION here - the diagnosis now lives in the
|
|
1112
|
+
# Observed/Likely/Possible tiers, so the fix line is just "what to do" and never
|
|
1113
|
+
# repeats a cause. The verbose per-check hint stays in the detailed cards below.
|
|
1114
|
+
fix = _FIX_GUIDANCE.get(g["cat"]) or g["fix"] or _ROOT_CAUSE.get(g["cat"], "")
|
|
1115
|
+
possible = _POSSIBLE_CAUSES.get(g["cat"], [])
|
|
1116
|
+
possible_html = (
|
|
1117
|
+
'<div class="rca-line"><span class="conf-tag possible">Possible</span>'
|
|
1118
|
+
f'<span>{_esc("; ".join(possible))}</span></div>' if possible else "")
|
|
907
1119
|
cards += (
|
|
908
1120
|
# Border color driven by PRIORITY (inline overrides the shared :has(.tag.*) rule,
|
|
909
1121
|
# which would otherwise pick a colour from the impact/effort chips too).
|
|
@@ -914,8 +1126,13 @@ def _action_plan_section(agg: dict) -> str:
|
|
|
914
1126
|
f'<span class="tag {_impact_cls(g["impact"])}" title="documents affected">impact {g["impact"]}%</span>'
|
|
915
1127
|
f'<span class="tag {g["effort_cls"]}" title="estimated engineering effort">est. effort {g["effort"]}</span>'
|
|
916
1128
|
'</div>'
|
|
917
|
-
|
|
918
|
-
|
|
1129
|
+
# Separate what we MEASURED (Observed) from the primary guess (Likely) and the
|
|
1130
|
+
# alternates we can't rule out (Possible), so a hypothesis never reads as a fact.
|
|
1131
|
+
'<div class="rca-cause">'
|
|
1132
|
+
f'<div class="rca-line"><span class="conf-tag observed">Observed</span><span>{evidence}</span></div>'
|
|
1133
|
+
f'<div class="rca-line"><span class="conf-tag likely">Likely</span><span>{_esc(_ROOT_CAUSE.get(g["cat"], ""))}</span></div>'
|
|
1134
|
+
f'{possible_html}'
|
|
1135
|
+
'</div>'
|
|
919
1136
|
+ (f'<div class="rec-fix">Fix: {_esc(fix)}</div>' if fix else "")
|
|
920
1137
|
+ '</div>'
|
|
921
1138
|
)
|
|
@@ -953,7 +1170,8 @@ def _issues_by_subsystem(agg: dict) -> str:
|
|
|
953
1170
|
f'{o["docs"]}/{g["total"]} docs ({round(100 * o["docs"] / g["total"])}%)</span></li>'
|
|
954
1171
|
for o in sorted(g["checks"], key=lambda o: -o["docs"])
|
|
955
1172
|
)
|
|
956
|
-
|
|
1173
|
+
fix_txt = g["fix"] or _FIX_GUIDANCE.get(g["cat"])
|
|
1174
|
+
fix_html = f'<div class="rec-fix">Fix: {_esc(fix_txt)}</div>' if fix_txt else ""
|
|
957
1175
|
cards += (
|
|
958
1176
|
f'<details class="cov-row"{" open" if g["severity"] == "hard" else ""}>'
|
|
959
1177
|
'<summary class="cov-sum">'
|
|
@@ -1422,35 +1640,113 @@ def _trends_section(trends: list[dict]) -> str:
|
|
|
1422
1640
|
</div>"""
|
|
1423
1641
|
|
|
1424
1642
|
|
|
1425
|
-
def _glossary_section(
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1643
|
+
def _glossary_section(agg: dict | None = None, corpus: dict | None = None, mode: str = "") -> str:
|
|
1644
|
+
"""A run-driven glossary: it explains only the vocabulary THIS run actually used.
|
|
1645
|
+
|
|
1646
|
+
Sections are included conditionally - a step that did not run, a category with no checks,
|
|
1647
|
+
a Platform Health rollup that could not be computed, the Action Plan tiers when there is
|
|
1648
|
+
no Action Plan, a Readiness Verdict without a corpus, or the mode that was not used, are
|
|
1649
|
+
all omitted. So the glossary tracks the report above it instead of listing the full
|
|
1650
|
+
catalogue. ``agg=None`` (e.g. a trends-only render) falls back to the complete set.
|
|
1651
|
+
"""
|
|
1652
|
+
present_steps = agg.get("present_steps") if agg else None
|
|
1653
|
+
present_cats = {row["name"] for row in agg.get("cat_rows", [])} if agg else None
|
|
1654
|
+
|
|
1655
|
+
sections: list[str] = []
|
|
1656
|
+
|
|
1657
|
+
def section(title: str, body: str) -> None:
|
|
1658
|
+
sections.append(f'<div class="glossary-section"><h4>{_esc(title)}</h4>{body}</div>')
|
|
1659
|
+
|
|
1660
|
+
def dl(pairs) -> str:
|
|
1661
|
+
return "<dl>" + "".join(f"<dt>{_esc(n)}</dt><dd>{_esc(d)}</dd>" for n, d in pairs) + "</dl>"
|
|
1662
|
+
|
|
1663
|
+
# Counting levels are the base vocabulary for every run - always shown.
|
|
1664
|
+
section("Counting Levels", dl([
|
|
1665
|
+
("Document", "One knowledge-base item (a file / object). Reports may also call it a 'case'. A run validates one or more documents."),
|
|
1666
|
+
("Pipeline step", "One stage of the pipeline applied to a document (e.g. S3 Storage, Redis Chunks). Each document runs several steps."),
|
|
1667
|
+
("Check", "One individual assertion inside a step (e.g. 'embedding dimension matches'). A step bundles many checks."),
|
|
1668
|
+
]))
|
|
1669
|
+
|
|
1670
|
+
# Pipeline steps: only those that appear in THIS run.
|
|
1671
|
+
step_defs = [
|
|
1430
1672
|
("API Upload", "Uploads the document through the ingestion API (GraphQL multipart) and confirms a document id is returned.", ("API Upload",)),
|
|
1431
1673
|
("S3 Storage", "Confirms the object landed in storage with the expected content type, size, recency and ETag/MD5 integrity.", ("S3 Storage",)),
|
|
1432
1674
|
("GraphDB / Neptune Metadata", "Validates document metadata in the knowledge graph: required predicates, identity uniqueness, cross-store agreement with object storage (eTag + storage key), RDF typing, namespace consistency, dangling links, orphan nodes, formats and declarative structure checks.", ("GraphDB Metadata", "Neptune Metadata")),
|
|
1433
|
-
("Redis Chunks", "
|
|
1675
|
+
("Redis Chunks", "Toggleable checks over the vector store: chunk existence, ids, embeddings, dimensions, ordering, duplicates and metadata.", ("Redis Chunks",)),
|
|
1434
1676
|
("Retrieval Quality", "Informational diagnostics: recall@K, MRR, exact-term and paraphrase robustness. Never blocks the pipeline.", ("Retrieval Quality",)),
|
|
1435
1677
|
("Cross-Stage Consistency", "Do object storage, the graph and the vector index agree about each document: presence, eTag, shared metadata, chunk-count sanity, ACL propagation.", ("Cross-Stage Consistency",)),
|
|
1436
1678
|
]
|
|
1437
1679
|
if present_steps is not None:
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
("Check",
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
("
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1680
|
+
pset = set(present_steps)
|
|
1681
|
+
step_defs = [t for t in step_defs if any(n in pset for n in t[2])]
|
|
1682
|
+
if step_defs:
|
|
1683
|
+
section("Pipeline Steps", dl([(n, d) for n, d, _ in step_defs]))
|
|
1684
|
+
|
|
1685
|
+
# Check categories: only those actually exercised this run.
|
|
1686
|
+
cat_pairs = [(n, d) for n, d in _CAT_ORDER if present_cats is None or n in present_cats]
|
|
1687
|
+
if cat_pairs:
|
|
1688
|
+
section("Check Categories", dl(cat_pairs))
|
|
1689
|
+
|
|
1690
|
+
# Platform Health: only when there were evaluable checks to roll up.
|
|
1691
|
+
if agg is not None and _platform_health(agg.get("cat_rows", [])) is not None:
|
|
1692
|
+
section("Platform Health",
|
|
1693
|
+
'<div class="score-scale"><span class="s-ok">90-100 healthy</span>'
|
|
1694
|
+
'<span class="s-warn">70-89 attention</span><span class="s-bad">below 70 weak</span></div>'
|
|
1695
|
+
+ dl([
|
|
1696
|
+
("Platform Health (0-100)", "The share of evaluable checks (hard + advisory) that passed, grouped by the subsystems present in this run and check-weighted into one composite. It is data-plane health, deterministic - not answer quality, which needs an LLM judge and lives in the eval layer."),
|
|
1697
|
+
("Dimension score", "The same pass rate for a single subsystem - e.g. 'Retrieval & Query 52' means 52% of its checks passed. Dimensions are dynamic (only subsystems that actually ran appear) and are shown worst-first so the weakest leads."),
|
|
1698
|
+
]).replace("<dl>", '<dl style="margin-top:10px">')
|
|
1699
|
+
+ '<dd style="margin-top:8px">A high overall can still hide one weak dimension - read the per-subsystem bars, not only the composite.</dd>')
|
|
1700
|
+
|
|
1701
|
+
# Signal Confidence: only when the run produced an Action Plan (i.e. there are findings).
|
|
1702
|
+
if agg is not None and _subsystem_groups(agg):
|
|
1703
|
+
section("Signal Confidence (Action Plan)",
|
|
1704
|
+
dl([
|
|
1705
|
+
("Observed", "What the run measured directly: the failing checks and how many documents they affect. Fact, not inference."),
|
|
1706
|
+
("Likely", "The single best hypothesis for that observed pattern. A strong guess, not a proven cause."),
|
|
1707
|
+
("Possible", "Other explanations the evidence cannot rule out - worth checking if the Likely cause does not hold."),
|
|
1708
|
+
])
|
|
1709
|
+
+ '<dd style="margin-top:8px">Each Action Plan card separates these three tiers, so a hypothesis is never presented as a measured fact.</dd>')
|
|
1710
|
+
|
|
1711
|
+
# Run mode: only the one that ran (both if unknown, e.g. a legacy render).
|
|
1712
|
+
mode_defs = {
|
|
1713
|
+
"Live Ingestion": "Push documents through the ingestion API and validate each stage as it lands (run_validation).",
|
|
1714
|
+
"Discovery": "Validate a knowledge base that is already populated, without re-ingesting (discover_and_validate).",
|
|
1715
|
+
}
|
|
1716
|
+
active_modes = [(mode, mode_defs[mode])] if mode in mode_defs else list(mode_defs.items())
|
|
1717
|
+
section("Run Mode" if mode in mode_defs else "Run Modes", dl(active_modes))
|
|
1718
|
+
|
|
1719
|
+
# Status scale is fundamental to reading any status pill - always shown.
|
|
1720
|
+
section("Status Scale",
|
|
1721
|
+
'<div class="score-scale"><span class="s-ok">PASSED</span><span class="s-warn">PARTIAL</span>'
|
|
1722
|
+
'<span class="s-bad">FAILED / ERROR</span><span class="s-na">SKIPPED</span></div>'
|
|
1723
|
+
'<dl style="margin-top:10px">'
|
|
1724
|
+
'<dt>PASSED</dt><dd>Every evaluable check passed.</dd>'
|
|
1725
|
+
'<dt>PARTIAL</dt><dd>Some steps passed and some failed within a document.</dd>'
|
|
1726
|
+
'<dt>FAILED / ERROR</dt><dd>A check failed (FAILED) or the step raised unexpectedly (ERROR).</dd>'
|
|
1727
|
+
'<dt>SKIPPED</dt><dd>Step not part of this project\'s pipeline / configured out, or BLOCKED because an upstream step it depends on did not produce the artifact it needs (e.g. the object never reached storage, so it cannot be chunked). Blocked steps are still listed with the upstream culprit - they are neutral, never counted as a failure. Independent branches (e.g. graph vs vector) are unaffected by each other.</dd>'
|
|
1728
|
+
'<dt>UNSUPPORTED</dt><dd>A check the backend cannot answer (e.g. a vector store that does not expose raw vectors). Counted as neutral - excluded from pass/fail totals, never a failure.</dd>'
|
|
1729
|
+
'</dl>'
|
|
1730
|
+
'<dd style="margin-top:8px">A document is PASSED only when every non-skipped step passes. Critical-step failures halt remaining steps.</dd>')
|
|
1731
|
+
|
|
1732
|
+
# Suppressed: only when this run actually accepted advisory issues.
|
|
1733
|
+
if agg is not None and agg.get("checks_suppressed"):
|
|
1734
|
+
section("Suppressed (accepted)", dl([
|
|
1735
|
+
("Suppressed", "An advisory check the project accepted as expected for this corpus "
|
|
1736
|
+
"(config `suppressions` / `--suppress`) - e.g. near-duplicate boilerplate in policy "
|
|
1737
|
+
"documents. Excluded from the hygiene and Platform Health scores but still counted "
|
|
1738
|
+
"and shown as 'N suppressed'. Only advisory checks can be suppressed; a hard gating "
|
|
1739
|
+
"failure is never suppressed, so an accepted known-issue can never mask a real one."),
|
|
1740
|
+
]))
|
|
1741
|
+
|
|
1742
|
+
# Readiness Verdict: only when a corpus supplied one.
|
|
1743
|
+
if corpus is not None:
|
|
1744
|
+
section("Readiness Verdict",
|
|
1745
|
+
'<div class="score-scale"><span class="s-ok">READY</span><span class="s-warn">AT RISK</span>'
|
|
1746
|
+
'<span class="s-bad">NOT READY</span></div>'
|
|
1747
|
+
'<dd style="margin-top:10px">Corpus-level verdict over the whole document set: completeness '
|
|
1748
|
+
'(expected vs actual), duplicates, and coverage - distinct from any single document\'s pass/fail.</dd>')
|
|
1749
|
+
|
|
1454
1750
|
return f"""
|
|
1455
1751
|
<div id="section-definitions">
|
|
1456
1752
|
<section class="section">
|
|
@@ -1460,29 +1756,7 @@ def _glossary_section(present_steps: list[str] | None = None) -> str:
|
|
|
1460
1756
|
<svg class="glossary-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
1461
1757
|
</div>
|
|
1462
1758
|
<div class="glossary-body" id="glossaryBody">
|
|
1463
|
-
<div class="glossary-grid">
|
|
1464
|
-
<div class="glossary-section"><h4>Counting Levels</h4><dl>{levels_html}</dl></div>
|
|
1465
|
-
<div class="glossary-section"><h4>Pipeline Steps</h4><dl>{defs}</dl></div>
|
|
1466
|
-
<div class="glossary-section"><h4>Check Categories</h4><dl>{cats}</dl></div>
|
|
1467
|
-
<div class="glossary-section"><h4>Run Modes</h4><dl>{modes_html}</dl></div>
|
|
1468
|
-
<div class="glossary-section">
|
|
1469
|
-
<h4>Status Scale</h4>
|
|
1470
|
-
<div class="score-scale"><span class="s-ok">PASSED</span><span class="s-warn">PARTIAL</span><span class="s-bad">FAILED / ERROR</span><span class="s-na">SKIPPED</span></div>
|
|
1471
|
-
<dl style="margin-top:10px">
|
|
1472
|
-
<dt>PASSED</dt><dd>Every evaluable check passed.</dd>
|
|
1473
|
-
<dt>PARTIAL</dt><dd>Some steps passed and some failed within a document.</dd>
|
|
1474
|
-
<dt>FAILED / ERROR</dt><dd>A check failed (FAILED) or the step raised unexpectedly (ERROR).</dd>
|
|
1475
|
-
<dt>SKIPPED</dt><dd>Step not part of this project's pipeline / configured out, or BLOCKED because an upstream step it depends on did not produce the artifact it needs (e.g. the object never reached storage, so it cannot be chunked). Blocked steps are still listed with the upstream culprit - they are neutral, never counted as a failure. Independent branches (e.g. graph vs vector) are unaffected by each other.</dd>
|
|
1476
|
-
<dt>UNSUPPORTED</dt><dd>A check the backend cannot answer (e.g. a vector store that does not expose raw vectors). Counted as neutral - excluded from pass/fail totals, never a failure.</dd>
|
|
1477
|
-
</dl>
|
|
1478
|
-
<dd style="margin-top:8px">A document is PASSED only when every non-skipped step passes. Critical-step failures halt remaining steps.</dd>
|
|
1479
|
-
</div>
|
|
1480
|
-
<div class="glossary-section">
|
|
1481
|
-
<h4>Readiness Verdict</h4>
|
|
1482
|
-
<div class="score-scale"><span class="s-ok">READY</span><span class="s-warn">AT RISK</span><span class="s-bad">NOT READY</span></div>
|
|
1483
|
-
<dd style="margin-top:10px">Corpus-level verdict over the whole document set: completeness (expected vs actual), duplicates, and coverage - distinct from any single document's pass/fail.</dd>
|
|
1484
|
-
</div>
|
|
1485
|
-
</div>
|
|
1759
|
+
<div class="glossary-grid">{"".join(sections)}</div>
|
|
1486
1760
|
</div>
|
|
1487
1761
|
</div>
|
|
1488
1762
|
</section>
|
|
@@ -1561,8 +1835,8 @@ function _insights(d){
|
|
|
1561
1835
|
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"};
|
|
1562
1836
|
function _categorise(label, step){
|
|
1563
1837
|
var l=(label||'').toLowerCase();
|
|
1564
|
-
if(/e_tag|required field|metadata consist|
|
|
1565
|
-
if(/boundaries|chunk count|size within|ordering|overlap/.test(l)) return 'Chunk Structure';
|
|
1838
|
+
if(/e_tag|required field|metadata consist|page metadata|directory|predicate|date format|email|field rule/.test(l)) return 'Metadata & Consistency';
|
|
1839
|
+
if(/boundaries|chunk count|chunk structure|all chunks|coheren|positional|section split|size within|ordering|overlap/.test(l)) return 'Chunk Structure';
|
|
1566
1840
|
if(/embedding|normaliz|near-duplicate|dimension|distance metric/.test(l)) return 'Embedding Quality';
|
|
1567
1841
|
if(/fidelity|coverage|contaminat|duplicate|header|footer|table\/figure/.test(l)) return 'Content Preservation';
|
|
1568
1842
|
if(/index|schema/.test(l)) return 'Index & Schema';
|
|
@@ -1835,6 +2109,7 @@ def generate_batch_dashboard(
|
|
|
1835
2109
|
wall_seconds: float | None = None,
|
|
1836
2110
|
concurrency: int | None = None,
|
|
1837
2111
|
evidence: dict | None = None,
|
|
2112
|
+
suppress: list[str] | None = None,
|
|
1838
2113
|
) -> str:
|
|
1839
2114
|
"""Generate a self-contained, interactive HTML dashboard. Returns abs path.
|
|
1840
2115
|
|
|
@@ -1854,7 +2129,8 @@ def generate_batch_dashboard(
|
|
|
1854
2129
|
_ICON = {s.name: s.icon for s in registered_steps()}
|
|
1855
2130
|
|
|
1856
2131
|
cases = [_result_payload(item.get("name", item["result"].file_name), item["result"]) for item in results]
|
|
1857
|
-
|
|
2132
|
+
# computed on FULL data, before any trim; suppressions accepted from config
|
|
2133
|
+
agg = _aggregate(cases, hide_skipped=hide_skipped, suppress=set(suppress or []))
|
|
1858
2134
|
# Wall clock vs compute time: agg["duration"] is the SUM of per-document durations, so
|
|
1859
2135
|
# under --parallel it far exceeds the elapsed time (a 502-doc run summed 1319s but
|
|
1860
2136
|
# finished in 329s). Carry the real wall clock and concurrency so the summary can show
|
|
@@ -1984,7 +2260,7 @@ def generate_batch_dashboard(
|
|
|
1984
2260
|
{_document_section(cases)}
|
|
1985
2261
|
{_details_section(cases, agg['present_steps'])}
|
|
1986
2262
|
{trends_html}
|
|
1987
|
-
{_glossary_section(agg
|
|
2263
|
+
{_glossary_section(agg, corpus, mode)}
|
|
1988
2264
|
</main>
|
|
1989
2265
|
{_MODAL}
|
|
1990
2266
|
<div id="compass-tip"></div>
|