@vue-skuilder/db 0.2.7 → 0.2.9
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.
- package/dist/{contentSource-Cplhv3bJ.d.ts → contentSource-C-0t0y0V.d.ts} +7 -0
- package/dist/{contentSource-kI9_jwTu.d.cts → contentSource-jSkcOt2s.d.cts} +7 -0
- package/dist/core/index.d.cts +67 -4
- package/dist/core/index.d.ts +67 -4
- package/dist/core/index.js +201 -39
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +198 -39
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DrBqOUa3.d.ts → dataLayerProvider-BB0oi9T0.d.ts} +1 -1
- package/dist/{dataLayerProvider-CiA2Rr0v.d.cts → dataLayerProvider-BDClIrFC.d.cts} +1 -1
- package/dist/impl/couch/index.d.cts +2 -2
- package/dist/impl/couch/index.d.ts +2 -2
- package/dist/impl/couch/index.js +195 -39
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +195 -39
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +2 -2
- package/dist/impl/static/index.d.ts +2 -2
- package/dist/impl/static/index.js +195 -39
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +195 -39
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +115 -81
- package/dist/index.d.ts +115 -81
- package/dist/index.js +440 -251
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +437 -251
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +29 -13
- package/package.json +3 -3
- package/src/core/interfaces/contentSource.ts +7 -0
- package/src/core/navigators/Pipeline.ts +93 -1
- package/src/core/navigators/PipelineDebugger.ts +11 -1
- package/src/core/navigators/SrsDebugger.ts +53 -0
- package/src/core/navigators/generators/prescribed.ts +76 -9
- package/src/core/navigators/generators/srs.ts +81 -37
- package/src/core/navigators/index.ts +9 -0
- package/src/study/SessionController.ts +260 -249
- package/src/study/SessionDebugger.ts +15 -25
- package/src/study/SessionOverlay.ts +108 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-
|
|
1
|
+
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-C-0t0y0V.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Main factory interface for data access
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-
|
|
1
|
+
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-jSkcOt2s.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Main factory interface for data access
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-4tlwHnXo.cjs';
|
|
2
2
|
import { Moment } from 'moment';
|
|
3
|
-
import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-
|
|
4
|
-
export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-
|
|
3
|
+
import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-jSkcOt2s.cjs';
|
|
4
|
+
export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-jSkcOt2s.cjs';
|
|
5
5
|
import * as _vue_skuilder_common from '@vue-skuilder/common';
|
|
6
6
|
import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
|
|
7
7
|
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.cjs';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-4tlwHnXo.js';
|
|
2
2
|
import { Moment } from 'moment';
|
|
3
|
-
import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-
|
|
4
|
-
export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-
|
|
3
|
+
import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-C-0t0y0V.js';
|
|
4
|
+
export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-C-0t0y0V.js';
|
|
5
5
|
import * as _vue_skuilder_common from '@vue-skuilder/common';
|
|
6
6
|
import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
|
|
7
7
|
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.js';
|
package/dist/impl/couch/index.js
CHANGED
|
@@ -738,6 +738,7 @@ __export(PipelineDebugger_exports, {
|
|
|
738
738
|
buildRunReport: () => buildRunReport,
|
|
739
739
|
captureRun: () => captureRun,
|
|
740
740
|
clearRunHistory: () => clearRunHistory,
|
|
741
|
+
getActivePipeline: () => getActivePipeline,
|
|
741
742
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
742
743
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
743
744
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -745,6 +746,9 @@ __export(PipelineDebugger_exports, {
|
|
|
745
746
|
function registerPipelineForDebug(pipeline) {
|
|
746
747
|
_activePipeline = pipeline;
|
|
747
748
|
}
|
|
749
|
+
function getActivePipeline() {
|
|
750
|
+
return _activePipeline;
|
|
751
|
+
}
|
|
748
752
|
function clearRunHistory() {
|
|
749
753
|
runHistory.length = 0;
|
|
750
754
|
}
|
|
@@ -1560,6 +1564,30 @@ Example:
|
|
|
1560
1564
|
}
|
|
1561
1565
|
});
|
|
1562
1566
|
|
|
1567
|
+
// src/core/navigators/SrsDebugger.ts
|
|
1568
|
+
var SrsDebugger_exports = {};
|
|
1569
|
+
__export(SrsDebugger_exports, {
|
|
1570
|
+
captureSrsBacklog: () => captureSrsBacklog,
|
|
1571
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
1572
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug
|
|
1573
|
+
});
|
|
1574
|
+
function captureSrsBacklog(snapshot) {
|
|
1575
|
+
snapshots.set(snapshot.courseId, snapshot);
|
|
1576
|
+
}
|
|
1577
|
+
function getSrsBacklogDebug() {
|
|
1578
|
+
return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1579
|
+
}
|
|
1580
|
+
function clearSrsBacklogDebug() {
|
|
1581
|
+
snapshots.clear();
|
|
1582
|
+
}
|
|
1583
|
+
var snapshots;
|
|
1584
|
+
var init_SrsDebugger = __esm({
|
|
1585
|
+
"src/core/navigators/SrsDebugger.ts"() {
|
|
1586
|
+
"use strict";
|
|
1587
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1563
1591
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1564
1592
|
var CompositeGenerator_exports = {};
|
|
1565
1593
|
__export(CompositeGenerator_exports, {
|
|
@@ -1927,7 +1955,7 @@ function shuffleInPlace(arr) {
|
|
|
1927
1955
|
function pickTopByScore(cards, limit) {
|
|
1928
1956
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1929
1957
|
}
|
|
1930
|
-
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, DEFAULT_PRACTICE_MIN_COUNT, DEFAULT_MAX_PRACTICE_PER_RUN, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, DISCOVERED_SUPPORT_SCORE, BASE_PRACTICE_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1958
|
+
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, DEFAULT_PRACTICE_MIN_COUNT, DEFAULT_MAX_PRACTICE_PER_RUN, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, DISCOVERED_SUPPORT_SCORE, BASE_PRACTICE_SCORE, PRACTICE_BASE_MULT, MAX_PRACTICE_MULTIPLIER, PRACTICE_STALENESS_BUMP_PER_DAY, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1931
1959
|
var init_prescribed = __esm({
|
|
1932
1960
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1933
1961
|
"use strict";
|
|
@@ -1944,6 +1972,9 @@ var init_prescribed = __esm({
|
|
|
1944
1972
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1945
1973
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1946
1974
|
BASE_PRACTICE_SCORE = 1;
|
|
1975
|
+
PRACTICE_BASE_MULT = 2;
|
|
1976
|
+
MAX_PRACTICE_MULTIPLIER = 4;
|
|
1977
|
+
PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
|
|
1947
1978
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1948
1979
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1949
1980
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2003,6 +2034,8 @@ var init_prescribed = __esm({
|
|
|
2003
2034
|
const emitted = [];
|
|
2004
2035
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
2005
2036
|
const groupRuntimes = [];
|
|
2037
|
+
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2038
|
+
const nextPracticeDebt = {};
|
|
2006
2039
|
for (const group of this.config.groups) {
|
|
2007
2040
|
const runtime = this.buildGroupRuntimeState({
|
|
2008
2041
|
group,
|
|
@@ -2060,10 +2093,13 @@ var init_prescribed = __esm({
|
|
|
2060
2093
|
userTagElo,
|
|
2061
2094
|
userGlobalElo,
|
|
2062
2095
|
activeIds,
|
|
2063
|
-
seenIds
|
|
2096
|
+
seenIds,
|
|
2097
|
+
priorPracticeDebt,
|
|
2098
|
+
nextPracticeDebt
|
|
2064
2099
|
});
|
|
2065
2100
|
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2066
2101
|
}
|
|
2102
|
+
nextState.practiceDebt = nextPracticeDebt;
|
|
2067
2103
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2068
2104
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
2069
2105
|
boostTags: hintSummary.boostTags,
|
|
@@ -2397,9 +2433,16 @@ var init_prescribed = __esm({
|
|
|
2397
2433
|
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2398
2434
|
* into the candidate pool. It exists because global-ELO retrieval
|
|
2399
2435
|
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2400
|
-
* freshly-introduced skill — putting them in the pool here
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2436
|
+
* freshly-introduced skill — putting them in the pool here guarantees presence.
|
|
2437
|
+
*
|
|
2438
|
+
* Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
|
|
2439
|
+
* cards score `base × multiplier`, where the multiplier starts at
|
|
2440
|
+
* PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
|
|
2441
|
+
* pressured reviews) and escalates by how long the debt has stayed open
|
|
2442
|
+
* (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
|
|
2443
|
+
* MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
|
|
2444
|
+
* the skill reaches `practiceMinCount` — so this no longer relies on the
|
|
2445
|
+
* session-scoped intro boost to actually surface.
|
|
2403
2446
|
*
|
|
2404
2447
|
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2405
2448
|
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
@@ -2414,7 +2457,9 @@ var init_prescribed = __esm({
|
|
|
2414
2457
|
userTagElo,
|
|
2415
2458
|
userGlobalElo,
|
|
2416
2459
|
activeIds,
|
|
2417
|
-
seenIds
|
|
2460
|
+
seenIds,
|
|
2461
|
+
priorPracticeDebt,
|
|
2462
|
+
nextPracticeDebt
|
|
2418
2463
|
} = args;
|
|
2419
2464
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2420
2465
|
if (patterns.length === 0) return [];
|
|
@@ -2424,6 +2469,20 @@ var init_prescribed = __esm({
|
|
|
2424
2469
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2425
2470
|
);
|
|
2426
2471
|
if (practiceTags.length === 0) return [];
|
|
2472
|
+
const now = Date.now();
|
|
2473
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2474
|
+
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
2475
|
+
for (const tag of practiceTags) {
|
|
2476
|
+
const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
|
|
2477
|
+
nextPracticeDebt[tag] = firstOwedAt;
|
|
2478
|
+
const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
|
|
2479
|
+
const mult = clamp(
|
|
2480
|
+
PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
|
|
2481
|
+
PRACTICE_BASE_MULT,
|
|
2482
|
+
MAX_PRACTICE_MULTIPLIER
|
|
2483
|
+
);
|
|
2484
|
+
tagMultiplier.set(tag, mult);
|
|
2485
|
+
}
|
|
2427
2486
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2428
2487
|
supportTags: practiceTags,
|
|
2429
2488
|
cardsByTag,
|
|
@@ -2439,18 +2498,25 @@ var init_prescribed = __esm({
|
|
|
2439
2498
|
const cards = [];
|
|
2440
2499
|
for (const cardId of practiceCardIds) {
|
|
2441
2500
|
emittedIds.add(cardId);
|
|
2501
|
+
let mult = PRACTICE_BASE_MULT;
|
|
2502
|
+
for (const tag of practiceTags) {
|
|
2503
|
+
if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
|
|
2504
|
+
mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
const score = BASE_PRACTICE_SCORE * mult;
|
|
2442
2508
|
cards.push({
|
|
2443
2509
|
cardId,
|
|
2444
2510
|
courseId,
|
|
2445
|
-
score
|
|
2511
|
+
score,
|
|
2446
2512
|
provenance: [
|
|
2447
2513
|
{
|
|
2448
2514
|
strategy: "prescribed",
|
|
2449
2515
|
strategyName: this.strategyName || this.name,
|
|
2450
2516
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2451
2517
|
action: "generated",
|
|
2452
|
-
score
|
|
2453
|
-
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2518
|
+
score,
|
|
2519
|
+
reason: `mode=practice;group=${group.id};debtMult=\xD7${mult.toFixed(2)};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2454
2520
|
}
|
|
2455
2521
|
]
|
|
2456
2522
|
});
|
|
@@ -2623,15 +2689,16 @@ var srs_exports = {};
|
|
|
2623
2689
|
__export(srs_exports, {
|
|
2624
2690
|
default: () => SRSNavigator
|
|
2625
2691
|
});
|
|
2626
|
-
var import_moment, DEFAULT_HEALTHY_BACKLOG,
|
|
2692
|
+
var import_moment, DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
|
|
2627
2693
|
var init_srs = __esm({
|
|
2628
2694
|
"src/core/navigators/generators/srs.ts"() {
|
|
2629
2695
|
"use strict";
|
|
2630
2696
|
import_moment = __toESM(require("moment"), 1);
|
|
2631
2697
|
init_navigators();
|
|
2698
|
+
init_SrsDebugger();
|
|
2632
2699
|
init_logger();
|
|
2633
2700
|
DEFAULT_HEALTHY_BACKLOG = 20;
|
|
2634
|
-
|
|
2701
|
+
MAX_BACKLOG_MULTIPLIER = 2;
|
|
2635
2702
|
SRSNavigator = class extends ContentNavigator {
|
|
2636
2703
|
/** Human-readable name for CardGenerator interface */
|
|
2637
2704
|
name;
|
|
@@ -2698,9 +2765,18 @@ var init_srs = __esm({
|
|
|
2698
2765
|
}
|
|
2699
2766
|
}
|
|
2700
2767
|
}
|
|
2701
|
-
const
|
|
2768
|
+
const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
|
|
2769
|
+
const notDue = reviews.filter((r) => !now.isAfter(import_moment.default.utc(r.reviewTime)));
|
|
2770
|
+
let nextDueIn = null;
|
|
2771
|
+
if (notDue.length > 0) {
|
|
2772
|
+
const next = notDue.reduce(
|
|
2773
|
+
(a, b) => import_moment.default.utc(a.reviewTime).isBefore(import_moment.default.utc(b.reviewTime)) ? a : b
|
|
2774
|
+
);
|
|
2775
|
+
const until = import_moment.default.duration(import_moment.default.utc(next.reviewTime).diff(now));
|
|
2776
|
+
nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
|
|
2777
|
+
}
|
|
2702
2778
|
if (dueReviews.length > 0) {
|
|
2703
|
-
const pressureNote =
|
|
2779
|
+
const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
|
|
2704
2780
|
logger.info(
|
|
2705
2781
|
`[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
|
|
2706
2782
|
);
|
|
@@ -2719,7 +2795,7 @@ var init_srs = __esm({
|
|
|
2719
2795
|
logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
|
|
2720
2796
|
}
|
|
2721
2797
|
const scored = dueReviews.map((review) => {
|
|
2722
|
-
const { score, reason } = this.computeUrgencyScore(review, now,
|
|
2798
|
+
const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
|
|
2723
2799
|
return {
|
|
2724
2800
|
cardId: review.cardId,
|
|
2725
2801
|
courseId: review.courseId,
|
|
@@ -2737,30 +2813,42 @@ var init_srs = __esm({
|
|
|
2737
2813
|
]
|
|
2738
2814
|
};
|
|
2739
2815
|
});
|
|
2740
|
-
|
|
2816
|
+
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
2817
|
+
captureSrsBacklog({
|
|
2818
|
+
courseId,
|
|
2819
|
+
scheduledTotal: reviews.length,
|
|
2820
|
+
dueNow: dueReviews.length,
|
|
2821
|
+
healthyBacklog: this.healthyBacklog,
|
|
2822
|
+
backlogMultiplier,
|
|
2823
|
+
maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
|
|
2824
|
+
topReviewScore: sorted.length > 0 ? sorted[0].score : null,
|
|
2825
|
+
nextDueIn,
|
|
2826
|
+
timestamp: Date.now()
|
|
2827
|
+
});
|
|
2828
|
+
return { cards: sorted.slice(0, limit) };
|
|
2741
2829
|
}
|
|
2742
2830
|
/**
|
|
2743
|
-
* Compute backlog pressure based on number of due reviews.
|
|
2831
|
+
* Compute the multiplicative backlog pressure based on number of due reviews.
|
|
2744
2832
|
*
|
|
2745
|
-
*
|
|
2746
|
-
* and
|
|
2833
|
+
* ×1.0 at or below the healthy threshold (no boost), increasing linearly above
|
|
2834
|
+
* it and maxing out at MAX_BACKLOG_MULTIPLIER at 3× the healthy backlog.
|
|
2747
2835
|
*
|
|
2748
|
-
* Examples (with default healthyBacklog=20):
|
|
2749
|
-
* - 10 due reviews →
|
|
2750
|
-
* - 20 due reviews →
|
|
2751
|
-
* - 40 due reviews →
|
|
2752
|
-
* - 60 due reviews →
|
|
2836
|
+
* Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
|
|
2837
|
+
* - 10 due reviews → ×1.00 (healthy)
|
|
2838
|
+
* - 20 due reviews → ×1.00 (at threshold)
|
|
2839
|
+
* - 40 due reviews → ×1.50 (2x threshold)
|
|
2840
|
+
* - 60 due reviews → ×2.00 (3x threshold, maxed)
|
|
2753
2841
|
*
|
|
2754
2842
|
* @param dueCount - Number of reviews currently due
|
|
2755
|
-
* @returns
|
|
2843
|
+
* @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2756
2844
|
*/
|
|
2757
|
-
|
|
2845
|
+
computeBacklogMultiplier(dueCount) {
|
|
2758
2846
|
if (dueCount <= this.healthyBacklog) {
|
|
2759
|
-
return
|
|
2847
|
+
return 1;
|
|
2760
2848
|
}
|
|
2761
2849
|
const excess = dueCount - this.healthyBacklog;
|
|
2762
|
-
const
|
|
2763
|
-
return Math.min(
|
|
2850
|
+
const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
|
|
2851
|
+
return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
|
|
2764
2852
|
}
|
|
2765
2853
|
/**
|
|
2766
2854
|
* Compute urgency score for a review card.
|
|
@@ -2775,19 +2863,20 @@ var init_srs = __esm({
|
|
|
2775
2863
|
* - 30 days (720h) → ~0.56
|
|
2776
2864
|
* - 180 days → ~0.30
|
|
2777
2865
|
*
|
|
2778
|
-
* 3. Backlog pressure = global
|
|
2779
|
-
*
|
|
2780
|
-
* - At 2x healthy: +0.25
|
|
2781
|
-
* - At 3x+ healthy: +0.50 (max)
|
|
2866
|
+
* 3. Backlog pressure = global *multiplier* when review backlog exceeds the
|
|
2867
|
+
* healthy threshold (×1.0 healthy → up to MAX_BACKLOG_MULTIPLIER at 3×).
|
|
2782
2868
|
*
|
|
2783
|
-
* Combined: base 0.5 +
|
|
2784
|
-
*
|
|
2869
|
+
* Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
|
|
2870
|
+
* Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 — under a
|
|
2871
|
+
* heavy backlog reviews scale onto the open scale to compete with (and exceed)
|
|
2872
|
+
* new cards; what keeps them from running away is the bounded multiplier, not
|
|
2873
|
+
* a hard ceiling.
|
|
2785
2874
|
*
|
|
2786
2875
|
* @param review - The scheduled card to score
|
|
2787
2876
|
* @param now - Current time
|
|
2788
|
-
* @param
|
|
2877
|
+
* @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2789
2878
|
*/
|
|
2790
|
-
computeUrgencyScore(review, now,
|
|
2879
|
+
computeUrgencyScore(review, now, backlogMultiplier) {
|
|
2791
2880
|
const scheduledAt = import_moment.default.utc(review.scheduledAt);
|
|
2792
2881
|
const due = import_moment.default.utc(review.reviewTime);
|
|
2793
2882
|
const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
|
|
@@ -2797,15 +2886,15 @@ var init_srs = __esm({
|
|
|
2797
2886
|
const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
|
|
2798
2887
|
const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
|
|
2799
2888
|
const baseScore = 0.5 + urgency * 0.45;
|
|
2800
|
-
const score =
|
|
2889
|
+
const score = baseScore * backlogMultiplier;
|
|
2801
2890
|
const reasonParts = [
|
|
2802
2891
|
`${Math.round(hoursOverdue)}h overdue`,
|
|
2803
2892
|
`interval: ${Math.round(intervalHours)}h`,
|
|
2804
2893
|
`relative: ${relativeOverdue.toFixed(2)}`,
|
|
2805
2894
|
`recency: ${recencyFactor.toFixed(2)}`
|
|
2806
2895
|
];
|
|
2807
|
-
if (
|
|
2808
|
-
reasonParts.push(`backlog:
|
|
2896
|
+
if (backlogMultiplier > 1) {
|
|
2897
|
+
reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
|
|
2809
2898
|
}
|
|
2810
2899
|
reasonParts.push("review");
|
|
2811
2900
|
const reason = reasonParts.join(", ");
|
|
@@ -4531,6 +4620,68 @@ var init_Pipeline = __esm({
|
|
|
4531
4620
|
// ---------------------------------------------------------------------------
|
|
4532
4621
|
// Card-space diagnostic
|
|
4533
4622
|
// ---------------------------------------------------------------------------
|
|
4623
|
+
/**
|
|
4624
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4625
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4626
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4627
|
+
* to cards the user hasn't seen yet.
|
|
4628
|
+
*
|
|
4629
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4630
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4631
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4632
|
+
* tag family). Nothing is written and no session is started.
|
|
4633
|
+
*
|
|
4634
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4635
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4636
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4637
|
+
* stays out), and
|
|
4638
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4639
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4640
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4641
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4642
|
+
*
|
|
4643
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4644
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4645
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4646
|
+
*
|
|
4647
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4648
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4649
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4650
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4651
|
+
*/
|
|
4652
|
+
async forecast(opts) {
|
|
4653
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4654
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4655
|
+
const courseId = this.course.getCourseID();
|
|
4656
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4657
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4658
|
+
cardId,
|
|
4659
|
+
courseId,
|
|
4660
|
+
score: 1,
|
|
4661
|
+
provenance: []
|
|
4662
|
+
}));
|
|
4663
|
+
cards = await this.hydrateTags(cards);
|
|
4664
|
+
const fullPool = cards.slice();
|
|
4665
|
+
const context = await this.buildContext();
|
|
4666
|
+
for (const filter of this.filters) {
|
|
4667
|
+
cards = await filter.transform(cards, context);
|
|
4668
|
+
}
|
|
4669
|
+
if (opts?.hints) {
|
|
4670
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4671
|
+
}
|
|
4672
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4673
|
+
if (unseenOnly) {
|
|
4674
|
+
let encountered;
|
|
4675
|
+
try {
|
|
4676
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4677
|
+
} catch {
|
|
4678
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4679
|
+
}
|
|
4680
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4681
|
+
}
|
|
4682
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4683
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4684
|
+
}
|
|
4534
4685
|
/**
|
|
4535
4686
|
* Scan every card in the course through the filter chain and report
|
|
4536
4687
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4795,6 +4946,7 @@ var init_3 = __esm({
|
|
|
4795
4946
|
"./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
|
|
4796
4947
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4797
4948
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4949
|
+
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
4798
4950
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4799
4951
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4800
4952
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -4827,11 +4979,14 @@ __export(navigators_exports, {
|
|
|
4827
4979
|
NavigatorRole: () => NavigatorRole,
|
|
4828
4980
|
NavigatorRoles: () => NavigatorRoles,
|
|
4829
4981
|
Navigators: () => Navigators,
|
|
4982
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
4830
4983
|
diversityRerank: () => diversityRerank,
|
|
4984
|
+
getActivePipeline: () => getActivePipeline,
|
|
4831
4985
|
getCardOrigin: () => getCardOrigin,
|
|
4832
4986
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4833
4987
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
4834
4988
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
4989
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
4835
4990
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
4836
4991
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
4837
4992
|
isFilter: () => isFilter,
|
|
@@ -4913,6 +5068,7 @@ var init_navigators = __esm({
|
|
|
4913
5068
|
"use strict";
|
|
4914
5069
|
init_diversityRerank();
|
|
4915
5070
|
init_PipelineDebugger();
|
|
5071
|
+
init_SrsDebugger();
|
|
4916
5072
|
init_logger();
|
|
4917
5073
|
init_();
|
|
4918
5074
|
init_2();
|