@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,5 +1,5 @@
|
|
|
1
|
-
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-
|
|
2
|
-
import { D as DataLayerProvider } from '../../dataLayerProvider-
|
|
1
|
+
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-jSkcOt2s.cjs';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-BDClIrFC.cjs';
|
|
3
3
|
import { S as StaticCourseManifest } from '../../types-BFUa1pa3.cjs';
|
|
4
4
|
import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
|
|
5
5
|
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-4tlwHnXo.cjs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-
|
|
2
|
-
import { D as DataLayerProvider } from '../../dataLayerProvider-
|
|
1
|
+
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, G as GeneratorResult } from '../../contentSource-C-0t0y0V.js';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-BB0oi9T0.js';
|
|
3
3
|
import { S as StaticCourseManifest } from '../../types-CHgpWQAY.js';
|
|
4
4
|
import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
|
|
5
5
|
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-4tlwHnXo.js';
|
|
@@ -617,6 +617,7 @@ __export(PipelineDebugger_exports, {
|
|
|
617
617
|
buildRunReport: () => buildRunReport,
|
|
618
618
|
captureRun: () => captureRun,
|
|
619
619
|
clearRunHistory: () => clearRunHistory,
|
|
620
|
+
getActivePipeline: () => getActivePipeline,
|
|
620
621
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
621
622
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
622
623
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -624,6 +625,9 @@ __export(PipelineDebugger_exports, {
|
|
|
624
625
|
function registerPipelineForDebug(pipeline) {
|
|
625
626
|
_activePipeline = pipeline;
|
|
626
627
|
}
|
|
628
|
+
function getActivePipeline() {
|
|
629
|
+
return _activePipeline;
|
|
630
|
+
}
|
|
627
631
|
function clearRunHistory() {
|
|
628
632
|
runHistory.length = 0;
|
|
629
633
|
}
|
|
@@ -1439,6 +1443,30 @@ Example:
|
|
|
1439
1443
|
}
|
|
1440
1444
|
});
|
|
1441
1445
|
|
|
1446
|
+
// src/core/navigators/SrsDebugger.ts
|
|
1447
|
+
var SrsDebugger_exports = {};
|
|
1448
|
+
__export(SrsDebugger_exports, {
|
|
1449
|
+
captureSrsBacklog: () => captureSrsBacklog,
|
|
1450
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
1451
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug
|
|
1452
|
+
});
|
|
1453
|
+
function captureSrsBacklog(snapshot) {
|
|
1454
|
+
snapshots.set(snapshot.courseId, snapshot);
|
|
1455
|
+
}
|
|
1456
|
+
function getSrsBacklogDebug() {
|
|
1457
|
+
return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1458
|
+
}
|
|
1459
|
+
function clearSrsBacklogDebug() {
|
|
1460
|
+
snapshots.clear();
|
|
1461
|
+
}
|
|
1462
|
+
var snapshots;
|
|
1463
|
+
var init_SrsDebugger = __esm({
|
|
1464
|
+
"src/core/navigators/SrsDebugger.ts"() {
|
|
1465
|
+
"use strict";
|
|
1466
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1442
1470
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1443
1471
|
var CompositeGenerator_exports = {};
|
|
1444
1472
|
__export(CompositeGenerator_exports, {
|
|
@@ -1806,7 +1834,7 @@ function shuffleInPlace(arr) {
|
|
|
1806
1834
|
function pickTopByScore(cards, limit) {
|
|
1807
1835
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1808
1836
|
}
|
|
1809
|
-
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;
|
|
1837
|
+
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;
|
|
1810
1838
|
var init_prescribed = __esm({
|
|
1811
1839
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1812
1840
|
"use strict";
|
|
@@ -1823,6 +1851,9 @@ var init_prescribed = __esm({
|
|
|
1823
1851
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1824
1852
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1825
1853
|
BASE_PRACTICE_SCORE = 1;
|
|
1854
|
+
PRACTICE_BASE_MULT = 2;
|
|
1855
|
+
MAX_PRACTICE_MULTIPLIER = 4;
|
|
1856
|
+
PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
|
|
1826
1857
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1827
1858
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1828
1859
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -1882,6 +1913,8 @@ var init_prescribed = __esm({
|
|
|
1882
1913
|
const emitted = [];
|
|
1883
1914
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
1884
1915
|
const groupRuntimes = [];
|
|
1916
|
+
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
1917
|
+
const nextPracticeDebt = {};
|
|
1885
1918
|
for (const group of this.config.groups) {
|
|
1886
1919
|
const runtime = this.buildGroupRuntimeState({
|
|
1887
1920
|
group,
|
|
@@ -1939,10 +1972,13 @@ var init_prescribed = __esm({
|
|
|
1939
1972
|
userTagElo,
|
|
1940
1973
|
userGlobalElo,
|
|
1941
1974
|
activeIds,
|
|
1942
|
-
seenIds
|
|
1975
|
+
seenIds,
|
|
1976
|
+
priorPracticeDebt,
|
|
1977
|
+
nextPracticeDebt
|
|
1943
1978
|
});
|
|
1944
1979
|
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
1945
1980
|
}
|
|
1981
|
+
nextState.practiceDebt = nextPracticeDebt;
|
|
1946
1982
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1947
1983
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1948
1984
|
boostTags: hintSummary.boostTags,
|
|
@@ -2276,9 +2312,16 @@ var init_prescribed = __esm({
|
|
|
2276
2312
|
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2277
2313
|
* into the candidate pool. It exists because global-ELO retrieval
|
|
2278
2314
|
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2279
|
-
* freshly-introduced skill — putting them in the pool here
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2315
|
+
* freshly-introduced skill — putting them in the pool here guarantees presence.
|
|
2316
|
+
*
|
|
2317
|
+
* Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
|
|
2318
|
+
* cards score `base × multiplier`, where the multiplier starts at
|
|
2319
|
+
* PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
|
|
2320
|
+
* pressured reviews) and escalates by how long the debt has stayed open
|
|
2321
|
+
* (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
|
|
2322
|
+
* MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
|
|
2323
|
+
* the skill reaches `practiceMinCount` — so this no longer relies on the
|
|
2324
|
+
* session-scoped intro boost to actually surface.
|
|
2282
2325
|
*
|
|
2283
2326
|
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2284
2327
|
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
@@ -2293,7 +2336,9 @@ var init_prescribed = __esm({
|
|
|
2293
2336
|
userTagElo,
|
|
2294
2337
|
userGlobalElo,
|
|
2295
2338
|
activeIds,
|
|
2296
|
-
seenIds
|
|
2339
|
+
seenIds,
|
|
2340
|
+
priorPracticeDebt,
|
|
2341
|
+
nextPracticeDebt
|
|
2297
2342
|
} = args;
|
|
2298
2343
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2299
2344
|
if (patterns.length === 0) return [];
|
|
@@ -2303,6 +2348,20 @@ var init_prescribed = __esm({
|
|
|
2303
2348
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2304
2349
|
);
|
|
2305
2350
|
if (practiceTags.length === 0) return [];
|
|
2351
|
+
const now = Date.now();
|
|
2352
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2353
|
+
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
2354
|
+
for (const tag of practiceTags) {
|
|
2355
|
+
const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
|
|
2356
|
+
nextPracticeDebt[tag] = firstOwedAt;
|
|
2357
|
+
const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
|
|
2358
|
+
const mult = clamp(
|
|
2359
|
+
PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
|
|
2360
|
+
PRACTICE_BASE_MULT,
|
|
2361
|
+
MAX_PRACTICE_MULTIPLIER
|
|
2362
|
+
);
|
|
2363
|
+
tagMultiplier.set(tag, mult);
|
|
2364
|
+
}
|
|
2306
2365
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2307
2366
|
supportTags: practiceTags,
|
|
2308
2367
|
cardsByTag,
|
|
@@ -2318,18 +2377,25 @@ var init_prescribed = __esm({
|
|
|
2318
2377
|
const cards = [];
|
|
2319
2378
|
for (const cardId of practiceCardIds) {
|
|
2320
2379
|
emittedIds.add(cardId);
|
|
2380
|
+
let mult = PRACTICE_BASE_MULT;
|
|
2381
|
+
for (const tag of practiceTags) {
|
|
2382
|
+
if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
|
|
2383
|
+
mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
const score = BASE_PRACTICE_SCORE * mult;
|
|
2321
2387
|
cards.push({
|
|
2322
2388
|
cardId,
|
|
2323
2389
|
courseId,
|
|
2324
|
-
score
|
|
2390
|
+
score,
|
|
2325
2391
|
provenance: [
|
|
2326
2392
|
{
|
|
2327
2393
|
strategy: "prescribed",
|
|
2328
2394
|
strategyName: this.strategyName || this.name,
|
|
2329
2395
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2330
2396
|
action: "generated",
|
|
2331
|
-
score
|
|
2332
|
-
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2397
|
+
score,
|
|
2398
|
+
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}`
|
|
2333
2399
|
}
|
|
2334
2400
|
]
|
|
2335
2401
|
});
|
|
@@ -2502,15 +2568,16 @@ var srs_exports = {};
|
|
|
2502
2568
|
__export(srs_exports, {
|
|
2503
2569
|
default: () => SRSNavigator
|
|
2504
2570
|
});
|
|
2505
|
-
var import_moment3, DEFAULT_HEALTHY_BACKLOG,
|
|
2571
|
+
var import_moment3, DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
|
|
2506
2572
|
var init_srs = __esm({
|
|
2507
2573
|
"src/core/navigators/generators/srs.ts"() {
|
|
2508
2574
|
"use strict";
|
|
2509
2575
|
import_moment3 = __toESM(require("moment"), 1);
|
|
2510
2576
|
init_navigators();
|
|
2577
|
+
init_SrsDebugger();
|
|
2511
2578
|
init_logger();
|
|
2512
2579
|
DEFAULT_HEALTHY_BACKLOG = 20;
|
|
2513
|
-
|
|
2580
|
+
MAX_BACKLOG_MULTIPLIER = 2;
|
|
2514
2581
|
SRSNavigator = class extends ContentNavigator {
|
|
2515
2582
|
/** Human-readable name for CardGenerator interface */
|
|
2516
2583
|
name;
|
|
@@ -2577,9 +2644,18 @@ var init_srs = __esm({
|
|
|
2577
2644
|
}
|
|
2578
2645
|
}
|
|
2579
2646
|
}
|
|
2580
|
-
const
|
|
2647
|
+
const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
|
|
2648
|
+
const notDue = reviews.filter((r) => !now.isAfter(import_moment3.default.utc(r.reviewTime)));
|
|
2649
|
+
let nextDueIn = null;
|
|
2650
|
+
if (notDue.length > 0) {
|
|
2651
|
+
const next = notDue.reduce(
|
|
2652
|
+
(a, b) => import_moment3.default.utc(a.reviewTime).isBefore(import_moment3.default.utc(b.reviewTime)) ? a : b
|
|
2653
|
+
);
|
|
2654
|
+
const until = import_moment3.default.duration(import_moment3.default.utc(next.reviewTime).diff(now));
|
|
2655
|
+
nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
|
|
2656
|
+
}
|
|
2581
2657
|
if (dueReviews.length > 0) {
|
|
2582
|
-
const pressureNote =
|
|
2658
|
+
const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
|
|
2583
2659
|
logger.info(
|
|
2584
2660
|
`[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
|
|
2585
2661
|
);
|
|
@@ -2598,7 +2674,7 @@ var init_srs = __esm({
|
|
|
2598
2674
|
logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
|
|
2599
2675
|
}
|
|
2600
2676
|
const scored = dueReviews.map((review) => {
|
|
2601
|
-
const { score, reason } = this.computeUrgencyScore(review, now,
|
|
2677
|
+
const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
|
|
2602
2678
|
return {
|
|
2603
2679
|
cardId: review.cardId,
|
|
2604
2680
|
courseId: review.courseId,
|
|
@@ -2616,30 +2692,42 @@ var init_srs = __esm({
|
|
|
2616
2692
|
]
|
|
2617
2693
|
};
|
|
2618
2694
|
});
|
|
2619
|
-
|
|
2695
|
+
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
2696
|
+
captureSrsBacklog({
|
|
2697
|
+
courseId,
|
|
2698
|
+
scheduledTotal: reviews.length,
|
|
2699
|
+
dueNow: dueReviews.length,
|
|
2700
|
+
healthyBacklog: this.healthyBacklog,
|
|
2701
|
+
backlogMultiplier,
|
|
2702
|
+
maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
|
|
2703
|
+
topReviewScore: sorted.length > 0 ? sorted[0].score : null,
|
|
2704
|
+
nextDueIn,
|
|
2705
|
+
timestamp: Date.now()
|
|
2706
|
+
});
|
|
2707
|
+
return { cards: sorted.slice(0, limit) };
|
|
2620
2708
|
}
|
|
2621
2709
|
/**
|
|
2622
|
-
* Compute backlog pressure based on number of due reviews.
|
|
2710
|
+
* Compute the multiplicative backlog pressure based on number of due reviews.
|
|
2623
2711
|
*
|
|
2624
|
-
*
|
|
2625
|
-
* and
|
|
2712
|
+
* ×1.0 at or below the healthy threshold (no boost), increasing linearly above
|
|
2713
|
+
* it and maxing out at MAX_BACKLOG_MULTIPLIER at 3× the healthy backlog.
|
|
2626
2714
|
*
|
|
2627
|
-
* Examples (with default healthyBacklog=20):
|
|
2628
|
-
* - 10 due reviews →
|
|
2629
|
-
* - 20 due reviews →
|
|
2630
|
-
* - 40 due reviews →
|
|
2631
|
-
* - 60 due reviews →
|
|
2715
|
+
* Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
|
|
2716
|
+
* - 10 due reviews → ×1.00 (healthy)
|
|
2717
|
+
* - 20 due reviews → ×1.00 (at threshold)
|
|
2718
|
+
* - 40 due reviews → ×1.50 (2x threshold)
|
|
2719
|
+
* - 60 due reviews → ×2.00 (3x threshold, maxed)
|
|
2632
2720
|
*
|
|
2633
2721
|
* @param dueCount - Number of reviews currently due
|
|
2634
|
-
* @returns
|
|
2722
|
+
* @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2635
2723
|
*/
|
|
2636
|
-
|
|
2724
|
+
computeBacklogMultiplier(dueCount) {
|
|
2637
2725
|
if (dueCount <= this.healthyBacklog) {
|
|
2638
|
-
return
|
|
2726
|
+
return 1;
|
|
2639
2727
|
}
|
|
2640
2728
|
const excess = dueCount - this.healthyBacklog;
|
|
2641
|
-
const
|
|
2642
|
-
return Math.min(
|
|
2729
|
+
const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
|
|
2730
|
+
return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
|
|
2643
2731
|
}
|
|
2644
2732
|
/**
|
|
2645
2733
|
* Compute urgency score for a review card.
|
|
@@ -2654,19 +2742,20 @@ var init_srs = __esm({
|
|
|
2654
2742
|
* - 30 days (720h) → ~0.56
|
|
2655
2743
|
* - 180 days → ~0.30
|
|
2656
2744
|
*
|
|
2657
|
-
* 3. Backlog pressure = global
|
|
2658
|
-
*
|
|
2659
|
-
* - At 2x healthy: +0.25
|
|
2660
|
-
* - At 3x+ healthy: +0.50 (max)
|
|
2745
|
+
* 3. Backlog pressure = global *multiplier* when review backlog exceeds the
|
|
2746
|
+
* healthy threshold (×1.0 healthy → up to MAX_BACKLOG_MULTIPLIER at 3×).
|
|
2661
2747
|
*
|
|
2662
|
-
* Combined: base 0.5 +
|
|
2663
|
-
*
|
|
2748
|
+
* Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
|
|
2749
|
+
* Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 — under a
|
|
2750
|
+
* heavy backlog reviews scale onto the open scale to compete with (and exceed)
|
|
2751
|
+
* new cards; what keeps them from running away is the bounded multiplier, not
|
|
2752
|
+
* a hard ceiling.
|
|
2664
2753
|
*
|
|
2665
2754
|
* @param review - The scheduled card to score
|
|
2666
2755
|
* @param now - Current time
|
|
2667
|
-
* @param
|
|
2756
|
+
* @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2668
2757
|
*/
|
|
2669
|
-
computeUrgencyScore(review, now,
|
|
2758
|
+
computeUrgencyScore(review, now, backlogMultiplier) {
|
|
2670
2759
|
const scheduledAt = import_moment3.default.utc(review.scheduledAt);
|
|
2671
2760
|
const due = import_moment3.default.utc(review.reviewTime);
|
|
2672
2761
|
const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
|
|
@@ -2676,15 +2765,15 @@ var init_srs = __esm({
|
|
|
2676
2765
|
const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
|
|
2677
2766
|
const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
|
|
2678
2767
|
const baseScore = 0.5 + urgency * 0.45;
|
|
2679
|
-
const score =
|
|
2768
|
+
const score = baseScore * backlogMultiplier;
|
|
2680
2769
|
const reasonParts = [
|
|
2681
2770
|
`${Math.round(hoursOverdue)}h overdue`,
|
|
2682
2771
|
`interval: ${Math.round(intervalHours)}h`,
|
|
2683
2772
|
`relative: ${relativeOverdue.toFixed(2)}`,
|
|
2684
2773
|
`recency: ${recencyFactor.toFixed(2)}`
|
|
2685
2774
|
];
|
|
2686
|
-
if (
|
|
2687
|
-
reasonParts.push(`backlog:
|
|
2775
|
+
if (backlogMultiplier > 1) {
|
|
2776
|
+
reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
|
|
2688
2777
|
}
|
|
2689
2778
|
reasonParts.push("review");
|
|
2690
2779
|
const reason = reasonParts.join(", ");
|
|
@@ -4410,6 +4499,68 @@ var init_Pipeline = __esm({
|
|
|
4410
4499
|
// ---------------------------------------------------------------------------
|
|
4411
4500
|
// Card-space diagnostic
|
|
4412
4501
|
// ---------------------------------------------------------------------------
|
|
4502
|
+
/**
|
|
4503
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4504
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4505
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4506
|
+
* to cards the user hasn't seen yet.
|
|
4507
|
+
*
|
|
4508
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4509
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4510
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4511
|
+
* tag family). Nothing is written and no session is started.
|
|
4512
|
+
*
|
|
4513
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4514
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4515
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4516
|
+
* stays out), and
|
|
4517
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4518
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4519
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4520
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4521
|
+
*
|
|
4522
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4523
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4524
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4525
|
+
*
|
|
4526
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4527
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4528
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4529
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4530
|
+
*/
|
|
4531
|
+
async forecast(opts) {
|
|
4532
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4533
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4534
|
+
const courseId = this.course.getCourseID();
|
|
4535
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4536
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4537
|
+
cardId,
|
|
4538
|
+
courseId,
|
|
4539
|
+
score: 1,
|
|
4540
|
+
provenance: []
|
|
4541
|
+
}));
|
|
4542
|
+
cards = await this.hydrateTags(cards);
|
|
4543
|
+
const fullPool = cards.slice();
|
|
4544
|
+
const context = await this.buildContext();
|
|
4545
|
+
for (const filter of this.filters) {
|
|
4546
|
+
cards = await filter.transform(cards, context);
|
|
4547
|
+
}
|
|
4548
|
+
if (opts?.hints) {
|
|
4549
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4550
|
+
}
|
|
4551
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4552
|
+
if (unseenOnly) {
|
|
4553
|
+
let encountered;
|
|
4554
|
+
try {
|
|
4555
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4556
|
+
} catch {
|
|
4557
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4558
|
+
}
|
|
4559
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4560
|
+
}
|
|
4561
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4562
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4563
|
+
}
|
|
4413
4564
|
/**
|
|
4414
4565
|
* Scan every card in the course through the filter chain and report
|
|
4415
4566
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4674,6 +4825,7 @@ var init_3 = __esm({
|
|
|
4674
4825
|
"./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
|
|
4675
4826
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4676
4827
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4828
|
+
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
4677
4829
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4678
4830
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4679
4831
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -4706,11 +4858,14 @@ __export(navigators_exports, {
|
|
|
4706
4858
|
NavigatorRole: () => NavigatorRole,
|
|
4707
4859
|
NavigatorRoles: () => NavigatorRoles,
|
|
4708
4860
|
Navigators: () => Navigators,
|
|
4861
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
4709
4862
|
diversityRerank: () => diversityRerank,
|
|
4863
|
+
getActivePipeline: () => getActivePipeline,
|
|
4710
4864
|
getCardOrigin: () => getCardOrigin,
|
|
4711
4865
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4712
4866
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
4713
4867
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
4868
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
4714
4869
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
4715
4870
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
4716
4871
|
isFilter: () => isFilter,
|
|
@@ -4792,6 +4947,7 @@ var init_navigators = __esm({
|
|
|
4792
4947
|
"use strict";
|
|
4793
4948
|
init_diversityRerank();
|
|
4794
4949
|
init_PipelineDebugger();
|
|
4950
|
+
init_SrsDebugger();
|
|
4795
4951
|
init_logger();
|
|
4796
4952
|
init_();
|
|
4797
4953
|
init_2();
|