# 03 — مدل داده

داده در دو جا زندگی می‌کند: **پکیج زبانی (JSON سمت کلاینت/CDN)** که «چه چیزی آموزش داده شود» را تعیین می‌کند، و **پایگاه‌داده‌ی سرور** که «چه کسی و چقدر یاد گرفته» را نگه می‌دارد.

## 1. نمودار ارتباط موجودیت‌ها (ERD)

```
┌──────────┐ 1     N ┌───────────┐ 1     N ┌────────────────┐ N     1 ┌─────────┐
│ parents  │─────────│ children  │─────────│ letter_progress│─────────│ letters │
└──────────┘         │           │         │ (pack+letter)  │         │ (در پک  │
                     │           │ 1     N └────────────────┘         │  JSON)  │
                     │           │────────┌────────────────┐         └─────────┘
                     │           │ 1     N│ session_events │              ▲
                     │           │────────└────────────────┘              │ id ارجاع
                     │           │ N     M ┌────────────────┐             │ متنی
                     └───────────┘─────────│ badges_awarded │─────────────┘
                                           └────────────────┘
```

> **نکته‌ی معماری:** `letters` جدول جداسازی‌شده نیست؛ کل پک JSON است و `letter_progress.letter_id` یک کلید متنی (مثل `fa_be`) به آن ارجاع می‌دهد. سرور محتوای پک را تکرار نمی‌کند — تک منبع حقیقت همان JSON نسخه‌بندی‌شده است.

## 2. پکیج زبانی — قالب JSON

### 2.1 ساختار ریشه

```jsonc
{
  "packId": "fa",              // شناسه‌ی یکتا: fa | en | nl
  "version": "1.0.0",          // semver — برای invalidating کش
  "lang": "fa-IR",             // BCP-47 → هم برای TTS هم i18n
  "dir": "rtl",                // rtl | ltr → html[dir] به‌صورت پویا
  "label": "فارسی",
  "ui": { "font": "Vazirmatn", "numerals": "fa" },
  "letters": [ /* §2.2 */ ],
  "words":   [ /* §2.3 */ ],
  "badges":  [ /* §2.4 */ ],
  "config": {
    "maxLettersPerRound": 8,       // سقف گزینه روی صفحه
    "confusableInjectionRate": 0.2 // نسبت حروف خطاخیز در هر دور
  }
}
```

### 2.2 شیء Letter (مهم‌ترین موجودیت)

```jsonc
{
  "id": "fa_be",
  "order": 2,                        // جایگاه در توالی آموزشی
  "char": "ب",                       // نمایش اصلی
  "forms": {                         // برای زبان‌های با خط متصل (فارسی)؛
    "isolated": "ب",                 // برای انگلیسی/هلندی هر چهار مقدار یکی است
    "initial":  "بـ",
    "medial":   "ـبـ",
    "final":    "ـب"
  },
  "name": "بِه",                      // نام حرف
  "phoneme": "/b/",                  // صدای اصلی (Phonics)
  "audio": {
    "name":  "audio/fa/letters/be/name.mp3",
    "sound": "audio/fa/letters/be/sound.mp3",
    "words": "audio/fa/letters/be/words.mp3"   // تلفظ کلمات نمونه
  },
  "stroke": "assets/strokes/fa/be.svg",          // مسیر تتبع برای بازی Trace
  "examples": ["fa_bal", "fa_baran"],            // id کلمات نمونه
  "confusables": ["fa_pe", "fa_te", "fa_se"]     // حروف خطاخیز (بازی Odd-One-Out)
}
```

**قواعد اعتبارسنجی پک (در PackLoader و در CI سرور):**
- `id` یکتا و با پیشوند `packId_`
- `forms.isolated === char`
- هر `examples[].letters` باید به حرف معتبر ارجاع دهد (بدون ارجاع معلق)
- `audio.*` باید فایل موجود باشد (چک در CI)

### 2.3 شیء Word

```jsonc
{
  "id": "fa_bal",
  "text": "بَل",
  "image": "assets/img/fa/words/bal.svg",     // تصویر برداری کودکانه
  "audio": "audio/fa/words/bal.mp3",
  "letters": ["fa_be", "fa_lam", "fa_lam"],   // توالی حروف برای Word-Builder
  "difficulty": 1                              // 1: سه‌حرفی ساده …
}
```

### 2.4 Badge (نشان)

```jsonc
{ "id": "fa_master_5", "type": "letters_mastered", "threshold": 5, "icon": "assets/badges/star5.svg" }
```

## 3. جداول پایگاه‌داده (PostgreSQL)

```sql
-- والدین: تنها نقطه‌ی ورود داده‌ی شخصی
CREATE TABLE parents (
  id            UUID PRIMARY KEY,
  email         TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,             -- argon2
  created_at    TIMESTAMPTZ DEFAULT now()
);

-- کودک: حداقل‌ترین داده‌ی ممکن (GDPR — سند 08)
CREATE TABLE children (
  id          UUID PRIMARY KEY,
  parent_id   UUID NOT NULL REFERENCES parents(id) ON DELETE CASCADE,
  nickname    TEXT NOT NULL,               -- نام مستعار آزاد؛ نام واقعی لازم نیست
  avatar      TEXT NOT NULL,               -- "fox" | "panda" | ...
  color       TEXT NOT NULL,               -- رنگ-رمز ورود
  birth_year  SMALLINT,                    -- فقط سال؛ نه تاریخ تولد کامل
  active_pack TEXT NOT NULL DEFAULT 'fa',
  stars_total INTEGER NOT NULL DEFAULT 0,  -- تداوم ستاره‌ها بین دستگاه‌ها (merge: max)
  created_at  TIMESTAMPTZ DEFAULT now()
);

-- توکن جلسه‌ی کودک، مقید به دستگاه (لاگین آواتاری)
CREATE TABLE child_devices (
  child_id   UUID REFERENCES children(id) ON DELETE CASCADE,
  token_hash TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now(),
  last_seen  TIMESTAMPTZ,
  PRIMARY KEY (child_id, token_hash)
);

-- هسته‌ی پیشرفت‌سنجی: یک سطر به ازای (کودک، حرف)
CREATE TABLE letter_progress (
  child_id    UUID REFERENCES children(id) ON DELETE CASCADE,
  pack_id     TEXT NOT NULL,
  letter_id   TEXT NOT NULL,               -- مثل 'fa_be' → ارجاع به پک JSON
  mastery     SMALLINT NOT NULL DEFAULT 0, -- 0..4 (سند 05)
  leitner_box SMALLINT NOT NULL DEFAULT 0, -- 0..4 → زمان مرور بعدی
  due_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  correct     INT NOT NULL DEFAULT 0,
  wrong       INT NOT NULL DEFAULT 0,
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (child_id, pack_id, letter_id)
);

-- رویداد خام (append-only) — سوخت گزارش والدین و آنالیز
CREATE TABLE session_events (
  id        BIGSERIAL PRIMARY KEY,
  child_id  UUID REFERENCES children(id) ON DELETE CASCADE,
  pack_id   TEXT NOT NULL,
  game_id   TEXT NOT NULL,
  letter_id TEXT,
  verb      TEXT NOT NULL,                 -- attempt | success | fail | hint | mastered
  result    JSONB NOT NULL DEFAULT '{}',   -- duration_ms, choice, difficulty…
  ts        TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_events_child_ts ON session_events (child_id, ts DESC);

CREATE TABLE badges_awarded (
  child_id   UUID REFERENCES children(id) ON DELETE CASCADE,
  badge_id   TEXT NOT NULL,                -- مثل 'fa_master_5'
  awarded_at TIMESTAMPTZ DEFAULT now(),
  PRIMARY KEY (child_id, badge_id)
);
```

## 4. کلید‌های localStorage (سمت کلاینت)

| کلید | محتوا | نقش |
|---|---|---|
| `ap.child` | پروفایل فعال + session token | ورود بدون رمز کودک |
| `ap.pack.<id>` | کش پک زبانی (با version) | اجرای آفلاین |
| `ap.progress` | تصویر local از letter_progress | بازخورد آنی |
| `ap.queue` | رویدادهای در انتظار سینک | SyncQueue (سقف ۵۰۰) |
| `ap.settings` | صدا، کاهش-حرکت (reduced-motion)، فونت دیسلکسیک | دسترس‌پذیری |

## 5. نسخه‌بندی و مهاجرت پک

- تغییرات `minor/patch` پک (افزودن کلمه/حرف) → سازگار با قدیمی؛ کلاینت با مقایسه‌ی `version` کش را نو می‌کند.
- تغییر شکلی حروف (حذف/تغییر id) → ممنوع در همان `packId`؛ باید پک جدید (`fa2`) منتشر شود تا `letter_progress` قدیمی بی‌اعتبار نشود.
