# Track C Status — Learning Runtime (Learner-First)

Pre-flight (2026-09-27): GATE 1 artifacts verified on origin/main — T-A-01…T-A-06 all
merged (last merge 4ad28ae `[merge][T-A-05]`). Note: `track_a.md` still reads "Gate 1
Not yet passed" — that text predates the orchestrator's T-A-05 merge commit; the six
Phase-0 branches are verifiably in `git branch -r --merged origin/main`. Proceeding.

| Task | Status | Branch | Notes / blockers |
|---|---|---|---|
| T-C-01 | ✅ merged 2026-09-27 | track-c/T-C-01-base-learning-model | BaseLearningModel ($connection='learning', extends A's BaseDomainModel → audited), `database/migrations/learning/` prefix 20260924_03 with schema marker; lint test: all `Models/Learning` on learning connection; cross-DB-FK test via SQLite `foreign_key_list` introspection (every FK target must live on `learning`). 82 passing, 0 failing (full suite incl. T-C-01). |
| T-C-02 | ✅ merged 2026-09-27 — **CONTRACT FOR D** | track-c/T-C-02-progress-service | `App\Services\Learning\ProgressService` frozen to README signatures (reflection-locked in test): `courseProgress(userId)`, `topicMastery(userId)`, `streak(userId)`, `attemptsSummary(userId)`, `timeSpent(userId, window)`, `weakTopics(userId)`. **D: bind/inject `ProgressService` (interface) now — Phase-0 stub `ProgressServiceStub` returns empty iterables; real impl replaces the binding in T-C-16 (Gate 3). No container binding added (AppServiceProvider is A-owned) — D binds the interface in its own provider.** 85 passing, 0 failing (full suite, incl. merged T-C-01). |
| T-C-03 | ✅ merged 2026-09-28 (verified in `git branch -r --merged origin/main`) | track-c/T-C-03-access-gate | `AccessGate::evaluate(userId, ContentAccess, contentScope)` → `AccessDecision` (allow/preview/deny + reason). Order = entitlement(A's README-frozen `App\Services\Billing\SubscriptionService` — T-A-17 impl not on main yet, so tested via Mockery against the frozen FQCN, same pattern as B's merged T-B-03; **A: no action needed, your impl binds by FQCN**) ∩ enrollment (port, T-C-04) ∩ parental (port, T-C-13) ∩ DRM (B T-B-06 flag on `ContentAccess` value object — never a direct catalog query). Preview only: unentitled + sample access + all other factors green; previews never bypass enrollment/parental/DRM. 10 AC tests, each flips exactly ONE factor. |
| T-C-04 | ✅ merged 2026-09-29 (commit f7197b9 `[merge][T-C-04]`) | track-c/T-C-04-enrollment-progress | **ROOT CAUSE FIXED (2026-09-29):** verified clean main = 296/296 green — the 3 `ResourceLibraryTest` failures were NOT pre-existing on main; they were caused by C's materialized `app/Services/Billing/SubscriptionService.php` interface file (deleted; A's T-A-17 creates it at the frozen FQCN) + test doubles switched to Mockery (T-C-03/B pattern). No C production code change. Ownership-clean: provider file + `bootstrap/providers.php` edit removed; container bindings moved into C-owned `routes/api_learning.php`. Full suite **328/328** on merge base. | Enrollment/drop/re-enroll (drop = status change, progress preserved; re-enroll restores completed status without re-emitting events). content_progress: position/speed, sticky completion at ≥90% of duration (watched-to-end), re-watch never reverts, resume = last saved position (row is the cross-device store). Course completion = all REQUIRED content (optional never blocks); progress % recomputed on complete AND read. Locked-in-order units: next unit 403 until prior unit's required items complete (new `unit_locked` deny reason on AccessDecision); rule changes never invalidate completed rows. Every playback/read path runs the full AccessGate (entitlement ∩ enrollment ∩ parental ∩ DRM) — enroll checks entitlement directly (bootstrap action, gate factor "enrollment" is out of scope pre-enrollment). Emits frozen `ContentCompleted` (userId, contentItemId — exact A T-A-06 double shape) + additive `CourseCompleted` (userId, courseId, README Change Log). B DEPS⚡ via new `CourseStructure` port → `CatalogCourseStructure` adapter over B's `CatalogService::courseTree`; A DEPS⚡: entitlement via the README-frozen FQCN `App\Services\Billing\SubscriptionService` (per-test Mockery mocks until A's T-A-17 ships). 34 new Learning tests (Learning namespace 47/47 on merge head), all ACs covered. |
| T-C-05 | ✅ merged 2026-09-30 (d9871d6 `[merge][T-C-05]`, branch head d527a62) — **338/338 passing, 0 failing** (full suite; +10 new; Learning ns 57/57) on origin/main **d9871d6** (post T-C-04 + T-C-05) | track-c/T-C-05-attempts-grading | Attempts, results, grading (quiz/exam/mock/past-paper). `attempts` + `answers` on the learning DB (migrations 20260924_030301/02, no cross-DB FKs). Per-question state answered/flagged/unanswered + marks_awarded/grading_mode/ai_rationale/human_override_reason. **Timer**: expired → auto-submitted ONCE, unanswered materialized as UNANSWERED with is_correct NULL (never zero-marked-answered). **Grading** (`GradingEngine`): MCQ exact; **multi-select full marks only** (exact set); fill-blank trimmed exact; true/false; **numeric with configured tolerance**; **short_answer/essay → AI rubric** (keyword coverage; rationale in ai_rationale). **Adaptive exam**: difficulty target + ability estimate updated per answer in `adaptive_state` (hidden — raw response asserted to contain no ability/difficulty/adaptive/question_limit), ends at configured question limit. **Practice**: unlimited retakes = NEW attempt rows (attempt_number increments, never overwrite), the B-owned exam/mock `attempt_count` allowance NEVER bumped. **Mock**: fixed time, single submission per window, **no feedback until `feedback_unlocked_at`**; 2nd start 409. **Results**: score, is_passed vs passing_score, per-question breakdown, weak_topics. **Topic statistics**: per-topic attempts/avg %/classification (strong/weak thresholds from B's grading_config) → `weakTopics` (feeds C focus mode + D insights). **Human override** (B's config): reason required, AI grade + rationale PRESERVED, is_correct re-derived, score recomputed. Emits the FROZEN `AttemptSubmitted(int userId, int assessmentId, int score, array weakTopics)` exactly per README contract. B DEPS⚡ via new C-owned `AssessmentBank` port → `CatalogAssessmentBank` adapter over B's T-B-08 `AssessmentService`/`QuestionService` (no cross-DB query; `AssessmentBankDouble` per-test, production binding in C-owned routes). **Every path runs the FULL AccessGate** against the assessment's course (test flips exactly one factor → 403, no rows). ACs (all 5): timer auto-submit + unanswered marked; multi-select partial = 0; adaptive hidden (field absent); practice retake doesn't decrement exam allowance; mock no-feedback until window closes. |
| T-C-06 | ✅ merged 2026-09-30 (d947a3d `[merge][T-C-06]`) — **349/349 passing, 0 failing** on origin/main (post T-C-04 + T-C-05 + T-C-06; Learning ns 68/68) | track-c/T-C-06-bookmarks-notes-highlights | Bookmarks, notes, highlights (learning DB; migrations 20260924_030401/02, no cross-DB FKs; every model carries `protected $connection='learning'` for A's app-wide ModelConnectionLintTest). **`bookmarks`**: one per (learner, content item) — the row IS the toggle (present = bookmarked); carries video `timestamp_seconds` + attached `note`; toggle-off deletes the row (note goes with it). **`notes`**: char-limited (2000), autosave = `updateOrCreate` (one row per learner+scope+course+content), scope `content`/`course`. **`highlights`**: text span (start/end offset, must be non-empty), color, attached note ON THE SAME ROW — deleting the highlight cascades its note by design (no FK). `BookmarkNoteService` validates the note limit + span range (`InvalidNoteDataException` → 422); course/content existence + membership through B's `CourseStructure` port (never a catalog query). `BookmarkNoteController` mirrors the T-C-04 gate pattern: **every CONTENT path (bookmark/content-note/highlight) runs `requireCourse` (enrollment → existence-safe 404) THEN the FULL AccessGate** (entitlement ∩ enrollment ∩ parental ∩ DRM — deny → 403); course-level listings (`GET /courses/{id}/bookmarks|notes`) require enrollment only (T-C-04 course-level pattern). Private + cross-device: other learners' rows are invisible (404 for non-enrolled; empty own-state for a co-enrolled learner). Routes under /api/learner/courses/* (8 endpoints, `auth:sanctum`). ACs (all 4): toggle bookmark add-then-again=remove; delete highlight cascades its note; note char-limit 422 over; private other-user 404. |
| T-C-07 | 🚀 pushed 2026-09-30 — **363/363 passing, 0 failing** (full suite on origin/main d947a3d post T-C-06; +14 new; Learning ns 81/81) | track-c/T-C-07-gamification | Gamification STATE on the learning DB (migrations 20260924_030501, no cross-DB FKs; 6 models all `protected $connection='learning'` for A's ModelConnectionLintTest). **`points_ledger`** IMMUTABLE (INSERT-only; `spendPoints` debits via a new negative row, never an UPDATE; rolling `expires_at` excluded from `balance()`; `source_type` nullable — redemptions have no content source). **`awarded_badges`** unique (user_id, badge_code) — `awardBadge` via `insertOrIgnore`: 2nd award is a NO-OP (AC), `evaluateBadges` auto-awards config criteria once. **`streaks`** (one row/learner): consecutive study days extend; **protection/grace rule** — a missed day does NOT always break the streak: with D-configured protection (`streakProtectionDays`) a gap bridges the streak (KEPT, one freeze consumed), beyond grace it resets to 1; `last_study_date` always advances; back-dated/same-day actions are idempotent no-ops. **`personal_goals`** auto-complete at target. **`challenges`**/`achievements` state present (deadline/status). CONFIG is DEPS⚡ D's `App\Services\Engagement\GamificationConfigService` (T-D-03/T-D-08 — NOT on main): C's `GamificationService` resolves it LAZILY by FQCN (injected instance > container binding > C-owned `GamificationConfigFallback` with empty rules) — C never materializes a file in D's namespace, nothing 500s before D lands, tests inject a double (the T-C-03 SubscriptionService / T-C-05 AssessmentBank pattern). **Frozen event `StreakUpdated(int userId, int currentStreakDays)`** (exact A T-A-06 double shape) emitted on every streak change. `RecordStudyAction` listener (event discovery, queued) on `ContentCompleted` → study action (idempotent/day) + `content_completed` points — so completing content drives streak+points end-to-end. Routes under /api/learner/gamification/* (8 endpoints; learner-scoped state, no content AccessGate — the gate rule covers playback/attempt/download paths). ACs (all 4): badge awarded once (2nd no-op); redeem insufficient points → graceful 409; streak protection (1 protected miss keeps streak 2, no-protection gap resets to 1); ledger immutable (2 distinct rows, no update path). |
| T-C-08 | 🚀 pushed 2026-09-24 | track-c/T-C-08-srs-runtime | Per-learner SRS state on the learning DB (migration 20260924_030601: `srs_items` + `srs_review_logs` + `srs_daily_counters`, no cross-DB FKs; all 3 models `protected $connection='learning'`). `SrsService`: due-queue (due-first → priority → oldest-due tie-break), rating loop (Again = bounded reset to floor, Good/Easy extend per B's `rating_map`), **config snapshot on the item** (mid-session config change: existing items keep their own interval; new items pick up the new one — AC), daily cap with carry-over (cap stored on the counter row; unused allowance of the last active day rolls into today), error-weighted resurfacing (never below floor, per-question cap, overdue re-error resurfaces), video revision reminder with skip-if-rewatched, mastery levels DERIVED from recall-rate thresholds (no manual override), graduation/archive + re-entry on failed recall (AC), retention prediction from B's Ebbinghaus curve. B DEPS⚡ via `App\Services\Catalog\SrsConfigService` (T-B-10 merged) — tests bind `SrsConfigServiceDouble`; production resolves the real service at its FQCN, zero C change needed. Listeners (queued, event discovery): `ScheduleCompletedContentForSrs` on frozen `ContentCompleted` (video_tutorial/podcast only, when eligible), `ResurfaceIncorrectAnswers` on frozen `AttemptSubmitted` (every question enters the SRS idempotently; incorrect ones resurface early). Routes `GET /api/learner/srs/due-queue`, `GET /api/learner/srs`, `POST /api/learner/srs/{itemId}/rate` (`auth:sanctum`; rating an unknown item → 404 existence-safe; bad rating → 422). ACs (all 4): Again bounded reset + Good extension; config-change mid-session keeps old interval; daily cap carry-over; archived + re-entry on failed recall. **507/507 passing, 0 failing** (full suite on origin/main 8c06d7f; Learning ns 101/101, +19 new). |
| T-C-09 | 🚀 pushed 2026-09-30 — **115/115 Learning passing, 0 failing** (+14 new; full suite re-run post-rebase on push) | track-c/T-C-09-ai-companion | AI study companion runtime (learning DB, migration 20260924_030701: 8 tables, all `protected $connection='learning'`, no cross-DB FKs). `AiCompanionService` consumes B's merged `AiFeatureConfigService` (DEPS⚡ T-B-11) at its FQCN — per-plan question limit (exceed → **429 plan-upgrade**, snapshot on first use, non-retroactive), escalation rules, companion/break settings; tests bind C-owned `AiFeatureConfigDouble`. Conversations (context retained — transcript BEFORE this question replayed; **2nd answer references 1st**), doubts (AI-resolved OR escalated: student-requested + auto-low-confidence → teacher handoff; async-ticket fallback in-band), mood check-ins (score ≤ 4 or 3-run low streak → burnout flag + break suggestion from B's break_rules), recommendations (acted-on tracked), chapter summaries (regenerable, version bumps), peer benchmark (**strictly anonymous** — cohort aggregates + learner percentile/coarse tier only; no peer id/name stored so a response can never identify an individual). **ADDITIVE queued event** `DoubtEscalatedToTeacher(int userId, int doubtId, string mode, ?string topic)` (not one of the frozen 15) → D parent-teacher comms; persistent record = learning.ai_doubts. Course-grounded paths enrollment-gated (requireCourse → 404 existence-safe). 9 learner-scoped routes under /api/learner/companion/*. ACs (all 4): question over plan limit → 429; benchmark response no peer names/IDs; doubt escalation event emitted; context retained across a conversation. README: one append-only Contract Change Log line. |
| T-C-10 | 🚀 pushed 2026-09-24 — **611/611 passing, 0 failing** (full suite on origin/main 0d1fd76 post T-C-12 + T-A-11; +17 new Learning tests) | track-c/T-C-10-study-plans-target-exams | Study plans + target-exam runtime (learning DB, migration 20260924_030801: `target_exams` / `target_exam_mappings` / `study_plans` / `study_plan_tasks` / `study_plan_milestones` / `study_plan_predictions`, all 6 models `protected $connection='learning'`, no cross-DB FKs). **Target exams**: dates, subject-topic mappings (importance high/medium/low + time_split_pct **must total 100% else 422**), **score prediction** (real-time; confidence interval widens with less data; **insufficient data → "not shown" + practice prompt, never false precision** — below B's `min_data_points`), **what-if scenarios** (hypothetical topic scores, no persistence), **readiness** (coverage + mastery + days-remaining label), **actual result post-exam → accuracy delta** stored on the latest prediction, post-exam reflection. Paused exam halts its study plan + reminders (next_reminder_at nulled) until resumed. **Study plans**: AI planner + target-exam plans (gap-first, learn-before-practice ordering from B's config), tasks spread over horizon, adherence tracking (on-track/behind/slid), **plan regenerates when prediction changes** (version bump, old plan → completed), check-in prompt fires ONCE when adherence drops below threshold (NotificationRequested `study_plan_checkin`), **milestone triggers fire once per student per plan** (`study_plan_milestone`, from B's config), **parent approval required for minors** (plan NOT applied until `approvePlan`). Consumes B's merged services (DEPS⚡ T-B-11): `AiFeatureConfigService::studyPlanConfig()` (horizon, task templates, milestone triggers, at-risk threshold) + `PredictionConfigService::effectiveConfig()/dataSufficiency()` (global config via `?ExamType=null`) — never a direct catalog query. Emits FROZEN `TargetExamRecorded(int userId, string examName, string examDate)` + `StudyPlanGenerated(int userId, int planId)` — shapes exactly match A's T-A-06 test doubles (`tests/Feature/IdentityBilling/TestDoubles/Events/`) → D. B cross-track contract: `target_exams.exam_type_id` column (nullable, indexed) — B's `ExamAiConfigService::deleteType` delete-guard does a plain cross-DB existence check against it (name/exam_date nullable at the DB level so B's probe row inserts; both enforced required at C's API layer). 17 learner-scoped routes under /api/learner/target-exams/* + /api/learner/study-plans/*. ACs (all 5): time-split≠100% → 422 (API + service); prediction below data threshold → null + practice prompt (API + service); paused exam → plan + reminders halted; minor plan not applied until parent approval; actual result → accuracy delta stored. |
| T-C-11, T-C-13…T-C-17 | ⏳ next: T-C-11 (learning paths, deps: T-C-04 ✅, T-C-05 ✅ + DEPS⚡ T-B-05 ✅ — unblocked) | — | After T-C-10 merges: T-C-11, then T-C-13 (parent link + parental controls), T-C-14…T-C-17. |
| (blocker — RESOLVED 2026-09-29) | ✅ was "T-B-07 tests fail on origin/main" — **WRONG DIAGNOSIS, retracted** | — | The earlier claim that 3 `ResourceLibraryTest` failures were pre-existing on main is retracted: clean main runs **296/296 green** (verified by detaching to `origin/main` and running the suite). The failures appeared **only on the T-C-04 branch**, caused by C's materialized `app/Services/Billing/SubscriptionService.php` interface file (container: "interface not instantiable" when B's controller is built; B's other tests pass because Mockery auto-defines a concrete class for the absent FQCN). Resolved by deleting C's interface file and switching C's test doubles to Mockery mocks (T-C-03/B pattern). **No outstanding blocker for B; full suite on the T-C-04 head is 314/314, 0 failing.** |
| T-C-12 | ✅ merged 2026-09-27 (skip-ahead) | track-c/T-C-12-learner-profile | `learner_profiles` (+ append-only `learner_profile_changes`, no cross-DB FKs; user_id plain indexed col). Learner profile + 3-step onboarding (basic info: grade/board/≥1 subject; prefs: learning style exactly-one visual/auditory/kinesthetic/mixed, study-time 30min–4h slider, avoid list) + goals (target_score 0–100, future exam_date). Avoid-subjects excluded from `recommendedSubjects()` only — required assessments still built from focus. Change history on every post-onboarding preference update (applies from point of change). Routes: `GET/PUT /api/learner/profile`, `POST /api/learner/onboarding/{basic,preferences,goals}` behind `auth:sanctum` (learner-owned; RBAC roles are admin-surface — no `student` role exists, file as A question). ACs: missing grade 422; target 105 422; avoid-vs-assessment; style uniqueness. 128 passing, 0 failing (full suite, post-rebase incl. T-B-01..03). |

## Gates

- Gate 1 (end Phase 0): **passed 2026-09-28** — all Phase-0 tasks merged to main
  (verified `git branch -r --merged origin/main`): T-C-01 (learning base model +
  no-cross-DB-FK test), T-C-02 (ProgressService frozen contract for D), T-C-03
  (AccessGate). D can compile against ProgressService and the full gate.

### Current state (re-verified 2026-09-30 — T-C-04/05/06 MERGED, T-C-07 PUSHED)

**✅ T-C-04 merged** (f7197b9) and **✅ T-C-05 merged** (d9871d6) — both on origin/main. (The 18+ cycle ownership hold — provider file + `bootstrap/providers.php` — is closed; history in the git log of `track-c/T-C-04-enrollment-progress` and earlier cycle notes.)

**✅ T-C-06 (bookmarks, notes, highlights) MERGED 2026-09-30** (d947a3d `[merge][T-C-06]`). Full suite on origin/main: **349/349 passing, 0 failing** (Learning ns 68/68), including A's app-wide `ModelConnectionLintTest` (all 3 new models declare `protected $connection = 'learning'`) and the cross-DB-FK guard.

**🚀 T-C-07 (gamification: points, streaks, badges, goals) PUSHED 2026-09-30** on `track-c/T-C-07-gamification`, off origin/main d947a3d (T-C-06 merge). Full suite on THIS head: **363/363 passing, 0 failing** (+14 new tests, Learning ns 81/81), including A's app-wide `ModelConnectionLintTest` (all 6 new models declare `protected $connection = 'learning'`) and the cross-DB-FK guard (plain indexed user_id columns, no FKs). Emits the FROZEN `StreakUpdated(userId, currentStreakDays)` exactly per the README contract / A's T-A-06 double.

**T-C-07 DEPS⚡ note (D):** gamification CONFIG is consumed from D's `App\Services\Engagement\GamificationConfigService` (T-D-03/T-D-08 — NOT on main yet). C does NOT materialize a file in D's namespace (that would collide with D's ownership and 500 on "class not instantiable" before D lands — the exact T-C-04 mistake, avoided). Instead `GamificationService` resolves the config LAZILY by FQCN string (injected instance > container binding > C-owned `GamificationConfigFallback` with empty rules), so: nothing 500s before D lands, C's tests inject a double (T-C-03/T-C-05 pattern), and the moment D binds its real service at the FQCN it is picked up with **zero C code change**. D: no action needed in Phase 0 — just ship T-D-03 with the FQCN + the 4 methods (`pointRules`, `badgeCriteria`, `pointsExpiryDays`, `streakProtectionDays`).

**Ownership audit (T-C-07 head) — ALL changed paths are C-owned:** `app/Models/Learning/{PointsLedger,AwardedBadge,Achievement,Challenge,PersonalGoal,Streak}.php`, `app/Services/Learning/{GamificationService,GamificationConfigFallback}.php`, `app/Services/Learning/Exceptions/GamificationConflictException.php`, `app/Events/Learning/StreakUpdated.php`, `app/Listeners/Learning/RecordStudyAction.php`, `app/Http/Controllers/Learning/GamificationController.php`, `database/migrations/learning/20260924_030501_create_gamification_tables.php`, `routes/api_learning.php` (+8 routes), `tests/Feature/Learning/{GamificationTest.php, TestDoubles/GamificationConfigDouble.php}`, `docs/status/track_c.md`, `docs/developer_C.md`. No A/B/D paths, no `app/Providers/`, no `bootstrap/providers.php`. `docs/README.md` intentionally UNCHANGED — `StreakUpdated` is an EXISTING frozen event (already in the README contract + A's T-A-06 double); C is only now its first real emitter (mirrors T-C-05's first `AttemptSubmitted` emission), so no new contract is introduced.

**Merge-note for the orchestrator:** this T-C-07 head descends from d947a3d (T-C-06 merge) and its `track_c.md` is the most current snapshot — on any conflict in that file, take the T-C-07 side. It also updates `docs/developer_C.md` (T-C-05/T-C-06 checked off as merged). README is untouched here, so it merges cleanly on top of d947a3d.

**Next: T-C-08 (SRS runtime state, DEPS: T-C-07 + DEPS⚡ T-B-10) PUSHED 2026-09-24** on `track-c/T-C-08-srs-runtime`, off origin/main 8c06d7f (T-D-16 merge). Full suite on THIS head: **507/507 passing, 0 failing** (+19 new tests, Learning ns 101/101), including A's app-wide `ModelConnectionLintTest` (all 3 new models declare `protected $connection = 'learning'`) and the cross-DB-FK guard (plain indexed user_id columns, no FKs). B's DEPS⚡ `App\Services\Catalog\SrsConfigService` (T-B-10) is merged; C consumes it at its FQCN — tests bind `SrsConfigServiceDouble`, production needs zero C change. README unchanged (no new contract: SRS consumes B's merged `SrsConfigService` + frozen events `ContentCompleted`/`AttemptSubmitted`; no additive event introduced). Next after merge: T-C-09 (AI study companion, DEPS: T-C-08 + DEPS⚡ T-B-11/T-A-13) and T-C-11 (learning paths, deps all merged).

**🚀 T-C-09 (AI study companion runtime) PUSHED 2026-09-30** on `track-c/T-C-09-ai-companion`, rebased onto latest origin/main (post T-C-08 merge + T-D-10/T-D-11/T-D-17 + T-A-10). Full suite on THIS head: **115/115 Learning passing, 0 failing** (+14 new tests). C owns the learner AI STATE (learning DB, migration 20260924_030701: ai_conversations / ai_messages / ai_doubts / ai_mood_checkins / ai_recommendations / ai_chapter_summaries / ai_benchmarks / ai_question_usage — 8 tables, all `protected $connection = 'learning'`, plain indexed user_id columns, no cross-DB FKs). Consumes B's merged `AiFeatureConfigService` (DEPS⚡ T-B-11) at its FQCN — per-plan question limit (exceed → 429 plan-upgrade, snapshot on first use, non-retroactive), escalation rules (`student_requested_always` / `low_confidence_pct` / `async_ticket_fallback`), companion/break settings; tests bind C-owned `AiFeatureConfigDouble`, production zero C change (same pattern as T-C-08). Features: conversations (context retained — transcript-before-this-question replayed, 2nd answer references 1st), doubts (AI-resolved OR escalated: student-requested + auto-low-confidence → teacher handoff, async-ticket fallback kept in-band), mood check-ins (score ≤ 4 or a 3-run low streak → burnout flag + break suggestion from B's break_rules), recommendations (acted-on tracked), chapter summaries (regenerable, version bumps), peer benchmark (strictly anonymous — cohort aggregates + learner percentile/coarse tier only, no peer id/name stored, so a response can never identify an individual). **ADDITIVE queued event** `App\Events\Learning\DoubtEscalatedToTeacher(int $userId, int $doubtId, string $mode, ?string $topic)` (not one of the frozen 15) → D for parent-teacher comms; the persistent record is C's learning.ai_doubts row (emitted only on genuine teacher handoffs: student_requested + auto_low_confidence). README Contract Change Log: one append-only line (see git diff). Ownership audit — ALL changed paths C-owned: `app/Models/Learning/Ai{Conversation,Message,Doubt,MoodCheckin,Recommendation,ChapterSummary,Benchmark,QuestionUsage}.php`, `app/Services/Learning/AiCompanionService.php` + `Exceptions/AiQuestionLimitExceededException.php`, `app/Events/Learning/DoubtEscalatedToTeacher.php`, `app/Http/Controllers/Learning/AiCompanionController.php`, `database/migrations/learning/20260924_030701_create_ai_companion_tables.php`, `routes/api_learning.php` (+9 companion routes, learner-scoped), `tests/Feature/Learning/{AiCompanionTest.php, TestDoubles/AiFeatureConfigDouble.php}`, `docs/status/track_c.md`, `docs/developer_C.md`, `docs/README.md` (append-only line). No A/B/D paths, no `app/Providers/`, no `bootstrap/providers.php`. Course-grounded paths (summary, course-ask) are enrollment-gated (T-C-04 requireCourse → existence-safe 404); free conversations need no gate (no content access). **D:** consume `DoubtEscalatedToTeacher` for parent-teacher comms (payload carries doubt_id → join-free read of your own engagement comms; the doubt detail lives in C's learning DB).


## Current state (re-verified 2026-09-24 — T-C-10 PUSHED)

**🚀 T-C-10 (study plans + target-exam runtime) PUSHED 2026-09-24** on `track-c/T-C-10-study-plans-target-exams`, rebased onto origin/main 0d1fd76 (post T-C-12 merge + T-A-11). Full suite on THIS head: **611/611 passing, 0 failing** (+17 new Learning tests, Learning ns 132/132), including A's app-wide `ModelConnectionLintTest` (all 6 new models declare `protected $connection = 'learning'`) and the cross-DB-FK guard (plain indexed user_id/topic_id columns, no FKs). **B cross-track contract surfaced by T-C-10:** B's merged `ExamAiConfigService::deleteType` delete-guard does a plain cross-DB existence check on `target_exams.exam_type_id` (B's test even simulates C's table) — so C's `target_exams` ships an `exam_type_id` nullable indexed column; B's probe inserts a bare `{exam_type_id,user_id,status}` row, which is why `name`/`exam_date` are nullable at the DB level (both remain required at C's API validation). **D:** consume the two new frozen events `TargetExamRecorded` + `StudyPlanGenerated` (shapes in the table row + `app/Events/Learning/`). Next after merge: T-C-11 (learning paths, all deps merged).

## Environment note

Local worktree `worktrees/track-C` was found with its checkout missing (only `.env` +
`vendor` present); restored via `git worktree add` onto the `track-C` branch (origin/main)
and `composer install` (79 packages). No repo files touched by the restore.
