# Track B Status

Pre-flight (2026-09-24): Track A Phase 0 complete — T-A-01…T-A-06 all merged to main
(T-A-05 landed as 4ad28ae, latest origin/main). BaseDomainModel, audit, RBAC
(`rbac.can:`), response envelope, and the queued events bus all present. GATE 1
pre-flight passed. (Old `track-b/status-pre-flight-blocked` branch is superseded.)

## CONTRACT ANNOUNCEMENT (for Tracks C & D) — T-B-02 shipped 2026-09-24

`App\Services\Catalog\CatalogService` is now the frozen read contract:

- `contentItem(int $contentItemId, ?int $userId = null): ?ContentItemDto`
- `courseTree(int $courseId): ?CourseDto`
- `questionBank(int $assessmentId): array<QuestionDto>`  (alias `questionsFor`)
- `topicCoverage(int $courseId): TopicCoverageDto`
- `prebuiltMaps(int $subjectId): array<MindMapDto>`
- `sampleContent(int $courseId): array<SampleContentDto>`

Inject via constructor (`CatalogService $catalog`). Phase 0 returns the
documented empty shape (`CatalogServiceStub`); real behavior lands with
T-B-05/06/08/12. DTOs live in `App\Services\Catalog\Dto\*` and carry an
`AccessState` (`full | sample | locked`) — sample = preview-only fields
(no full transcript/media URL). Queued events: `App\Events\Catalog\{
CoursePublished, ContentApproved, AssessmentRetired}` (frozen shapes).
Implementors read `catalog` only — no cross-DB joins or FKs.

| Task | Status | Branch | Notes / blockers |
|---|---|---|---|
| T-B-01 | 🚀 pushed 2026-09-24 | track-b/T-B-01-catalog-base-model | BaseCatalogModel (connection lock) + catalog migration baseline + test infra. 4 passing / 0 failing (Catalog filter); full suite 78 passing. |
| T-B-02 | 🚀 pushed 2026-09-24 | track-b/T-B-01-catalog-base-model (Phase-0 combined) | **CATALOG CONTRACT SHIPPED** — `App\Services\Catalog\CatalogService` (frozen interface) + `CatalogServiceStub` + access-state DTOs (full/sample/locked) + 3 queued catalog contract events. C & D: inject the interface via constructor; docblock states the cross-DB rule. Additive methods + optional `contentItem(userId)` logged in README Contract Change Log. Full suite 85 passing. |
| T-B-03 | ✅ merged 2026-09-24 | track-b/T-B-01-catalog-base-model (Phase-0 combined) | AccessGate (full/sample/locked via SubscriptionService mock) + CourseDto/ContentItemDto/AssessmentDto mappers (sample strips transcript/mediaUrl/mediaKey). Merged `df7ef31`. |
| GATE 1 | ✅ **passed** 2026-09-24 (merged `df7ef31`) | | C & D can compile against the CatalogService stub (T-C-02 already consumes the Learning contract; B contract announced above). Phase-0 branch merged to main. |
| T-B-04 | ✅ merged 2026-09-24 | track-b/T-B-04-curriculum-trees | Curriculum organization (academic + professional trees). Merged to main as `bc0a862`. 12 models, 3 migrations, `CurriculumService`, `api_catalog.php` routes gated by `rbac.can:curriculum.manage`. Additive event `BoardCertificationRevoked` logged in README Contract Change Log. |
| T-B-05 | 🚀 pushed 2026-09-24 | track-b/T-B-05-course-management | Course management (academic + professional). Models: `Course`/`CourseModule`/`CourseUnit`/`CourseAssignment`/`BatchAssessmentSchedule`/`LearningPath{,Course,Assignment}`; migrations `20260924_020101…020103` (course structure, assignments/schedules/paths, `course.manage` RBAC seed). `CourseService`: one-time placement validation (nodes must be active, tree-consistent → 422), readiness gate (publish 409 with missing-unit list), archive-vs-delete (assignments → 409, archive preserves refs), separate visibility/availability, reviewed unassignment (impact_reviewed + reason), batch schedule reschedule (records prior-attempts disposition, emits re-notification event to D), learning paths (sequenced, learner-or-group). Routes: browse auth-only + tree-scoped, writes under `rbac.can:course.manage`. Additive event `BatchAssessmentRescheduled` logged in README Contract Change Log. Catalog filter 64 passing / 0 failing; full suite 197 passing, 0 failing. |
| T-B-06 | 🚀 pushed 2026-09-24 | track-b/T-B-06-content-items-media | Content items + media pipeline. Models: `ContentItem` (type video_tutorial/podcast/article; lifecycle status + media pipeline state; raw effectiveness counters for C→D), `ContentVersion` (append-only), `ContentSubtitle` (timed text, multi-language), `ContentAssignment` (course/unit/topic targets). Migrations `20260924_020201…020203` (content + versions, subtitles + assignments, `content.manage` RBAC seed). `ContentService`: per-type media policy (format/quality/size → 422 with SPECIFIC rejection reason), uploaded→processing→playable via additive queued event `ContentMediaUploaded` + own queued listener `ProcessContentMedia` (self-registered from the B-owned service — AppServiceProvider is A-owned), append-only versioning (replace/restore append rows; restore records `restored_from_version`; learners see latest), subtitle coverage vs supported set (missing flagged, never silent), active-target assignment (422 on retired nodes), reviewed unassignment (confirmation + reason), delete dependency check (409 on active assignments/unit attachment; archive = default retirement), raw counters only. `CatalogServiceStub::contentItem()` → real impl (frozen signature unchanged; full/sample/locked via AccessGate→SubscriptionService only). Routes: `POST /content/videos` (AC) + read/write groups, writes under `rbac.can:content.manage`. 17 new tests; Catalog filter 81 passing; full suite 214 passing, 0 failing. |
| T-B-07 | 🚀 pushed 2026-09-24 | track-b/T-B-07-resource-library | Resource library (PDFs, worksheets, formula sheets, case studies, research, infographics) + flashcard decks + mind maps + FAQs. Models: `Resource` (append-only versioning same as T-B-06; placement to video/topic/subject; external-link validation state), `ResourceVersion`, `FlashcardDeck` (AI → draft hard rule; community → moderation states pending/approved/removed-with-reason; source video mark), `Flashcard` (front/back/image/audio), `MindMap` (per topic; clickable nodes with content links; color-coded by importance), `Faq` (per video; pending→approved; teacher_verified with verifier). Migrations `20260924_020301…020303` (resources+versions+decks+flashcards, mind_maps+faqs, `resource.manage` RBAC seed). `ResourceService`: entitlement-gated downloads via A's `SubscriptionService` only (DEPS⚡ T-A-23 → unentitled = 403; download policy type set), delete dependency check (409 for published/referenced resources; archive = default retirement), append-only versioning (replace/restore), external link validation (resolve + correct-target), broken-link job (flags dead links, never silent). `FlashcardService`: AI decks always created as draft (hard rule), human review is the ONLY path into the bank; community moderation (approve/remove-with-reason); student FAQs → pending → moderated → (optional) teacher-verified. Routes: `/resources/*` + `/flashcards/*`, writes under `rbac.can:resource.manage`; downloads auth + entitlement-gated; bank surface learner-facing, approved-only. 14 new tests; Catalog filter 95 passing; full suite 228 passing, 0 failing. |
| T-B-08 | 🚀 pushed 2026-09-24 | track-b/T-B-08-question-bank-assessments | Question bank + assessment definitions (catalog DB). Models: `Question` (7 types; difficulty + Bloom; topic/subject tags; source manual|ai-with-video; status draft→approved, rejected recorded; duplicates flagged), `AssessmentQuestion` (active/retired attach), `Assessment` (formats incl. video_wise; passing score future-attempts-only; retake policy; adaptive opt-in floor/ceiling+calibration; exam pattern; grading config; high-stakes; attempt_count plain counter read-only for B), `AssessmentConfigVersion` (append-only audit), `RegenerationRequest` (video-wise prompt, not silent overwrite), `PastPaper` (subject/year/exam, availability-conditional, batch-scoped, memo). Migrations `20260924_020401…020404` (questions+assessments-questions, assessments+config-versions+regeneration, past_papers, `assessment.manage` RBAC seed). `QuestionService`: AI questions forced draft on create (hard rule); human review = ONLY path into bank; rejection recorded (status+reason) and never enters; duplicates flagged (422); attach only approved; multi-select full-marks rule documented. `AssessmentService`: passing-score change appends audit row + bumps version (FUTURE attempts only — recorded outcomes in C's learning DB never retrochanged); adaptive bounds floor≤ceiling (422 else); delete gate (attempts>0 → 409, retire only); video-wise auto-quiz → AI-draft questions (DEPS T-B-06); content update → pending regeneration PROMPT (regenerate/discard-with-reason); past papers availability-conditional + batch-scoped. `CatalogServiceStub::questionBank/questionsFor` → REAL (frozen signature unchanged; approved-only across the contract). Routes: `/assessments/*` writes under `rbac.can:assessment.manage`; question-bank read is auth-only approved-only. 18 new tests; Catalog filter 113 passing; full suite 246 passing, 0 failing. |
| T-B-09 | 🚀 pushed (rebased on origin/main) 2026-09-24 | track-b/T-B-09-approval-workflow | Content approval workflow + moderation (catalog DB). Migrations 20260924_020501…020503 (review_items; content_flags + moderation_reports; content.review RBAC seed). review_items: pending → in_review → approved | rejected | flagged; approval recorded (reviewer + time + risk level); reassignment recorded (from + reason); risk by content type → workflow variant. Flags: marker on pending OR live content, type-specific required action, resolution MANDATORY (never left open). Community moderation: POST /moderation/reports intake → review → action / dismiss-with-reason → close (never left open); external_record_id plain cross-DB ref, no engagement join. Content NEVER auto-approved, NEVER live until approved — approve is the only path that flips video to published + emits the frozen ContentApproved; rejection definitive (409 on re-review), specific reasons (422), resubmission = new review item, creator notified via ADDITIVE ContentRejected event (D). Queue filters type/subject/creator/status + per-reviewer workload + queue aging metric → additive ReviewQueueAging event (D dashboard). Age-appropriate gate: age_rating × learner age band, enforced on the catalog learner read — CatalogService::contentItem gains an ADDITIVE ?int $learnerAgeBandMax param (frozen 1/2-arg call sites untouched); over-age content → null, not bypassable. Routes /review/* + /moderation/* under deny-by-default rbac.can:content.review. 15 new tests. Rebased 2026-09-24 onto origin/main (post T-B-10/T-B-11) — full suite 296 passing, 0 failing; branch is now fast-forward-mergeable. |
| T-B-10 | 🚀 pushed 2026-09-24 | b/T-B-10-srs-config | SRS platform configuration (catalog DB — C owns the learner SRS STATE in learning; B owns the platform CONFIG). Migrations 20260924_020601…020602 (9 SRS config tables + srs.manage RBAC seed). srs_schedule_configs (interval ladder + floor/cap + config_version) + per-grade-band overrides (OVERRIDE > GLOBAL — the override covering the learner age wins); ebbinghaus_curve_points (MUST decay monotonically: days strictly increasing, retention non-increasing — invalid combos 422; preview endpoint renders the curve from the SAME model); recall_adjustment_rules (rating map COMPLETE: too_easy/good/hard → exactly one schedule index, no compounding; max extension / min reset caps); srs_eligibilities (flashcard sets / quiz questions / videos — non-eligible content never enters SRS, excluded from C's due-queue); srs_concept_mappings (NO ORPHAN CONCEPTS — every mapping must resolve to an existing catalog topic, else 422); srs_resurfacing_rules (on_error/on_schedule/both, error-weighting never below floor, per-question cap); srs_video_revision_reminders (cap + skip-if-rewatched); srs_mastery_model_configs (level thresholds from recall rate — NO manual override; confidence ratings; at-risk threshold; graduation criteria/archive/re-entry/periodic re-check). Config changes apply to NEWLY SCHEDULED reviews only — SrsConfigService::snapshot() is what C snapshots at scheduling (DEPS⚡ T-C-08). Routes: GET /srs/snapshot + /srs/curve auth-only; writes under deny-by-default rbac.can:srs.manage. 14 new tests; Catalog filter 127 passing; full suite 260 passing, 0 failing. |
| T-B-11 | 🚀 pushed 2026-09-24 | b/T-B-11-exam-ai-config | Target-exam templates + AI feature configuration (catalog DB — C owns the INSTANCE in learning; B owns the platform CONFIG). Migrations `20260924_020701…020703` (exam types/templates/topic mappings/prediction configs + study-plan + AI feature configs + `exam.manage` RBAC seed). `ExamTemplateService`: exam types (duplicate (name,kind) 422; delete of a type with active students → 409 via plain cross-DB existence check on C's `learning.target_exams` (read-only, defensive — never a join); disable always OK), VERSIONED templates (sections sum ≤ total 422; edit creates v(n+1), old version frozen/archived read-only — never edited in place), topic importance default chain (topic mapping → subject default → global medium), bulk mapping import with ROW-LEVEL reject report (versioned on live types). `PredictionConfigService`: override > global, versioned (in-use versions never deleted; ROLLBACK EXACT — re-activate the historical version verbatim), zero-width confidence interval 422, drift → additive queued event `PredictionAccuracyDrift` (→D), insufficient-data contract with C (below min_data_points → "not shown — prompt to practice", never a guess). `AiFeatureConfigService`: learn-before-practice ordering enforced (violations NEVER generated, 422), revision cycles clamped ≥ SRS minimum (same-DB read of B's own SRS floor), milestone triggers (fire once/plan — C enforces at runtime; parent notification flag → D), AI features (tutor/mentor/planner/companion/score-prediction; escalation: student-requested ALWAYS succeeds, low-confidence threshold, handoff context auto-generated, async-ticket fallback), planner distraction-free (suppressed notifications QUEUED + released on re-entry). Config changes NON-RETROACTIVE (config_version; C snapshots). Routes: `GET /exams/*` auth-only; writes under deny-by-default `rbac.can:exam.manage`. 21 new tests; Catalog filter 148 passing; full suite 281 passing, 0 failing. |
| T-B-12 | 🚀 pushed 2026-09-24 | b/T-B-12-coverage-gaps-retirement | Catalog coverage, gaps & retirement jobs (catalog DB). `CoverageService`: `topicCoverage` (course's subject-topic universe → content/assessment per topic; planted gaps VISIBLE, never silent), node coverage (board/subject), content-without-approval report (the T-B-09 gate made visible: uploaded-but-unapproved = gap, approved = not), questions-without-topic-tag report, orphan-resource report. `RetirementService`: content-update → dependent video_wise quiz PENDING regeneration prompt (event `ContentMediaUploaded` → listener `FlagContentUpdateRetirements`, idempotent per content version, quiz flagged never silently overwritten), retire-course (impact-reviewed unassignment of active course-target assignments, history preserved, idempotent), external-link validation job (dead/malformed links flagged `external_link_broken`, never silent), orphan detection. `CatalogServiceStub::topicCoverage/prebuiltMaps/sampleContent` → REAL (frozen signatures unchanged; `prebuiltMaps` published-only; `sampleContent` published+playable-only, 2-min preview bound + unlock message). Controller `Catalog/Coverage/CatalogCoverageController` + `Question::topicTagIds()` helper. Routes: `/coverage/*` auth-only reads; `/retirement/courses/{course}` under `rbac.can:content.manage`, `/retirement/external-links/validate` under `rbac.can:resource.manage`. 14 new tests (CoverageAndRetirementTest). **REGRESSION FIX (Laravel 13 event auto-discovery):** `ProcessContentMedia::handle` was untyped so Laravel 13's `app/Listeners/*` discovery skipped it and it relied on `ContentService::__construct` self-registration; its `hasListeners()` guard is order-sensitive (any other listener on `ContentMediaUploaded` — now `FlagContentUpdateRetirements` — suppressed it, leaving media stuck in `uploaded`). Type-hinted `ProcessContentMedia::handle(ContentMediaUploaded $event)` so it is auto-discovered at boot, independent of service-construction order; constructor registration kept only as the discovery-disabled fallback. Catalog filter 177 passing / 0 failing; **full suite 310 passing, 0 failing.** |
| T-B-13 | ⏳ queued | — | Catalog seeding & fixtures (DEPS T-B-12). |
