# 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 |
|---|---|---|---|
<<<<<<< HEAD
| 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 | тЬЕ merged 2026-09-24 (84382b3) | 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 | тЬЕ merged 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 | тЬЕ merged 2026-09-24 (1c7d2d3) | 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 | тЬЕ merged 2026-09-24 (7694d27) тАФ **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 | тЬЕ merged 2026-09-24 (1123728) тАФ **622/622 passing, 0 failing** (full suite on origin/main 7694d27, post T-C-10 merge; +6 new Learning tests, Learning ns 138/138) | track-c/T-C-11-learning-paths | Learning-path runtime (institute/corporate; B T-B-05 owns the DEFINITIONS тАФ catalog DB тАФ C owns the learner STATE + runtimes). Learning DB, migration 20260924_030901: `learning_path_enrollments` (one per learner+path, status in_progress/completed, deadline_at, total/completed_steps) + `learning_path_step_progress` (per-step course row, sequence snapshot, locked/in_progress/completed) + `certificates` (one per learner+course, UUID `certificate_id`, nullable `learning_path_id` = triggering path) тАФ all 3 models `protected $connection='learning'`, plain indexed cross-domain columns, NO cross-DB FKs (cross-DB-FK guard passes). **Cross-DB boundary (DEPSтЪб T-B-05):** B's path surface is model-in/data-out with no id-based reader, so C's domain layer consumes path definitions ONLY through the new C-owned `PathCatalog` PORT (`pathDetail(id)`, `learnerPaths(userId)`); production binding = thin `CatalogPathCatalog` adapter (the single integration seam that reads B's `LearningPath`/`LearningPathCourse`/`LearningPathAssignment` to build C value objects тАФ no join into learning DB, no cross-DB FK), tests bind `PathCatalogDouble` (C's suite stays hermetic тАФ zero catalog tables). **`LearningPathService`:** enroll (idempotent, snapshots definition, first step unlocked, rest locked); **prerequisite step-lock** (step stays locked while ANY earlier step is incomplete тЖТ `step_prerequisite` тЖТ 403 at controller; first step never locked; a completed path is fully re-visitable); **criteria-gated step completion** тАФ a step completes ONLY when its course actually completes (T-C-04 `CourseCompleted` event), a partial watch never advances the path; unlock-next on completion; **deadline** (settable) тЖТ derived `behind_pace` (deadline passed + path incomplete; never stored); **path completion** when all steps complete; **certificates** тАФ one per (learner, course), idempotent, UUID, records triggering path. **Certificate runtime:** `OnCourseCompleted` listener (queued, event discovery) on T-C-04's frozen `CourseCompleted` тЖТ issues the certificate + advances the path runtime. Emits the FROZEN `App\Events\Learning\CertificateIssued(int userId, int courseId, string certificateId)` тАФ shape exactly matches A's T-A-06 test double тЖТ **D: ingest `CertificateIssued` for the certificate/achievement surface** (persistent record is C's `learning.certificates` row). 5 learner routes under `/api/learner/learning-paths/*` (index, enroll, show, deadline, steps/{course}/start) behind `auth:sanctum`. **AccessGate rule preserved:** the path layer only ADDS the prerequisite lock; every playback/attempt/download of a step's content STILL runs the full T-C-04 AccessGate (entitlement тИй enrollment тИй parental тИй DRM) тАФ path enrollment alone confers NO content access (tested: path-enrolled but not course-enrolled тЖТ 404 on content; after course enrollment тЖТ allowed). ACs (all 4): prerequisite unmet тЖТ next step 403; criteria-gated completion (partial тЙа complete); deadline exceeded тЖТ behind_pace; full completion тЖТ certificate + `CertificateIssued` ├ЧN. |
| T-C-16 | ЁЯЪА PUSHED 2026-09-24 тАФ **633/633 passing, 0 failing** (full suite on origin/main head; +11 new Learning tests, Learning ns 149/149, incl. A's app-wide `ModelConnectionLintTest` + the cross-DB-FK guard) | track-c/T-C-16-progress-read-models | **Phase 4 read models + REAL `ProgressService`** (Gate 3 obligation "ProgressService real by Gate 3" now met). Learning DB, migration 20260924_031001: `study_sessions` (the time-spent session log тАФ one per completed content item, duration from B's `CourseStructure` port) + `learner_activity` (append-only activity log) + `shared_reports` (weekly/monthly snapshots, shareable) тАФ all 3 models `protected $connection='learning'`, plain indexed user_id columns, NO cross-DB FKs (cross-DB-FK guard passes). **REAL `App\Services\Learning\ProgressServiceImpl`** (replaces the T-C-02 stub binding in C-owned `routes/api_learning.php` тАФ the frozen `ProgressService` interface is UNCHANGED, so D's injected mock/double still works and D now injects live data; reads the learning DB only, via C-owned `CourseStructureResolver` + `AssessmentBank` ports тАФ no catalog/identity query). **`LearnerReadModel`** composes the frozen `ProgressService` + T-C-07 gamification + T-C-10 readiness into: progress %, time-spent (per session log), read-only activity feed, performance dashboard (score trend + accuracy), strength/weakness (topic accuracy vs B's `topic_weak_threshold` тЖТ "focus on"/"maintain"), grade-level **benchmark percentile + gap** via the new C-owned `GradeBenchmark` PORT (DEPSтЪб тАФ D supplies the analytics cohort aggregates `cohortForGrade(gradeLevel)`; `NullGradeBenchmark` default until D lands, so the benchmark degrades gracefully to "no cohort" тАФ no 500), learning-pattern insight (peak study hour, 7-day consistency label), streak/consistency. **Automated reports** (weekly/monthly): snapshot of time/completion/scores/weak-topics/streak; **downloadable** as CSV (`GET /api/learner/reports/{id}/download`, ownership-scoped 404) + **shareable with the parent** (`POST /api/learner/reports/{id}/share` тЖТ appears in the parent dashboard). **6 thin queued listeners** (event discovery, no EventServiceProvider) append `learner_activity` rows on the frozen events: `ContentCompleted`тЖТ`content_completed`+study session, `AttemptSubmitted`тЖТ`attempt_submitted`+score-drop alert, `CourseCompleted`тЖТ`course_completed`, `StreakUpdated`тЖТ`streak_updated`, `CertificateIssued`тЖТ`certificate_issued`, `StudyPlanGenerated`тЖТ`study_plan_generated`. **`ActivityLogger`** (C-owned) is the single append point тАФ the learner can only READ the log (no write endpoint; AC-tested on the route table). **`PerformanceAlert`** ADDITIVE queued event (`int $userId, string $type, array $payload`) тАФ the "performance alerts тЖТ D" requirement (score_drop | missed_plan | streak_risk); fired on a genuine planted score drop (current < previous on the same assessment; an improving attempt does NOT fire тАФ both tested). **Parent-facing projections** (`/api/parent/children/{childId}/{activity,exam-prep,reports}`): child-scoped, read-only; the per-child monitoring-consent scope is enforced by the T-C-13 parent-link runtime (engaged once A's T-A-10 lands) тАФ these are the data contract D's parent dashboard consumes. **No AccessGate:** read-model projections consume no catalog content (the content was already gated at T-C-04/05). **D:** (1) bind your real cohort service to `App\Services\Learning\GradeBenchmark` to activate grade-level benchmarking (one-line, no C change); (2) ingest the ADDITIVE `PerformanceAlert` event for the performance-alert surface. **`CertificateIssued` emission verified complete:** every course completion (path or not) тЖТ `OnCourseCompleted` тЖТ `LearningPathService::issueCertificate` тЖТ frozen `CertificateIssued(userId, courseId, certificateId)` тАФ T-C-11's runtime, unchanged. ACs (all 5): activity log immutable to learner (no write endpoint тАФ route-table test); report share тЖТ visible in parent dashboard (unshared invisible); weak-topic = accuracy below threshold; benchmark percentile grade-level (cohort via the port + graceful no-cohort); alert on planted score drop (improving does not fire). |
| T-C-13 | 🚀 PUSHED 2026-10-02 — **663/663 passing, 0 failing** (full suite on origin/main 5c8120d; +28 new Learning tests, Learning ns 179/179, incl. A's app-wide `ModelConnectionLintTest` + the cross-DB-FK guard) | track-c/T-C-13-parental-controls | **Parent-child link runtime + parental controls.** Link records live in A's `identity_billing` (T-A-10); C never queries that DB — it consumes the link facts through the NEW C-owned `ParentChildLink` PORT (`isLinked`, `isVerified`, `consentScope`, `childrenOf`) returning plain value objects. **DEPS⚡ resolved by port (NOT a hard block on T-A-10):** the production default binding is `NullParentChildLink` (deny-by-default: no parent is linked → all parent reads 404 existence-safe), and tests bind `ParentChildLinkDouble` (C's suite stays hermetic — zero identity tables). When A merges T-A-10, ONE line rebinds the singleton to A's adapter — no C call-site churn (the T-C-07 `GamificationConfigService` / T-C-08 `SrsConfigService` / T-C-16 `GradeBenchmark` pattern). **Parental-control settings live on the `learning` connection** (migration 20260924_031101: `parental_control_settings` / `parental_age_overrides` / `parental_content_requests` / `parental_study_sessions` / `parental_session_schedules` / `parental_plan_approvals` / `parental_control_activity` — all 7 models `protected $connection='learning'`, plain indexed user_id columns, NO cross-DB FKs). **`DefaultParentalRestrictionAccess`** (the real T-C-13 gate factor, replacing the T-C-03 port note) reads settings + overrides + approved requests and composes: **screen-time** (daily limit, resets daily, access PAUSED at limit, extendable; warn approaching), **time-of-day windows**, **content restrictions** (non-core only — **core learning NEVER blockable: attempting to restrict a core type → 409**), **age filter** (grade-aligned; core exempt; override requires a reason → 422, logged), **restricted-content requests** (child submits → parent approves time-boxed / denies; an approval lifts the restriction for the box). `ContentAccess` (T-C-03 value object) gains two ADDITIVE optional fields `type` + `gradeLevel` so the gate carries catalog content facts without a cross-DB query; wired at the 3 production call sites (Enrollment/Attempt/BookmarkNote). **Mandatory study sessions** (parent schedules, child completes; missed → alert) + **recurring schedules** (conflict detection → 409, weekly requires a weekday, pause for holidays). **AI study-plan approval:** a minor's plan is NOT applied until parent approval (`markTaskDone` on an unapplied plan → 409 via `StudyPlanNotAppliedException`; modify-then-approve supported; approval history row). Control presets (Balanced/Strict/Relaxed) + control **activity log**. **ADDITIVE queued event `App\Events\Learning\ParentalAlert(int userId, string type, array payload)`** (not one of the frozen 15; `session_missed` | `screen_time_limit` | `approaching_limit`) → **D:** ingest `ParentalAlert` for the parent-notification surface. **Parent-facing projections** (T-C-16's `ParentProgressController`) are now LINK-CHECKED: not-linked/unknown → existence-safe 404; linked-but-unverified → LIMITED (fine-grained `recent_activity` + exam-prep withheld until verified); verified → full; shared reports shown in both scopes (child's explicit share is the consent). **No AccessGate on the control endpoints** (no catalog content consumed); every playback/attempt path STILL runs the full AccessGate — the parental factor is now the REAL `DefaultParentalRestrictionAccess`. **ACs (all 6, tested):** unverified link → LIMITED child data; verified → full; core-learning restriction attempt → 409; screen-time limit reached → playback denied (real gate) until reset/extend; unapproved minor AI plan not applied (markTaskDone 409); override without reason → 422. Ownership audit — ALL changed paths C-owned: `app/Services/Learning/{ParentChildLink,NullParentChildLink,DefaultParentalRestrictionAccess,ParentalControlService,Exceptions/{ParentalControlConflictException,StudyPlanNotAppliedException}}.php`, `app/Models/Learning/Parental{ControlSettings,AgeOverride,ContentRequest,StudySession,SessionSchedule,PlanApproval,ControlActivity}.php`, `app/Http/Controllers/Learning/LearnerParentalController.php`, `app/Http/Controllers/Parent/{ParentalControlController,ParentProgressController}.php`, `app/Events/Learning/ParentalAlert.php`, `database/migrations/learning/20260924_031101_create_parental_control_tables.php`, `routes/api_learning.php` (+20 parent/learner routes + 3 container bindings), `tests/Feature/Learning/{ParentalControlTest.php (28 tests), TestDoubles/ParentChildLinkDouble.php}`. No A/B/D paths, no `app/Providers/`, no `bootstrap/providers.php`. README: one append-only Contract Change Log line (ParentalAlert). |
| T-C-14…T-C-17 | ⏳ next: **T-C-17 (E2E + seeding)** — deps T-C-16 + T-C-13 (both pushed) + DEPS⚡ T-B-13/T-A-07; **T-C-14/T-C-15** (offline learning + remaining) still track A's T-A-10/T-A-17 for real-link / real-entitlement E2E assertions | — | **T-C-13 is PUSHED** (parent-link runtime + parental controls, via the C-owned `ParentChildLink` port — NOT a hard block on T-A-10; production default is deny-by-default `NullParentChildLink`, one-line rebind when A's T-A-10 merges). After T-C-13 + T-C-16 merge: T-C-17 (seed + full learner E2E; cross-domain assertions — entitlement via A's `SubscriptionService`, parent consent scope via the T-C-13 link runtime). |
| (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). |
=======
| 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 | ✅ merged 2026-09-24 (84382b3) | 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 | ✅ merged 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 | ✅ merged 2026-09-24 (1c7d2d3) | 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 | ✅ merged 2026-09-24 (7694d27) — **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 | ✅ merged 2026-09-24 (1123728) — **622/622 passing, 0 failing** (full suite on origin/main 7694d27, post T-C-10 merge; +6 new Learning tests, Learning ns 138/138) | track-c/T-C-11-learning-paths | Learning-path runtime (institute/corporate; B T-B-05 owns the DEFINITIONS — catalog DB — C owns the learner STATE + runtimes). Learning DB, migration 20260924_030901: `learning_path_enrollments` (one per learner+path, status in_progress/completed, deadline_at, total/completed_steps) + `learning_path_step_progress` (per-step course row, sequence snapshot, locked/in_progress/completed) + `certificates` (one per learner+course, UUID `certificate_id`, nullable `learning_path_id` = triggering path) — all 3 models `protected $connection='learning'`, plain indexed cross-domain columns, NO cross-DB FKs (cross-DB-FK guard passes). **Cross-DB boundary (DEPS⚡ T-B-05):** B's path surface is model-in/data-out with no id-based reader, so C's domain layer consumes path definitions ONLY through the new C-owned `PathCatalog` PORT (`pathDetail(id)`, `learnerPaths(userId)`); production binding = thin `CatalogPathCatalog` adapter (the single integration seam that reads B's `LearningPath`/`LearningPathCourse`/`LearningPathAssignment` to build C value objects — no join into learning DB, no cross-DB FK), tests bind `PathCatalogDouble` (C's suite stays hermetic — zero catalog tables). **`LearningPathService`:** enroll (idempotent, snapshots definition, first step unlocked, rest locked); **prerequisite step-lock** (step stays locked while ANY earlier step is incomplete → `step_prerequisite` → 403 at controller; first step never locked; a completed path is fully re-visitable); **criteria-gated step completion** — a step completes ONLY when its course actually completes (T-C-04 `CourseCompleted` event), a partial watch never advances the path; unlock-next on completion; **deadline** (settable) → derived `behind_pace` (deadline passed + path incomplete; never stored); **path completion** when all steps complete; **certificates** — one per (learner, course), idempotent, UUID, records triggering path. **Certificate runtime:** `OnCourseCompleted` listener (queued, event discovery) on T-C-04's frozen `CourseCompleted` → issues the certificate + advances the path runtime. Emits the FROZEN `App\Events\Learning\CertificateIssued(int userId, int courseId, string certificateId)` — shape exactly matches A's T-A-06 test double → **D: ingest `CertificateIssued` for the certificate/achievement surface** (persistent record is C's `learning.certificates` row). 5 learner routes under `/api/learner/learning-paths/*` (index, enroll, show, deadline, steps/{course}/start) behind `auth:sanctum`. **AccessGate rule preserved:** the path layer only ADDS the prerequisite lock; every playback/attempt/download of a step's content STILL runs the full T-C-04 AccessGate (entitlement ∩ enrollment ∩ parental ∩ DRM) — path enrollment alone confers NO content access (tested: path-enrolled but not course-enrolled → 404 on content; after course enrollment → allowed). ACs (all 4): prerequisite unmet → next step 403; criteria-gated completion (partial ≠ complete); deadline exceeded → behind_pace; full completion → certificate + `CertificateIssued` ×N. |
| T-C-16 | 🚀 PUSHED 2026-09-24 — **633/633 passing, 0 failing** (full suite on origin/main head; +11 new Learning tests, Learning ns 149/149, incl. A's app-wide `ModelConnectionLintTest` + the cross-DB-FK guard) | track-c/T-C-16-progress-read-models | **Phase 4 read models + REAL `ProgressService`** (Gate 3 obligation "ProgressService real by Gate 3" now met). Learning DB, migration 20260924_031001: `study_sessions` (the time-spent session log — one per completed content item, duration from B's `CourseStructure` port) + `learner_activity` (append-only activity log) + `shared_reports` (weekly/monthly snapshots, shareable) — all 3 models `protected $connection='learning'`, plain indexed user_id columns, NO cross-DB FKs (cross-DB-FK guard passes). **REAL `App\Services\Learning\ProgressServiceImpl`** (replaces the T-C-02 stub binding in C-owned `routes/api_learning.php` — the frozen `ProgressService` interface is UNCHANGED, so D's injected mock/double still works and D now injects live data; reads the learning DB only, via C-owned `CourseStructureResolver` + `AssessmentBank` ports — no catalog/identity query). **`LearnerReadModel`** composes the frozen `ProgressService` + T-C-07 gamification + T-C-10 readiness into: progress %, time-spent (per session log), read-only activity feed, performance dashboard (score trend + accuracy), strength/weakness (topic accuracy vs B's `topic_weak_threshold` → "focus on"/"maintain"), grade-level **benchmark percentile + gap** via the new C-owned `GradeBenchmark` PORT (DEPS⚡ — D supplies the analytics cohort aggregates `cohortForGrade(gradeLevel)`; `NullGradeBenchmark` default until D lands, so the benchmark degrades gracefully to "no cohort" — no 500), learning-pattern insight (peak study hour, 7-day consistency label), streak/consistency. **Automated reports** (weekly/monthly): snapshot of time/completion/scores/weak-topics/streak; **downloadable** as CSV (`GET /api/learner/reports/{id}/download`, ownership-scoped 404) + **shareable with the parent** (`POST /api/learner/reports/{id}/share` → appears in the parent dashboard). **6 thin queued listeners** (event discovery, no EventServiceProvider) append `learner_activity` rows on the frozen events: `ContentCompleted`→`content_completed`+study session, `AttemptSubmitted`→`attempt_submitted`+score-drop alert, `CourseCompleted`→`course_completed`, `StreakUpdated`→`streak_updated`, `CertificateIssued`→`certificate_issued`, `StudyPlanGenerated`→`study_plan_generated`. **`ActivityLogger`** (C-owned) is the single append point — the learner can only READ the log (no write endpoint; AC-tested on the route table). **`PerformanceAlert`** ADDITIVE queued event (`int $userId, string $type, array $payload`) — the "performance alerts → D" requirement (score_drop | missed_plan | streak_risk); fired on a genuine planted score drop (current < previous on the same assessment; an improving attempt does NOT fire — both tested). **Parent-facing projections** (`/api/parent/children/{childId}/{activity,exam-prep,reports}`): child-scoped, read-only; the per-child monitoring-consent scope is enforced by the T-C-13 parent-link runtime (engaged once A's T-A-10 lands) — these are the data contract D's parent dashboard consumes. **No AccessGate:** read-model projections consume no catalog content (the content was already gated at T-C-04/05). **D:** (1) bind your real cohort service to `App\Services\Learning\GradeBenchmark` to activate grade-level benchmarking (one-line, no C change); (2) ingest the ADDITIVE `PerformanceAlert` event for the performance-alert surface. **`CertificateIssued` emission verified complete:** every course completion (path or not) → `OnCourseCompleted` → `LearningPathService::issueCertificate` → frozen `CertificateIssued(userId, courseId, certificateId)` — T-C-11's runtime, unchanged. ACs (all 5): activity log immutable to learner (no write endpoint — route-table test); report share → visible in parent dashboard (unshared invisible); weak-topic = accuracy below threshold; benchmark percentile grade-level (cohort via the port + graceful no-cohort); alert on planted score drop (improving does not fire). |
| T-C-14 | 🚀 PUSHED 2026-09-24 — **645/645 passing, 0 failing** (full suite on this head; +12 new Learning tests, Learning ns 160/160, incl. A's app-wide `ModelConnectionLintTest` + the cross-DB-FK guard) | track-c/T-C-14-offline-learning | **Offline learning runtime** (learning DB, migration 20260924_031201: `offline_devices` / `offline_downloads` / `offline_attempts` / `offline_sync_queue` — all 4 models `protected $connection='learning'` via `BaseLearningModel`, plain indexed user_id columns, NO cross-DB FKs — cross-DB-FK guard passes). **Devices:** register (token, name, platform) / list / remove; **device limit enforced** (`OfflineService::MAX_DEVICES = 3`) — the 4th active device → 409; **removing a device revokes its offline access** (its downloads are deleted + its unsynced answers are keyed by device_token, so playback on a removed device 403s immediately). **Downloads:** `POST /api/learner/offline/downloads` per (device, kind, course, content) for video/lesson/course/notes/resources/worksheets/quiz/deck/paper — **every download runs the FULL AccessGate** (entitlement ∩ enrollment ∩ parental ∩ DRM — per-factor 403 tests); a sample/preview branch does NOT grant a download; DRM-locked content is not downloadable; **lapsed subscription → all ready downloads expire + playback blocked** (`playbackAccess` re-checks entitlement live, so a lapse cuts offline access at once); re-subscribe → re-download allowed. **Offline quiz/deck/paper:** must be DOWNLOADED before offline use (409 otherwise); answers saved per device → `offline_attempts` row with `graded=false`, **NO score, NO `attempts` row — grading happens ONLY ON SYNC** (structural: `sync()` is the only caller of the T-C-05 pipeline). **`sync(userId, deviceToken)`**: grades each ungraded row through the REAL T-C-05 `AttemptService` (fresh attempt row, real grading engine, frozen `AttemptSubmitted` emitted once per graded quiz — D ingestion unchanged), marks it graded, attaches attempt_id. **Conflicts:** offline+online edit of the same record → latest-change-wins with merge where possible (multi-field merge tested); unmergeable → `offline_sync_queue` row (pending, retry counter, manual-review flag) — the client reports conflicts via `POST /offline/conflicts`, `sync()` runs the service-level resolver/merge before grading, and manual-review rows resolve via the service `resolveConflict` (admin surface is D's). ACs (all 4): lapsed subscription → downloads expire + playback 403; offline quiz answered → no score until sync, then real attempt + score + `AttemptSubmitted`; device limit 4th device → 409; conflict latest-wins (mergeable) + unmergeable → sync-queue manual review. **Also fixed (pre-existing, date-sensitive test bug):** `StudyPlanTest::test_check_in_fires_once_when_adherence_drops` asserted `check_in_fired_at->day === 1` (day-of-MONTH == 1 — only passes on the 1st; failed 3/3 on the 24th). Now asserts the fired timestamp is UNCHANGED after a second same-day `markTaskDone` (same intent — 'no second fire today' — but date-independent). No StudyPlan production change. **DEPS note:** the earlier '⛔ blocked on T-A-10' note over-applied to T-C-14 — offline learning is the learner's OWN devices (no parent-child link involved) and T-A-17 entitlement is consumed via the frozen-FQCN Mockery pattern in `BootsLearningRuntime` (same as T-C-03/04/06) — no A-side file required. T-C-13/T-C-15 DO still need T-A-10 (account-links). **Next: T-C-17 (E2E + seeding)** — deps T-C-16 (merged) + DEPS⚡ T-B-13/T-A-07; T-C-13/T-C-15 await T-A-10 merge. |
| (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). |
>>>>>>> ca328e2 ([feat][T-C-14] Offline learning runtime (devices, downloads, sync, conflicts))

## 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-07тАжT-C-11 ALL MERGED, T-C-13 BLOCKED on T-A-10)

All of T-C-07 (84382b3), T-C-08, T-C-09 (1c7d2d3), T-C-10 (7694d27) and T-C-11 (1123728) are now on origin/main. Full suite at T-C-11 head: **622/622 passing, 0 failing** (Learning ns 138/138).

**тЫФ T-C-13 BLOCKED:** DEPSтЪб T-A-10 (account-links тАФ the parent-child link records) is on `origin/track-a/T-A-10-account-links` but **NOT merged to main** (`git branch -r --merged origin/main` shows no T-A-10; track_a.md still lists T-A-10тАжT-A-23/T-A-25 as "тПн next"). T-C-11's work is complete and merged; T-C-13's link runtime + parental controls cannot be built/tested against real link records until A merges T-A-10. T-C-11 status row + developer_C.md T-C-10/T-C-11 check-offs recorded here.

**ЁЯЪА 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).

<<<<<<< HEAD
**ЁЯЪА T-C-16 (Phase 4 read models + REAL ProgressService) PUSHED 2026-09-24** on `track-c/T-C-16-progress-read-models`, off origin/main head. Full suite on THIS head: **633/633 passing, 0 failing** (+11 new Learning tests, Learning ns 149/149), 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). **Gate 3 obligation met:** the REAL `App\Services\Learning\ProgressServiceImpl` now backs the frozen `ProgressService` contract (T-C-02) тАФ the stub binding in C-owned `routes/api_learning.php` is swapped for the implementation; the frozen interface is UNCHANGED so D's injected mock/double still works. **NEW C-owned `GradeBenchmark` PORT** (DEPSтЪб тАФ D's T-B-13-style cohort analytics supplies `cohortForGrade(gradeLevel)`; `NullGradeBenchmark` is the default binding until D lands, so grade-level benchmarking degrades gracefully to "no cohort" тАФ no 500). **ADDITIVE queued event** `App\Events\Learning\PerformanceAlert(int $userId, string $type, array $payload)` (not one of the frozen 15) тАФ the "performance alerts тЖТ D" requirement (score_drop | missed_plan | streak_risk); fired on a genuine planted score drop. **D:** (1) bind your real cohort service to `App\Services\Learning\GradeBenchmark` to activate benchmarking (one-line, no C change); (2) ingest `PerformanceAlert` for the performance-alert surface. README: one append-only Contract Change Log line. `CertificateIssued` emission verified complete (T-C-11 runtime: every course completion тЖТ `OnCourseCompleted` тЖТ `issueCertificate` тЖТ frozen `CertificateIssued`). Ownership audit тАФ ALL changed paths C-owned: `app/Models/Learning/{StudySession,LearnerActivity,SharedReport}.php`, `app/Services/Learning/{ProgressServiceImpl,LearnerReadModel,GradeBenchmark,NullGradeBenchmark,ActivityLogger}.php`, `app/Events/Learning/PerformanceAlert.php`, `app/Listeners/Learning/{LogAttemptActivity,LogCertificateIssued,LogCourseCompleted,LogStreakActivity,LogStudyPlanGenerated,RecordStudySession}.php`, `app/Http/Controllers/Learning/LearnerReadModelController.php`, `app/Http/Controllers/Parent/ParentProgressController.php`, `database/migrations/learning/20260924_031001_create_progress_read_model_tables.php`, `routes/api_learning.php` (+learner/parent routes + GradeBenchmark binding), `tests/Feature/Learning/ReadModelsProgressTest.php` (11 tests, 116 assertions), `docs/status/track_c.md`, `docs/README.md` (append-only line). No A/B/D paths, no `app/Providers/`, no `bootstrap/providers.php`. Next after merge: **T-C-17 (E2E + seeding)** тАФ deps T-C-16 (this) + DEPSтЪб T-B-13/T-A-07. T-C-13/T-C-14/T-C-15 remain тЫФ BLOCKED on T-A-10/T-A-17 (not yet merged to main).
=======
**🚀 T-C-16 (Phase 4 read models + REAL ProgressService) PUSHED 2026-09-24** on `track-c/T-C-16-progress-read-models`, off origin/main head. Full suite on THIS head: **633/633 passing, 0 failing** (+11 new Learning tests, Learning ns 149/149), 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). **Gate 3 obligation met:** the REAL `App\Services\Learning\ProgressServiceImpl` now backs the frozen `ProgressService` contract (T-C-02) — the stub binding in C-owned `routes/api_learning.php` is swapped for the implementation; the frozen interface is UNCHANGED so D's injected mock/double still works. **NEW C-owned `GradeBenchmark` PORT** (DEPS⚡ — D's T-B-13-style cohort analytics supplies `cohortForGrade(gradeLevel)`; `NullGradeBenchmark` is the default binding until D lands, so grade-level benchmarking degrades gracefully to "no cohort" — no 500). **ADDITIVE queued event** `App\Events\Learning\PerformanceAlert(int $userId, string $type, array $payload)` (not one of the frozen 15) — the "performance alerts → D" requirement (score_drop | missed_plan | streak_risk); fired on a genuine planted score drop. **D:** (1) bind your real cohort service to `App\Services\Learning\GradeBenchmark` to activate benchmarking (one-line, no C change); (2) ingest `PerformanceAlert` for the performance-alert surface. README: one append-only Contract Change Log line. `CertificateIssued` emission verified complete (T-C-11 runtime: every course completion → `OnCourseCompleted` → `issueCertificate` → frozen `CertificateIssued`). Ownership audit — ALL changed paths C-owned: `app/Models/Learning/{StudySession,LearnerActivity,SharedReport}.php`, `app/Services/Learning/{ProgressServiceImpl,LearnerReadModel,GradeBenchmark,NullGradeBenchmark,ActivityLogger}.php`, `app/Events/Learning/PerformanceAlert.php`, `app/Listeners/Learning/{LogAttemptActivity,LogCertificateIssued,LogCourseCompleted,LogStreakActivity,LogStudyPlanGenerated,RecordStudySession}.php`, `app/Http/Controllers/Learning/LearnerReadModelController.php`, `app/Http/Controllers/Parent/ParentProgressController.php`, `database/migrations/learning/20260924_031001_create_progress_read_model_tables.php`, `routes/api_learning.php` (+learner/parent routes + GradeBenchmark binding), `tests/Feature/Learning/ReadModelsProgressTest.php` (11 tests, 116 assertions), `docs/status/track_c.md`, `docs/README.md` (append-only line). No A/B/D paths, no `app/Providers/`, no `bootstrap/providers.php`. Next after merge: **T-C-17 (E2E + seeding)** — deps T-C-16 (this) + DEPS⚡ T-B-13/T-A-07. T-C-13/T-C-15 remain ⛔ BLOCKED on T-A-10 (account-links, not yet merged to main). **T-C-14 (offline learning) is UNBLOCKED and PUSHED** (see its table row — it needs no account-links; entitlement via the frozen-FQCN Mockery pattern).
>>>>>>> ca328e2 ([feat][T-C-14] Offline learning runtime (devices, downloads, sync, conflicts))

## 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.
