@vue-skuilder/db 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/{contentSource-BotbOOfX.d.ts → contentSource-BmnmvH8C.d.ts} +41 -0
  2. package/dist/{contentSource-C90LH-OH.d.cts → contentSource-DfBbaLA-.d.cts} +41 -0
  3. package/dist/core/index.d.cts +94 -4
  4. package/dist/core/index.d.ts +94 -4
  5. package/dist/core/index.js +530 -83
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +528 -83
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-DGKp4zFB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-SBpz9jQf.d.ts → dataLayerProvider-CG9GfaAY.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.cts +2 -2
  12. package/dist/impl/couch/index.d.ts +2 -2
  13. package/dist/impl/couch/index.js +526 -83
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +526 -83
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +2 -2
  18. package/dist/impl/static/index.d.ts +2 -2
  19. package/dist/impl/static/index.js +526 -83
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +526 -83
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index.d.cts +247 -14
  24. package/dist/index.d.ts +247 -14
  25. package/dist/index.js +1419 -140
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +1409 -137
  28. package/dist/index.mjs.map +1 -1
  29. package/docs/navigators-architecture.md +22 -4
  30. package/docs/todo-review-urgency-adaptation.md +205 -0
  31. package/package.json +3 -3
  32. package/src/core/interfaces/userDB.ts +44 -0
  33. package/src/core/navigators/Pipeline.ts +86 -5
  34. package/src/core/navigators/PipelineAssembler.ts +7 -21
  35. package/src/core/navigators/PipelineDebugger.ts +426 -0
  36. package/src/core/navigators/generators/CompositeGenerator.ts +21 -0
  37. package/src/core/navigators/generators/elo.ts +14 -1
  38. package/src/core/navigators/generators/srs.ts +146 -18
  39. package/src/core/navigators/index.ts +9 -0
  40. package/src/impl/couch/user-course-relDB.ts +12 -0
  41. package/src/study/MixerDebugger.ts +555 -0
  42. package/src/study/SessionController.ts +95 -19
  43. package/src/study/SessionDebugger.ts +442 -0
  44. package/src/study/SourceMixer.ts +36 -17
  45. package/src/study/TODO-session-scheduling.md +133 -0
  46. package/src/study/index.ts +2 -0
  47. package/src/study/services/EloService.ts +79 -4
  48. package/src/study/services/ResponseProcessor.ts +130 -72
  49. package/src/study/services/SrsService.ts +9 -0
  50. package/tests/core/navigators/PipelineAssembler.test.ts +4 -4
package/dist/index.d.cts CHANGED
@@ -1,11 +1,11 @@
1
- import { U as UserDBInterface, s as CourseRegistrationDoc, S as StudySessionItem, W as WeightedCard, h as StudyContentSource, C as CourseDBInterface } from './contentSource-C90LH-OH.cjs';
2
- export { J as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, G as ClassroomRegistrationDoc, e as ContentNavigationStrategyData, f as ContentNavigator, q as ContentSourceID, d as CourseInfo, K as CourseRegistration, b as CoursesDBInterface, a7 as DocumentUpdater, a1 as LearnableWeight, N as NavigatorConstructor, Z as NavigatorRole, _ as NavigatorRoles, Y as Navigators, a2 as OrchestrationContext, j as ScheduledCard, H as SessionTrackingData, V as StrategyContribution, i as StudentClassroomDBInterface, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, T as TeacherClassroomDBInterface, I as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, L as UserOutcomeRecord, B as UsrCrsDataInterface, a3 as computeDeviation, a5 as computeEffectiveWeight, a4 as computeSpread, a6 as createOrchestrationContext, X as getCardOrigin, O as getRegisteredNavigator, Q as getRegisteredNavigatorNames, r as getStudySource, P as hasRegisteredNavigator, R as initializeNavigatorRegistry, a0 as isFilter, $ as isGenerator, p as isReview, a8 as newInterval, M as registerNavigator } from './contentSource-C90LH-OH.cjs';
3
- import { D as DataLayerProvider } from './dataLayerProvider-DGKp4zFB.cjs';
1
+ import { U as UserDBInterface, s as CourseRegistrationDoc, S as StudySessionItem, W as WeightedCard, h as StudyContentSource, C as CourseDBInterface } from './contentSource-DfBbaLA-.cjs';
2
+ export { J as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, G as ClassroomRegistrationDoc, e as ContentNavigationStrategyData, f as ContentNavigator, q as ContentSourceID, d as CourseInfo, K as CourseRegistration, b as CoursesDBInterface, a7 as DocumentUpdater, a1 as LearnableWeight, N as NavigatorConstructor, Z as NavigatorRole, _ as NavigatorRoles, Y as Navigators, a2 as OrchestrationContext, j as ScheduledCard, H as SessionTrackingData, V as StrategyContribution, i as StudentClassroomDBInterface, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, T as TeacherClassroomDBInterface, I as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, L as UserOutcomeRecord, B as UsrCrsDataInterface, a3 as computeDeviation, a5 as computeEffectiveWeight, a4 as computeSpread, a6 as createOrchestrationContext, X as getCardOrigin, O as getRegisteredNavigator, Q as getRegisteredNavigatorNames, r as getStudySource, P as hasRegisteredNavigator, R as initializeNavigatorRegistry, a0 as isFilter, $ as isGenerator, p as isReview, a8 as newInterval, M as registerNavigator } from './contentSource-DfBbaLA-.cjs';
3
+ import { D as DataLayerProvider } from './dataLayerProvider-BeRXVMs5.cjs';
4
4
  import { C as CardHistory, c as CardRecord } from './types-legacy-JXDxinpU.cjs';
5
5
  export { d as CardData, e as CourseListData, g as DataShapeData, f as DisplayableData, D as DocType, b as DocTypePrefixes, F as Field, G as GuestUsername, Q as QualifiedCardID, h as QuestionData, i as QuestionRecord, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from './types-legacy-JXDxinpU.cjs';
6
6
  import { Loggable } from './core/index.cjs';
7
- export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, CardGenerator, CardGeneratorFactory, FilterContext, GeneratorContext, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, parseCardHistoryID, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, validateProcessorConfig } from './core/index.cjs';
8
- import { TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common';
7
+ export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, CardGenerator, CardGeneratorFactory, FilterContext, FilterImpact, GeneratorContext, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, validateProcessorConfig } from './core/index.cjs';
8
+ import { TaggedPerformance, TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common';
9
9
  import { S as StaticCourseManifest } from './types-W8n-B6HG.cjs';
10
10
  export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig } from './types-W8n-B6HG.cjs';
11
11
  import { F as FileSystemAdapter } from './index-Ba7hYbHj.cjs';
@@ -29,6 +29,17 @@ declare class EloService {
29
29
  * @param k Optional K-factor for ELO calculation
30
30
  */
31
31
  updateUserAndCardElo(userScore: number, course_id: string, card_id: string, userCourseRegDoc: CourseRegistrationDoc, currentCard: StudySessionRecord, k?: number): Promise<void>;
32
+ /**
33
+ * Updates both user and card ELO ratings with per-tag granularity.
34
+ * Tags in taggedPerformance but not on card will be created dynamically.
35
+ *
36
+ * @param taggedPerformance Performance object with _global and per-tag scores
37
+ * @param course_id Course identifier
38
+ * @param card_id Card identifier
39
+ * @param userCourseRegDoc User's course registration document (will be mutated)
40
+ * @param currentCard Current card session record
41
+ */
42
+ updateUserAndCardEloPerTag(taggedPerformance: TaggedPerformance, course_id: string, card_id: string, userCourseRegDoc: CourseRegistrationDoc, currentCard: StudySessionRecord): Promise<void>;
32
43
  }
33
44
 
34
45
  /**
@@ -37,6 +48,11 @@ declare class EloService {
37
48
  declare class SrsService {
38
49
  private user;
39
50
  constructor(user: UserDBInterface);
51
+ /**
52
+ * Remove a scheduled review from the user's database.
53
+ * Used to clean up orphaned reviews (e.g., card deleted from course DB).
54
+ */
55
+ removeReview(reviewID: string): void;
40
56
  /**
41
57
  * Calculates the next review time for a card based on its history and
42
58
  * schedules it in the user's database.
@@ -54,6 +70,13 @@ declare class ResponseProcessor {
54
70
  private srsService;
55
71
  private eloService;
56
72
  constructor(srsService: SrsService, eloService: EloService);
73
+ /**
74
+ * Parses performance data into global score and optional per-tag scores.
75
+ *
76
+ * @param performance - Numeric or structured performance from QuestionRecord
77
+ * @returns Parsed performance with global score and optional tag scores
78
+ */
79
+ private parsePerformance;
57
80
  /**
58
81
  * Processes a user's response to a card, handling SRS scheduling and ELO updates.
59
82
  * @param cardRecord User's response record
@@ -269,16 +292,16 @@ interface SourceMixer {
269
292
  mix(batches: SourceBatch[], limit: number): WeightedCard[];
270
293
  }
271
294
  /**
272
- * Simple quota-based mixer: allocates equal representation to each source,
273
- * taking the top-N cards by score from each.
295
+ * Quota-based mixer with interleaved output.
274
296
  *
275
- * Guarantees balanced representation across sources regardless of absolute
276
- * score differences. A low-scoring source gets the same quota as a high-scoring
277
- * source.
297
+ * Allocates equal representation to each source (top-N by score), then
298
+ * interleaves the results by dealing from a randomly-shuffled source order.
299
+ * Within each source, cards are dealt in score-descending order.
278
300
  *
279
- * This is the KISS approach - simple, predictable, and fair in terms of
280
- * source representation (though not necessarily optimal in terms of absolute
281
- * card quality).
301
+ * This ensures that cards from different courses are spread throughout the
302
+ * queue rather than clustered by score bands, which matters because
303
+ * SessionController consumes queues front-to-back and sessions often end
304
+ * before reaching the tail.
282
305
  */
283
306
  declare class QuotaRoundRobinMixer implements SourceMixer {
284
307
  mix(batches: SourceBatch[], limit: number): WeightedCard[];
@@ -312,6 +335,8 @@ declare class SessionController<TView = unknown> extends Loggable {
312
335
  private eloService;
313
336
  private hydrationService;
314
337
  private mixer;
338
+ private dataLayer;
339
+ private courseNameCache;
315
340
  private sources;
316
341
  private _sessionRecord;
317
342
  set sessionRecord(r: StudySessionRecord[]);
@@ -494,6 +519,214 @@ declare class TagFilteredContentSource implements StudyContentSource {
494
519
  getFilter(): TagFilter;
495
520
  }
496
521
 
522
+ /**
523
+ * Summary of a single source's contribution to the mix.
524
+ */
525
+ interface SourceSummary {
526
+ sourceIndex: number;
527
+ sourceId: string;
528
+ sourceName?: string;
529
+ totalCards: number;
530
+ reviewCount: number;
531
+ newCount: number;
532
+ topScore: number;
533
+ bottomScore: number;
534
+ scoreRange: [number, number];
535
+ avgScore: number;
536
+ }
537
+ /**
538
+ * Per-source selection breakdown.
539
+ */
540
+ interface SourceSelectionBreakdown {
541
+ sourceId: string;
542
+ sourceName?: string;
543
+ reviewsProvided: number;
544
+ newProvided: number;
545
+ reviewsSelected: number;
546
+ newSelected: number;
547
+ totalSelected: number;
548
+ selectionRate: number;
549
+ }
550
+ /**
551
+ * Detailed card information in the mixer context.
552
+ */
553
+ interface MixerCardInfo {
554
+ cardId: string;
555
+ courseId: string;
556
+ origin: 'review' | 'new' | 'failed' | 'unknown';
557
+ score: number;
558
+ sourceIndex: number;
559
+ selected: boolean;
560
+ rankInSource?: number;
561
+ rankInMix?: number;
562
+ }
563
+ /**
564
+ * Complete record of a single mixer execution.
565
+ */
566
+ interface MixerRunReport {
567
+ runId: string;
568
+ timestamp: Date;
569
+ mixerType: string;
570
+ requestedLimit: number;
571
+ quotaPerSource?: number;
572
+ sourceSummaries: SourceSummary[];
573
+ cards: MixerCardInfo[];
574
+ finalCount: number;
575
+ reviewsSelected: number;
576
+ newSelected: number;
577
+ sourceBreakdowns: SourceSelectionBreakdown[];
578
+ }
579
+ /**
580
+ * Capture a mixer run for later inspection.
581
+ */
582
+ declare function captureMixerRun(mixerType: string, batches: SourceBatch[], sourceIds: string[], sourceNames: (string | undefined)[], requestedLimit: number, quotaPerSource: number | undefined, mixedResult: WeightedCard[]): void;
583
+ /**
584
+ * Console API object exposed on window.skuilder.mixer
585
+ */
586
+ declare const mixerDebugAPI: {
587
+ /**
588
+ * Get raw run history for programmatic access.
589
+ */
590
+ readonly runs: MixerRunReport[];
591
+ /**
592
+ * Show summary of a specific mixer run.
593
+ */
594
+ showRun(idOrIndex?: string | number): void;
595
+ /**
596
+ * Show summary of the last mixer run.
597
+ */
598
+ showLastMix(): void;
599
+ /**
600
+ * Explain source balance in the last run.
601
+ */
602
+ explainSourceBalance(): void;
603
+ /**
604
+ * Compare score distributions across sources.
605
+ */
606
+ compareScores(): void;
607
+ /**
608
+ * Show detailed information for a specific card.
609
+ */
610
+ showCard(cardId: string): void;
611
+ /**
612
+ * Show all runs in compact format.
613
+ */
614
+ listRuns(): void;
615
+ /**
616
+ * Export run history as JSON for bug reports.
617
+ */
618
+ export(): string;
619
+ /**
620
+ * Clear run history.
621
+ */
622
+ clear(): void;
623
+ /**
624
+ * Show help.
625
+ */
626
+ help(): void;
627
+ };
628
+ /**
629
+ * Mount the debug API on window.skuilder.mixer
630
+ */
631
+ declare function mountMixerDebugger(): void;
632
+
633
+ /**
634
+ * Snapshot of queue state at a given moment.
635
+ */
636
+ interface QueueSnapshot {
637
+ timestamp: Date;
638
+ reviewQLength: number;
639
+ newQLength: number;
640
+ failedQLength: number;
641
+ reviewQNext3?: string[];
642
+ newQNext3?: string[];
643
+ }
644
+ /**
645
+ * Record of a single card presentation.
646
+ */
647
+ interface CardPresentation {
648
+ timestamp: Date;
649
+ sequenceNumber: number;
650
+ cardId: string;
651
+ courseId: string;
652
+ courseName?: string;
653
+ origin: 'review' | 'new' | 'failed';
654
+ queueSource: 'reviewQ' | 'newQ' | 'failedQ';
655
+ score?: number;
656
+ }
657
+ /**
658
+ * Complete session execution record.
659
+ */
660
+ interface SessionRunReport {
661
+ sessionId: string;
662
+ startTime: Date;
663
+ endTime?: Date;
664
+ initialQueues: QueueSnapshot;
665
+ presentations: CardPresentation[];
666
+ queueSnapshots: QueueSnapshot[];
667
+ }
668
+ /**
669
+ * Start tracking a new session.
670
+ */
671
+ declare function startSessionTracking(reviewQLength: number, newQLength: number, failedQLength: number): void;
672
+ /**
673
+ * Record a card presentation.
674
+ */
675
+ declare function recordCardPresentation(cardId: string, courseId: string, courseName: string | undefined, origin: 'review' | 'new' | 'failed', queueSource: 'reviewQ' | 'newQ' | 'failedQ', score?: number): void;
676
+ /**
677
+ * Take a snapshot of current queue state.
678
+ */
679
+ declare function snapshotQueues(reviewQLength: number, newQLength: number, failedQLength: number, reviewQNext3?: string[], newQNext3?: string[]): void;
680
+ /**
681
+ * End the current session tracking.
682
+ */
683
+ declare function endSessionTracking(): void;
684
+ /**
685
+ * Console API object exposed on window.skuilder.session
686
+ */
687
+ declare const sessionDebugAPI: {
688
+ /**
689
+ * Get raw session history for programmatic access.
690
+ */
691
+ readonly sessions: SessionRunReport[];
692
+ /**
693
+ * Get active session if any.
694
+ */
695
+ readonly active: SessionRunReport | null;
696
+ /**
697
+ * Show current queue state.
698
+ */
699
+ showQueue(): void;
700
+ /**
701
+ * Show presentation history for current or past session.
702
+ */
703
+ showHistory(sessionIndex?: number): void;
704
+ /**
705
+ * Analyze course interleaving pattern.
706
+ */
707
+ showInterleaving(sessionIndex?: number): void;
708
+ /**
709
+ * List all tracked sessions.
710
+ */
711
+ listSessions(): void;
712
+ /**
713
+ * Export session history as JSON for bug reports.
714
+ */
715
+ export(): string;
716
+ /**
717
+ * Clear session history.
718
+ */
719
+ clear(): void;
720
+ /**
721
+ * Show help.
722
+ */
723
+ help(): void;
724
+ };
725
+ /**
726
+ * Mount the debug API on window.skuilder.session
727
+ */
728
+ declare function mountSessionDebugger(): void;
729
+
497
730
  interface CourseLookupDoc {
498
731
  _id: string;
499
732
  _rev: string;
@@ -723,4 +956,4 @@ interface CouchDbUserDoc extends PouchDB.Authentication.User {
723
956
  entitlements: UserEntitlements;
724
957
  }
725
958
 
726
- export { type AggregatedDocument, type AttachmentUploadResult, CardHistory, CardRecord, type CouchDbUserDoc, CourseDBInterface, CourseLookup, CourseRegistrationDoc, type CustomQuestionsData, DEFAULT_MIGRATION_OPTIONS, type DataLayerConfig, DataLayerProvider, type DocumentCounts, ENV, type Entitlement, FileSystemAdapter, Loggable, type MigrationOptions, type MigrationResult, NOT_SET, type ProcessedDataShape, type ProcessedQuestionData, QuotaRoundRobinMixer, type ResponseResult, type RestoreProgress, type SessionAction, SessionController, type SourceBatch, type SourceMixer, StaticCourseManifest, type StaticCourseValidation, StaticToCouchDBMigrator, StudyContentSource, StudySessionItem, type StudySessionRecord, TagFilteredContentSource, type UserAccountStatus, UserDBInterface, type UserEntitlements, type ValidationIssue, type ValidationResult, WeightedCard, _resetDataLayer, ensureAppDataDirectory, getAppDataDirectory, getDataLayer, getDbPath, initializeDataDirectory, initializeDataLayer, isDataShapeRegistered, isDataShapeSchemaAvailable, isQuestionTypeRegistered, processCustomQuestionsData, registerBlanksCard, registerCustomQuestionTypes, registerDataShape, registerQuestionType, registerSeedData, removeCustomQuestionTypes, removeDataShape, removeQuestionType, validateMigration, validateStaticCourse };
959
+ 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, Loggable, type MigrationOptions, type MigrationResult, type MixerCardInfo, type MixerRunReport, NOT_SET, type ProcessedDataShape, type ProcessedQuestionData, type QueueSnapshot, QuotaRoundRobinMixer, type ResponseResult, type RestoreProgress, type SessionAction, SessionController, type SessionRunReport, type SourceBatch, type SourceMixer, type SourceSelectionBreakdown, type SourceSummary, 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 };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { U as UserDBInterface, s as CourseRegistrationDoc, S as StudySessionItem, W as WeightedCard, h as StudyContentSource, C as CourseDBInterface } from './contentSource-BotbOOfX.js';
2
- export { J as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, G as ClassroomRegistrationDoc, e as ContentNavigationStrategyData, f as ContentNavigator, q as ContentSourceID, d as CourseInfo, K as CourseRegistration, b as CoursesDBInterface, a7 as DocumentUpdater, a1 as LearnableWeight, N as NavigatorConstructor, Z as NavigatorRole, _ as NavigatorRoles, Y as Navigators, a2 as OrchestrationContext, j as ScheduledCard, H as SessionTrackingData, V as StrategyContribution, i as StudentClassroomDBInterface, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, T as TeacherClassroomDBInterface, I as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, L as UserOutcomeRecord, B as UsrCrsDataInterface, a3 as computeDeviation, a5 as computeEffectiveWeight, a4 as computeSpread, a6 as createOrchestrationContext, X as getCardOrigin, O as getRegisteredNavigator, Q as getRegisteredNavigatorNames, r as getStudySource, P as hasRegisteredNavigator, R as initializeNavigatorRegistry, a0 as isFilter, $ as isGenerator, p as isReview, a8 as newInterval, M as registerNavigator } from './contentSource-BotbOOfX.js';
3
- import { D as DataLayerProvider } from './dataLayerProvider-SBpz9jQf.js';
1
+ import { U as UserDBInterface, s as CourseRegistrationDoc, S as StudySessionItem, W as WeightedCard, h as StudyContentSource, C as CourseDBInterface } from './contentSource-BmnmvH8C.js';
2
+ export { J as ActivityRecord, A as AdminDBInterface, v as AssignedCard, g as AssignedContent, u as AssignedCourse, t as AssignedTag, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, G as ClassroomRegistrationDoc, e as ContentNavigationStrategyData, f as ContentNavigator, q as ContentSourceID, d as CourseInfo, K as CourseRegistration, b as CoursesDBInterface, a7 as DocumentUpdater, a1 as LearnableWeight, N as NavigatorConstructor, Z as NavigatorRole, _ as NavigatorRoles, Y as Navigators, a2 as OrchestrationContext, j as ScheduledCard, H as SessionTrackingData, V as StrategyContribution, i as StudentClassroomDBInterface, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, T as TeacherClassroomDBInterface, I as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, L as UserOutcomeRecord, B as UsrCrsDataInterface, a3 as computeDeviation, a5 as computeEffectiveWeight, a4 as computeSpread, a6 as createOrchestrationContext, X as getCardOrigin, O as getRegisteredNavigator, Q as getRegisteredNavigatorNames, r as getStudySource, P as hasRegisteredNavigator, R as initializeNavigatorRegistry, a0 as isFilter, $ as isGenerator, p as isReview, a8 as newInterval, M as registerNavigator } from './contentSource-BmnmvH8C.js';
3
+ import { D as DataLayerProvider } from './dataLayerProvider-CG9GfaAY.js';
4
4
  import { C as CardHistory, c as CardRecord } from './types-legacy-JXDxinpU.js';
5
5
  export { d as CardData, e as CourseListData, g as DataShapeData, f as DisplayableData, D as DocType, b as DocTypePrefixes, F as Field, G as GuestUsername, Q as QualifiedCardID, h as QuestionData, i as QuestionRecord, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from './types-legacy-JXDxinpU.js';
6
6
  import { Loggable } from './core/index.js';
7
- export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, CardGenerator, CardGeneratorFactory, FilterContext, GeneratorContext, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, parseCardHistoryID, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, validateProcessorConfig } from './core/index.js';
8
- import { TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common';
7
+ export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, CardGenerator, CardGeneratorFactory, FilterContext, FilterImpact, GeneratorContext, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, validateProcessorConfig } from './core/index.js';
8
+ import { TaggedPerformance, TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common';
9
9
  import { S as StaticCourseManifest } from './types-CJrLM1Ew.js';
10
10
  export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig } from './types-CJrLM1Ew.js';
11
11
  import { F as FileSystemAdapter } from './index-BWvO-_rJ.js';
@@ -29,6 +29,17 @@ declare class EloService {
29
29
  * @param k Optional K-factor for ELO calculation
30
30
  */
31
31
  updateUserAndCardElo(userScore: number, course_id: string, card_id: string, userCourseRegDoc: CourseRegistrationDoc, currentCard: StudySessionRecord, k?: number): Promise<void>;
32
+ /**
33
+ * Updates both user and card ELO ratings with per-tag granularity.
34
+ * Tags in taggedPerformance but not on card will be created dynamically.
35
+ *
36
+ * @param taggedPerformance Performance object with _global and per-tag scores
37
+ * @param course_id Course identifier
38
+ * @param card_id Card identifier
39
+ * @param userCourseRegDoc User's course registration document (will be mutated)
40
+ * @param currentCard Current card session record
41
+ */
42
+ updateUserAndCardEloPerTag(taggedPerformance: TaggedPerformance, course_id: string, card_id: string, userCourseRegDoc: CourseRegistrationDoc, currentCard: StudySessionRecord): Promise<void>;
32
43
  }
33
44
 
34
45
  /**
@@ -37,6 +48,11 @@ declare class EloService {
37
48
  declare class SrsService {
38
49
  private user;
39
50
  constructor(user: UserDBInterface);
51
+ /**
52
+ * Remove a scheduled review from the user's database.
53
+ * Used to clean up orphaned reviews (e.g., card deleted from course DB).
54
+ */
55
+ removeReview(reviewID: string): void;
40
56
  /**
41
57
  * Calculates the next review time for a card based on its history and
42
58
  * schedules it in the user's database.
@@ -54,6 +70,13 @@ declare class ResponseProcessor {
54
70
  private srsService;
55
71
  private eloService;
56
72
  constructor(srsService: SrsService, eloService: EloService);
73
+ /**
74
+ * Parses performance data into global score and optional per-tag scores.
75
+ *
76
+ * @param performance - Numeric or structured performance from QuestionRecord
77
+ * @returns Parsed performance with global score and optional tag scores
78
+ */
79
+ private parsePerformance;
57
80
  /**
58
81
  * Processes a user's response to a card, handling SRS scheduling and ELO updates.
59
82
  * @param cardRecord User's response record
@@ -269,16 +292,16 @@ interface SourceMixer {
269
292
  mix(batches: SourceBatch[], limit: number): WeightedCard[];
270
293
  }
271
294
  /**
272
- * Simple quota-based mixer: allocates equal representation to each source,
273
- * taking the top-N cards by score from each.
295
+ * Quota-based mixer with interleaved output.
274
296
  *
275
- * Guarantees balanced representation across sources regardless of absolute
276
- * score differences. A low-scoring source gets the same quota as a high-scoring
277
- * source.
297
+ * Allocates equal representation to each source (top-N by score), then
298
+ * interleaves the results by dealing from a randomly-shuffled source order.
299
+ * Within each source, cards are dealt in score-descending order.
278
300
  *
279
- * This is the KISS approach - simple, predictable, and fair in terms of
280
- * source representation (though not necessarily optimal in terms of absolute
281
- * card quality).
301
+ * This ensures that cards from different courses are spread throughout the
302
+ * queue rather than clustered by score bands, which matters because
303
+ * SessionController consumes queues front-to-back and sessions often end
304
+ * before reaching the tail.
282
305
  */
283
306
  declare class QuotaRoundRobinMixer implements SourceMixer {
284
307
  mix(batches: SourceBatch[], limit: number): WeightedCard[];
@@ -312,6 +335,8 @@ declare class SessionController<TView = unknown> extends Loggable {
312
335
  private eloService;
313
336
  private hydrationService;
314
337
  private mixer;
338
+ private dataLayer;
339
+ private courseNameCache;
315
340
  private sources;
316
341
  private _sessionRecord;
317
342
  set sessionRecord(r: StudySessionRecord[]);
@@ -494,6 +519,214 @@ declare class TagFilteredContentSource implements StudyContentSource {
494
519
  getFilter(): TagFilter;
495
520
  }
496
521
 
522
+ /**
523
+ * Summary of a single source's contribution to the mix.
524
+ */
525
+ interface SourceSummary {
526
+ sourceIndex: number;
527
+ sourceId: string;
528
+ sourceName?: string;
529
+ totalCards: number;
530
+ reviewCount: number;
531
+ newCount: number;
532
+ topScore: number;
533
+ bottomScore: number;
534
+ scoreRange: [number, number];
535
+ avgScore: number;
536
+ }
537
+ /**
538
+ * Per-source selection breakdown.
539
+ */
540
+ interface SourceSelectionBreakdown {
541
+ sourceId: string;
542
+ sourceName?: string;
543
+ reviewsProvided: number;
544
+ newProvided: number;
545
+ reviewsSelected: number;
546
+ newSelected: number;
547
+ totalSelected: number;
548
+ selectionRate: number;
549
+ }
550
+ /**
551
+ * Detailed card information in the mixer context.
552
+ */
553
+ interface MixerCardInfo {
554
+ cardId: string;
555
+ courseId: string;
556
+ origin: 'review' | 'new' | 'failed' | 'unknown';
557
+ score: number;
558
+ sourceIndex: number;
559
+ selected: boolean;
560
+ rankInSource?: number;
561
+ rankInMix?: number;
562
+ }
563
+ /**
564
+ * Complete record of a single mixer execution.
565
+ */
566
+ interface MixerRunReport {
567
+ runId: string;
568
+ timestamp: Date;
569
+ mixerType: string;
570
+ requestedLimit: number;
571
+ quotaPerSource?: number;
572
+ sourceSummaries: SourceSummary[];
573
+ cards: MixerCardInfo[];
574
+ finalCount: number;
575
+ reviewsSelected: number;
576
+ newSelected: number;
577
+ sourceBreakdowns: SourceSelectionBreakdown[];
578
+ }
579
+ /**
580
+ * Capture a mixer run for later inspection.
581
+ */
582
+ declare function captureMixerRun(mixerType: string, batches: SourceBatch[], sourceIds: string[], sourceNames: (string | undefined)[], requestedLimit: number, quotaPerSource: number | undefined, mixedResult: WeightedCard[]): void;
583
+ /**
584
+ * Console API object exposed on window.skuilder.mixer
585
+ */
586
+ declare const mixerDebugAPI: {
587
+ /**
588
+ * Get raw run history for programmatic access.
589
+ */
590
+ readonly runs: MixerRunReport[];
591
+ /**
592
+ * Show summary of a specific mixer run.
593
+ */
594
+ showRun(idOrIndex?: string | number): void;
595
+ /**
596
+ * Show summary of the last mixer run.
597
+ */
598
+ showLastMix(): void;
599
+ /**
600
+ * Explain source balance in the last run.
601
+ */
602
+ explainSourceBalance(): void;
603
+ /**
604
+ * Compare score distributions across sources.
605
+ */
606
+ compareScores(): void;
607
+ /**
608
+ * Show detailed information for a specific card.
609
+ */
610
+ showCard(cardId: string): void;
611
+ /**
612
+ * Show all runs in compact format.
613
+ */
614
+ listRuns(): void;
615
+ /**
616
+ * Export run history as JSON for bug reports.
617
+ */
618
+ export(): string;
619
+ /**
620
+ * Clear run history.
621
+ */
622
+ clear(): void;
623
+ /**
624
+ * Show help.
625
+ */
626
+ help(): void;
627
+ };
628
+ /**
629
+ * Mount the debug API on window.skuilder.mixer
630
+ */
631
+ declare function mountMixerDebugger(): void;
632
+
633
+ /**
634
+ * Snapshot of queue state at a given moment.
635
+ */
636
+ interface QueueSnapshot {
637
+ timestamp: Date;
638
+ reviewQLength: number;
639
+ newQLength: number;
640
+ failedQLength: number;
641
+ reviewQNext3?: string[];
642
+ newQNext3?: string[];
643
+ }
644
+ /**
645
+ * Record of a single card presentation.
646
+ */
647
+ interface CardPresentation {
648
+ timestamp: Date;
649
+ sequenceNumber: number;
650
+ cardId: string;
651
+ courseId: string;
652
+ courseName?: string;
653
+ origin: 'review' | 'new' | 'failed';
654
+ queueSource: 'reviewQ' | 'newQ' | 'failedQ';
655
+ score?: number;
656
+ }
657
+ /**
658
+ * Complete session execution record.
659
+ */
660
+ interface SessionRunReport {
661
+ sessionId: string;
662
+ startTime: Date;
663
+ endTime?: Date;
664
+ initialQueues: QueueSnapshot;
665
+ presentations: CardPresentation[];
666
+ queueSnapshots: QueueSnapshot[];
667
+ }
668
+ /**
669
+ * Start tracking a new session.
670
+ */
671
+ declare function startSessionTracking(reviewQLength: number, newQLength: number, failedQLength: number): void;
672
+ /**
673
+ * Record a card presentation.
674
+ */
675
+ declare function recordCardPresentation(cardId: string, courseId: string, courseName: string | undefined, origin: 'review' | 'new' | 'failed', queueSource: 'reviewQ' | 'newQ' | 'failedQ', score?: number): void;
676
+ /**
677
+ * Take a snapshot of current queue state.
678
+ */
679
+ declare function snapshotQueues(reviewQLength: number, newQLength: number, failedQLength: number, reviewQNext3?: string[], newQNext3?: string[]): void;
680
+ /**
681
+ * End the current session tracking.
682
+ */
683
+ declare function endSessionTracking(): void;
684
+ /**
685
+ * Console API object exposed on window.skuilder.session
686
+ */
687
+ declare const sessionDebugAPI: {
688
+ /**
689
+ * Get raw session history for programmatic access.
690
+ */
691
+ readonly sessions: SessionRunReport[];
692
+ /**
693
+ * Get active session if any.
694
+ */
695
+ readonly active: SessionRunReport | null;
696
+ /**
697
+ * Show current queue state.
698
+ */
699
+ showQueue(): void;
700
+ /**
701
+ * Show presentation history for current or past session.
702
+ */
703
+ showHistory(sessionIndex?: number): void;
704
+ /**
705
+ * Analyze course interleaving pattern.
706
+ */
707
+ showInterleaving(sessionIndex?: number): void;
708
+ /**
709
+ * List all tracked sessions.
710
+ */
711
+ listSessions(): void;
712
+ /**
713
+ * Export session history as JSON for bug reports.
714
+ */
715
+ export(): string;
716
+ /**
717
+ * Clear session history.
718
+ */
719
+ clear(): void;
720
+ /**
721
+ * Show help.
722
+ */
723
+ help(): void;
724
+ };
725
+ /**
726
+ * Mount the debug API on window.skuilder.session
727
+ */
728
+ declare function mountSessionDebugger(): void;
729
+
497
730
  interface CourseLookupDoc {
498
731
  _id: string;
499
732
  _rev: string;
@@ -723,4 +956,4 @@ interface CouchDbUserDoc extends PouchDB.Authentication.User {
723
956
  entitlements: UserEntitlements;
724
957
  }
725
958
 
726
- export { type AggregatedDocument, type AttachmentUploadResult, CardHistory, CardRecord, type CouchDbUserDoc, CourseDBInterface, CourseLookup, CourseRegistrationDoc, type CustomQuestionsData, DEFAULT_MIGRATION_OPTIONS, type DataLayerConfig, DataLayerProvider, type DocumentCounts, ENV, type Entitlement, FileSystemAdapter, Loggable, type MigrationOptions, type MigrationResult, NOT_SET, type ProcessedDataShape, type ProcessedQuestionData, QuotaRoundRobinMixer, type ResponseResult, type RestoreProgress, type SessionAction, SessionController, type SourceBatch, type SourceMixer, StaticCourseManifest, type StaticCourseValidation, StaticToCouchDBMigrator, StudyContentSource, StudySessionItem, type StudySessionRecord, TagFilteredContentSource, type UserAccountStatus, UserDBInterface, type UserEntitlements, type ValidationIssue, type ValidationResult, WeightedCard, _resetDataLayer, ensureAppDataDirectory, getAppDataDirectory, getDataLayer, getDbPath, initializeDataDirectory, initializeDataLayer, isDataShapeRegistered, isDataShapeSchemaAvailable, isQuestionTypeRegistered, processCustomQuestionsData, registerBlanksCard, registerCustomQuestionTypes, registerDataShape, registerQuestionType, registerSeedData, removeCustomQuestionTypes, removeDataShape, removeQuestionType, validateMigration, validateStaticCourse };
959
+ 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, Loggable, type MigrationOptions, type MigrationResult, type MixerCardInfo, type MixerRunReport, NOT_SET, type ProcessedDataShape, type ProcessedQuestionData, type QueueSnapshot, QuotaRoundRobinMixer, type ResponseResult, type RestoreProgress, type SessionAction, SessionController, type SessionRunReport, type SourceBatch, type SourceMixer, type SourceSelectionBreakdown, type SourceSummary, 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 };