- .ai-specs/PROJECT_CONTEXT.md: new 'Language Model (UI vs. Content)' section (serviceLang ru->cu, fallback chain, scripture, Bible tab independence, fonts, calendar authority) + content-policy pointer in Critical Invariants. - .ai-specs/WORKING_POLICIES.md: new 'Liturgical Content Policies' — never translate service/Bible texts (sourced only), cu texts verbatim (no normalisation/latinisation), service assembly follows the Typicon. - DATA_EN.md/DATA_RUS.md: ru/ holds enriched lives only; services fall back to cu; cu is the authoritative calendar + service texts.
25 KiB
Static Data Format Documentation
Table of Contents
- Static Data Format Overview
- Directory Structure & Organization
- Data Models & Schemas
- File Routing Rules
- Data Components Breakdown
- Data Management Tools
- Enrichment Process
- Quality Assurance Workflow
- Common Issues & Solutions
- Quick Reference
Static Data Format Overview
The Ponomar PWA uses a JSON-based static data system organized by language. All liturgical data is pre-converted from XML sources and structured for efficient serving and client-side rendering.
Core Principles
- Language-based organization: Each language has its own directory under
static/data/ - JSON storage: All data stored as JSON files for easy parsing
- Date-based routing: Calendar data organized by Julian calendar dates (MM-DD format)
- CId-based identification: Canonical Identifiers (CIds) uniquely identify saints and feasts
- Prefix-based file routing: Lives files routed by CId prefix (01-12 → month files)
Supported Languages
en— Englishcu— Church Slavonicru— Russianel— Greek (includesel/monofor monochord)fr— Frenchzh— Chinese (includeszh/Hansandzh/Hantfor Simplified/Traditional)shared— Cross-language resources
Directory Structure & Organization
static/data/
├── en/
│ ├── menaion-bundle.json # Date → CId mapping (authoritative)
│ ├── en-menaion-bundle.json # Language-specific overrides
│ ├── lives/ # Saint lives by month
│ │ ├── 01.json through 12.json # Month-prefixed CIds (01XX-12XX)
│ │ ├── misc/ # Non-month-prefixed CIds
│ │ │ ├── 1.json # CIds starting with digit 1
│ │ │ ├── 2.json
│ ├── menaion/ # Daily menaion entries (366 days)
│ ├── calendar/ # Calendar metadata
│ └── services/ # Service templates
├── cu/ # Church Slavonic (authoritative calendar + service texts)
├── ru/ # Russian (enriched lives; services fall back to cu)
├── el/ # Greek (includes mono/)
├── fr/ # French
├── zh/ # Chinese (includes Hans/, Hant/)
└── shared/ # Cross-language resources
├── calendar/ # Movable feast calendar
├── services/ # Shared service templates
└── lives-index.json # CId → scripture lookup index
File Types
*-bundle.json— Calendar mapping fileslives/*.json— Saint lives bundlesmenaion/*.json— Daily liturgical entriesservices/*.json— Service templates and logiccalendar/*.json— Calendar calculations and metadata
Data Models & Schemas
Bundle Entry (menaion-bundle.json)
{
"01-01": [
{ "id": "010101" },
{ "id": "772426" },
{ "id": "010102" }
]
}
Structure: Object keyed by date MM-DD with array of entries, each containing only "id" field with CId value.
Life Entry (lives/*.json)
{
"772426": {
"name": {
"nominative": "Прп. Сильвестра Печерского (XII)",
"short": "Прп. Сильвестра Печерского"
},
"life": {
"id": "minei-0101-772426",
"copyright": "Четьи-Минеи свт. Димитрия Ростовского",
"text": "<p>Преподобный отец наш Сильвестр...</p>"
}
}
}
Structure: Object keyed by CId (string), each containing:
nameobject with case variants (nominative,short,genitive, etc.)lifeobject (optional) withid,copyright, andtextfields
Calendar Entry
{
"pascha": { "year": 2025, "date": "04-20" },
"fixed": [
{ "date": "01-01", "rank": 5, "celebration": "Circumcision" }
]
}
Structure: Contains movable feast calculations and fixed calendar data.
Service Template Entry
{
"ROYALHOURS": {
"0": {
"text": "In the name of the Father...",
"type": "0"
}
}
}
Structure: Hierarchical service structure with text nodes and conditional logic.
File Routing Rules
Lives File Routing
Lives bundles are routed based on CId prefix:
| CId Pattern | Target File | Examples |
|---|---|---|
01XX-01ZZ |
01.json |
010101, 014567 |
02XX-02ZZ |
02.json |
023456, 027890 |
| ... | ... | ... |
12XX-12ZZ |
12.json |
123456, 127890 |
| Other CIds | misc/{n}.json |
772426→misc/7.json, 373→misc/3.json |
Examples:
010101→01.json1253→12.json772426→misc/7.json373→misc/3.json
Bundle File Naming
- Main bundle:
menaion-bundle.json(per language) - Language overrides:
{lang}-menaion-bundle.json - No prefix-based routing for bundles (single file per language)
Data Components Breakdown
Menaion Bundles
Purpose: Authoritative date → CId mapping for calendar rendering
Structure: Date-keyed JSON with arrays of CIds
{
"01-01": [{ "id": "010101" }, { "id": "772426" }],
"01-02": [{ "id": "010201" }, { "id": "772455" }]
}
Coverage: 366 Julian calendar days × multiple entries per day File Size: ~50-100KB per language Usage: Calendar view determines which saints to display on each day
Lives Bundles
Purpose: Saint biographies and lives for enrichment
Structure: CId-keyed JSON with name variants and optional life text
{
"772426": {
"name": {
"nominative": "Прп. Сильвестра Печерского (XII)",
"short": "Прп. Сильвестра Печерского"
},
"life": {
"id": "minei-0101-772426",
"copyright": "Четьи-Минеи свт. Димитрия Ростовского",
"text": "<p>Complete life text in HTML...</p>"
}
}
}
Coverage: 3046 total CIds, 1203 with life.text enriched Routing: Prefix-based (01.json-12.json + misc/{digit}.json) Usage: Lives display on saint detail pages
Calendar Data
Purpose: Movable feast calculations and calendar metadata
Components:
shared/calendar/triodion.json— Pre-Lent through Holy Weekshared/calendar/pentecostarion.json— Pascha through All Saints{lang}/calendar/— Language-specific calendar data
Key Features:
- Paschalion calculations
- Movable feast dating
- Fasting period definitions
- Holiday and commemoration metadata
Service Templates
Purpose: Structured liturgical service data with conditional logic
Organization:
services/
├── octoecheos/ # Eight tones × days
├── triodion/ # 37 Lenten sections
├── pentecostarion/ # 50 Paschal days
├── menaion/ # 366 daily entries + 34 feasts
├── canons/ # Paraclete, Octoechos, Great Canon
└── shared/templates/ # Reusable service components
Structure: Hierarchical JSON with text nodes and conditional prefixes
- P-prefix nodes: Optional content that may be included
- Var/I-prefix nodes: Conditional selection logic
- Plain nodes: Required content always included
Bible Metadata
Purpose: Bible version mappings and reading pericopes
Structure: Version identifiers and pericope mappings across 13 Bible versions
- Church Slavonic
- Russian Synodal
- KJV
- Brenton (Septuagint)
- Vulgate
- Other versions
Prayer Rules & Commands
Purpose: Daily cycle structures and prayer rule definitions
Components:
- Prayer Rules: Daily reading schedules
- Commands: Service command structures
- Horologion: Hours, Kathismata, Compline, etc.
Fasting Rules
Purpose: Period definitions and fasting exceptions
Structure: JSON with fasting periods, exceptions, and detailed rules
- Pre-Lent periods
- Great Lent
- Fast-free periods
- Fish/strict fasting days
Data Management Tools
Conversion Pipeline (scripts/convert-*.ts)
Master Script
convert-all.ts — Orchestrates all conversion steps
npm run convert
Converts XML data from Java Ponomar sources to JSON format.
Individual Converters
convert-lives.ts — Saint lives XML → JSON conversion
- Input:
ponomar/Ponomar/languages/{lang}/xml/lives/*.xml - Output:
{lang}/lives/{01.json|misc/{digit}.json}bundles - Features:
- XML parsing with attribute extraction
- Name case normalization
- Grammar data extraction
- Service data extraction
- Scripture reference extraction
- Bundle generation by month prefix
convert-calendar.ts — Calendar metadata conversion
- Input: XML calendar data
- Output: JSON calendar structures
- Features:
- Movable feast calculations
- Fixed feast mappings
- Period definitions
convert-services.ts — Service template conversion
- Input: XML service definitions
- Output: JSON service templates
- Features:
- Hierarchical structure preservation
- Conditional logic extraction
- Text node normalization
convert-bible-meta.ts — Bible metadata extraction
- Input: XML bible metadata
- Output: JSON version mappings and pericopes
convert-fasting.ts — Fasting rules conversion
- Input: XML fasting definitions
- Output: JSON fasting rules and periods
convert-commands.ts — Command structure conversion
- Input: XML command definitions
- Output: JSON command structures
convert-service-rules.ts — Service rule extraction
- Input: XML service rule definitions
- Output: JSON service rules
Migration Tools (scripts/migrate-data.ts)
Purpose: Normalize data format across versions and languages
Operations:
- Strip
@_prefixes from lives name keys (all languages) - Remove
SIdfrom menaion entries (keep id = CId) (all languages) - Rename service directories to lowercase (cu, el, fr, zh/Hans, zh/Hant, ru)
- Flatten Octoecheos structure (cu, fr, zh/Hans, zh/Hant)
- Convert
TEXT→textin command files - Move shared services into
templates/subdirectory - Normalize name key casing (Nominative → nominative, Short → short)
- Migrate merged calendar files (triodion.json, pentecostarion.json)
Usage:
npm run migrate # Apply migrations
npm run migrate:dry # Preview changes
Validation Suite (scripts/validate-*.ts)
Main Validator
validate-data.ts — Comprehensive data validation
npx tsx scripts/validate-data.ts
Validates:
- Life entry structure (required fields)
- Lives bundle format (CId keys, name objects)
- Calendar data integrity
- Service template structure
- Prayer format compliance
- Command node structure
- Commemoration entries
- Fasting rule definitions
Output: File count, errors, warnings
Specialized Validators
validate-i18n.ts — Internationalization completeness
npx tsx scripts/validate-i18n.ts
Checks coverage across languages (en/ru/cu).
validate-tabs.ts — Service tab/template consistency
npx tsx scripts/validate-tabs.ts
Validates that service tabs match template structures.
validate-menaion.ts — Menaion data validation
npx tsx scripts/validate-menaion.ts
Validates menaion entries against calendar rules.
validate-prayer-rule.ts — Prayer rule structure validation
npx tsx scripts/validate-prayer-rule.ts
Validates prayer rule format and completeness.
Period-specific Validators:
validate-triodion.ts— Triodion period validationvalidate-pentecostarion.ts— Pentecostarion validationvalidate-holy-week.ts— Holy Week validationvalidate-great-canon.ts— Great Canon validationvalidate-great-compline.ts— Great Compline validationvalidate-libcs.ts— Library content validation
Utility Tools (scripts/tools/)
search-texts.ts — Full-text search across data files
npx tsx scripts/tools/search-texts.ts <query> [--lang en] [--type prayers]
stats.ts — Data statistics and size analysis
npx tsx scripts/tools/stats.ts [--lang en]
Provides file counts, sizes, and category breakdowns.
Enrichment Process
Overview
The enrichment process fills empty life.text fields in ru/lives bundles using Chetyi-Minei (St. Dimitri of Rostov) hagiographic texts.
Source Preparation: Chetyi-Minei Import
convert-minei.ts — Converts Chetyi-Minei EPUB to date-keyed JSON
Input Layout (extracted EPUB):
extracted-epub/
├── index_split_000.xhtml through index_split_9999.xhtml
├── toc.ncx
└── content.opf
Process:
- Parse day files sequentially
- Extract headings:
h2for dates ("Память 1 января"),h3for chapter titles - Extract paragraphs:
<p class="paragraph1">for content - Collect footnotes from
<a title="...">anchors - Append notes as "Примечания:" block
- Apply heading date corrections (e.g.,
calibre_toc_10023→01-27) - Build output structure:
{ "MM-DD": [{ title, html }] }
Usage:
npx tsx scripts/convert-minei.ts <extracted-epub-dir> [output.json]
Output: scripts/output/minei.json (gitignored, ~33MB)
Enrichment Pipeline
enrich-lives.ts — Fuzzy-matches minei titles to CId names and fills empty lives
Inputs:
scripts/output/minei.json— Source lives by date (MM-DD)static/data/cu/menaion-bundle.json— Authoritative date→CId mapstatic/data/ru/lives/*.json— Target bundles for enrichment
Matching Strategy:
- Tokenization: Split titles into tokens, filter ROLE_WORDS
- Dice Coefficient: Calculate similarity over token sets
- Status Classification:
already-filled— Target already haslife.text→ skipauto— Score ≥ 0.6, empty target → writereview— Score 0.35–0.6 → manual reviewunmatched— Score < 0.35 or no candidates → unmatched
Manual Overrides:
const MANUAL_FIXES: Record<string, string> = {
'02-02|сказание о сретении': '373',
'03-24|воспоминание о чуде': '822',
// 214 entries total
}
Key format: {date}|{distinctive substring} → CId
Collision Handling: Multiple sources map to same CId
- First source: writes
life.htmldirectly (no heading) - Subsequent sources: append
<p><b>{title}</b></p>{html}
File Routing:
function bundleFileForCid(cid: string): string {
return /^(0[1-9]|1[0-2])\d+/.test(cid)
? `${cid.substring(0, 2)}.json`
: `misc/${cid.charAt(0)}.json`;
}
Usage:
# Dry run: report only
npx tsx scripts/enrich-lives.ts
# Limit to one month
npx tsx scripts/enrich-lives.ts --month 01
# Apply changes
npx tsx scripts/enrich-lives.ts --write
# Apply changes for specific month
npx tsx scripts/enrich-lives.ts --write --month 01
Output: scripts/output/lives-match-report.json with match status per source
Workflow Example
Phase 1: Source Preparation
npx tsx scripts/convert-minei.ts ~/Downloads/chetyi-minei-epub/
# Output: scripts/output/minei.json
Phase 2: Dry Run Analysis
npx tsx scripts/enrich-lives.ts
# Shows: Auto-applied: 184, needs review: 127, unmatched: 94
# Review: scripts/output/lives-match-report.json
Phase 3: Manual Review & Fixes
- Analyze
reviewentries in report - Add MANUAL_FIXES for problematic matches
- Re-run until
reviewcount acceptable
Phase 4: Write & Validate
npx tsx scripts/enrich-lives.ts --write
# Shows: Wrote 7 updated bundle(s)
npx tsx scripts/validate-data.ts
npx tsc --noEmit
npm run build
Phase 5: Commit
git add scripts/enrich-lives.ts scripts/output/lives-match-report.json static/data/ru/lives/
git commit -m "enrich 121 more ru lives with Chetyi-Minei texts"
Quality Controls
Collision Verification: Check multi-chapter entries have correct headings
python3 -c "
import json, re
misc = json.load(open('static/data/ru/lives/misc/4.json'))
for cid in ['493', '1674', '257401']: # Known collision targets
text = misc[cid]['life']['text']
headings = re.findall(r'<b>([^<]+)</b>', text)
print(f'{cid}: {len(headings)} chapters - {headings}')
"
Coverage Tracking: Monitor enrichment progress
python3 -c "
import json, glob, os
total = has = 0
for fp in glob.glob('static/data/ru/lives/*.json'):
data = json.load(open(fp))
for cid, entry in data.items():
total += 1
if isinstance(entry.get('life'), dict) and entry.get('life', {}).get('text'):
has += 1
print(f'Coverage: {has}/{total} ({has*100//total}%) lives with text')
"
Quality Assurance Workflow
Pre-Commit Checklist
- Data Validation
npx tsx scripts/validate-data.ts
# Expected: "All files valid!" or specific error count
- Type Safety
npm run check # or npx tsc --noEmit
# Expected: No TypeScript errors
- Build Verification
npm run build
# Expected: Successful build with bundle sizes
- Functional Testing
npm run dev
# Manual test: Calendar view, saint detail pages, lives display
Continuous Monitoring
Automated Checks:
- File count validation (should be 274 files)
- JSON parseability (all data files)
- Required field presence (nominative, life.text for enriched entries)
- CId consistency (same CId should have same name across languages)
Issue Detection:
- Corrupted JSON files → parse errors in validate-data.ts
- Missing required fields → validation warnings
- Type mismatches → TypeScript errors
- Build failures → invalid data structures
Issue Resolution
Common Validation Errors:
-
"Missing nominative" — Name object lacks nominative case
- Solution: Add
name.nominativefield to affected entries
- Solution: Add
-
"Failed to parse" — Corrupted JSON file
- Solution: Re-run conversion or fix JSON syntax
-
"Rank should be 0-8" — Invalid rank value
- Solution: Normalize rank to range 0-8
-
Build bundle size changes — Unexpected file size increases
- Solution: Check for duplicate or malformed content
Common Issues & Solutions
OCR Artifacts in Minei Texts
Problem: Chetyi-Minei conversion produces OCR issues like:
- Latin characters in Russian text (
Taиcииinstead ofТаисии) - Footnote markers
[1]in body text - Erratic punctuation
[1]in titles
Solution:
- Keep OCR artifacts as-is (part of source text)
- Manual fixes use exact OCR substrings for matching
- Examples:
'02-15|onissima, odnogo iz lika'→'450'
Homonym Resolution on Same Date
Problem: Multiple saints share similar names on same date
Examples:
- 03-24: Иаков исповедник (68501) vs other Иаков entries
- 02-22: Варадат vs Фалассий (both map to 493)
- 09-03: Анфим, Аристион, Василиса on same date
Solution:
- Use distinctive substrings in MANUAL_FIXES
- Ensure uniqueness per date (verify with verification script)
- For collisions: both sources map to same CId (append behavior)
Cross-Date Narratives
Problem: Some minei chapters are narratives not tied to a specific feast
Examples:
- 02-02: "Сказание о Сретении Господнем" (presentation narrative)
- 03-24: "Воспоминание о чуде в Печерском монастыре" (miracle story)
- 08-28: "Память святой праведной Анны" (prophetess Anna)
Solution:
- Identify appropriate CId for the saint/event
- Map with MANUAL_FIXES even if not on same date (though current pipeline is date-based)
- For entries with no appropriate CId: remain unmatched
Bundle Mis-Mappings
Problem: Some CIds are listed on wrong calendar dates
Example:
- 868 Иаков Боровицкий listed under 05-22, actual feast 10-23
Solution:
- Manually correct menaion-bundle.json
- Move CIds from incorrect date to correct date
- Verify with calendar sources and tradition
File Routing Edge Cases
Problem: Ambiguous routing for CIds at boundaries
Edge Cases:
00prefixes → misc/{first-digit}.json (not 00.json)99prefixes → misc/{first-digit}.json (not 09.json)- Non-numeric CIds → misc/{first-digit}.json
Solution:
/^(0[1-9]|1[0-2])\d+/.test(cid) // Match only 01-12 followed by digits
Multi-Chapter Collision Display
Problem: When multiple sources append to same CId, first chapter lacks heading
Behavior:
- First source: writes
life.htmldirectly (no title) - Subsequent sources: append
<p><b>{title}</b></p>{html} - Result: First chapter unheaded, others clearly marked
Acceptable Trade-off: Consistent with all single-chapter lives (which have no heading)
Quick Reference
NPM Scripts Summary
# Development
npm run dev # Start development server
npm run build # Production build
npm run preview # Preview production build
# Data Conversion
npm run convert # Convert all XML sources to JSON
npm run migrate # Normalize to current format
npm run migrate:dry # Preview migration changes
# Validation
npm run check # Type check (tsc --noEmit)
npx tsx scripts/validate-data.ts # Main validator
npx tsx scripts/validate-i18n.ts # i18n completeness
npx tsx scripts/validate-tabs.ts # Service consistency
# Tools
npx tsx scripts/tools/search-texts.ts <query> # Search data files
npx tsx scripts/tools/stats.ts # Data statistics
# Enrichment
npx tsx scripts/convert-minei.ts <epub-dir> # Import Chetyi-Minei
npx tsx scripts/enrich-lives.ts # Dry run enrichment
npx tsx scripts/enrich-lives.ts --write # Apply enrichment
File Extensions
| Extension | Purpose | Format |
|---|---|---|
.json |
All static data files | JSON |
.text |
Plain text resources | Plain text |
.xml |
Source data (not in repo) | XML |
Git Strategy
Tracked: static/data/ (except output files)
Ignored:
scripts/output/minei.json(33MB, generated)node_modules/dist/
Important File Paths
# Data directories
static/data/cu/menaion-bundle.json # Calendar authority
static/data/ru/lives/ # Target for enrichment
static/data/shared/services/ # Shared service templates
# Scripts
scripts/enrich-lives.ts # Enrichment pipeline
scripts/convert-minei.ts # Minei import
scripts/validate-data.ts # Main validator
# Output files
scripts/output/minei.json # Minei source (gitignored)
scripts/output/lives-match-report.json # Match report
Coverage Statistics
Current Status:
- Total CIds: 3046
- Lives with text: 1203 (39%)
- Empty lives: 1856 (61%)
Enrichment Progress:
- Chetyi-Minei chapters: 1154 total
- Matched: 1127 (auto 64 + already-filled 1063)
- Unmatched: 27 (floor: narratives, no-target, already-filled)
Status Classifications
| Status | Meaning | Action |
|---|---|---|
auto |
Score ≥ 0.6, empty target | Automatically written |
already-filled |
Target already has life.text | Skip (redundant) |
review |
Score 0.35–0.6 | Manual review required |
unmatched |
Score < 0.35 or no candidates | No suitable target |
Key Constants
File Paths:
const MINEI_PATH = 'scripts/output/minei.json';
const MENAION_BUNDLE_PATH = 'static/data/cu/menaion-bundle.json';
const LIVES_DIR = 'static/data/ru/lives';
const REPORT_PATH = 'scripts/output/lives-match-report.json';
Matching Thresholds:
const AUTO_THRESHOLD = 0.6; // Automatic write
const REVIEW_THRESHOLD = 0.35; // Manual review
Copyright String:
const COPYRIGHT = 'Четьи-Минеи свт. Димитрия Ростовского';
Version History
- Initial release — Complete data format and enrichment process documentation
- Updated — Added bundle mis-mapping section, cross-date narrative handling, QA workflow
Contributing
When contributing to the data format or enrichment pipeline:
- Test thoroughly: Run validation, typecheck, and build before committing
- Document changes: Update this documentation for any schema changes
- Preserve compatibility: Maintain existing file routing rules and naming conventions
- Validate data: Run
validate-data.tsand fix any errors
Support
For questions or issues with the static data format:
- Review this documentation
- Check existing issues in the repository
- Run validation tools for error diagnosis
- Examine the enrichment pipeline for matching problems