Spaces:
Running
Validation
Registry-backed segment validation. Single source of truth for category metadata: inspector/services/validation/registry.py, hand-mirrored to inspector/frontend/src/tabs/segments/domain/registry.ts. Parity asserted by inspector/frontend/src/__tests__/registry/parity.test.ts.
Two orderings exist and are distinct:
accordion_order(registry field) β UI render order inValidationPanel.svelte.- dict insertion order in
_REGISTRYβ drivesPER_SEGMENT_CATEGORIES/_flags_to_categories/category_countsemission. These two orders are NOT the same (e.g.low_confidence_v2is insertion-pos 6 butaccordion_order=6whileaudio_bleeding/boundary_adj/repetitionscarryaccordion_order7/8/9 yet sit at insertion positions 8/9/7). When adding a category, set BOTH and keep registry.py β registry.ts byte-aligned.
Categories
Accordion order (registry accordion_order). card_type = FE card subcomponent dispatch.
| order | kind | severity | scope | detection | where | card_type | key module |
|---|---|---|---|---|---|---|---|
| 1 | failed |
error | per_segment | empty matched_ref |
server+client | generic | classifier.py / detail.py |
| 2 | missing_verses |
error | per_verse | verse in word_counts not covered by any seg key |
server | missingVerses | _structural.py::_check_structural_errors |
| 3 | missing_words |
error | per_verse | word-index gap in verse coverage map | server | missingWords | _missing.py::_build_missing_words |
| 4 | structural_errors |
error | per_chapter | t_from>=t_to, w_from<1, w_to<w_from, time overlap |
server | error | _structural.py |
| 5 | low_confidence |
warning | per_segment | confidence < LOW_CONFIDENCE_THRESHOLD (count); < LOW_CONFIDENCE_DETAIL_THRESHOLD (detail list, synthetic low_confidence_detail) |
server | generic | classifier.py::classify_flags |
| 6 | low_confidence_v2 |
warning | per_segment | segment_uid β probe_failed_uids (MFA tight-beam probe sidecar, load_probe_v2) |
server | generic | classifier.py + _build_detail_lists |
| 7 | audio_bleeding |
warning | per_segment | by-ayah only: not seg_belongs_to_entry(matched_ref, entry_ref) |
server | generic | classifier.py |
| 8 | boundary_adj |
warning | per_segment | persisted is_boundary_adj (fall-through compute_is_boundary_adj) |
server | generic | classifier.py::_check_boundary_adj |
| 9 | repetitions |
warning | per_segment | seg.wrap_word_ranges truthy (has_repeated_words alone does NOT classify) |
server | generic | classifier.py |
| 10 | cross_verse |
warning | per_segment | s_ayah != e_ayah |
server | generic | classifier.py |
| 11 | qalqala |
info | per_segment | persisted qalqala_letter non-null (fall-through compute_qalqala_letter) |
server | generic | classifier.py + segments/qalqala.py |
| 12 | muqattaat |
info | per_segment | s_word==1 and (surah,s_ayah) β MUQATTAAT_VERSES |
server | generic | classifier.py |
| 13 | basmala_amin |
info | per_segment | per-chapter scan: first seg overlapping 1:1, last overlapping 1:7, + missed-Basmala augmentation gated on len(deleted_basmala_chapters) >= MISSED_BASMALA_FLAG_MIN_DELETED |
server | generic | detail.py::_build_detail_lists |
Detection runs server-side in the validate pass. The client classifies snapshots/issue records for display only (utils/validation/classified-issues.ts), it does not own primary detection. Parent-repo validators/validate_segments.py (pipeline, gitignored) references the same kind set β accordion + post-pipeline validator must stay aligned.
Suppression: is_suppressed_for(seg, cat) = is_ignored_for (reads seg.ignored_categories, legacy "_all" marker, legacy ignored=True bool) OR is_resolved_by_edit (transient seg._resolved_by_edit, injected by the validate route from build_resolved_by_edit_index, stripped before return). failed is gated only on is_ignored_for (resolved-by-edit does not suppress errors).
Server engine β inspector/services/validation/
| File | Role |
|---|---|
__init__.py |
Orchestrator validate_reciter_segments(reciter). ThreadPoolExecutor(4) fan-out of the 4 independent bucket reads (load_detailed / _load_resolved_idx_cached / load_probe_v2 / load_seg_verses). canonical=None throughout (phonemizer out of runtime). Builds category_counts + per-category arrays + split_group_index. Re-exports registry symbols + classifier API. |
registry.py |
_REGISTRY dict of frozen IssueDefinition rows. Derived tuples: ALL/PER_SEGMENT/PER_VERSE/PER_CHAPTER/CAN_IGNORE/AUTO_SUPPRESS/PERSISTS_IGNORE_CATEGORIES. filter_persistent_ignores, registry_as_dict. |
classifier.py |
Per-segment classification β single source of truth. classify_flags/classify_segment/classify_segment_full/classify_entry. is_ignored_for/is_resolved_by_edit/is_suppressed_for. compute_is_boundary_adj (raw rule, no suppression) + _check_boundary_adj (persisted-field short-circuit + suppression). qalqala persisted-field short-circuit (local import of compute_qalqala_letter). |
snapshot_classifier.py |
classify_snapshot(snap) β routes a loose SegSnapshot dict (history op-log shape) through classify_segment; no logic reimplemented. Used by save-flow history enrichment. |
detail.py |
_build_detail_lists β single entry walk producing every per-category detail array + verse_segments coverage map + sequence_gaps + basmala_amin (per-chapter scan, missed-Basmala augmentation). _compute_surah_offsets / _word_ord (prefix-sum, O(1) ordinals). Each item carries classified_issues. Also identity helpers resolve_segment_by_uid / resolve_segment_for_issue / filter_stale_issues. |
_missing.py |
_build_missing_words β verse coverage gaps β issue dicts with auto_fix/auto_fix_up/auto_fix_down targeting; merges sequence_gaps. |
_structural.py |
_check_structural_errors β reads segments.json (verse-aggregated) via load_seg_verses. Returns (errors, missing_verses, stats). |
qalqala helper canonical path: inspector/services/segments/qalqala.py::compute_qalqala_letter. services.qalqala is an alias resolving to the same module (used by classifier.py fall-through and the retired perf report).
Routes β inspector/routes/segments/validation.py (/api/seg, bp seg_val)
| route | handler | notes |
|---|---|---|
GET /api/seg/validate/<reciter> |
seg_validate |
full-reciter validate. Cached via cache.get/set_seg_validate_cache; invalidated on save (invalidate_seg_caches). orjson_cached_response. No per-chapter param (dropped β accordions are global). |
GET /api/seg/stats/<reciter> |
seg_stats |
compute_stats; cached cache.get/set_seg_stats_cache. |
GET /api/seg/edit-history/<reciter> |
seg_edit_history |
load_edit_history. |
There is no separate trigger-validation route; /validate is the single classify entry point and the cache is the dedupe layer.
Client β inspector/frontend/src/tabs/segments/
utils/validation/:
| File | Role |
|---|---|
classified-issues.ts |
Build/normalize per-segment issue records consumed by the UI from the response. |
card-lead-seg.ts |
Lead-segment selection for a card. |
conf-class.ts |
getConfClass() β CSS class from confidence score. |
missing-verse-context.ts |
Surrounding-segment lookup for a missing-verse card. |
refresh.ts |
Fetch /api/seg/validate β setValidation. |
resolve-issue.ts |
Mark issue resolved (ignore / fix / split-resolved). |
split-group.ts |
Group split-resolved issues into chains (consumes split_group_index). |
stale.ts |
Stale-state predicates β when classification needs re-run. |
domain/registry.ts β TS twin of registry.py (camelCase fields). Exports same derived category tuples + filterPersistentIgnores + ERROR_CAT_LABELS.
Store: stores/validation.ts β segValidation writable (SegValidateResponse | null), splitGroupIndex derived, accordionViewActive derived from valUiOpenCategory, setValidation/clearValidation.
Components: components/validation/{ValidationPanel,ErrorCard,GenericIssueCard,MissingVersesCard,MissingWordsCard,WaslBoundary,AccordionGuideModal}.svelte. ValidationPanel filters stale items by live uid (filterStaleIssues) before render and owns accordion order.
Registry-pair invariant
registry.py β registry.ts β parent validators/validate_segments.py must align. Adding/reordering a category:
- Add row to
_REGISTRYininspector/services/validation/registry.py(setaccordion_orderAND mind dict insertion position). - Mirror row in
inspector/frontend/src/tabs/segments/domain/registry.ts(camelCase).parity.test.tsfails otherwise. - Add detection hook: per-segment β
classify_flagsinclassifier.py; cross-segment scan βdetail.py/_missing.py/_structural.py. - Wire the count into
category_counts+ a response array key in__init__.py::validate_reciter_segments. - Add accordion position in
ValidationPanel.svelteand a card subcomponent ifcard_typeis new. - Update parent
validators/validate_segments.pyif it should also fire post-pipeline.
IssueDefinition flags govern persistence/suppression: can_ignore (Ignore button), auto_suppress (edit writes cat into ignored_categories for per-segment scope), persists_ignore (survives save serialization β filter_persistent_ignores strips the rest before write).
Persisted classifier fields
Two fields stamped on every detailed.json segment, read at validate time to collapse per-validate CPU to a dict lookup.
| field | type | source-of-truth helper | rule |
|---|---|---|---|
qalqala_letter |
str | None |
services/segments/qalqala.py::compute_qalqala_letter |
last Arabic letter of dk_text_for_ref(matched_ref) if β QALQALA_LETTERS (Ω Ψ· Ψ¨ Ψ¬ Ψ―), else None |
is_boundary_adj |
bool |
services/validation/classifier.py::compute_is_boundary_adj |
raw rule, NO suppression: one-word seg (s_word==e_word) outside muqattaΚΌat / single-word-verse / STANDALONE_REFS / STANDALONE_WORDS allow-lists |
Stamped by (all call the same source-of-truth helper β byte-equivalent across writers):
- Save flow β
services/segments/save.py::_stamp_persisted_classifier_fields(seg, single_word_verses)on every mutated seg.is_boundary_adjcomputed structural-only (canonical=None). - Extraction pipeline β
.local/extractionoutputs (passescanonicalto capture the phonemic side, where applicable). - Backfill β
scripts/backfills/backfill_qalqala_letter.py,scripts/backfills/backfill_boundary_adj.py.
Read path (classifier.py):
- qalqala:
classify_flagsreadsseg["qalqala_letter"]when the key is present; legacy fall-through computescompute_qalqala_letter(seg)(local import to avoid a load cycle). Same helper β identical value. - boundary_adj:
_check_boundary_adjshort-circuits onis_suppressed_forfirst, then returns persistedseg["is_boundary_adj"]when present, else falls through tocompute_is_boundary_adj. Persisted value is the raw rule; suppression layers on top at read time.
Text source: compute_qalqala_letter and compute_is_boundary_adj derive seg text from dk_text_for_ref(matched_ref) (Migration #5 dropped per-seg matched_text). The seg.get("matched_text") or dk_text_for_ref(...) fall-through is the back-compat path for any pre-Migration-5 seg still carrying matched_text.
Phonemizer is out of the validate runtime. compute_is_boundary_adj accepts canonical for back-compat but ignores it (retired in Migration #5 with phonemes_asr); the rule is structural-only. validate_reciter_segments passes canonical=None. The only quranic_phonemizer consumer left is the offline scripts/backfills/backfill_boundary_adj.py; no canonical_phonemes.pkl loads during validate; eager-boot init removed from app.py.
Snapshot lifecycle
Save flow (services/segments/save.py) routes history op-log snapshots through classify_snapshot (β classify_segment), stamping classified_issues on each targets_before / targets_after and snapshots.{before,after} dict (via _history_visible_categories, with probe_failed_uids so v2 flags appear in the delta). snapshot_classifier.py is distinct from the live pass only in that it derives positional inputs from the bare snapshot and skips heavy detail rendering β no separate classification logic.
The before/after category delta on each op record drives the per-batch resolved-issue stats in the edit-history view and the validation-summary line in the save-preview surface.
failed resolution: once a ref is committed the seg's confidence becomes 1.0 and the empty-matched_ref condition no longer holds, so the category auto-drops on the next validate.
Bench / drift harness
bench/ (snapshot.py / drift.py / measure.py) + committed canonical snapshots bench/ground_truth/<slug>.json are the drift gate for any change to a perf-sensitive validation path. The gate asserts byte-equivalent per-category output against the ground-truth snapshot across the WIP reciter set. Any change to compute placement, caching, parallelism, or a persisted-field writer must pass drift before landing.
Backfill scripts use parallel-then-promote: write to a staging path (archive/backfill/<slug>/detailed.json), drift-check in-memory against bench/ground_truth/<slug>.json, then atomically promote to reciters/<slug>/detailed.json only on byte-equivalent match. All persisted-field writers (save / extraction / backfill / classifier fall-through) must produce identical values β drift check is the only guarantee of that.
(The bench/ tree and parent-repo validators/ are gitignored / live outside the inspector working tree; paths above are canonical for when the harness is checked out.)