Sangeetha-Grantha

Metadata Value
Status Completed
Version 1.1.0
Last Updated 2026-07-19
Author Sangeetha Grantha Team

TRACK-130: Consolidate Matching Heuristics & De-duplicate the CLI Extraction Pipeline

Goal: The transliteration-collapse table — matching-critical, and claimed to mirror Kotlin’s TransliterationCollapse — exists twice in Python with different contents: src/normalizer.py (_TRANSLITERATION_COLLAPSE_RULES, includes chh, adds kh inline later) and src/identity_candidates.py (normalize_identity_text, includes kh and vowel collapses). Separately, src/cli.py’s extract command re-implements the PDF pipeline that now lives in PdfExtractionStrategy and has already drifted (no ragamalika handling, no diacritic normalization). Consolidate both.

Origin: Python best-practices evaluation, 2026-07-19 (item 5). Divergent copies of a matching table across two languages is how identity resolution quietly breaks.

Definition of Done

Inputs

Out of Scope

Steps

  1. Write the pinning test first: run a fixture list of ~50 real names through both normalizers, snapshot outputs.
  2. Extract the shared table + named variant rule-sets into heuristics/; point both consumers at it; pinning test must stay green.
  3. Add the cross-consumer consistency test.
  4. Refactor cli.py to instantiate PdfExtractionStrategy with finalize=lambda e, *_: e; extract the shared page-range helper.
  5. Document the detect_script limitation; implement counting-based detection only if all regression fixtures pass unchanged.

Deliverables

Verification

Outcome (2026-07-19)

Pinned first, then consolidated

tests/fixtures/normalization/pinned_outputs.json was generated from the pre-consolidation code: 61 real composer/raga/title/tala/deity/temple names × 8 normalization paths = 488 pinned outputs, enforced by tests/test_normalization_pins.py. Every pin stayed green through the consolidation, so no matching key moved.

What the two copies actually differed by

  normalizer.py identity_candidates.py
chhc yes (before sh/ch) no
khk after the loop mid-sequence
gh/ph order ph, gh gh, ph
long vowels (aa/ee/oo/uu) raga branch only always

Consequence of the chh gap: "chh" normalised to "c" for matching keys but to "ch" for identity keys — the two answered differently for the same input. That divergence is now explicit rather than accidental, and each consumer keeps its existing sequence exactly.

The gh/ph swap and the kh position are provably immaterial — the patterns share no characters, so neither can create or destroy the other. Verified beyond the pins by brute-forcing 400,000 random strings per variant through the old and new orderings: 0 mismatches.

Kotlin counterpart — finding

There is no live Kotlin collapse table to mirror. NameNormalizationService delegated consonant collapse to Python in its Phase 3 “Simplify and Ship” pass (“transliteration collapse is now handled by Python normalizer”). Kotlin retains only the long-vowel collapses for ragas, which is the counterpart of LONG_VOWEL_COLLAPSE_RULES. The module docstring records this, so the next reader does not go hunting for a mirror that no longer exists. Python is authoritative for the consonant table.

CLI de-duplication — the drift, measured

cli.py extract now builds an ExtractionTask and delegates to PdfExtractionStrategy with a no-op finalize. Diffed against pre-refactor output on the real ragamalika_multi_variant fixture rendered to PDF:

  ragas emitted
Before [{"name": "Ragamalika Talam: Adi", "order": 1}]
After SrI/Arabhi (pallavi), gauri/nATa (anupallavi), gauLa (charanam)

That is the drift the track predicted: the CLI had no ragamalika sub-raga detection and no cleanup_raga_tala_name, so an unparsed metadata blob was being stored as the raga name. On two simpler synthetic fixtures the before/after output is byte-identical apart from extractionTimestamp — i.e. delegation changes nothing except where the CLI was actually broken.

Page-range parsing ("3-7"(2, 6)) now exists once as extraction_strategies.parse_page_range, used by both callers and covered by a parametrised test.

detect_script — documented, not changed

The first-character bias is real and easy to trigger in this corpus, because section headers are romanised:

detect_script("pallavi श्री विश्व नाथं भजेहम्")  # -> "latin"

Documented in the module and method docstrings. The counting-based fix was not implemented: it would change the script label on emitted lyric variants and so change extraction output, which conflicts with this track’s pinned-output remit. The docstring records what a future fix must re-pin first.

Verification

ruff check 0, ruff format --check clean (55 files), mypy 0, pytest 292 unit + 18 integration passing (up from 220 unit — 65 normalization pin/consistency tests and 7 CLI delegation tests added).