import { U as UserDBInterface, s as CourseRegistrationDoc, S as StudySessionItem, W as WeightedCard, R as ReplanHints, h as StudyContentSource, G as GeneratorResult, C as CourseDBInterface } from './contentSource-C-0t0y0V.js'; export { K as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag, a4 as CardGenerator, a6 as CardGeneratorFactory, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, H as ClassroomRegistrationDoc, e as ContentNavigationStrategyData, f as ContentNavigator, q as ContentSourceID, d as CourseInfo, L as CourseRegistration, b as CoursesDBInterface, ad as DocumentUpdater, a5 as GeneratorContext, a7 as LearnableWeight, N as NavigatorConstructor, a0 as NavigatorRole, a1 as NavigatorRoles, $ as Navigators, a8 as OrchestrationContext, j as ScheduledCard, I as SessionTrackingData, Z as StrategyContribution, i as StudentClassroomDBInterface, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, T as TeacherClassroomDBInterface, J as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, M as UserOutcomeRecord, B as UsrCrsDataInterface, a9 as computeDeviation, ab as computeEffectiveWeight, aa as computeSpread, ac as createOrchestrationContext, _ as getCardOrigin, P as getRegisteredNavigator, X as getRegisteredNavigatorNames, V as getRegisteredNavigatorRole, r as getStudySource, Q as hasRegisteredNavigator, Y as initializeNavigatorRegistry, a3 as isFilter, a2 as isGenerator, p as isReview, ae as newInterval, O as registerNavigator } from './contentSource-C-0t0y0V.js'; import { D as DataLayerProvider } from './dataLayerProvider-BB0oi9T0.js'; import { C as CardHistory, c as CardRecord, d as QuestionRecord } from './types-legacy-4tlwHnXo.js'; export { e as CardData, f as CourseListData, h as DataShapeData, g as DisplayableData, D as DocType, b as DocTypePrefixes, F as Field, G as GuestUsername, Q as QualifiedCardID, i as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from './types-legacy-4tlwHnXo.js'; import { SrsBacklogDebug, Loggable } from './core/index.js'; export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, DIVERSITY_FLOOR, DIVERSITY_STRENGTH, DiversityRerankOptions, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineForecaster, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, clearSrsBacklogDebug, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getActivePipeline, getCardHistoryID, getDefaultLearnableWeight, getSrsBacklogDebug, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.js'; import { TaggedPerformance, TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common'; import { S as StaticCourseManifest } from './types-CHgpWQAY.js'; export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig } from './types-CHgpWQAY.js'; import { F as FileSystemAdapter } from './index-BLLT5BYE.js'; export { C as CouchDBToStaticPacker, a as FileStats, b as FileSystemError } from './index-BLLT5BYE.js'; import 'moment'; /** * Service responsible for ELO rating calculations and updates. */ declare class EloService { private dataLayer; private user; constructor(dataLayer: DataLayerProvider, user: UserDBInterface); /** * Updates both user and card ELO ratings based on user performance. * @param userScore Score between 0-1 representing user performance * @param course_id Course identifier * @param card_id Card identifier * @param userCourseRegDoc User's course registration document (will be mutated) * @param currentCard Current card session record * @param k Optional K-factor for ELO calculation */ updateUserAndCardElo(userScore: number, course_id: string, card_id: string, userCourseRegDoc: CourseRegistrationDoc, currentCard: StudySessionRecord, k?: number): Promise; /** * Updates both user and card ELO ratings with per-tag granularity. * Tags in taggedPerformance but not on card will be created dynamically. * * @param taggedPerformance Performance object with _global and per-tag scores * @param course_id Course identifier * @param card_id Card identifier * @param userCourseRegDoc User's course registration document (will be mutated) * @param currentCard Current card session record */ updateUserAndCardEloPerTag(taggedPerformance: TaggedPerformance, course_id: string, card_id: string, userCourseRegDoc: CourseRegistrationDoc, currentCard: StudySessionRecord): Promise; } /** * Service responsible for Spaced Repetition System (SRS) scheduling logic. */ declare class SrsService { private user; constructor(user: UserDBInterface); /** * Remove a scheduled review from the user's database. * Used to clean up orphaned reviews (e.g., card deleted from course DB). */ removeReview(reviewID: string): void; /** * Calculates the next review time for a card based on its history and * schedules it in the user's database. * @param history The full history of the card. * @param item The study session item, used to determine if a previous review needs to be cleared. */ scheduleReview(history: CardHistory, item: StudySessionItem): Promise; } /** * Service responsible for orchestrating the complete response processing workflow. * Coordinates SRS scheduling and ELO updates for user card interactions. */ declare class ResponseProcessor { private srsService; private eloService; constructor(srsService: SrsService, eloService: EloService); /** * Parses performance data into global score and optional per-tag scores. * * @param performance - Numeric or structured performance from QuestionRecord * @returns Parsed performance with global score and optional tag scores */ private parsePerformance; /** * Processes a user's response to a card, handling SRS scheduling and ELO updates. * @param cardRecord User's response record * @param cardHistory Promise resolving to the card's history * @param studySessionItem Current study session item * @param courseRegistrationDoc User's course registration (for ELO updates) * @param currentCard Current study session record * @param courseId Course identifier * @param cardId Card identifier * @param maxAttemptsPerView Maximum attempts allowed per view * @param maxSessionViews Maximum session views for this card * @param sessionViews Current number of session views * @returns ResponseResult with navigation and UI instructions */ processResponse(cardRecord: CardRecord, cardHistory: Promise>, studySessionItem: StudySessionItem, courseRegistrationDoc: CourseRegistrationDoc, currentCard: StudySessionRecord, courseId: string, cardId: string, maxAttemptsPerView: number, maxSessionViews: number, sessionViews: number): Promise; /** * Handles processing for correct responses: SRS scheduling and ELO updates. */ private processCorrectResponse; /** * Handles processing for incorrect responses: ELO updates only. */ private processIncorrectResponse; } interface HydratedCard { item: StudySessionItem; view: TView; data: any[]; tags: string[]; } interface MigrationOptions { chunkBatchSize: number; validateRoundTrip: boolean; cleanupOnFailure: boolean; timeout: number; } interface MigrationResult { success: boolean; documentsRestored: number; attachmentsRestored: number; designDocsRestored: number; courseConfigRestored: number; errors: string[]; warnings: string[]; migrationTime: number; tempDatabaseName?: string; } interface ValidationResult { valid: boolean; documentCountMatch: boolean; attachmentIntegrity: boolean; viewFunctionality: boolean; issues: ValidationIssue[]; } interface ValidationIssue { type: 'error' | 'warning'; category: 'documents' | 'attachments' | 'views' | 'metadata' | 'course_config'; message: string; details?: any; } interface DocumentCounts { [docType: string]: number; } interface RestoreProgress { phase: 'manifest' | 'design_docs' | 'course_config' | 'documents' | 'attachments' | 'validation'; current: number; total: number; message: string; } interface StaticCourseValidation { valid: boolean; manifestExists: boolean; chunksExist: boolean; attachmentsExist: boolean; errors: string[]; warnings: string[]; courseId?: string; courseName?: string; } interface AggregatedDocument { _id: string; _attachments?: Record; docType: string; [key: string]: any; } interface AttachmentUploadResult { success: boolean; attachmentName: string; docId: string; error?: string; } declare const DEFAULT_MIGRATION_OPTIONS: MigrationOptions; declare class StaticToCouchDBMigrator { private options; private progressCallback?; private fs?; constructor(options?: Partial, fileSystemAdapter?: FileSystemAdapter); /** * Set a progress callback to receive updates during migration */ setProgressCallback(callback: (progress: RestoreProgress) => void): void; /** * Migrate a static course to CouchDB */ migrateCourse(staticPath: string, targetDB: PouchDB.Database): Promise; /** * Load and parse the manifest file */ private loadManifest; /** * Restore design documents to CouchDB */ private restoreDesignDocuments; /** * Aggregate documents from all chunks */ private aggregateDocuments; /** * Load documents from a single chunk file */ private loadChunk; /** * Upload documents to CouchDB in batches */ private uploadDocuments; /** * Upload attachments from filesystem to CouchDB */ private uploadAttachments; /** * Upload a single attachment file */ private uploadSingleAttachment; /** * Restore CourseConfig document from manifest */ private restoreCourseConfig; /** * Calculate expected document counts from manifest */ private calculateExpectedCounts; /** * Clean up database after failed migration */ private cleanupFailedMigration; /** * Report progress to callback if available */ private reportProgress; /** * Check if a path is a local file path (vs URL) */ private isLocalPath; } /** * Validate that a static course directory contains all required files */ declare function validateStaticCourse(staticPath: string, fs?: FileSystemAdapter): Promise; /** * Validate the result of a migration by checking document counts and integrity */ declare function validateMigration(targetDB: PouchDB.Database, expectedCounts: DocumentCounts, manifest: StaticCourseManifest): Promise; /** * Get the application data directory for the current platform * Uses ~/.tuilder as requested by user for simplicity */ declare function getAppDataDirectory(): string; /** * Ensure the application data directory exists * Creates directory recursively if it doesn't exist */ declare function ensureAppDataDirectory(): Promise; /** * Get the full path for a PouchDB database file * @param dbName - The database name (e.g., 'userdb-Colin') */ declare function getDbPath(dbName: string): string; /** * Initialize data directory for PouchDB usage * Should be called once at application startup */ declare function initializeDataDirectory(): Promise; /** * Represents a batch of content fetched from a single StudyContentSource. */ interface SourceBatch { sourceIndex: number; weighted: WeightedCard[]; } /** * Strategy interface for mixing content from multiple sources into a unified * set of weighted candidates. * * Different implementations can provide different balancing strategies: * - QuotaRoundRobinMixer: Equal representation per source * - MinMaxNormalizingMixer: Score normalization then global sort * - PercentileBucketMixer: Bucketed round-robin * etc. */ interface SourceMixer { /** * Mix weighted cards from multiple sources into a unified, ordered list. * * @param batches - Content batches from each source * @param limit - Target number of cards to return * @returns Mixed and ordered weighted cards */ mix(batches: SourceBatch[], limit: number): WeightedCard[]; } /** * Quota-based mixer with interleaved output. * * Allocates equal representation to each source (top-N by score), then * interleaves the results by dealing from a randomly-shuffled source order. * Within each source, cards are dealt in score-descending order. * * This ensures that cards from different courses are spread throughout the * queue rather than clustered by score bands, which matters because * SessionController consumes queues front-to-back and sessions often end * before reaching the tail. */ declare class QuotaRoundRobinMixer implements SourceMixer { mix(batches: SourceBatch[], limit: number): WeightedCard[]; } /** A single queued item, carrying the now-load-bearing rank score + origin. */ interface SessionQueueItemDebug { cardID: string; /** Item status: 'new' | 'review' | 'failed-new' | 'failed-review'. */ status: string; /** Card nature, collapsed from status: 'new' | 'review'. */ origin: 'new' | 'review'; /** Pipeline suitability score at queue-build time; `+INF` marks a required card. */ score?: number; } /** Per-queue debug view: total length, cumulative draws, and head-first items. */ interface SessionQueueDebug { length: number; dequeueCount: number; /** Items in queue order, head (next draw) first. */ cards: SessionQueueItemDebug[]; } /** * A card the learner has interacted with this session (one entry per card in * the session record, regardless of which queue — if any — still holds it). */ interface SessionDrawnCardDebug { cardID: string; /** Queue status at draw time: 'new' | 'review' | 'failed-new' | 'failed-review'. */ status: string; /** Number of CardRecords logged for this card this session (≥1). */ attempts: number; /** Latest record's correctness; null for non-question (info) records. */ correct: boolean | null; /** Total time spent across all of this card's records, in ms. */ timeSpentMs: number; } /** Live snapshot of the controller, read fresh on each overlay tick. */ interface SessionDebugSnapshot { secondsRemaining: number; hasCardGuarantee: boolean; minCardsGuarantee: number; wellIndicatedRemaining: number; /** cardID of the card currently in front of the learner, if any. */ currentCard: string | null; /** Session-durable hints re-merged into every pipeline run this session. */ sessionHints: ReplanHints | null; /** True while a replan is executing (in-flight). */ replanActive: boolean; /** Reason for the in-flight replan (caller label, or '(auto)'); may be stale when idle. */ replanLabel: string | null; /** The single rank-ordered supply (new + review interleaved), head first. */ supplyQ: SessionQueueDebug; failedQ: SessionQueueDebug; /** SRS backlog state per course (drives the "is review starvation permanent?" read). */ reviewBacklog: SrsBacklogDebug[]; /** Every card the learner has interacted with this session, draw order. */ drawnCards: SessionDrawnCardDebug[]; } /** * Options for requesting a mid-session replan. * * All fields are optional — callers can pass just the fields they need. * When omitted, defaults match the existing behaviour (full 20-card * replace with no hints). */ interface ReplanOptions { /** Scoring hints forwarded to the pipeline (boost/exclude/require). */ hints?: ReplanHints; /** * Session-durable scoring hints. Unlike `hints` (one-shot, applied to * exactly the run this replan triggers), `sessionHints` are stashed on * the controller and re-merged into *every* subsequent pipeline run for * the remainder of the session — including the bare auto-replans * (depletion/quality) that carry no caller hints, and the wedge-breaker. * * Use for "emphasis that should outlive a single queue rebuild" — e.g. * boosting a just-failed concept tag, or a post-lesson concept boost set * at session start. Without this, a one-shot `hints` boost evaporates on * the next replan and the freshly-rebuilt (replace-mode) queue clobbers * whatever it surfaced. * * Semantics (KISS): setting `sessionHints` *replaces* the prior session * hints wholesale (caller beware — no accumulation, no decay). They live * until session end or until explicitly overwritten. Normal usage applies * a fixed boost, so repeated identical requests are no-ops. * * Merged with per-run `hints` via the pipeline's `mergeHints` (boosts * multiply, require/exclude lists concatenate). */ sessionHints?: ReplanHints; /** * Like `sessionHints`, but *merged* into the existing session-durable hints * (via `mergeHints`) instead of replacing them. Use when emphasis should * *accumulate* across replans rather than clobber — e.g. introducing a second * concept mid-session must not wipe the first concept's boost, nor any * `difficultyBooster`/`conceptBackoff` state on other concepts. * * Merge semantics (see `mergeHints`): boosts MULTIPLY, require/exclude lists * concat-dedup. Re-emphasising the *same* tag therefore compounds — callers * boosting a tag they may have already boosted should clamp at the call site. * * If both `sessionHints` and `mergeSessionHints` are supplied, the replace is * applied first, then the merge — but they are normally mutually exclusive. */ mergeSessionHints?: ReplanHints; /** * Maximum number of new cards to return from the pipeline. * Default: 20 (the standard session batch size). */ limit?: number; /** * How to integrate the new cards into the existing supplyQ. * - `'replace'` (default): atomically swap the entire supplyQ. * - `'merge'`: insert new cards at the front, keeping existing cards. */ mode?: 'replace' | 'merge'; /** * Guarantee that at least this many cards will be served after the * replan, even if the session timer has expired. Prevents intro cards * from surfacing at the end of a session with zero follow-up exercise. * Decremented on each card draw while active. */ minFollowUpCards?: number; /** * Human-readable label for debugging / provenance. * Appears in console logs and in card provenance entries created * by ephemeral hint application. */ label?: string; } interface StudySessionRecord { card: { course_id: string; card_id: string; card_elo: number; tags: string[]; }; item: StudySessionItem; records: CardRecord[]; } type SessionAction = 'dismiss-success' | 'dismiss-failed' | 'marked-failed' | 'dismiss-error'; interface ResponseResult { nextCardAction: Exclude | 'none'; shouldLoadNextCard: boolean; /** * When true, the card requested deferred advancement via `deferAdvance`. * The record was logged and ELO updated, but navigation was suppressed. * StudySession should stash `nextCardAction` and wait for a * `ready-to-advance` event from the card before calling `nextCard()`. */ deferred?: boolean; isCorrect: boolean; performanceScore?: number; shouldClearFeedbackShadow: boolean; } /** * Read-only snapshot of a single processed response, handed to every * registered {@link OutcomeObserver} after ELO/SRS have been recorded. * * Only emitted for question records (non-question dismisses are skipped). */ interface SessionOutcome { /** The user's response. Includes `isCorrect`, `performance`, `priorAttemps`. */ readonly record: QuestionRecord; /** * The card that was answered, including its `tags` — the primary key an * observer matches against (e.g. `gpc:exercise:*`). `card_elo` reflects * pre-update state; the ELO write for this response is already in flight. */ readonly card: StudySessionRecord['card']; /** The navigation decision produced for this response (read-only). */ readonly result: Readonly; } /** * The narrow capability surface handed to an {@link OutcomeObserver}. This is * the *only* way an observer can affect the session — it cannot touch ELO, * the queues, the timer, or mutate the `ResponseResult`. A misbehaving * observer degrades to "wrong boost", never "corrupted session". */ interface SessionControls { /** Current session-durable hints, or null. For read-modify-write. */ getSessionHints(): ReplanHints | null; /** Replace the session-durable hints wholesale (no decay). */ setSessionHints(hints: ReplanHints | null): void; /** * Merge `hints` into the existing session-durable hints via the pipeline's * `mergeHints` (boosts multiply, require/exclude lists concat-dedup). * Convenience for the common "add a boost on top of what's there" case. * Note: multiplicative + no decay — clamp boost factors yourself if a * repeatedly-failed tag could compound unboundedly. */ mergeSessionHints(hints: ReplanHints): void; /** Request a replan (e.g. `{ mode: 'merge' }` for immediate visibility). */ requestReplan(opts?: ReplanOptions): Promise; } /** * A consumer-supplied hook invoked after each question response is processed. * * Fires on *every* question response (gate inside on `record.isCorrect` / * `result.nextCardAction` as needed). Awaited but isolated: a throwing * observer is caught and logged, never wedging the session. Keep the * synchronous body cheap and `void` any long work (e.g. a triggered replan) * so you don't stall navigation. * * Registered via `StudySessionConfig.outcomeObservers` → constructor options. */ type OutcomeObserver = (outcome: SessionOutcome, controls: SessionControls) => void | Promise; interface SessionServices { response: ResponseProcessor; } declare class SessionController extends Loggable { _className: string; services: SessionServices; private srsService; private eloService; private hydrationService; private mixer; private dataLayer; private courseNameCache; /** * Default pipeline batch size for new-card planning. * Set via constructor options; falls back to 20 when not specified. * Individual replans can override via `ReplanOptions.limit`. */ private _defaultBatchLimit; private sources; private _sessionRecord; set sessionRecord(r: StudySessionRecord[]); private _currentCard; /** * The single supply queue: `new` + `review` items interleaved in pipeline * rank order (the mixer's score-ordered, source-interleaved output, with * `+INF` required cards floated to the front). Drawn front-to-back; reviews * and new compete on one cross-comparable scale rather than being re-mixed * by a probability gate. Replaced/re-ranked wholesale on replan. See * `docs/decision-single-supply-queue.md`. */ private supplyQ; private failedQ; /** * Supply draws since the last failed-queue *event* (a failed draw, or a card * entering failedQ on failure). Drives the light steady failed-interleave * (§7): after this many consecutive supply draws, a pending failed card is * drawn so remediation doesn't starve mid-session. Incremented on each supply * draw; reset to 0 both when a failed card is drawn AND when one is added to * failedQ — the latter gives a just-failed card spacing instead of an instant * retry (the counter would otherwise already be ≥ threshold from the preceding * supply run). */ private _supplyDrawsSinceFailed; /** * Promise tracking a currently in-progress replan, or null if idle. * Used by nextCard() to await completion before drawing from queues. */ private _replanPromise; /** * Reason for the replan currently executing in `_runReplan`, surfaced by the * debug overlay's spinner. The caller's `opts.label` when present, else * `'(auto)'`. Only meaningful while `_replanPromise` is non-null; cleared * when the in-flight chain settles. */ private _activeReplanLabel; /** * Number of well-indicated supply cards remaining before the queue * degrades to poorly-indicated content. Decremented on each supplyQ * draw; when it hits 0, a replan is triggered automatically * (user state has changed from completing good cards). */ private _wellIndicatedRemaining; /** * When true, suppresses the quality-based auto-replan trigger in * nextCard(). Set after a burst replan (small limit) to prevent the * auto-replan from clobbering the burst cards before they're consumed. * Cleared when the depletion-triggered replan fires (supplyQ exhausted). */ private _suppressQualityReplan; /** * When > 0, the session timer cannot end the session. Decremented on * each nextCard() draw. Set by replans that include `minFollowUpCards`. */ private _minCardsGuarantee; /** * Session-durable scoring hints. Re-merged into every pipeline run for * the rest of the session (initial plan + every replan, including bare * auto-replans and the wedge-breaker), via `_applyHintsToSources`. * * Set by `setSessionHints()` (e.g. session-start post-lesson boost) or by * any replan carrying `ReplanOptions.sessionHints` (e.g. a just-failed * concept boost). Replace semantics, no decay — lives until overwritten * or session end. See `ReplanOptions.sessionHints` for rationale. * * Note: the controller-managed auto-excludes (current card, session * record, imminent draw) are intentionally NOT folded in here — those are * recomputed per-run in `_runReplan` and would otherwise go stale. */ private _sessionHints; /** * Card IDs that have been *served* (drawn/consumed) this session. Populated * at the single consumption choke-point (removeItemFromQueue), so it reflects * a draw the instant it happens — earlier than `_sessionRecord`, which only * lands once the card is *responded to*. * * Used to keep already-served cards out of supplyQ on every (re)plan, across * ALL origins: a `new` card shown once must never re-enter, and once replans * re-pull reviews, an answered/in-flight review must not re-enter the supply * before its SRS reschedule clears the due-window (the review-loop guard, * decision doc §4). This is the general guard against re-presentation — * including the case where a replan in flight captured a now-drawn card (e.g. * a +INF require-injected follow-up the depletion prefetch grabbed just before * it was drawn). failedQ is separate and controller-owned, so failed cards * legitimately recur there without being gated here. */ private _servedCardIds; /** * Consumer-supplied hooks invoked after each question response is processed. * Seeded from constructor options (threaded from * `StudySessionConfig.outcomeObservers`). See {@link OutcomeObserver}. */ private _outcomeObservers; /** * Lazily-built, stable capability object handed to observers. Bound to * `this`; constructed once so observers can rely on referential identity. */ private _sessionControls; private startTime; private endTime; private _secondsRemaining; get secondsRemaining(): number; /** True when a card guarantee is active, preventing timer-based session end. */ get hasCardGuarantee(): boolean; get report(): string; get detailedReport(): string; private _intervalHandle; /** * @param sources - Array of content sources to mix for the session * @param time - Session duration in seconds * @param dataLayer - Data layer provider * @param getViewComponent - Function to resolve view components * @param mixer - Optional source mixer strategy (defaults to QuotaRoundRobinMixer) * @param options - Optional session-level configuration * @param options.defaultBatchLimit - Default supply working-set size (default: 20). * Smaller values for newer users cause more frequent replans, keeping plans * aligned with rapidly-changing user state. */ constructor(sources: StudyContentSource[], time: number, dataLayer: DataLayerProvider, getViewComponent: (viewId: string) => TView, mixer?: SourceMixer, options?: { defaultBatchLimit?: number; outcomeObservers?: OutcomeObserver[]; }); private tick; /** * Returns a rough, erring toward conservative, guess at * the amount of time the failed cards queue will require * to clean up. * * (seconds) */ private estimateCleanupTime; prepareSession(): Promise; /** * Request a mid-session replan. Re-runs the pipeline with current user state * and atomically replaces (or merges into) the supplyQ contents. Safe to call * at any time during a session. * * Concurrency policy: * - Two unhinted auto-replans never run in parallel; the second coalesces * into the first (returns the same promise). * - A hint-bearing replan that arrives while another replan is in flight * is queued to run **after** the in-flight one rather than dropped. * This preserves caller intent (label, requireCards, excludeTags, * limit, minFollowUpCards) instead of silently discarding it. Without * queueing, a background auto-replan that started just before a * completion-triggered replan would clobber the queue with unhinted * results (e.g. surfacing another gpc-intro card right after one * completed, skipping the prescribed `c-wst-*` follow-up). * * Re-pulls and re-ranks the whole supply (including reviews); does NOT affect * failedQ (controller-owned remediation). * * If nextCard() is called while a replan is in flight, it will automatically * await the replan before drawing from queues, ensuring the user always sees * cards scored against their latest state. * * Typical trigger: application-level code (e.g. after a GPC intro completion) * calls this to ensure newly-unlocked content appears in the session. */ requestReplan(options?: ReplanOptions | ReplanHints): Promise; /** * True when a requestReplan call carries caller intent that must not be * silently dropped. Bare unhinted auto-replans (depletion / quality * triggers in nextCard) return false and may coalesce. */ private _replanHasIntent; /** * Body of a single replan: populate auto-excludes, stash hints on * sources, log, then run the pipeline. Extracted so it can be invoked * either immediately (no in-flight replan) or queued (chained after * the in-flight one resolves). * * IMPORTANT: hint stash and the queue-state snapshot used to build * excludeCards happen at *invocation* time, not at *queue* time. For a * queued replan that means excludes reflect the state after the prior * replan landed — which is what we want, since the prior replan's * supplyQ.peek(0) is the imminent draw we need to exclude. */ private _runReplan; /** * Set the session-durable scoring hints (replace semantics, no decay). * * Unlike a one-shot replan hint, these are re-merged into every pipeline * run for the rest of the session — including the initial plan when set * before `prepareSession()`, every replan, the bare auto-replans, and the * wedge-breaker. Pass `null` to clear. * * Typical callers: * - `StudySession` at session start, threading `StudySessionConfig.initHints` * (e.g. a post-lesson concept boost) — so the boost outlives the first * queue rebuild instead of being clobbered by the first auto-replan. * - A consumer view on a failure, boosting the just-failed concept tag. * * Does not itself trigger a replan; the next plan/replan picks it up. */ setSessionHints(hints: ReplanHints | null): void; /** * Read the current session-durable hints (for read-modify-write callers, * e.g. an outcome observer that clamps a compounding boost). */ getSessionHints(): ReplanHints | null; /** * Live state snapshot for the debug overlay (window.skuilder.session * .dbgOverlay()). Reads directly from the private queues and hints, so it * always reflects the current moment — unlike the passive SessionDebugger * snapshots, which only capture what was explicitly pushed to them. */ getDebugSnapshot(): SessionDebugSnapshot; /** * Merge `hints` into the durable session hints via the pipeline's * `mergeHints` (boosts multiply, require/exclude lists concat-dedup). * Convenience over get-then-set for the common additive case. Note the * multiplicative, no-decay semantics — clamp boost factors at the call * site if a repeatedly-emphasised tag could compound unboundedly. */ mergeSessionHints(hints: ReplanHints): void; /** * Merge the durable `_sessionHints` with this run's one-shot hints and * push the result to every source for consumption on the next pipeline * run. Centralised so the initial plan and all replan paths apply session * emphasis identically. No-op when there are no hints of either kind. */ private _applyHintsToSources; /** * Build (once) the stable capability object handed to outcome observers. * Methods are bound to `this`; the object identity is stable across calls * so observers may key off it. */ private _getSessionControls; /** * Notify registered outcome observers about a processed response. * * Only question records are surfaced (non-question dismisses are skipped). * Observers run after ELO/SRS are recorded and before navigation. Each is * awaited but isolated in try/catch — a throwing observer is logged and * skipped, never wedging the session. Keep observers cheap and `void` any * long work (e.g. a triggered replan) to avoid stalling the draw. */ private _notifyOutcomeObservers; /** * Run a replan, bypassing requestReplan()'s coalesce logic. * * Use this when correctness depends on a *fresh* pipeline run, not on * the existence of *some* in-flight replan. Specifically: the * wedge-breaker path in nextCard(), where coalescing into a previous * run that we now know produced insufficient content would re-create * the bug we're trying to prevent. * * Still tracks _replanPromise like requestReplan() does so concurrent * observers (auto-trigger guards in nextCard()) see consistent state. */ private _replanUncoalesced; /** * Normalise the requestReplan argument. Accepts either a ReplanOptions * object (new API) or a plain Record (legacy callers * that passed hints directly). Distinguishes the two by checking for * the presence of ReplanOptions-specific keys. */ private normalizeReplanOptions; /** Minimum well-indicated cards before an additive retry is attempted */ private static readonly MIN_WELL_INDICATED; /** * Score threshold for considering a card "well-indicated." * Cards below this score are treated as fallback filler — present only * because no strategy hard-removed them, but likely penalized by one * or more filters. Strategy-agnostic: the SessionController doesn't * know or care which strategy assigned the score. */ private static readonly WELL_INDICATED_SCORE; /** * supplyQ length at or below which the opportunistic depletion-prefetch * fires. Sets the lead time available for the background replan to land * before the user actually empties the queue and falls into the * (synchronous) wedge-breaker path. * * Set to a small absolute value (not a fraction of batch limit) because * pipeline latency is roughly fixed regardless of batch size — what * matters is "how many cards of user-pace do we have left." 3 cards * × ~3-5s/card = ~10-15s of lead time, comfortably exceeding typical * pipeline latency. */ private static readonly DEPLETION_PREFETCH_THRESHOLD; /** * Internal replan execution. Runs the pipeline, rebuilds the supplyQ, * atomically swaps it in, and triggers hydration for the new contents. * * If the initial replan produces fewer than MIN_WELL_INDICATED cards that * pass all hierarchy filters, one additive retry is attempted — merging * any new high-quality candidates into the front of the queue. */ private _executeReplan; addTime(seconds: number): void; get failedCount(): number; toString(): string; reportString(): string; /** * Returns debug information about the current session state. * Used by SessionControllerDebug component for runtime inspection. */ getDebugInfo(): { api: { mode: string; description: string; }; supplyQueue: { length: number; dequeueCount: number; items: { courseID: any; cardID: any; status: any; score: any; }[]; }; failedQueue: { length: number; dequeueCount: number; items: { courseID: any; cardID: any; status: any; score: any; }[]; }; hydratedCache: { count: number; cardIds: string[]; }; replan: { inProgress: boolean; suppressQualityReplan: boolean; defaultBatchLimit: number; minCardsGuarantee: number; }; }; /** * Fetch weighted content from all sources, mix across sources, and populate * the single supply queue in pipeline rank order. * * Reviews and new cards compete on one cross-comparable scale (SRS 0.5–1.0 * w/ backlog pressure vs ELO 0.0–1.0) — there is no origin split and no * second mixer. The working set is `supplyLimit` cards (the top of the mixed * ranking, plus any `+INF` required cards floated to the front); replans * re-pull and re-rank the whole supply, so a heavy review backlog surfaces as * a refreshed top-ranked working set rather than a frozen 200-card snapshot. * * @param options.replan - If true, this is a mid-session replan rather than * initial session setup. Atomically replaces supplyQ contents and treats * empty results as non-fatal. * @param options.additive - If true (replan only), merge high-quality * candidates into the front of the existing supplyQ instead of replacing it. * @returns Number of "well-indicated" cards (passed all hierarchy filters) * in the new content. Returns -1 if no content was loaded. */ private getWeightedContent; /** * Build a supply item from a weighted candidate. Review-origin cards carry * their `reviewID` so SRS outcome tracking and re-presentation work; new * cards do not. `score` is carried on both for the debug overlay. */ private _buildSupplyItem; /** * Returns items that should be pre-hydrated. * Deterministic: top N items from each queue to ensure coverage. * Failed queue items will typically already be hydrated (from initial render). */ private _getItemsToHydrate; /** * Selects the next item to present to the user. * * The supplyQ is already rank-ordered (the pipeline + mixer did the mixing, * with `+INF` required cards floated to the front), so the primary path is a * deterministic front-to-back draw — no second new-vs-review mixer. The only * remaining decisions are (a) when the session ends and (b) when to interleave * a remediation card from failedQ. See decision doc §2/§3/§7. */ private _selectNextItemToHydrate; /** Supply draws between forced failed-queue interleaves (light steady cadence). */ private static readonly FAILED_INTERLEAVE_EVERY; /** * Slack (seconds) below which the endgame failed-pressure kicks in: when the * time left after clearing remediation drops under this, bias hard to failed * so the session doesn't end with un-cleared remediation. Mirrors the old * `availableTime > 20` ladder thresholds. */ private static readonly FAILED_ENDGAME_SLACK_SECONDS; /** * Whether to interleave a failed (remediation) card now instead of drawing * the supply head. Replaces the old `newBound`/`reviewBound` probability * ladder's failed path (decision doc §7). * * @param supplyAvailable - whether supplyQ has a card to draw instead. */ private _shouldInterleaveFailed; nextCard(action?: SessionAction): Promise | null>; /** * Public API for processing user responses to cards. * @param cardRecord User's response record * @param cardHistory Promise resolving to the card's history * @param courseRegistrationDoc User's course registration document * @param currentCard Current study session record * @param courseId Course identifier * @param cardId Card identifier * @param maxAttemptsPerView Maximum attempts allowed per view * @param maxSessionViews Maximum session views for this card * @param sessionViews Current number of session views * @returns ResponseResult with navigation and UI instructions */ submitResponse(cardRecord: CardRecord, cardHistory: Promise>, courseRegistrationDoc: CourseRegistrationDoc, currentCard: StudySessionRecord, courseId: string, cardId: string, maxAttemptsPerView: number, maxSessionViews: number, sessionViews: number): Promise; private dismissCurrentCard; /** * Remove an item from its source queue after consumption by nextCard(). */ private removeItemFromQueue; /** * Remove a satisfied card ID from the durable session-hint `requireCards` * list. Called when a card is consumed (see removeItemFromQueue). No-op if * the card was not a durable requirement. * * Matches literal IDs only: a glob/pattern requirement (which may stand for * several cards) is NOT considered satisfied by a single draw and is left in * place — durable patterns are the caller's responsibility, one-shot `hints` * remain the right tool for them. */ private _clearDurableRequirement; /** * End the session and record learning outcomes. * * This method aggregates all responses from the session and records a * UserOutcomeRecord if evolutionary orchestration is enabled. */ endSession(): Promise; } /** * A StudyContentSource that filters cards based on tag inclusion/exclusion. * * This enables ephemeral, tag-scoped study sessions where users can focus * on specific topics within a course without permanent configuration. * * Filter logic: * - If `include` is non-empty: card must have at least one of the included tags * - If `exclude` is non-empty: card must not have any of the excluded tags * - Both filters are applied (include first, then exclude) */ declare class TagFilteredContentSource implements StudyContentSource { private courseId; private filter; private user; private resolvedCardIds; constructor(courseId: string, filter: TagFilter, user: UserDBInterface); /** * Resolves the TagFilter to a set of eligible card IDs. * * - Cards in `include` tags are OR'd together (card needs at least one) * - Cards in `exclude` tags are removed from the result */ private resolveFilteredCardIds; /** * Get cards with suitability scores for presentation. * * Filters cards by tag inclusion/exclusion and assigns score=1.0 to all. * TagFilteredContentSource does not currently support pluggable navigation * strategies - it returns flat-scored candidates. * * @param limit - Maximum number of cards to return * @returns Cards sorted by score descending (all scores = 1.0) */ getWeightedCards(limit: number): Promise; /** * Clears the cached resolved card IDs. * Call this if the underlying tag data may have changed during a session. */ clearCache(): void; /** * Returns the course ID this source is filtering. */ getCourseId(): string; /** * Returns the active tag filter. */ getFilter(): TagFilter; } /** * Summary of a single source's contribution to the mix. */ interface SourceSummary { sourceIndex: number; sourceId: string; sourceName?: string; totalCards: number; reviewCount: number; newCount: number; topScore: number; bottomScore: number; scoreRange: [number, number]; avgScore: number; } /** * Per-source selection breakdown. */ interface SourceSelectionBreakdown { sourceId: string; sourceName?: string; reviewsProvided: number; newProvided: number; reviewsSelected: number; newSelected: number; totalSelected: number; selectionRate: number; } /** * Detailed card information in the mixer context. */ interface MixerCardInfo { cardId: string; courseId: string; origin: 'review' | 'new' | 'failed' | 'unknown'; score: number; sourceIndex: number; selected: boolean; rankInSource?: number; rankInMix?: number; } /** * Complete record of a single mixer execution. */ interface MixerRunReport { runId: string; timestamp: Date; mixerType: string; requestedLimit: number; quotaPerSource?: number; sourceSummaries: SourceSummary[]; cards: MixerCardInfo[]; finalCount: number; reviewsSelected: number; newSelected: number; sourceBreakdowns: SourceSelectionBreakdown[]; } /** * Capture a mixer run for later inspection. */ declare function captureMixerRun(mixerType: string, batches: SourceBatch[], sourceIds: string[], sourceNames: (string | undefined)[], requestedLimit: number, quotaPerSource: number | undefined, mixedResult: WeightedCard[]): void; /** * Console API object exposed on window.skuilder.mixer */ declare const mixerDebugAPI: { /** * Get raw run history for programmatic access. */ readonly runs: MixerRunReport[]; /** * Show summary of a specific mixer run. */ showRun(idOrIndex?: string | number): void; /** * Show summary of the last mixer run. */ showLastMix(): void; /** * Explain source balance in the last run. */ explainSourceBalance(): void; /** * Compare score distributions across sources. */ compareScores(): void; /** * Show detailed information for a specific card. */ showCard(cardId: string): void; /** * Show all runs in compact format. */ listRuns(): void; /** * Export run history as JSON for bug reports. */ export(): string; /** * Clear run history. */ clear(): void; /** * Show help. */ help(): void; }; /** * Mount the debug API on window.skuilder.mixer */ declare function mountMixerDebugger(): void; /** * Snapshot of queue state at a given moment. */ interface QueueSnapshot { timestamp: Date; supplyQLength: number; failedQLength: number; supplyQNext3?: string[]; } /** * Record of a single card presentation. */ interface CardPresentation { timestamp: Date; sequenceNumber: number; cardId: string; courseId: string; courseName?: string; origin: 'review' | 'new' | 'failed'; queueSource: 'supplyQ' | 'failedQ'; score?: number; } /** * Complete session execution record. */ interface SessionRunReport { sessionId: string; startTime: Date; endTime?: Date; initialQueues: QueueSnapshot; presentations: CardPresentation[]; queueSnapshots: QueueSnapshot[]; } /** * Start tracking a new session. */ declare function startSessionTracking(supplyQLength: number, failedQLength: number): void; /** * Record a card presentation. */ declare function recordCardPresentation(cardId: string, courseId: string, courseName: string | undefined, origin: 'review' | 'new' | 'failed', queueSource: 'supplyQ' | 'failedQ', score?: number): void; /** * Take a snapshot of current queue state. */ declare function snapshotQueues(supplyQLength: number, failedQLength: number, supplyQNext3?: string[]): void; /** * End the current session tracking. */ declare function endSessionTracking(): void; /** * Console API object exposed on window.skuilder.session */ declare const sessionDebugAPI: { /** * Get raw session history for programmatic access. */ readonly sessions: SessionRunReport[]; /** * Get active session if any. */ readonly active: SessionRunReport | null; /** * Show current queue state. */ showQueue(): void; /** * Toggle the pinned, live-updating DOM overlay for the active controller * (queues, session hints, timer). No-ops in non-browser hosts. */ dbgOverlay(): void; /** * Show presentation history for current or past session. */ showHistory(sessionIndex?: number): void; /** * Analyze course interleaving pattern. */ showInterleaving(sessionIndex?: number): void; /** * List all tracked sessions. */ listSessions(): void; /** * Export session history as JSON for bug reports. */ export(): string; /** * Clear session history. */ clear(): void; /** * Show help. */ help(): void; }; /** * Mount the debug API on window.skuilder.session */ declare function mountSessionDebugger(): void; interface CourseLookupDoc { _id: string; _rev: string; name: string; disambiguator?: string; } /** * A Lookup table of existant courses. Each docID in this DB correspondes to a * course database whose name is `coursedb-{docID}` */ declare class CourseLookup { private static _dbInstance; /** * Static getter for the PouchDB database instance. * Connects using ENV variables and caches the instance. * Throws an error if required ENV variables are not set. */ private static get _db(); /** * Adds a new course to the lookup database, and returns the courseID * @param courseName * @returns */ static add(courseName: string): Promise; /** * Adds a new course to the lookup database with a specific courseID * @param courseId The specific course ID to use * @param courseName The course name * @param disambiguator Optional disambiguator * @returns Promise */ static addWithId(courseId: string, courseName: string, disambiguator?: string): Promise; /** * Removes a course from the index * @param courseID */ static delete(courseID: string): Promise; static allCourseWare(): Promise; static updateDisambiguator(courseID: string, disambiguator?: string): Promise; static isCourse(courseID: string): Promise; } /** * Interface for custom questions data structure returned by allCustomQuestions() */ interface CustomQuestionsData { courses: { name: string; }[]; questionClasses: { name: string; dataShapes?: DataShape[]; views?: { name?: string; }[]; seedData?: unknown[]; }[]; dataShapes: DataShape[]; views: { name?: string; }[]; meta: { questionCount: number; dataShapeCount: number; viewCount: number; courseCount: number; packageName: string; sourceDirectory: string; }; } /** * Interface for processed question data for registration */ interface ProcessedQuestionData { name: string; course: string; questionClass: { name: string; dataShapes?: DataShape[]; views?: { name?: string; }[]; seedData?: unknown[]; }; dataShapes: DataShape[]; views: { name?: string; }[]; } /** * Interface for processed data shape for registration */ interface ProcessedDataShape { name: string; course: string; dataShape: DataShape; } /** * Check if a data shape is already registered in the course config with valid schema */ declare function isDataShapeRegistered(dataShape: ProcessedDataShape, courseConfig: CourseConfig): boolean; declare function isDataShapeSchemaAvailable(dataShape: ProcessedDataShape, courseConfig: CourseConfig): boolean; /** * Check if a question type is already registered in the course config */ declare function isQuestionTypeRegistered(question: ProcessedQuestionData, courseConfig: CourseConfig): boolean; /** * Process custom questions data into registration-ready format */ declare function processCustomQuestionsData(customQuestions: CustomQuestionsData): { dataShapes: ProcessedDataShape[]; questions: ProcessedQuestionData[]; }; /** * Register a data shape in the course config */ declare function registerDataShape(dataShape: ProcessedDataShape, courseConfig: CourseConfig): boolean; /** * Register a question type in the course config */ declare function registerQuestionType(question: ProcessedQuestionData, courseConfig: CourseConfig): boolean; /** * Remove a data shape from the course config * @returns true if the data shape was removed, false if it wasn't found */ declare function removeDataShape(dataShapeName: string, courseConfig: CourseConfig): boolean; /** * Remove a question type from the course config * @returns true if the question type was removed, false if it wasn't found */ declare function removeQuestionType(questionTypeName: string, courseConfig: CourseConfig): boolean; /** * Remove data shapes and question types from course config and persist to database */ declare function removeCustomQuestionTypes(dataShapeNames: string[], questionTypeNames: string[], courseConfig: CourseConfig, courseDB: CourseDBInterface): Promise<{ success: boolean; removedCount: number; errorMessage?: string; }>; /** * Register seed data for a question type * * @param question - The processed question data * @param courseDB - The course database interface * @param username - The username to attribute seed data to */ declare function registerSeedData(question: ProcessedQuestionData, courseDB: CourseDBInterface, username: string): Promise; /** * Register BlanksCard (markdown fillIn) question type specifically */ declare function registerBlanksCard(BlanksCard: { name: string; views?: { name?: string; }[]; }, BlanksCardDataShapes: DataShape[], courseConfig: CourseConfig, courseDB: CourseDBInterface, username?: string): Promise<{ success: boolean; errorMessage?: string; }>; /** * Main function to register all custom question types and data shapes * * @param customQuestions - The custom questions data from allCustomQuestions() * @param courseConfig - The course configuration object * @param courseDB - The course database interface * @param username - The username to attribute seed data to */ declare function registerCustomQuestionTypes(customQuestions: CustomQuestionsData, courseConfig: CourseConfig, courseDB: CourseDBInterface, username?: string): Promise<{ success: boolean; registeredCount: number; errorMessage?: string; }>; declare const NOT_SET: "NOT_SET"; interface DBEnv { COUCHDB_SERVER_URL: string; COUCHDB_SERVER_PROTOCOL: string; COUCHDB_USERNAME?: string; COUCHDB_PASSWORD?: string; LOCAL_STORAGE_PREFIX: string; } declare const ENV: DBEnv; interface DataLayerConfig { type: 'couch' | 'static'; options: { staticContentPath?: string; localStoragePrefix?: string; manifests?: Record; COUCHDB_SERVER_URL?: string; COUCHDB_SERVER_PROTOCOL?: string; COUCHDB_USERNAME?: string; COUCHDB_PASSWORD?: string; COURSE_IDS?: string[]; /** * Per-app tuning for the CouchDB→PouchDB course sync. Only applies when * `type === 'couch'` and the course has `localSync.enabled === true`. * See CourseSyncService.ReplicationOptions for defaults. */ courseSync?: { replication?: { batchSize?: number; batchesLimit?: number; }; }; }; } /** * Initialize the data layer with the specified configuration */ declare function initializeDataLayer(config: DataLayerConfig): Promise; /** * Get the initialized data layer instance * @throws Error if not initialized */ declare function getDataLayer(): DataLayerProvider; /** * Reset the data layer (primarily for testing) */ declare function _resetDataLayer(): Promise; type UserAccountStatus = 'pending_verification' | 'verified' | 'suspended'; interface Entitlement { status: 'trial' | 'paid'; registrationDate: string; purchaseDate: string; expires?: string; } type UserEntitlements = Record; interface CouchDbUserDoc extends PouchDB.Authentication.User { name: string; email: string; status: UserAccountStatus; verificationToken?: string | null; verificationTokenExpiresAt?: string | null; passwordResetToken?: string | null; passwordResetTokenExpiresAt?: string | null; entitlements: UserEntitlements; } export { type AggregatedDocument, type AttachmentUploadResult, CardHistory, type CardPresentation, CardRecord, type CouchDbUserDoc, CourseDBInterface, CourseLookup, CourseRegistrationDoc, type CustomQuestionsData, DEFAULT_MIGRATION_OPTIONS, type DataLayerConfig, DataLayerProvider, type DocumentCounts, ENV, type Entitlement, FileSystemAdapter, GeneratorResult, Loggable, type MigrationOptions, type MigrationResult, type MixerCardInfo, type MixerRunReport, NOT_SET, type OutcomeObserver, type ProcessedDataShape, type ProcessedQuestionData, QuestionRecord, type QueueSnapshot, QuotaRoundRobinMixer, ReplanHints, type ReplanOptions, type ResponseResult, type RestoreProgress, type SessionAction, SessionController, type SessionControls, type SessionOutcome, type SessionRunReport, type SourceBatch, type SourceMixer, type SourceSelectionBreakdown, type SourceSummary, SrsBacklogDebug, StaticCourseManifest, type StaticCourseValidation, StaticToCouchDBMigrator, StudyContentSource, StudySessionItem, type StudySessionRecord, TagFilteredContentSource, type UserAccountStatus, UserDBInterface, type UserEntitlements, type ValidationIssue, type ValidationResult, WeightedCard, _resetDataLayer, captureMixerRun, endSessionTracking, ensureAppDataDirectory, getAppDataDirectory, getDataLayer, getDbPath, initializeDataDirectory, initializeDataLayer, isDataShapeRegistered, isDataShapeSchemaAvailable, isQuestionTypeRegistered, mixerDebugAPI, mountMixerDebugger, mountSessionDebugger, processCustomQuestionsData, recordCardPresentation, registerBlanksCard, registerCustomQuestionTypes, registerDataShape, registerQuestionType, registerSeedData, removeCustomQuestionTypes, removeDataShape, removeQuestionType, sessionDebugAPI, snapshotQueues, startSessionTracking, validateMigration, validateStaticCourse };