Sangeetha-Grantha

Metadata Value
Status Accepted
Version 1.4.1
Last Updated 2026-09-10
Author Sangeetha Grantha Team
Document Type Decision record
Deciders Sangeetha Grantha Team (Seshadri)
Extends ADR-016 — raga naming authority (the bootstrap this ADR builds identity on top of)
Implemented by TRACK-136 (Phases 1–3); TRACK-132 is Phase 0 (remediation); TRACK-137 cleans residual orphan twins

ADR-017: Raga Reference Entity Identity & Resolution


[!NOTE] Decision record: preserve the original rationale and check its decision/supersession status. Current runtime guidance is in system architecture and Flyway migrations.

Context

ADR-016 settled which spelling wins and introduced a match_key (§5) so convention drift cannot mint duplicates. TRACK-132 is cleaning up the ~313 krithi-links that landed on duplicate raga rows before that rule existed.

That work is necessary but it is remediation, not a fix: it removes today’s duplicates without removing the reason duplicates get created. Every failure catalogued in TRACK-132 has the same root cause — the raga’s name is being used as its identity — and a merge migration does not change that. Left as-is, the next bulk import regenerates the mess against the freshly-cleaned rows, and someone writes TRACK-2xx.

Name is many-to-many with identity

A raga name cannot serve as a primary key, because the mapping is many-to-many in both directions:

Direction Example Consequence today
One raga → many spellings Yadukula Kāmbhoji = yadukula kAmbhOji Import mints a twin; corpus splits across two rows
One raga → many tradition-names Dharmavati (Govindacharya) = Dhāmavathi (Venkatamakhin); Gamanashrama (53) = Gamakakriyā No home for the second name except “duplicate” or “lossy merge”
Many ragas → one near-name KanadāKannada; Kalāvathi (mela 31) ≠ Kalāvati (mela 16) A too-aggressive normaliser silently corrupts links

The four bug classes TRACK-132 documents — silent twin-minting on import, seed migrations that re-create existing ragas (V40: ~85 links), un-mergeable nomenclature pairs, and un-distinguishable homonyms — are all the same defect seen from four sides. ADR-016 §5 named the fix in one sentence (“identity is a match key, not the display name”); this ADR specifies the architecture that makes it real and closes the remaining three sides.

What ADR-016 left open

Decision

Adopt an entity + alias + controlled-resolution model for ragas, in four parts. Parts 1–3 are the architecture; Part 4 is the phased path (Phase 0 = the existing TRACK-132 remediation, unchanged).

1. Identity model: a raga is an entity with many aliases

Split identity from surface form.

2. One normalisation function, owned by the database

ADR-016 §5 warned that if Python’s normalize_for_matching() and the SQL match_key drift, the guardrail passes rows the matcher would have merged. Remove the possibility of drift:

The normaliser is a constraint-satisfaction problem with a frozen test suite, not a bag of replace() calls. It must be lenient enough to collapse the 114 twin groups yet strict enough to keep the homonyms apart. TRACK-132 §1 already isolated the exact contract — treat it as the identity spec:

Must merge (twins) Must stay apart (real ragas)
th/t, dh/d, sh/s, ch/c, gh/g, kh/k, bh/b, jh/j, w/v, oo/ū, ee/ī, ITRANS caps, terminal -am/-aM, spacing, case kanadakannada (nn→n forbidden, 11 links); kalavati th 31≠16; srimati sh/ee 2≠8; terminal -i preserved (Bhairavi, 28 links); initial vowel preserved (AbhogiBhogi); digraphs mapped not deleted (Ranjani/Niranjani/Shreeranjani stay 3)

These cases are the acceptance test for any change to raga_match_key. Freeze them as a regression gate in CI.

3. Resolution is a controlled operation — never a silent insert

The import’s normalize → match → else create is the twin-minting bug. Replace it with lookup + review-queue, so an unknown raga is a reviewable event, not an auto-mutation:

flowchart TB
  NAME[Incoming raga name]
  KEY["raga_match_key(name)"]
  LOOK[Lookup ragas ∪ aliases]
  subgraph outcomes["Resolution"]
    direction TB
    HIT[exactly one hit → raga_id]
    AMB[multiple hits → AMBIGUOUS enqueue]
    UNK[no hit → UNKNOWN enqueue]
  end
  NAME --> KEY --> LOOK --> outcomes

A curator then resolves each queue item one of three ways:

  1. Attach as a new alias to an existing raga (the common case — a spelling we hadn’t seen),
  2. Confirm a genuinely new raga (creates the ragas row with its lakshana), or
  3. Disambiguate a homonym — pick which identity each occurrence meant.

This rides on the existing Curator UI and review paradigm. It structurally eliminates the failure mode that produced the entire TRACK-132 backlog: the import can no longer create a raga at all.

Seed migrations resolve the same way. ADR-016 §6 established that a seed must check existing reference data before inserting; here that becomes mechanical — reference seeds live only in the R__ repeatable and upsert on the identity key (ON CONFLICT (match_key, mela_disambiguator)), never on raw name, so a V__ seed like V40 (which minted 6 duplicates by matching on name) is impossible to repeat.

4. Lakshana-based duplicate detection and integrity constraints

Name-collision catches only name-shaped duplicates. Add the structural axis, as alarms feeding the review queue — never as auto-merges, because same-scale is ambiguous:

Provenance, and ADR-016’s “application becomes the authority”

ADR-016 pointed at Sangeetha Grantha becoming a citable authority rather than a Wikipedia mirror; this model is how that becomes true rather than aspirational. Every ragas row and every raga_aliases row carries source + confidence, so “what is this raga, and on whose authority?” is answerable from the data. Expert clarifications (e.g. the 2026-08-29 Carnatic-musician query sheet that resolved the A2/A3/B/C questions) enter as first-class provenance on the alias/identity rows, consistent with the versioned-canon discipline of ADR-014.

Refinements from implementation review (2026-08-29)

Two points sharpened while planning TRACK-136; they refine Decision §1–§2 without changing the direction:

Scope and phasing

Phase What Where
0. Remediate (unchanged) Merge existing duplicates, repoint krithi_ragas by order_index + krithis.primary_raga_id, handle the SET-NULL FK traps, patch the normaliser. Still required independent of this ADR. TRACK-132
1. Structural identity raga_aliases table; raga_match_key() function; generated STORED match_key on both tables; UNIQUE across the union; backfill aliases (incl. the expert-confirmed nomenclature names). Twin-minting becomes structurally impossible from here. New track
2. Controlled resolution Rewire import to lookup + review-queue-on-miss; seeds upsert on match_key. Root cause removed. New track
3. Integrity + provenance Scale-collision alarm; janya-subset and mela-as-janya checks in CI; source/confidence on identity and alias rows. New track

Phase 1 must land in the same migration sequence as, or immediately after, Phase 0 — if Phase 0 merges without the UNIQUE match_key/alias structure in place, the next import can regenerate duplicates against the freshly-cleaned rows. That ordering is the single most important commitment in this ADR.

Deliberately out of scope here (candidates for a later ADR, noted so they are not lost): applying the same entity/alias/resolution framework to talas and composers. composer_aliases is a half-step already; generalising the pattern is worthwhile but is not a raga decision.

Consequences

Positive

Negative / accepted

References


Section index · Documentation home · Feature status