← domain #10
Source : treatise_meta.py
data/library/books/brihaddesi_sharma_1992/formal_grammar/treatise_meta.py · 659 lines · 25253 bytes
"""Domaine 10 — treatise-meta (BRHADDEŚĪ / Nāda / Rāgalakṣaṇa / Saṅkarailā / Nrtta)
Synthèse 6c.3 du Brihaddesi (Sharma 1992, vols I & II), domaine méta-textuel :
identité réflexive du traité, théorie du Nāda comme principe ontologique
upstream, Rāgalakṣaṇa (spécification de rāga), Saṅkarailā (elā mixte),
Nrtta (danse pure), références croisées (SR = Saṅgītaratnākara, etc.).
Anti-fabrication stricte : chaque type, constante, opération et contrainte
cite ≥1 evidence (rule_id 6b ou affirmation_id de la DB pipeline). Aucune
valeur plausible-mais-non-sourcée n'est introduite — voir UNRESOLVED.
Ce domaine est essentiellement déclaratif (52 affirmations, 18 règles, 16
concepts-avec-règles). Beaucoup de concepts ont 0 règle directe ; cela
reflète la nature méta-textuelle du domaine (citations, identifiants
manuscrits, attributions). Les opérations modélisent les questions méta
exploitables (is_brihaddesi_topic, requires_ragalakshana_components,
nada_origin_chain, classify_ela, etc.).
Python 3.10+. Importable directement, sans dépendance externe.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
# =============================================================================
# IDENTITÉ DU TRAITÉ — métadonnées
# =============================================================================
# evidence: brhaddesi_treatise_metadata (rule), aff#2018, aff#2020, aff#2022,
# aff#2064, aff#2079, aff#2080
TREATISE_TITLE_IAST: str = "Bṛhaddeśī"
TREATISE_AUTHOR_IAST: str = "Mātaṅga Muni"
TREATISE_FIRST_PUBLISHED: int = 1992
TREATISE_PUBLISHER: str = "IGNCA" # Indira Gandhi National Centre for the Arts
# aff#72: dated approximately 750 CE (attribution — circa, not pinned).
TREATISE_DATE_CE_CIRCA: int = 750 # affirmation_id=72 ("approximately 750 CE")
# evidence: aff#57 (etymology), R_7_brhaddesi_structure
TREATISE_ETYMOLOGY_GLOSS: str = (
"Bṛhad + deśī — the 'great deśī [text]', a treatise systematically "
"classifying regional/desi musical phenomena."
)
# =============================================================================
# STRUCTURE EN SECTIONS — table verbatim de Sharma vol. I p.007
# =============================================================================
# evidence: R_7_brhaddesi_structure (rule), aff#2064 (six-section ToC quote),
# aff#2070..aff#2074 (sections I-V individually attested),
# aff#2849 (section VI = Varṇālaṅkāra), aff#2068+aff#2868 (a Pada-gīti
# section VII is mentioned by editor — explicitly OUTSIDE the
# canonical 6-section list per aff#2064 / R_7).
#
# The rule R_7 closes the structure at six sections. Pada-gīti (VII) is
# editorial / manuscript-tradition extension; we keep it tagged separately to
# preserve provenance.
class Section(str, Enum):
"""The six canonical sections of the Brihaddesi per Sharma 1992 ToC.
evidence: R_7_brhaddesi_structure, aff#2064
"""
DESI = "desi" # I — aff#2070
NADA = "nada" # II — aff#2071
SRUTI = "sruti" # III — aff#2072
SVARA = "svara" # IV — aff#2073
GRAMA_MURCHANA = "grama_murchana" # V — aff#2074
VARNA_ALANKARA = "varna_alankara" # VI — aff#2849
CANONICAL_SECTIONS: tuple[Section, ...] = (
Section.DESI,
Section.NADA,
Section.SRUTI,
Section.SVARA,
Section.GRAMA_MURCHANA,
Section.VARNA_ALANKARA,
)
# Extra-canonical editorial section attested in the manuscript tradition.
# evidence: aff#2068 ("Pada-gīti constitutes Section VII"), aff#2868,
# aff#2789. Not in R_7's canonical six.
EDITORIAL_SECTION_PADA_GITI: str = "pada_giti"
# Chapter-level structural assertions (within sections / cross-cutting).
# evidence per entry below.
CHAPTERS: dict[int, dict[str, str]] = {
3: { # R_656_ragalakshana_chapter (aff#74), ragalakshana_chapter_iii
# (aff#1593), aff#860
"name_iast": "Rāgalakṣaṇa",
"topic": "nature and definition of rāga",
"evidence": "R_656_ragalakshana_chapter; ragalakshana_chapter_iii; aff#860",
},
5: { # R_1076_desiragadhyaya_chapter5 (aff#1294)
"name_iast": "deśī-rāgādhyāya",
"topic": "treatment of deśī rāgas",
"evidence": "R_1076_desiragadhyaya_chapter5; aff#1294",
},
6: { # aff#1525 ("Thus ends the sixth chapter")
"name_iast": "(sixth chapter, unnamed in this assertion)",
"topic": "concluding chapter as attested by colophon",
"evidence": "aff#1525",
},
}
# Nāda-prakaraṇa internal topic list (section II micro-structure).
# evidence: R_c1619_nadaprakaranam_struct (aff#2107), aff#2055
NADAPRAKARANA_TOPICS: tuple[str, ...] = (
"prashamsa", # glory
"utpatti", # origin
"lakshanam", # definition
"bhedah", # kinds
)
# =============================================================================
# TÉMOIGNAGE MANUSCRIT — MS A, MS B, P.t.
# =============================================================================
# evidence: aff#2875 (MS A, MS B, P.t. mentioned together as variant sources),
# aff#1562 (MS A), aff#2865 (MS B), aff#2875 (P.t.)
class ManuscriptWitness(str, Enum):
"""Manuscript / editorial witnesses cited in Sharma's apparatus.
evidence: aff#2875 (joint mention), aff#1562 (MS A), aff#2865 (MS B)
P.t. is attested in aff#2875 and recurs in apparatus footnotes; its full
expansion is not given in the affirmations corpus — see UNRESOLVED.
"""
MS_A = "MS_A"
MS_B = "MS_B"
PT = "P.t."
# =============================================================================
# CROSS-REFERENCES — autres traités cités
# =============================================================================
# evidence per entry. The biblio is non-extensible — we only list treatises
# the Brihaddesi (or Sharma's edition apparatus) actually cites.
@dataclass(frozen=True)
class TreatiseReference:
"""A cross-reference from / about the Brihaddesi to another treatise.
evidence: per-entry below.
"""
sigil: str # short label as used in Sharma's apparatus
full_name_iast: str | None
role: str # e.g. 'authority', 'cited_definition', 'variant_recension'
evidence: str # affirmation_id / rule_id chain
CROSS_REFERENCES: tuple[TreatiseReference, ...] = (
TreatiseReference(
sigil="SR",
full_name_iast="Saṅgītaratnākara",
role="later_treatise_citing_brihaddesi_concepts",
# aff#1607 (SR II.2.25-26 defines Ākṣiptikā),
# aff#1791 (SR IV.232), aff#3067 (cakra/svara correspondence)
evidence="aff#1607; aff#1791; aff#3067",
),
TreatiseReference(
sigil="NS",
full_name_iast="Nāṭyaśāstra",
role="authority_with_variant_recension",
# aff#2871: 'cf. NŚ XXIX,44-48 and the variant recension ... 75-78'
evidence="aff#2871",
),
TreatiseReference(
sigil="Saktikāgama",
full_name_iast="Śaktikāgama",
role="cited_for_identification",
# aff#2900: Saktikāgama cited re Sakti = vowel ikāra
evidence="aff#2900",
),
# Sabdakalpadruma (cluster 1983) appears as a concept but no affirmation
# body pins its role — see UNRESOLVED.
)
# =============================================================================
# NĀDA — principe ontologique upstream
# =============================================================================
# evidence: R_nada_def, aff#2108..aff#2113, aff#2115, aff#2119
class NadaOrigin(str, Enum):
"""Posited origins of nāda. The Brihaddesi gives one primary statement
(fire+air) and explicitly notes alternative authorities — aff#2119.
evidence: aff#2113 (vahni+māruta), aff#2119 ('iti kecit ... ity anye vadanti')
"""
VAHNI_MARUTA = "vahni_maruta" # fire + air combination — aff#2113
KANDA_STHANA = "kanda_sthana" # alt: 'kandasthāna-samuttha samīra' — aff#2119
UNSPECIFIED = "unspecified" # for assertions that don't pin an origin
# Per aff#2925 (R_5p3_2036), in the context of nāda-production specifically,
# 'guhā' (lit. cave) refers to the navel, not the generic 'cavity of the heart'.
NADA_PRODUCTION_GUHA_LOCUS: str = "navel"
NADA_PRODUCTION_GUHA_NOTE: str = (
"Guhā in nāda-production context = navel (first point); generic sense "
"elsewhere = cavity of the heart. evidence: aff#2925, R_5p3_2036"
)
# Things that necessarily depend on nāda — "X cannot exist without nāda".
# Closed enumeration from the source verses on p.012.
# evidence: aff#2108 (gīta), aff#2109 (svara), aff#2110 (nṛtta),
# aff#2111 (jagat), aff#2115 (vāṅmaya)
NADA_DEPENDENTS: frozenset[str] = frozenset({
"gita", # music — aff#2108
"svara", # musical notes — aff#2109
"nrtta", # dance — aff#2110
"jagat", # world — aff#2111 ('tasmāt nādātmakam jagat')
"vanmaya", # all speech/language — aff#2115, R_524_vanmaya_rel
})
# Entities characterized as nāda-rūpa (form-of-nāda).
# evidence: R_2017_nada_rupa_four_deities (aff#2916), R_2016_para_sakti_ultimate
NADA_RUPA_ENTITIES: tuple[str, ...] = (
"Brahmā", "Śiva", "Viṣṇu", "Parā Śakti",
)
# =============================================================================
# RĀGALAKṢAṆA — spécification de rāga
# =============================================================================
# evidence: R_656_ragalakshana_chapter, ragalakshana_chapter_iii (aff#1593),
# aff#74, aff#860
@dataclass(frozen=True)
class RagalakshanaChapterMeta:
"""Metadata about the Rāgalakṣaṇa chapter (chapter III).
The affirmations in this domain give us only the *chapter assignment*,
not the internal component list of rāga-lakṣaṇa (that belongs to other
domains — jāti, mūrchanā, varṇa, etc.). So `required_components` is
deliberately left empty here; component-level rules live in the melodic
and structural domains. See UNRESOLVED.
evidence: R_656_ragalakshana_chapter, ragalakshana_chapter_iii
"""
chapter_number: int = 3
name_iast: str = "Rāgalakṣaṇa"
topic: str = "nature and definition of rāga"
required_components: tuple[str, ...] = () # deliberately empty — UNRESOLVED
RAGALAKSHANA_META = RagalakshanaChapterMeta()
# =============================================================================
# ŚAṬPADĪ / RAMAṆĪ / SAṄKARAILĀ — formes structurelles méta
# =============================================================================
# evidence: R_1170_01 (Saṅkarailā), R_1196_ramani_satpadi, R_1201_nadadhya_ramani
class YatiType(str, Enum):
"""Yati (caesura/pause pattern) types attested for ṣaṭpadī feet.
evidence: R_1196_ramani_satpadi (samā yati attested for ramaṇī)
Other yati types exist in tradition but are not pinned by domain-10 rules.
"""
SAMA = "sama" # equal — aff#1473
@dataclass(frozen=True)
class Shatpadi:
"""A six-footed (ṣaṭ-pad-ī) metrical / melodic structure.
The treatise enumerates several named ṣaṭpadīs; this dataclass models the
generic structure. Specific named subtypes (e.g. ramaṇī) are validated
by predicate functions below.
evidence: R_1196_ramani_satpadi (six feet stated explicitly)
"""
feet: tuple[dict, ...] # 6 feet, each described by a dict of attributes
def __post_init__(self) -> None:
if len(self.feet) != 6:
raise ValueError(
f"Shatpadi must have exactly 6 feet, got {len(self.feet)}"
)
@dataclass(frozen=True)
class Ela:
"""An elā (melodic form) with 4 feet, whose 4th foot's elaboration
relative to the 3rd determines its sub-classification.
The 4-foot structure is implicit in R_1170_01 (it references "the fourth
foot" and "the third foot"). evidence: R_1170_01 (aff#1441), aff#1442
"""
feet_elaboration: tuple[str, ...] # length 4 — sketch of each foot's elaboration
def __post_init__(self) -> None:
if len(self.feet_elaboration) != 4:
raise ValueError(
f"Ela must have 4 feet, got {len(self.feet_elaboration)}"
)
# =============================================================================
# NRTTA — danse pure
# =============================================================================
# evidence: nrtta_definition_and_nada_dependency (aff#2909, aff#2910)
@dataclass(frozen=True)
class NrttaSpec:
"""Pure dance specification.
evidence: nrtta_definition_and_nada_dependency
"""
pure_dance: bool = True # no artha-of-kāvya intent — aff#2909
depends_on_nada: bool = True # accompanied on instruments — aff#2910
accompaniment_emphasis: tuple[str, ...] = ("drums",) # aff#2910 ('specially drums')
NRTTA = NrttaSpec()
# =============================================================================
# OPÉRATIONS — questions méta exploitables
# =============================================================================
def is_brihaddesi_topic(section: Section) -> bool:
"""True iff `section` is one of the six canonical Brihaddesi sections.
evidence: R_7_brhaddesi_structure, aff#2064
"""
return section in CANONICAL_SECTIONS
def section_of_chapter(chapter_number: int) -> str | None:
"""Return chapter name if pinned in the corpus, else None.
Note: the Brihaddesi's section/chapter numbering is partly conflated in
the apparatus. Only chapters 3, 5, 6 have explicit colophons in the
affirmations corpus — see UNRESOLVED for 1, 2, 4, 7.
evidence: CHAPTERS dict per-entry.
"""
entry = CHAPTERS.get(chapter_number)
if entry is None:
return None
return entry["name_iast"]
def nada_origin_chain(target: str) -> tuple[str, ...]:
"""For `target` ∈ NADA_DEPENDENTS, return the upstream chain
[target ← nāda ← origin]. Pure declarative chain; does not invent
intermediate causes.
evidence: R_nada_def, aff#2108..aff#2115
"""
if target not in NADA_DEPENDENTS:
return ()
return (target, "nada", NadaOrigin.VAHNI_MARUTA.value)
def is_nada_rupa(entity: str) -> bool:
"""True iff `entity` is one of the four deities characterized as
nāda-rūpa per R_2017_nada_rupa_four_deities (aff#2916).
"""
return entity in NADA_RUPA_ENTITIES
def classify_ela(third_elab: str, fourth_elab: str) -> str:
"""Per R_1170_01 / aff#1441: an elā becomes saṅkara (mixed) when the
fourth foot is elaborated like the third foot.
Returns 'sankarailā' or 'unmarked_ela'. We do NOT enumerate other elā
sub-types here — they belong to other domains. evidence: R_1170_01
"""
if third_elab == fourth_elab:
return "sankarailā"
return "unmarked_ela"
def is_ramani(form: Shatpadi) -> bool:
"""True iff `form` is a ramaṇī: ṣaṭpadī with samā yati in feet 1 and 2
AND every foot adorned with nāda (nādāḍhyā).
evidence: R_1196_ramani_satpadi (aff#1473), R_1201_nadadhya_ramani (aff#1480)
"""
if len(form.feet) != 6:
return False
f1, f2 = form.feet[0], form.feet[1]
if f1.get("yati") != YatiType.SAMA.value:
return False
if f2.get("yati") != YatiType.SAMA.value:
return False
return all(f.get("adorned_by_nada") is True for f in form.feet)
def pancamanyasa_origin() -> str:
"""Per R_1280_01 (aff#1608): pañcamanyāsa originates from Kaiśikī jāti.
Pure lookup — no inference.
"""
return "Kaiśikī_jāti"
def is_sadjamsa_pancamanyasa_compound(term_a: str, term_b: str) -> bool:
"""Per R_1281_sadjamsa_pancamanyasa_compound (aff#1609): ṣadjāṁśaḥ
pairs as a compound with pañcamanyāsaḥ (textual emendation).
"""
pair = frozenset({term_a, term_b})
return pair == frozenset({"sadjamsa", "pancamanyasa"})
def has_textual_overlap_with_sadjamadhyama(host_concept: str) -> bool:
"""Per sadjodicyava_textual_overlap_with_sadjamadhyama (aff#1592):
the description of ṣaḍjodīcyavā contains material pertaining to
ṣaḍjamadhyamā. Closed assertion — only ṣaḍjodīcyavā is the documented host.
"""
return host_concept == "sadjodicyava"
def requires_ragalakshana_components() -> tuple[str, ...]:
"""Return the list of components that a rāga-lakṣaṇa specification must
contain.
Returns empty tuple here: the domain-10 rules pin the *chapter location*
of Rāgalakṣaṇa (chapter III) and its *topic* (nature/definition of rāga),
but do NOT enumerate its component slots — those live in jāti / mūrchanā /
varṇa-alaṅkāra domains. See UNRESOLVED.
evidence: R_656_ragalakshana_chapter, ragalakshana_chapter_iii
"""
return RAGALAKSHANA_META.required_components
def cite_treatise(sigil: str) -> TreatiseReference | None:
"""Look up a treatise cross-reference by short sigil.
evidence: CROSS_REFERENCES (per-entry)
"""
for ref in CROSS_REFERENCES:
if ref.sigil == sigil:
return ref
return None
# =============================================================================
# CONTRAINTES — validations
# =============================================================================
def is_valid_canonical_section(section: Section) -> bool:
"""A section is canonical iff it is one of the six in R_7's list.
evidence: R_7_brhaddesi_structure
"""
return section in CANONICAL_SECTIONS
def is_valid_nada_dependent(claim_target: str) -> bool:
"""True iff `claim_target` is in the closed set of nāda-dependents
explicitly asserted by the source verses on p.012.
evidence: aff#2108..aff#2115
"""
return claim_target in NADA_DEPENDENTS
def is_valid_nadaprakarana_topic(topic: str) -> bool:
"""Per R_c1619_nadaprakaranam_struct, only 4 topics are listed for
nāda-prakaraṇa: prashamsa, utpatti, lakshanam, bhedah.
evidence: R_c1619_nadaprakaranam_struct (aff#2107)
"""
return topic in NADAPRAKARANA_TOPICS
def is_valid_manuscript_witness(label: str) -> bool:
"""True iff `label` is one of the three manuscript witnesses attested.
evidence: aff#2875
"""
return label in {m.value for m in ManuscriptWitness}
# =============================================================================
# UNRESOLVED — concepts mentionnés mais non formellement contraints
# =============================================================================
UNRESOLVED: tuple[dict[str, str], ...] = (
{
"concept": "Section VII (Pada-gīti)",
"reason": "Attested in editorial apparatus (aff#2068, aff#2868, "
"aff#2789) but explicitly OUTSIDE R_7_brhaddesi_structure's "
"canonical six-section list. Modelled as EDITORIAL_SECTION_"
"PADA_GITI without being added to CANONICAL_SECTIONS.",
},
{
"concept": "P.t. (manuscript expansion)",
"reason": "Attested as a witness sigil (aff#2875, plus apparatus "
"footnotes), but the full expansion of 'P.t.' is not given "
"in the affirmations corpus.",
},
{
"concept": "Rāgalakṣaṇa component slots",
"reason": "R_656 / ragalakshana_chapter_iii pin chapter III's topic "
"as 'nature and definition of rāga' but do NOT enumerate "
"the slots a rāga-lakṣaṇa must fill (graha, aṁśa, nyāsa, "
"tāra, mandra, alpatva, bahutva, etc. live in other "
"domains). requires_ragalakshana_components() returns ().",
},
{
"concept": "Chapters 1, 2, 4, 7 names",
"reason": "Only chapters 3 (Rāgalakṣaṇa), 5 (deśī-rāgādhyāya), 6 "
"(unnamed in colophon aff#1525) have explicit name "
"assertions in this domain. Others not in CHAPTERS dict.",
},
{
"concept": "BŖHADDEŚĪ vs BRHADDEŚĪ (cluster 169 vs 2)",
"reason": "Two cluster forms — BRHADDEŚĪ (cluster 2, 64 occurrences) "
"and BŖHADDEŚĪ (cluster 169, 6 occurrences) — refer to the "
"same treatise. Domain 10 keeps both; the canonical IAST is "
"Bṛhaddeśī.",
},
{
"concept": "Nāda — alternative origin (kandasthāna-samīra)",
"reason": "aff#2119 explicitly records dissenting authorities: 'iti "
"kecit ... ity anye vadanti'. The primary origin "
"(vahni+māruta, aff#2113) is what R_nada_def fixes; the "
"alternative is modelled in NadaOrigin enum but not in "
"nada_origin_chain() output, by design (the rule pins one).",
},
{
"concept": "Sabdakalpadruma role",
"reason": "Sabdakalpadruma (cluster 1983) appears as a concept but no "
"affirmation describes its role w.r.t. the Brihaddesi.",
},
{
"concept": "Other ṣaṭpadī subtypes (padmini, mohini, etc.)",
"reason": "Clusters 580 (padmini) and 649 (Mohini) appear with zero "
"affirmations in domain 10. ramaṇī is the only ṣaṭpadī "
"with a complete structural rule (R_1196).",
},
)
# =============================================================================
# Self-test
# =============================================================================
if __name__ == "__main__":
# Sanity: canonical sections
assert len(CANONICAL_SECTIONS) == 6
assert all(is_brihaddesi_topic(s) for s in CANONICAL_SECTIONS)
assert is_valid_canonical_section(Section.NADA)
# Sanity: chapter lookup
assert section_of_chapter(3) == "Rāgalakṣaṇa"
assert section_of_chapter(5) == "deśī-rāgādhyāya"
assert section_of_chapter(42) is None # not pinned
# Sanity: nāda origin chain
chain = nada_origin_chain("gita")
assert chain == ("gita", "nada", "vahni_maruta"), chain
assert nada_origin_chain("svara")[1] == "nada"
assert nada_origin_chain("not_a_dependent") == ()
assert is_valid_nada_dependent("nrtta")
assert is_valid_nada_dependent("jagat")
assert not is_valid_nada_dependent("raga") # not in the closed list
# Sanity: nāda-rūpa
assert is_nada_rupa("Brahmā")
assert is_nada_rupa("Parā Śakti")
assert not is_nada_rupa("Indra")
# Sanity: nāda-prakaraṇa topics
for t in NADAPRAKARANA_TOPICS:
assert is_valid_nadaprakarana_topic(t)
assert not is_valid_nadaprakarana_topic("madhura")
assert len(NADAPRAKARANA_TOPICS) == 4
# Sanity: ela classification (R_1170_01)
assert classify_ela("X", "X") == "sankarailā"
assert classify_ela("X", "Y") == "unmarked_ela"
# Sanity: ramaṇī predicate
ramani_form = Shatpadi(feet=(
{"yati": "sama", "adorned_by_nada": True},
{"yati": "sama", "adorned_by_nada": True},
{"yati": "other", "adorned_by_nada": True},
{"yati": "other", "adorned_by_nada": True},
{"yati": "other", "adorned_by_nada": True},
{"yati": "other", "adorned_by_nada": True},
))
assert is_ramani(ramani_form)
not_ramani = Shatpadi(feet=(
{"yati": "other", "adorned_by_nada": True},
{"yati": "sama", "adorned_by_nada": True},
{"yati": "other", "adorned_by_nada": True},
{"yati": "other", "adorned_by_nada": True},
{"yati": "other", "adorned_by_nada": True},
{"yati": "other", "adorned_by_nada": True},
))
assert not is_ramani(not_ramani)
# Sanity: Ela structural constraint
e = Ela(feet_elaboration=("a", "b", "c", "c"))
assert classify_ela(e.feet_elaboration[2], e.feet_elaboration[3]) == "sankarailā"
# Sanity: textual overlap assertion
assert has_textual_overlap_with_sadjamadhyama("sadjodicyava")
assert not has_textual_overlap_with_sadjamadhyama("sadjagrama")
# Sanity: pañcamanyāsa origin
assert pancamanyasa_origin() == "Kaiśikī_jāti"
# Sanity: sadjamsa-pancamanyasa compound
assert is_sadjamsa_pancamanyasa_compound("sadjamsa", "pancamanyasa")
assert is_sadjamsa_pancamanyasa_compound("pancamanyasa", "sadjamsa")
assert not is_sadjamsa_pancamanyasa_compound("sadjamsa", "madhyamsa")
# Sanity: cross-references
sr = cite_treatise("SR")
assert sr is not None and sr.full_name_iast == "Saṅgītaratnākara"
ns = cite_treatise("NS")
assert ns is not None and "Nāṭyaśāstra" in ns.full_name_iast
assert cite_treatise("Foo") is None
# Sanity: manuscript witnesses
for w in ("MS_A", "MS_B", "P.t."):
assert is_valid_manuscript_witness(w)
assert not is_valid_manuscript_witness("MS_C")
# Sanity: Rāgalakṣaṇa
assert RAGALAKSHANA_META.chapter_number == 3
assert requires_ragalakshana_components() == () # deliberately empty
# Sanity: Nrtta
assert NRTTA.pure_dance is True
assert NRTTA.depends_on_nada is True
assert "drums" in NRTTA.accompaniment_emphasis
# Sanity: shape constraints
try:
Shatpadi(feet=({},) * 5)
raise AssertionError("Shatpadi should require 6 feet")
except ValueError:
pass
try:
Ela(feet_elaboration=("a", "b", "c"))
raise AssertionError("Ela should require 4 feet")
except ValueError:
pass
print("treatise_meta.py — all self-tests pass")