# The knowledge context The raag clock's attributed timing claims (divergence preserved, never adjudicated) and the composition form metadata read only from headings, never guessed. - Page: - Updated: 2026-09-27 - Verified against: `364c452` on 2026-09-27 - From the Sri Guru Granth Sahib Ji — Knowledge Base wiki. Sri Guru Granth Sahib Ji is quoted verbatim, cited by Ang, and never altered. --- The knowledge context holds scholarship *about* the Granth that is not in the printed text. It has two parts: - **Raag timing.** When in the day or season each raag is traditionally sung. The website's raag clock and divergence pages read it. - **Composition forms.** What a composition's own heading says it is: a pauri, a salok, an ashtpadi, which ghar, whether it has a rahao. Both follow one rule: **record what a source says, never decide what is true.** Timing is stored as attributed claims with a citation each, and disagreement between traditions is kept side by side. A form is recorded only when the heading states it; otherwise the field stays empty. The service is `webapp/sggs/knowledge.py`; the tables are built by sggs-data's `pipeline/timing/`. ```mermaid flowchart TB accTitle: Timing claims and forms are built from cited sources and headings, then served read-only accDescr: In sggs-data, seed_timing.py loads timing claims, each tied to a cited source, and derive_bani_forms.py reads composition headings from the verified text and records a form only when a heading states it. guard_scripture.py proves the scripture tables are byte-identical before and after, or the build stops. The knowledge service reads the seven declared tables and answers four routes: the clock, one raag, the divergences, and one composition's forms. src[Timing sources:
5, each cited] --> seed[seed_timing.py:
46 attributed claims] txt[The verified text:
composition headings] --> der[derive_bani_forms.py:
a form only if stated] seed --> g[guard_scripture.py:
scripture unchanged,
or the build stops] der --> g g --> db[(The installed database,
sliced for knowledge)] db --> clock[timing/clock
timing/raag] db --> div[timing/divergence] db --> forms[forms?comp_id=] class g gate class db store classDef gate stroke-width: 2px classDef store stroke-dasharray: 4 3 ``` ## Raag timing ### What is stored | Table | Holds | |---|---| | `timing_sources` | 5 sources, each with a tradition (`gurmat_sangeet` or `hindustani`), a URL where one exists, and notes on where it came from | | `raag_timing_claims` | 46 claims over the 31 raags, each tied to a source (`NOT NULL`) | Each claim has a `claim_type`: | Type | Count | Meaning | |---|---|---| | `primary` | 32 | The main placement in a tradition | | `variant` | 5 | A different placement recorded by another source | | `seasonal` | 2 | A raag tied to a season rather than an hour | | `ceremonial` | 7 | A liturgical or ceremonial use | `confidence` is `consistent` (sources agree), `majority` (the dominant placement where variants exist) or `disputed` (a recorded disagreement). Sources that could not be tied to a verifiable publication are labelled "(compiled)" and their notes say where they came from. No citation is invented. ### The pahar convention A pahar is a three-hour watch. Claims store pahars 1 to 8 counted from 6 AM: pahar 1 is 06:00–09:00 and pahar 8 is 03:00–06:00. **Pahar 7 (00:00–03:00) deliberately has no raags**, and the clock shows that silence as content rather than as a gap. The fixed-clock reading is a rendering choice; traditionally the watches follow sunrise and sunset. ### The routes - `/api/timing/clock` returns every claim grouped by type, the pahar convention, and the note "divergence is preserved, never adjudicated". The claims change only with a dataset, so the response is cached in the process (`core._TIMING_CACHE`) after the first request. - `/api/timing/raag?name=` returns one raag's claims. It accepts the raag's Gurmukhi name or its roman name; an unknown name returns an empty list with the note "no such raag", and a missing `name` is a 400. - `/api/timing/divergence` lists the raags where sources disagree, with every claim and its citation. Currently 5 raags qualify. **What counts as a divergence.** A raag qualifies if it has a `variant` claim, or if two *different* sources place it in different pahars. One source giving a raag a span across two pahars is an extension, not a disagreement, and does not qualify. The query encodes this (`COUNT(DISTINCT source_id) > 1`), so the page never shows a source disagreeing with itself. ## Composition forms ### What is stored Four tables keyed by `comp_id`, one row per composition (4,527): | Table | Holds | Stated for | |---|---|---| | `shabd_raag_map` | The raag, first Ang and first line of the composition | every composition | | `shabd_structural_form` | `form` (pada, ashtpadi, solahe, chhant, vaar, pauri, salok) and `pada_count` | 1,378; the other 3,149 are `NULL` | | `shabd_musical_markers` | `ghar` (1–17), `partaal`, `has_rahao`, `has_rahao_dooja`, `dhunni`, `jati` | ghar on 403, rahao on 2,525, partaal on 17 | | `shabd_poetic_genre` | A named genre stated in the heading (`barah_maha`, `bavan_akhri`, …) | 57 | Every row carries a `source_label` recording which heading it came from. ### The rules `derive_bani_forms.py` in sggs-data reads the verified text and applies these rules: - **Only a title-like heading counts.** A composition's first line is sometimes verse, so matching applies only to headings that carry a Mahala marker, a raag name, or a title word. - **`comp_type` is never trusted alone.** That pipeline column is known to be mislabelled (see [known issues](https://docs.gurbanisoul.com/engineering/known-issues/)); it is kept only as a hint inside `source_label`. - **Matching is on NFC-normalised text.** - **Anything unmatched stays `NULL` and is counted in the build report.** `NULL` means "the heading does not say", not "unknown to us, fill in later". `/api/forms?comp_id=` joins the four tables for one composition. It returns `forms: null` for a `comp_id` that has no row (the id gaps are permanent, see the [line record](https://docs.gurbanisoul.com/data/line-record/)), and a 400 when `comp_id` is missing or 0. ## Guarantees - **Scripture is untouched.** The timing layer's migration runs only `CREATE TABLE` and `CREATE INDEX` statements for the tables it adds, and aborts on anything else. Every writer checks a committed baseline (row counts, data and schema hashes of every pre-existing table) before and after it runs, and `guard_scripture.py` is the standing gate in the rebuild. - **Read-only and declared.** The module's `TABLES` lists exactly the seven tables it reads, and the service's database slice is cut from that list; `webapp/tests/test_declared_tables.py` refuses any other read ([bounded contexts and the gateway](https://docs.gurbanisoul.com/architecture/bounded-contexts-and-gateway/)). - **Degrades instead of failing.** Without the timing layer, each route returns `available: false` rather than an error. - **Tested end to end.** The golden contract replays the clock, every raag with claims (by both names), the divergences, and one composition per form and genre, so a dataset that changes any of them fails CI until the change is intended and re-baselined. ## Changing a claim A new source, a corrected time or a new variant is a dataset change. Add it to sggs-data's `pipeline/timing/timing_seed.json` with its citation, rebuild, and bring it here with a `dataset.lock.json` bump ([the three repositories and their pins](https://docs.gurbanisoul.com/architecture/three-repositories-and-pins/)). Never replace an existing claim because another source disagrees: add the second claim as a `variant`, and the divergence page will show both.