import { getDataLayer } from '@db/factory'; import { UserDBInterface } from '..'; import { StudentClassroomDB } from '../../impl/couch/classroomDB'; import type { GeneratorResult, ReplanHints } from '../navigators/generators/types'; import { TagFilter, hasActiveFilter } from '@vue-skuilder/common'; import { TagFilteredContentSource } from '../../study/TagFilteredContentSource'; import { OrchestrationContext } from '../orchestration'; export type StudySessionFailedItem = StudySessionFailedNewItem | StudySessionFailedReviewItem; export interface StudySessionFailedNewItem extends StudySessionItem { status: 'failed-new'; } export interface StudySessionFailedReviewItem extends StudySessionReviewItem { status: 'failed-review'; } export interface StudySessionNewItem extends StudySessionItem { status: 'new'; } export interface StudySessionReviewItem extends StudySessionItem { reviewID: string; status: 'review' | 'failed-review'; } export function isReview(item: StudySessionItem): item is StudySessionReviewItem { const ret = item.status === 'review' || item.status === 'failed-review' || 'reviewID' in item; // console.log(`itemIsReview: ${ret} // \t${JSON.stringify(item)}`); return ret; } export interface StudySessionItem { status: 'new' | 'review' | 'failed-new' | 'failed-review'; contentSourceType: 'course' | 'classroom'; contentSourceID: string; // qualifiedID: `${string}-${string}` | `${string}-${string}-${number}`; cardID: string; courseID: string; elo?: number; /** * Pipeline suitability score at queue-build time, carried for observability * (the debug overlay renders the now-load-bearing supply ranking). `+INF` * marks a mandatory required card. Not used for any draw decision — the * supplyQ is already ordered, so the controller draws front-to-back. */ score?: number; // reviewID?: string; } export interface ContentSourceID { type: 'course' | 'classroom'; id: string; /** * Optional tag filter for scoped study sessions. * When present, creates a TagFilteredContentSource instead of a regular course source. */ tagFilter?: TagFilter; } // #region docs_StudyContentSource /** * Interface for sources that provide study content to SessionController. * * Content sources return scored candidates via getWeightedCards(), which * SessionController uses to populate study queues. * * See: packages/db/docs/navigators-architecture.md */ export interface StudyContentSource { /** * Get cards with suitability scores for presentation. * * Returns unified scored candidates that can be sorted and selected by SessionController. * The card origin ('new' | 'review' | 'failed') is determined by provenance metadata. * * @param limit - Maximum number of cards to return * @returns Cards sorted by score descending */ getWeightedCards(limit: number): Promise; /** * Get the orchestration context for this source. * Used for recording learning outcomes. */ getOrchestrationContext?(): Promise; /** * Set ephemeral hints for the next pipeline run. * No-op for sources that don't support hints. */ setEphemeralHints?(hints: ReplanHints): void; } // #endregion docs_StudyContentSource export async function getStudySource( source: ContentSourceID, user: UserDBInterface ): Promise { if (source.type === 'classroom') { return await StudentClassroomDB.factory(source.id, user); } else { // Check if this is a tag-filtered course source if (hasActiveFilter(source.tagFilter)) { return new TagFilteredContentSource(source.id, source.tagFilter!, user); } // Regular course source return getDataLayer().getCourseDB(source.id) as unknown as StudyContentSource; } }