← domain #9
Source : authority_lakshana.py
data/library/books/brihaddesi_sharma_1992/formal_grammar/authority_lakshana.py · 796 lines · 31670 bytes
"""Domaine 9 — méta-grammaire de l'autorité et du lakṣaṇa-discours
Synthèse 6c.3 du Brihaddesi (Sharma 1992) depuis :
- 33 concepts du cluster domain_id=9
- 20 règles génératives 6b
- 75 affirmations sourcées
- partition Leiden domain_id=9 (Bharata, Mātaṅga, lakṣaṇa, tāra, antaramārga,
sandhis, nirvahana, Deśī rāgas, Sārngadeva, …)
Ce domaine est META : il n'opère pas sur des svaras concrets, il décrit
COMMENT le Brihaddesi structure ses propres affirmations :
- qui est cité (Bharata = autorité ultime du Nāṭyaśāstra,
Mātaṅga = auteur du Brihaddesi, Sārngadeva = expansion postérieure) ;
- ce que c'est qu'une définition (lakṣaṇa : 2-fold = general + particular,
10 lakṣaṇas pour les jātis, 5-fold pour audūvita) ;
- comment une exposition se déploie (les 5 sandhis du drame :
mukha, pratimukha, garbha, vimarsa, nirvahana — chacun reçoit une
assignation de rāga par Bharata) ;
- les voies intermédiaires (antaramārga) entre mārga et deśī ;
- le registre tāra (extension du chant à partir de l'aṁśa).
Anti-fabrication : chaque type/constante/opération/contrainte cite ≥1
evidence (rule_id 6b ou affirmation_id). Toute valeur sans citation va
dans UNRESOLVED.
Python 3.10+. Importable directement, aucune dépendance externe.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Iterable
# =============================================================================
# CONSTANTES — énumérations fermées
# =============================================================================
class Authority(str, Enum):
"""Authorities cited or referenced in the Brihaddesi.
BHARATA is the ultimate authority (author of the Nāṭyaśāstra), cited
repeatedly by Mātaṅga with the formula "Similarly has said Bharata —".
MATANGA is the author of the Brihaddesi itself — self-reference is
explicit (cid=9, 12 attributions, 0 rules).
SARNGADEVA is a later author (Saṅgītaratnākara) whose extensions are
noted when they exceed Bharata + Mātaṅga.
KUMBHA, SARDULA appear once each as ancillary authorities.
evidence: aff#171, aff#182, aff#191, aff#221, aff#312, aff#2150, aff#2468
(Bharata cited by Mātaṅga);
aff#532, aff#543, aff#1595, aff#2878 (Mātaṅga self-reference);
aff#1558, aff#1907, aff#1961 (Sārngadeva extension);
aff#3108 (Kumbha contradicting Mātaṅga); aff#1957 (Šārdūla).
"""
BHARATA = "bharata"
MATANGA = "matanga"
SARNGADEVA = "sarngadeva"
KUMBHA = "kumbha"
SARDULA = "sardula"
# Stratification chronologique / hiérarchique des autorités telle qu'elle
# émerge du texte. Bharata est antérieur et ultime (rang 0) ; Mātaṅga (rang 1)
# se réfère à lui ; Sārngadeva (rang 2) étend Bharata+Mātaṅga ; Kumbha et
# Šārdūla (rang 3) sont postérieurs et marginaux.
# evidence: aff#537 (Bharata antérieur, contrasté avec Mātaṅga),
# aff#1556 + aff#1557 (Bharata et Mātaṅga = même strate de 10
# lakṣaṇas, Sārngadeva ajoute 2),
# aff#3108 (Kumbha postérieur à Mātaṅga, le contredit).
AUTHORITY_RANK: dict[Authority, int] = {
Authority.BHARATA: 0, # ultimate (NŚ)
Authority.MATANGA: 1, # author of Brihaddesi
Authority.SARNGADEVA: 2, # Saṅgītaratnākara, later
Authority.KUMBHA: 3, # later commentator
Authority.SARDULA: 3, # ancillary
}
class LakshanaScope(str, Enum):
"""Lakṣaṇa is two-fold: general (sāmānya) and particular (viśiṣṭa /
"aṁśa-and-the-like"). General is 4-fold; particular is in the form of
aṁśa and similar features.
evidence: aff#531 (general 4-fold / special aṁśa-and-like),
aff#540 (lakṣaṇa is two-fold), R_lakshana_class
"""
GENERAL = "general" # sāmānya — 4-fold
PARTICULAR = "particular" # viśiṣṭa — aṁśa-and-like
class LakshanaCompleteness(str, Enum):
"""A jāti's identity is described by its lakṣaṇas. The status of a jāti
depends on how many of its canonical 10 lakṣaṇas are preserved vs.
altered:
- SUDDHA: all 10 canonical lakṣaṇas conform (śuddhā = "pure")
- VIKRTA: ≥1 lakṣaṇa altered (vikṛtā = "modified")
- INCOMPLETE: <10 lakṣaṇas specified (under-described)
evidence: aff#107 (alteration of one/two/many lakṣaṇas transforms
śuddhā → vikṛtā), R_lakshanas_jati,
aff#1823 (the 10 lakṣaṇas of jātis include graha, aṁśa, tāra,
mandra, etc.).
"""
SUDDHA = "suddha"
VIKRTA = "vikrta"
INCOMPLETE = "incomplete"
class DramaSandhi(str, Enum):
"""The five sandhis (junctures) of drama per Nāṭyaśāstra XIX,
enumerated identically in the Brihaddesi.
evidence: R_272_sandhis_enum (full enumeration with source NS_XIX),
aff#1903 (Garbha-sandhi is third among five; mukha + pratimukha
are first two; vimarsa + nirvahana are last two),
aff#1930 (mukha + pratimukha are first two among five).
"""
MUKHA = "mukha" # 1st — opening
PRATIMUKHA = "pratimukha" # 2nd — counter-opening
GARBHA = "garbha" # 3rd — sprouting, fruit still enshrouded
VIMARSA = "vimarsa" # 4th — reflection / criticism
NIRVAHANA = "nirvahana" # 5th — achievement of the fruit (phala)
# Ordinal position of each sandhi — exposed because R_160_nirvahana and the
# definitional affirmations cite ordinals explicitly.
# evidence: R_272_sandhis_enum (5 sandhis enumerated in order),
# R_160_nirvahana (nirvahana := ordinal == 5),
# aff#1903 (garbha = 3rd), aff#1911 (nirvahana = 5th).
SANDHI_ORDINAL: dict[DramaSandhi, int] = {
DramaSandhi.MUKHA: 1,
DramaSandhi.PRATIMUKHA: 2,
DramaSandhi.GARBHA: 3,
DramaSandhi.VIMARSA: 4,
DramaSandhi.NIRVAHANA: 5,
}
class TaraGatiSpan(int, Enum):
"""The extent (gati) of tāra register from the aṁśa svara. Three
permissible spans coexist in the source: 4, 5, or 7 svaras above the
aṁśa. Beyond these is "undesirable".
The "threefold extent" claim (aff#167) is reconciled with the explicit
enumeration {4, 5, 7} in aff#179 — these are exactly three values.
evidence: aff#154 (5th svara, sometimes 6th),
aff#165 (5th svara from aṁśa),
aff#166 (5th svara or 4 svaras),
aff#167 (extent is threefold),
aff#179 (4th, 5th or 7th svara from aṁśa),
R_4p0_018, R_64_tara_extent.
note: aff#154 mentions "sometimes 6th" as a softer variant; not retained
in the canonical set since R_64_tara_extent enumerates {4,5,7} —
see UNRESOLVED.
"""
FOURTH = 4
FIFTH = 5
SEVENTH = 7
class Vritti(str, Enum):
"""Dramatic vṛtti (mode of presentation). Bhāratī is the first of the
four vṛttis, characterised by predominance of verbal expression.
evidence: r_brd_457_bharati_def, aff#1993 (first among four vṛttis,
predominance of verbal expression), aff#1432 (Bhadrāvatī elā
combined with bhāratī vṛtti).
note: only Bhāratī is operationally pinned in domain 9; the other three
vṛttis (kaiśikī, ārabhaṭī, sāttvatī) are named in NŚ but no
domain-9 rule specifies them → see UNRESOLVED.
"""
BHARATI = "bharati"
# -----------------------------------------------------------------------------
# Bharata's assignments of rāgas to sandhis
# -----------------------------------------------------------------------------
# Bharata prescribes which rāga / svara-class fits which dramatic juncture.
# The four pinned cases in domain 9 are:
# evidence: aff#625 (ṣaḍja-grāma rāga → pratimukha),
# aff#626 (sādhārita rāga → garbha),
# aff#627 (pañcama → avamarsa, i.e. vimarsa-adjacent),
# aff#632 (madhyama-grāma rāga → mukha),
# aff#614 + r_brd_381_nirvahana_use (śuddhakaiśikamadhyama →
# nirvahana).
SANDHI_RAGA_ASSIGNMENT: dict[DramaSandhi, str] = {
DramaSandhi.MUKHA: "madhyamagrama_raga", # aff#632
DramaSandhi.PRATIMUKHA: "sadjagrama_raga", # aff#625
DramaSandhi.GARBHA: "sadharita_raga", # aff#626
DramaSandhi.VIMARSA: "pancama", # aff#627 (avamarsa)
DramaSandhi.NIRVAHANA: "suddhakaisikamadhya_raga", # aff#614,
# r_brd_381_nirvahana_use
}
# -----------------------------------------------------------------------------
# Lakṣaṇa counts per object-of-description
# -----------------------------------------------------------------------------
# Different objects of description carry different lakṣaṇa-counts. The two
# canonical ones in domain 9:
# evidence: aff#1823 (10 lakṣaṇas of jātis: graha, aṁśa, tāra, mandra, …),
# R_lakshanas_jati,
# aff#2403 (audūvita lakṣaṇa is fivefold),
# R_lakshana_class (general 4-fold; jāti=10; audūvita=5-fold).
LAKSHANA_COUNT: dict[str, int] = {
"jati": 10, # aff#1823, R_lakshanas_jati
"audūvita": 5, # aff#2403, R_lakshana_class
"lakshana_general": 4, # aff#531, R_lakshana_class (general 4-fold)
}
# The named features explicitly listed among the 10 jāti-lakṣaṇas. Only
# those named in source affirmations are kept; the remaining slots are
# left as UNRESOLVED.
# evidence: aff#1823 (the 10 lakṣaṇas of jātis include graha, aṁśa, tāra,
# mandra etc.), R_lakshanas_jati.
JATI_LAKSHANA_NAMED: tuple[str, ...] = (
"graha", "amsa", "tara", "mandra",
)
# Śārngadeva's expansion: he adds two markers — samnyāsa-vinyāsa and
# antaramārga — to the 10 lakṣaṇas accepted by Bharata + Mātaṅga, yielding 12.
# evidence: aff#1551, aff#1556, aff#1557, aff#1558,
# R_1257_jati_laksanas_sarngadeva.
SARNGADEVA_ADDED_LAKSHANAS: frozenset[str] = frozenset({
"samnyasa_vinyasa",
"antaramarga",
})
# Number of jāti-lakṣaṇas accepted per authority. NOT 10 → 12 by individual
# rule of addition: Sārngadeva's set is a strict superset.
# evidence: aff#1556 (Bharata = 10), aff#1557 (Mātaṅga = 10),
# aff#1558 + R_1257_jati_laksanas_sarngadeva (Sārngadeva = 12).
LAKSHANA_COUNT_BY_AUTHORITY: dict[Authority, int] = {
Authority.BHARATA: 10,
Authority.MATANGA: 10,
Authority.SARNGADEVA: 12,
}
# -----------------------------------------------------------------------------
# Deśī rāgas typology
# -----------------------------------------------------------------------------
# Mātaṅga: 3 types of deśī rāgas. Sārngadeva: 4 types (adds upānga).
# evidence: aff#1961, aff#1962, desi_raga_types_count.
DESI_RAGA_TYPE_COUNT: dict[Authority, int] = {
Authority.MATANGA: 3, # aff#1961
Authority.SARNGADEVA: 4, # aff#1962 (adds upānga)
}
DESI_RAGA_ADDED_BY_SARNGADEVA: str = "upanga" # aff#1962
# -----------------------------------------------------------------------------
# Antaramārga components — the six aṅgas whose mutual connection defines it
# -----------------------------------------------------------------------------
# evidence: R_c150_antaramarga (components = {graha, apanyasa, vinyasa,
# samnyasa, nyasa, amsa}; mutual connection),
# aff#1553, aff#205 (antaramārga + nyāsa manifests jāti).
ANTARAMARGA_COMPONENTS: frozenset[str] = frozenset({
"graha", "apanyasa", "vinyasa", "samnyasa", "nyasa", "amsa",
})
# =============================================================================
# TYPES — dataclasses
# =============================================================================
@dataclass(frozen=True)
class Citation:
"""A reference made by Mātaṅga to a prior authority. The standard form
of citation in the Brihaddesi is the formula "Similarly has said
Bharata —" followed by a verse, optionally tagged with an NŚ locus.
evidence: aff#148, aff#171, aff#182, aff#191, aff#221, aff#312, aff#2150,
aff#2468 (canonical "Similarly has said Bharata —" pattern);
aff#178, aff#222 (NŚ XXVIII.70 / NŚ XXVIII.72 loci);
aff#1873 (citation occurring after Anu. 129).
"""
citing: Authority # who is making the citation (usually MATANGA)
cited: Authority # who is being cited (usually BHARATA)
locus: str | None = None # e.g. "NS_XXVIII.72", "NS_XIX", or None
topic: str | None = None # bare concept name being cited about
def __post_init__(self) -> None:
if self.citing == self.cited:
raise ValueError(
"Citation requires citing != cited "
"(self-reference is not a citation)"
)
if AUTHORITY_RANK[self.citing] < AUTHORITY_RANK[self.cited]:
# rank 0 = most ancient; citing must not be older than cited.
raise ValueError(
f"{self.citing.value} (rank {AUTHORITY_RANK[self.citing]}) "
f"cannot cite {self.cited.value} "
f"(rank {AUTHORITY_RANK[self.cited]}): authority order "
"violated"
)
@dataclass(frozen=True)
class Lakshana:
"""A lakṣaṇa is a defining-characteristic. A treatise IS its set of
lakṣaṇas. Each lakṣaṇa has:
- a scope (general vs. particular);
- a target object (the thing being defined: a jāti, audūvita, etc.);
- a name (e.g. "graha", "aṁśa", "tāra"…) when it is itself a feature.
evidence: aff#531, aff#540 (two-fold structure),
aff#914 (each bhāṣā has lakṣaṇas),
aff#1180 (a bhāṣā should be sung in accordance with its
lakṣaṇas), R_lakshana_class.
"""
name: str
scope: LakshanaScope
target: str # what is being defined ("jati", "audūvita", "bhasha", …)
@dataclass(frozen=True)
class LakshanaSet:
"""A set of lakṣaṇas describing one object. Used to compute its
completeness status (śuddhā / vikṛtā / incomplete).
evidence: aff#107 (alteration of ≥1 lakṣaṇa transforms śuddhā into
vikṛtā), aff#1823 (10 lakṣaṇas of jātis),
R_lakshanas_jati.
"""
target: str # e.g. "jati"
expected_count: int # canonical N (10 for jāti)
present: frozenset[str] # named lakṣaṇas actually specified
altered: frozenset[str] = field(default_factory=frozenset)
def __post_init__(self) -> None:
if self.altered - self.present:
raise ValueError(
"altered lakṣaṇas must be a subset of present lakṣaṇas: "
f"unknown altered={self.altered - self.present}"
)
@dataclass(frozen=True)
class TaraRange:
"""The tāra register extending from a starting svara (the aṁśa) by a
permitted gati-span (4, 5, or 7).
evidence: aff#165 (extent of tāra begins from aṁśa),
aff#179 (4th / 5th / 7th from aṁśa),
R_64_tara_extent, R_4p0_018, aff#1859 (tāra properly begins
from octave of ga).
"""
amsa: str # bare name of the aṁśa svara (e.g. "sa", "ga"), kept
# as str because Svara enum lives in domain 3
span: TaraGatiSpan
def __post_init__(self) -> None:
if self.span not in (TaraGatiSpan.FOURTH, TaraGatiSpan.FIFTH,
TaraGatiSpan.SEVENTH):
raise ValueError(
f"tāra span must be 4, 5 or 7; got {self.span}"
)
@dataclass(frozen=True)
class Antaramarga:
"""The intermediate melodic pathway. Defined by:
- mutual connection of the 6 aṅgas (graha, apanyāsa, vinyāsa, samnyāsa,
nyāsa, aṁśa);
- non-repetition (anabhyāsa) of svaras;
- leaping (vilanghana) of svaras;
- effect: many svaras become non-aṁśas; with nyāsa it manifests jātis;
non-aṁśa svaras may NOT be sparse here.
evidence: R_4p1_368, R_c150_antaramarga, aff#205, aff#207, aff#485,
aff#1553, aff#1864, aff#1873.
"""
non_repetition: bool = True # anabhyāsa, aff#1864, R_4p1_368
leaping: bool = True # vilanghana, R_4p1_368
components: frozenset[str] = field(
default_factory=lambda: ANTARAMARGA_COMPONENTS
)
def __post_init__(self) -> None:
# mutual connection requires that all 6 aṅgas be present
# (R_c150_antaramarga: "mutual_connection(components)")
if not self.components.issuperset(ANTARAMARGA_COMPONENTS):
missing = ANTARAMARGA_COMPONENTS - self.components
raise ValueError(
f"Antaramārga requires all 6 aṅgas in mutual connection; "
f"missing: {sorted(missing)}"
)
@dataclass(frozen=True)
class SandhiAssignment:
"""An assignment of a rāga/svara-class to a dramatic juncture, per
Bharata's prescription.
evidence: aff#614, aff#625, aff#626, aff#627, aff#632,
r_brd_381_nirvahana_use.
"""
sandhi: DramaSandhi
raga: str
attributed_to: Authority = Authority.BHARATA
# =============================================================================
# OPÉRATIONS — dérivations exécutables
# =============================================================================
def lakshana_completeness(ls: LakshanaSet) -> LakshanaCompleteness:
"""Classify a LakshanaSet as śuddhā / vikṛtā / incomplete.
Rule (aff#107, R_lakshanas_jati):
- if fewer than expected_count lakṣaṇas are present → INCOMPLETE
- elif any lakṣaṇa is altered → VIKRTA
- else → SUDDHA
evidence: aff#107 ("alteration of one, two or many lakṣaṇas transforms
śuddhā into vikṛtā"), R_lakshanas_jati, R_lakshana_class.
"""
if len(ls.present) < ls.expected_count:
return LakshanaCompleteness.INCOMPLETE
if ls.altered:
return LakshanaCompleteness.VIKRTA
return LakshanaCompleteness.SUDDHA
def authority_strength(claim_attributed_to: Authority) -> int:
"""Return a stratification rank for a claim's authority. Lower = older =
stronger in the Brihaddesi's epistemic order (Bharata = 0).
Use case: when two claims conflict, the one attributed to the lower-rank
authority prevails (or at minimum, must be addressed first).
evidence: AUTHORITY_RANK (see its citations: aff#537, aff#1556,
aff#1557, aff#1558, aff#3108).
"""
return AUTHORITY_RANK[claim_attributed_to]
def resolve_authority_conflict(
claim_a: tuple[str, Authority],
claim_b: tuple[str, Authority],
) -> tuple[str, Authority]:
"""Given two conflicting claims, return the one whose authority is more
senior (lower rank = older/ultimate). This does NOT prove the claim
correct — it only reflects the text's own epistemic ordering, in which
Bharata's word is the touchstone.
Tie-breaker: same rank → returns claim_a unchanged (caller must mark as
UNRESOLVED).
evidence: aff#537 (Bharata as prior authority contrasted with Mātaṅga),
aff#3108 (Kumbha contradicts Mātaṅga, no resolution offered),
aff#1931 (Bharata's text doesn't say X — used to challenge a
claim).
"""
rank_a = AUTHORITY_RANK[claim_a[1]]
rank_b = AUTHORITY_RANK[claim_b[1]]
return claim_a if rank_a <= rank_b else claim_b
def cite_chain(citations: Iterable[Citation]) -> list[Authority]:
"""Return the linear chain of authorities walked by a list of citations,
in the order they cite (citing → cited).
Example: [Mātaṅga cites Bharata] → [BHARATA, MATANGA]-reversed-walk =
[MATANGA, BHARATA].
evidence: standard citation pattern (aff#171, aff#182, aff#191, aff#221,
aff#312, aff#2150, aff#2468 — "Similarly has said Bharata —"),
aff#2929 (Simhabhūpāla cites Mātaṅga: chained citation).
"""
chain: list[Authority] = []
for c in citations:
if not chain:
chain.append(c.citing)
elif chain[-1] != c.citing:
# not a continuous chain — caller is composing distinct citations
chain.append(c.citing)
chain.append(c.cited)
return chain
def sandhi_for_raga(raga: str) -> DramaSandhi | None:
"""Inverse lookup: which sandhi does Bharata assign to this rāga?
evidence: SANDHI_RAGA_ASSIGNMENT (see its evidence affirmations).
Returns None if rāga is not among the 5 pinned cases.
"""
for sandhi, assigned in SANDHI_RAGA_ASSIGNMENT.items():
if assigned == raga:
return sandhi
return None
def raga_for_sandhi(sandhi: DramaSandhi) -> str:
"""Forward lookup: which rāga does Bharata assign to this sandhi?
evidence: SANDHI_RAGA_ASSIGNMENT.
"""
return SANDHI_RAGA_ASSIGNMENT[sandhi]
def tara_endpoint_index(amsa_index: int, span: TaraGatiSpan) -> int:
"""Compute the absolute svara-position reached by tāra extension from
the aṁśa. The aṁśa is at position `amsa_index`; tāra ends `span`
positions above (where the count is inclusive of the aṁśa per
R_4p0_018: "from the use of aṁśa svara").
evidence: R_4p0_018 ("ascent from aṁśa to the fifth svara above"),
aff#165, aff#179.
"""
return amsa_index + int(span) - 1
def jati_lakshana_set_for_authority(
authority: Authority,
present: Iterable[str],
altered: Iterable[str] = (),
) -> LakshanaSet:
"""Build a LakshanaSet for a jāti according to the count accepted by
`authority` (10 for Bharata/Mātaṅga, 12 for Sārngadeva).
evidence: LAKSHANA_COUNT_BY_AUTHORITY, R_1257_jati_laksanas_sarngadeva,
R_lakshanas_jati.
"""
if authority not in LAKSHANA_COUNT_BY_AUTHORITY:
raise ValueError(
f"Lakṣaṇa count not specified for {authority.value} in domain 9"
)
return LakshanaSet(
target="jati",
expected_count=LAKSHANA_COUNT_BY_AUTHORITY[authority],
present=frozenset(present),
altered=frozenset(altered),
)
# =============================================================================
# CONTRAINTES — validations
# =============================================================================
def is_well_formed_sandhi_sequence(seq: Iterable[DramaSandhi]) -> bool:
"""A well-formed dramatic exposition presents the five sandhis in their
canonical ordinal order (1 → 2 → 3 → 4 → 5).
evidence: R_272_sandhis_enum (enumeration in order),
aff#1903 (garbha = 3rd, mukha+pratimukha = first two, vimarsa+
nirvahana = last two), aff#1930.
"""
seq = list(seq)
if not seq:
return True
ordinals = [SANDHI_ORDINAL[s] for s in seq]
return all(ordinals[i] < ordinals[i + 1] for i in range(len(ordinals) - 1))
def is_consistent_lakshana_count(target: str, n: int) -> bool:
"""Verify that a claimed lakṣaṇa-count for a target matches the
Brihaddesi's canonical count.
Returns True for unknown targets (under-constrained).
evidence: LAKSHANA_COUNT (see citations).
"""
expected = LAKSHANA_COUNT.get(target)
if expected is None:
return True
return n == expected
def citation_respects_authority_order(c: Citation) -> bool:
"""A citation in the Brihaddesi is always: a younger authority citing
an older one. Bharata is never cited as citing anyone (he is rank 0).
evidence: aff#537 (Mātaṅga sets himself "after" Bharata),
AUTHORITY_RANK (cf. aff#3108 — even when Kumbha contradicts
Mātaṅga, Mātaṅga remains the cited senior in that pair).
"""
return AUTHORITY_RANK[c.citing] > AUTHORITY_RANK[c.cited]
def is_valid_tara_span(span: int) -> bool:
"""True iff `span` is one of the three permitted tāra extents.
evidence: R_64_tara_extent (∈ {4,5,7}), aff#179.
"""
return span in (int(TaraGatiSpan.FOURTH),
int(TaraGatiSpan.FIFTH),
int(TaraGatiSpan.SEVENTH))
# =============================================================================
# UNRESOLVED — concepts mentionnés mais non formellement épinglés
# =============================================================================
UNRESOLVED: tuple[dict[str, str], ...] = (
{
"concept": "the 6 unnamed jāti-lakṣaṇas",
"reason": "R_lakshanas_jati and aff#1823 explicitly enumerate only "
"4 of 10 (graha, aṁśa, tāra, mandra) and trail off with "
"'etc.' — the remaining 6 names are not pinned by any "
"domain-9 rule.",
},
{
"concept": "tāra span = 6 svaras",
"reason": "aff#154 mentions 'sometimes even the ascent up to the "
"sixth svara is tāra' as a softer variant. R_64_tara_extent "
"and aff#179 give canonical {4,5,7}. Not retained as a "
"valid TaraGatiSpan member — would conflict with the "
"'threefold extent' claim of aff#167.",
},
{
"concept": "the other three vṛttis (kaiśikī, ārabhaṭī, sāttvatī)",
"reason": "Bhāratī is the first of four (aff#1993, r_brd_457_"
"bharati_def) but the other three are not pinned by any "
"domain-9 rule — only Bhāratī is enumerated.",
},
{
"concept": "jāti-lakṣaṇa altered-feature semantics",
"reason": "aff#107 says alteration of '1, 2, or many' lakṣaṇas "
"yields vikṛtā — but no rule specifies WHICH alteration "
"produces WHICH named vikṛti, so the mapping vikṛtā-name "
"↔ altered-set is not pinned.",
},
{
"concept": "Mātaṅga's seven gītis vs. Kasyapa",
"reason": "aff#543 attributes seven gītis to Mātaṅga; aff#1595 "
"records that 'Mātaṅga' was replaced with 'Kasyapa' in "
"the printed text — disputed authorship, not modelled.",
},
{
"concept": "antara/kākalī starting mūrchanā",
"reason": "aff#3108: Mātaṅga accepts mūrchanās starting on antara/"
"kākalī, Kumbha later denies it without acknowledging "
"Mātaṅga's position. Disagreement not resolved by any "
"rule.",
},
{
"concept": "NŚ XXVIII.72 as a free-standing citation node",
"reason": "cluster_id=716 is a bare locus with 2 citation "
"affirmations (aff#178 NS XXVIII.70, aff#222 NS XXVIII.72) "
"but no rule binds it to any operation in domain 9.",
},
{
"concept": "dhvani theory ↔ Mātaṅga influence",
"reason": "aff#2993 attributes a philosophical lineage from Mātaṅga "
"to dhvani theory in literature — outside the operational "
"scope of a formal grammar; recorded for traceability.",
},
)
# =============================================================================
# Self-test
# =============================================================================
if __name__ == "__main__":
# Authorities & ranks
assert AUTHORITY_RANK[Authority.BHARATA] == 0
assert AUTHORITY_RANK[Authority.MATANGA] == 1
assert (AUTHORITY_RANK[Authority.SARNGADEVA]
> AUTHORITY_RANK[Authority.MATANGA])
# Citation: Mātaṅga citing Bharata is valid; reverse is invalid
c1 = Citation(
citing=Authority.MATANGA,
cited=Authority.BHARATA,
locus="NS_XXVIII.72",
topic="nyasa_apanyasa",
)
assert citation_respects_authority_order(c1)
try:
Citation(citing=Authority.BHARATA, cited=Authority.MATANGA)
assert False, "Bharata cannot cite Mātaṅga (rank order violated)"
except ValueError:
pass
try:
Citation(citing=Authority.BHARATA, cited=Authority.BHARATA)
assert False, "Citation requires citing != cited"
except ValueError:
pass
# Authority strength
a = ("dhaivata may be omitted in sadjagrama", Authority.MATANGA)
b = ("dhaivata may NOT be omitted in sadjagrama", Authority.BHARATA)
winner = resolve_authority_conflict(a, b)
assert winner[1] == Authority.BHARATA, "Bharata wins on rank"
# Cite chain
chain = cite_chain([c1])
assert chain == [Authority.MATANGA, Authority.BHARATA]
# Lakshana completeness
ls_full_pure = jati_lakshana_set_for_authority(
Authority.BHARATA,
present=["graha", "amsa", "tara", "mandra", "nyasa", "apanyasa",
"sancara", "graha2", "alpatva", "bahutva"],
)
assert lakshana_completeness(ls_full_pure) == LakshanaCompleteness.SUDDHA
ls_full_altered = jati_lakshana_set_for_authority(
Authority.BHARATA,
present=["graha", "amsa", "tara", "mandra", "nyasa", "apanyasa",
"sancara", "graha2", "alpatva", "bahutva"],
altered=["amsa"],
)
assert lakshana_completeness(ls_full_altered) == LakshanaCompleteness.VIKRTA
ls_incomplete = jati_lakshana_set_for_authority(
Authority.BHARATA,
present=["graha", "amsa"],
)
assert lakshana_completeness(ls_incomplete) == LakshanaCompleteness.INCOMPLETE
# Sārngadeva expects 12, not 10
ls_sarnga_only_10 = jati_lakshana_set_for_authority(
Authority.SARNGADEVA,
present=["graha", "amsa", "tara", "mandra", "nyasa", "apanyasa",
"sancara", "graha2", "alpatva", "bahutva"],
)
assert lakshana_completeness(ls_sarnga_only_10) == \
LakshanaCompleteness.INCOMPLETE
ls_sarnga_full = jati_lakshana_set_for_authority(
Authority.SARNGADEVA,
present=["graha", "amsa", "tara", "mandra", "nyasa", "apanyasa",
"sancara", "graha2", "alpatva", "bahutva",
"samnyasa_vinyasa", "antaramarga"],
)
assert lakshana_completeness(ls_sarnga_full) == LakshanaCompleteness.SUDDHA
# Sandhi ordering
assert is_well_formed_sandhi_sequence([
DramaSandhi.MUKHA, DramaSandhi.PRATIMUKHA, DramaSandhi.GARBHA,
DramaSandhi.VIMARSA, DramaSandhi.NIRVAHANA,
])
assert not is_well_formed_sandhi_sequence([
DramaSandhi.NIRVAHANA, DramaSandhi.MUKHA,
])
# Sandhi assignment lookups
assert raga_for_sandhi(DramaSandhi.NIRVAHANA) == "suddhakaisikamadhya_raga"
assert sandhi_for_raga("madhyamagrama_raga") == DramaSandhi.MUKHA
assert sandhi_for_raga("not_a_known_raga") is None
# Tāra range
tr = TaraRange(amsa="sa", span=TaraGatiSpan.FIFTH)
assert tr.span == TaraGatiSpan.FIFTH
assert is_valid_tara_span(7)
assert not is_valid_tara_span(6) # see UNRESOLVED
# If aṁśa is position 0, span 5 reaches position 4 (5th svara inclusive)
assert tara_endpoint_index(0, TaraGatiSpan.FIFTH) == 4
# Antaramārga: must include all 6 aṅgas
am = Antaramarga()
assert am.non_repetition and am.leaping
try:
Antaramarga(components=frozenset({"graha", "amsa"}))
assert False, "Antaramārga requires all 6 components"
except ValueError:
pass
# Lakṣaṇa count consistency
assert is_consistent_lakshana_count("jati", 10)
assert not is_consistent_lakshana_count("jati", 12) # Bharata/Mātaṅga
assert is_consistent_lakshana_count("audūvita", 5)
assert not is_consistent_lakshana_count("audūvita", 4)
assert is_consistent_lakshana_count("unknown_target", 99) # under-constrained
# Sandhi ordinals (cross-check enum vs. dict)
for s in DramaSandhi:
assert SANDHI_ORDINAL[s] in (1, 2, 3, 4, 5)
assert SANDHI_ORDINAL[DramaSandhi.NIRVAHANA] == 5
assert SANDHI_ORDINAL[DramaSandhi.GARBHA] == 3
# Deśī rāga type counts
assert DESI_RAGA_TYPE_COUNT[Authority.MATANGA] == 3
assert DESI_RAGA_TYPE_COUNT[Authority.SARNGADEVA] == 4
print("authority_lakshana.py — all self-tests pass")