@vue-skuilder/db 0.1.32-a → 0.1.32-c
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/core/index.d.cts +16 -12
- package/dist/core/index.d.ts +16 -12
- package/dist/core/index.js +2279 -227
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2256 -200
- package/dist/core/index.mjs.map +1 -1
- package/dist/{contentSource-Bdwkvqa8.d.ts → dataLayerProvider-BAn-LRh5.d.ts} +626 -83
- package/dist/{contentSource-DF1nUbPQ.d.cts → dataLayerProvider-BJqBlMIl.d.cts} +626 -83
- package/dist/impl/couch/index.d.cts +18 -3
- package/dist/impl/couch/index.d.ts +18 -3
- package/dist/impl/couch/index.js +2323 -224
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2311 -208
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +5 -4
- package/dist/impl/static/index.d.ts +5 -4
- package/dist/impl/static/index.js +2283 -231
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2268 -212
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-BWvO-_rJ.d.ts → index-X6wHrURm.d.ts} +1 -1
- package/dist/{index-Ba7hYbHj.d.cts → index-m8MMGxxR.d.cts} +1 -1
- package/dist/index.d.cts +9 -381
- package/dist/index.d.ts +9 -381
- package/dist/index.js +9626 -8815
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9559 -8748
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CJrLM1Ew.d.ts → types-DZ5dUqbL.d.ts} +1 -1
- package/dist/{types-W8n-B6HG.d.cts → types-ZL8tOPQZ.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-C7r0T4OV.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-C7r0T4OV.d.ts} +1 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +2 -2
- package/package.json +2 -2
- package/src/core/interfaces/contentSource.ts +2 -1
- package/src/core/navigators/Pipeline.ts +51 -25
- package/src/core/navigators/PipelineDebugger.ts +49 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +92 -5
- package/src/core/navigators/filters/relativePriority.ts +7 -1
- package/src/core/navigators/generators/prescribed.ts +618 -43
- package/src/core/navigators/index.ts +2 -1
- package/src/impl/couch/CourseSyncService.ts +72 -4
- package/src/impl/couch/courseDB.ts +11 -0
- package/src/impl/static/courseDB.ts +13 -0
- package/src/study/SessionController.ts +276 -24
- package/src/study/services/EloService.ts +22 -3
- package/src/study/services/ResponseProcessor.ts +7 -3
- package/dist/dataLayerProvider-BKmVoyJR.d.ts +0 -67
- package/dist/dataLayerProvider-BQdfJuBN.d.cts +0 -67
|
@@ -529,13 +529,20 @@ function captureRun(report) {
|
|
|
529
529
|
runHistory.pop();
|
|
530
530
|
}
|
|
531
531
|
}
|
|
532
|
-
function
|
|
532
|
+
function parseCardElo(provenance) {
|
|
533
|
+
const eloEntry = provenance.find((p) => p.strategy === "elo");
|
|
534
|
+
if (!eloEntry?.reason) return void 0;
|
|
535
|
+
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
536
|
+
return match ? parseInt(match[1], 10) : void 0;
|
|
537
|
+
}
|
|
538
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
|
|
533
539
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
534
540
|
const cards = allCards.map((card) => ({
|
|
535
541
|
cardId: card.cardId,
|
|
536
542
|
courseId: card.courseId,
|
|
537
543
|
origin: getOrigin(card),
|
|
538
544
|
finalScore: card.score,
|
|
545
|
+
cardElo: parseCardElo(card.provenance),
|
|
539
546
|
provenance: card.provenance,
|
|
540
547
|
tags: card.tags,
|
|
541
548
|
selected: selectedIds.has(card.cardId)
|
|
@@ -545,6 +552,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
545
552
|
return {
|
|
546
553
|
courseId,
|
|
547
554
|
courseName,
|
|
555
|
+
userElo,
|
|
548
556
|
generatorName,
|
|
549
557
|
generators,
|
|
550
558
|
generatedCount,
|
|
@@ -565,6 +573,7 @@ function printRunSummary(run) {
|
|
|
565
573
|
console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
|
|
566
574
|
logger.info(`Run ID: ${run.runId}`);
|
|
567
575
|
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
576
|
+
logger.info(`User ELO: ${run.userElo ?? "unknown"}`);
|
|
568
577
|
logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
|
|
569
578
|
if (run.generators && run.generators.length > 0) {
|
|
570
579
|
console.group("Generator breakdown:");
|
|
@@ -651,8 +660,12 @@ var init_PipelineDebugger = __esm({
|
|
|
651
660
|
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
652
661
|
logger.info(`Course: ${card.courseId}`);
|
|
653
662
|
logger.info(`Origin: ${card.origin}`);
|
|
663
|
+
logger.info(`Card ELO: ${card.cardElo ?? "unknown"} | User ELO: ${run.userElo ?? "unknown"}`);
|
|
654
664
|
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
655
665
|
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
666
|
+
if (card.tags && card.tags.length > 0) {
|
|
667
|
+
logger.info(`Tags (${card.tags.length}): ${card.tags.join(", ")}`);
|
|
668
|
+
}
|
|
656
669
|
logger.info("Provenance:");
|
|
657
670
|
logger.info(formatProvenance(card.provenance));
|
|
658
671
|
console.groupEnd();
|
|
@@ -816,6 +829,27 @@ var init_PipelineDebugger = __esm({
|
|
|
816
829
|
}
|
|
817
830
|
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
818
831
|
},
|
|
832
|
+
/**
|
|
833
|
+
* Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
|
|
834
|
+
*
|
|
835
|
+
* @param tagFilter - Optional glob pattern(s) to filter tags.
|
|
836
|
+
* Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
|
|
837
|
+
*/
|
|
838
|
+
async showTagElo(tagFilter) {
|
|
839
|
+
if (!_activePipeline) {
|
|
840
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const status = await _activePipeline.getTagEloStatus(tagFilter);
|
|
844
|
+
const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
|
|
845
|
+
if (entries.length === 0) {
|
|
846
|
+
logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ""}.`);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
console.table(
|
|
850
|
+
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
851
|
+
);
|
|
852
|
+
},
|
|
819
853
|
/**
|
|
820
854
|
* Show help.
|
|
821
855
|
*/
|
|
@@ -827,6 +861,7 @@ Commands:
|
|
|
827
861
|
.showLastRun() Show summary of most recent pipeline run
|
|
828
862
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
829
863
|
.showCard(cardId) Show provenance trail for a specific card
|
|
864
|
+
.showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
|
|
830
865
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
831
866
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
832
867
|
.showRegistry() Show navigator registry (classes + roles)
|
|
@@ -1140,60 +1175,423 @@ var prescribed_exports = {};
|
|
|
1140
1175
|
__export(prescribed_exports, {
|
|
1141
1176
|
default: () => PrescribedCardsGenerator
|
|
1142
1177
|
});
|
|
1143
|
-
|
|
1178
|
+
function dedupe(arr) {
|
|
1179
|
+
return [...new Set(arr)];
|
|
1180
|
+
}
|
|
1181
|
+
function isoNow() {
|
|
1182
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1183
|
+
}
|
|
1184
|
+
function clamp(value, min, max) {
|
|
1185
|
+
return Math.max(min, Math.min(max, value));
|
|
1186
|
+
}
|
|
1187
|
+
function matchesTagPattern(tag, pattern) {
|
|
1188
|
+
if (pattern === "*") return true;
|
|
1189
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1190
|
+
const re = new RegExp(`^${escaped}$`);
|
|
1191
|
+
return re.test(tag);
|
|
1192
|
+
}
|
|
1193
|
+
function pickTopByScore(cards, limit) {
|
|
1194
|
+
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1195
|
+
}
|
|
1196
|
+
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, LOCKED_TAG_PREFIXES, LESSON_GATE_PENALTY_TAG_HINT, PrescribedCardsGenerator;
|
|
1144
1197
|
var init_prescribed = __esm({
|
|
1145
1198
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1146
1199
|
"use strict";
|
|
1147
1200
|
init_navigators();
|
|
1148
1201
|
init_logger();
|
|
1202
|
+
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1203
|
+
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
1204
|
+
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1205
|
+
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1206
|
+
DEFAULT_MIN_COUNT = 3;
|
|
1207
|
+
BASE_TARGET_SCORE = 1;
|
|
1208
|
+
BASE_SUPPORT_SCORE = 0.8;
|
|
1209
|
+
MAX_TARGET_MULTIPLIER = 8;
|
|
1210
|
+
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1211
|
+
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1212
|
+
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1149
1213
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1150
1214
|
name;
|
|
1151
1215
|
config;
|
|
1152
1216
|
constructor(user, course, strategyData) {
|
|
1153
1217
|
super(user, course, strategyData);
|
|
1154
1218
|
this.name = strategyData.name || "Prescribed Cards";
|
|
1155
|
-
|
|
1156
|
-
const parsed = JSON.parse(strategyData.serializedData);
|
|
1157
|
-
this.config = { cardIds: parsed.cardIds || [] };
|
|
1158
|
-
} catch {
|
|
1159
|
-
this.config = { cardIds: [] };
|
|
1160
|
-
}
|
|
1219
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
1161
1220
|
logger.debug(
|
|
1162
|
-
`[Prescribed] Initialized with ${this.config.
|
|
1221
|
+
`[Prescribed] Initialized with ${this.config.groups.length} groups and ${this.config.groups.reduce((n, g) => n + g.targetCardIds.length, 0)} targets`
|
|
1163
1222
|
);
|
|
1164
1223
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1224
|
+
get strategyKey() {
|
|
1225
|
+
return "PrescribedProgress";
|
|
1226
|
+
}
|
|
1227
|
+
async getWeightedCards(limit, context) {
|
|
1228
|
+
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1167
1229
|
return [];
|
|
1168
1230
|
}
|
|
1169
1231
|
const courseId = this.course.getCourseID();
|
|
1170
1232
|
const activeCards = await this.user.getActiveCards();
|
|
1171
1233
|
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1234
|
+
const seenCards = await this.user.getSeenCards(courseId).catch(() => []);
|
|
1235
|
+
const seenIds = new Set(seenCards);
|
|
1236
|
+
const progress = await this.getStrategyState() ?? {
|
|
1237
|
+
updatedAt: isoNow(),
|
|
1238
|
+
groups: {}
|
|
1239
|
+
};
|
|
1240
|
+
const hierarchyConfigs = await this.loadHierarchyConfigs();
|
|
1241
|
+
const courseReg = await this.user.getCourseRegDoc(courseId).catch(() => null);
|
|
1242
|
+
const userGlobalElo = typeof courseReg?.elo === "number" ? courseReg.elo : courseReg?.elo?.global?.score ?? context?.userElo ?? 1e3;
|
|
1243
|
+
const userTagElo = typeof courseReg?.elo === "number" ? {} : courseReg?.elo?.tags ?? {};
|
|
1244
|
+
const allTargetIds = dedupe(this.config.groups.flatMap((g) => g.targetCardIds));
|
|
1245
|
+
const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
|
|
1246
|
+
const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
|
|
1247
|
+
const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
|
|
1248
|
+
const nextState = {
|
|
1249
|
+
updatedAt: isoNow(),
|
|
1250
|
+
groups: {}
|
|
1251
|
+
};
|
|
1252
|
+
const emitted = [];
|
|
1253
|
+
const emittedIds = /* @__PURE__ */ new Set();
|
|
1254
|
+
for (const group of this.config.groups) {
|
|
1255
|
+
const runtime = this.buildGroupRuntimeState({
|
|
1256
|
+
group,
|
|
1257
|
+
priorState: progress.groups[group.id],
|
|
1258
|
+
activeIds,
|
|
1259
|
+
seenIds,
|
|
1260
|
+
tagsByCard,
|
|
1261
|
+
hierarchyConfigs,
|
|
1262
|
+
userTagElo,
|
|
1263
|
+
userGlobalElo
|
|
1264
|
+
});
|
|
1265
|
+
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1266
|
+
const directCards = this.buildDirectTargetCards(
|
|
1267
|
+
runtime,
|
|
1268
|
+
courseId,
|
|
1269
|
+
emittedIds
|
|
1270
|
+
);
|
|
1271
|
+
const supportCards = this.buildSupportCards(
|
|
1272
|
+
runtime,
|
|
1273
|
+
courseId,
|
|
1274
|
+
emittedIds
|
|
1275
|
+
);
|
|
1276
|
+
emitted.push(...directCards, ...supportCards);
|
|
1277
|
+
}
|
|
1278
|
+
if (emitted.length === 0) {
|
|
1279
|
+
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1280
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1281
|
+
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1282
|
+
});
|
|
1175
1283
|
return [];
|
|
1176
1284
|
}
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1285
|
+
const finalCards = pickTopByScore(emitted, limit);
|
|
1286
|
+
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1287
|
+
for (const card of finalCards) {
|
|
1288
|
+
const prov = card.provenance[0];
|
|
1289
|
+
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1290
|
+
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1291
|
+
if (!groupId) continue;
|
|
1292
|
+
if (!surfacedByGroup.has(groupId)) {
|
|
1293
|
+
surfacedByGroup.set(groupId, { targetIds: [], supportIds: [] });
|
|
1294
|
+
}
|
|
1295
|
+
surfacedByGroup.get(groupId)[mode].push(card.cardId);
|
|
1296
|
+
}
|
|
1297
|
+
for (const group of this.config.groups) {
|
|
1298
|
+
const groupState = nextState.groups[group.id];
|
|
1299
|
+
const surfaced = surfacedByGroup.get(group.id);
|
|
1300
|
+
if (surfaced && (surfaced.targetIds.length > 0 || surfaced.supportIds.length > 0)) {
|
|
1301
|
+
groupState.lastSurfacedAt = isoNow();
|
|
1302
|
+
groupState.sessionsSinceSurfaced = 0;
|
|
1303
|
+
if (surfaced.supportIds.length > 0) {
|
|
1304
|
+
groupState.lastSupportAt = isoNow();
|
|
1189
1305
|
}
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1309
|
+
logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
|
|
1310
|
+
});
|
|
1192
1311
|
logger.info(
|
|
1193
|
-
`[Prescribed] Emitting ${
|
|
1312
|
+
`[Prescribed] Emitting ${finalCards.length} cards (${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=target")).length} target, ${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=support")).length} support)`
|
|
1194
1313
|
);
|
|
1314
|
+
return finalCards;
|
|
1315
|
+
}
|
|
1316
|
+
parseConfig(serializedData) {
|
|
1317
|
+
try {
|
|
1318
|
+
const parsed = JSON.parse(serializedData);
|
|
1319
|
+
const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
|
|
1320
|
+
const groups = groupsRaw.map((raw, i) => ({
|
|
1321
|
+
id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
|
|
1322
|
+
targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
|
|
1323
|
+
supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
|
|
1324
|
+
supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
|
|
1325
|
+
freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
|
|
1326
|
+
maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
|
|
1327
|
+
maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
|
|
1328
|
+
hierarchyWalk: {
|
|
1329
|
+
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
1330
|
+
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
1331
|
+
},
|
|
1332
|
+
retireOnEncounter: raw.retireOnEncounter !== false
|
|
1333
|
+
})).filter((g) => g.targetCardIds.length > 0);
|
|
1334
|
+
return { groups };
|
|
1335
|
+
} catch {
|
|
1336
|
+
return { groups: [] };
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
async loadHierarchyConfigs() {
|
|
1340
|
+
try {
|
|
1341
|
+
const strategies = await this.course.getNavigationStrategies();
|
|
1342
|
+
return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
|
|
1343
|
+
try {
|
|
1344
|
+
const parsed = JSON.parse(s.serializedData);
|
|
1345
|
+
return {
|
|
1346
|
+
prerequisites: parsed.prerequisites || {}
|
|
1347
|
+
};
|
|
1348
|
+
} catch {
|
|
1349
|
+
return { prerequisites: {} };
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
logger.debug(`[Prescribed] Failed to load hierarchy configs: ${e}`);
|
|
1354
|
+
return [];
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
buildGroupRuntimeState(args) {
|
|
1358
|
+
const {
|
|
1359
|
+
group,
|
|
1360
|
+
priorState,
|
|
1361
|
+
activeIds,
|
|
1362
|
+
seenIds,
|
|
1363
|
+
tagsByCard,
|
|
1364
|
+
hierarchyConfigs,
|
|
1365
|
+
userTagElo,
|
|
1366
|
+
userGlobalElo
|
|
1367
|
+
} = args;
|
|
1368
|
+
const encounteredTargets = /* @__PURE__ */ new Set();
|
|
1369
|
+
for (const cardId of group.targetCardIds) {
|
|
1370
|
+
if (activeIds.has(cardId) || seenIds.has(cardId)) {
|
|
1371
|
+
encounteredTargets.add(cardId);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (priorState?.encounteredCardIds?.length) {
|
|
1375
|
+
for (const cardId of priorState.encounteredCardIds) {
|
|
1376
|
+
encounteredTargets.add(cardId);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const pendingTargets = group.targetCardIds.filter((id) => !encounteredTargets.has(id));
|
|
1380
|
+
const targetTags = /* @__PURE__ */ new Map();
|
|
1381
|
+
for (const cardId of pendingTargets) {
|
|
1382
|
+
targetTags.set(cardId, tagsByCard.get(cardId) ?? []);
|
|
1383
|
+
}
|
|
1384
|
+
const blockedTargets = [];
|
|
1385
|
+
const surfaceableTargets = [];
|
|
1386
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1387
|
+
for (const cardId of pendingTargets) {
|
|
1388
|
+
const tags = targetTags.get(cardId) ?? [];
|
|
1389
|
+
const resolution = this.resolveBlockedSupportTags(
|
|
1390
|
+
tags,
|
|
1391
|
+
hierarchyConfigs,
|
|
1392
|
+
userTagElo,
|
|
1393
|
+
userGlobalElo,
|
|
1394
|
+
group.hierarchyWalk?.enabled !== false,
|
|
1395
|
+
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1396
|
+
);
|
|
1397
|
+
if (resolution.blocked) {
|
|
1398
|
+
blockedTargets.push(cardId);
|
|
1399
|
+
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1400
|
+
} else {
|
|
1401
|
+
surfaceableTargets.push(cardId);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
const supportCandidates = dedupe([
|
|
1405
|
+
...group.supportCardIds ?? [],
|
|
1406
|
+
...this.findSupportCardsByTags(
|
|
1407
|
+
group,
|
|
1408
|
+
tagsByCard,
|
|
1409
|
+
[...supportTags]
|
|
1410
|
+
)
|
|
1411
|
+
]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
|
|
1412
|
+
const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
|
|
1413
|
+
const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
1414
|
+
const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
|
|
1415
|
+
const pressureMultiplier = pendingTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.75 + Math.min(2, pendingTargets.length * 0.1), 1, MAX_TARGET_MULTIPLIER);
|
|
1416
|
+
const supportMultiplier = blockedTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.5 + Math.min(1.5, blockedTargets.length * 0.15), 1, MAX_SUPPORT_MULTIPLIER);
|
|
1417
|
+
return {
|
|
1418
|
+
group,
|
|
1419
|
+
encounteredTargets,
|
|
1420
|
+
pendingTargets,
|
|
1421
|
+
blockedTargets,
|
|
1422
|
+
surfaceableTargets,
|
|
1423
|
+
targetTags,
|
|
1424
|
+
supportCandidates,
|
|
1425
|
+
supportTags: [...supportTags],
|
|
1426
|
+
pressureMultiplier,
|
|
1427
|
+
supportMultiplier
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
buildNextGroupState(runtime, prior) {
|
|
1431
|
+
const carriedSessions = prior?.sessionsSinceSurfaced ?? 0;
|
|
1432
|
+
const surfacedThisRun = false;
|
|
1433
|
+
return {
|
|
1434
|
+
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1435
|
+
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1436
|
+
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1437
|
+
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
1438
|
+
blockedTargetIds: [...runtime.blockedTargets].sort(),
|
|
1439
|
+
lastResolvedSupportTags: [...runtime.supportTags].sort()
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
buildDirectTargetCards(runtime, courseId, emittedIds) {
|
|
1443
|
+
const maxDirect = runtime.group.maxDirectTargetsPerRun ?? DEFAULT_MAX_DIRECT_PER_RUN;
|
|
1444
|
+
const directIds = runtime.surfaceableTargets.filter((id) => !emittedIds.has(id)).slice(0, maxDirect);
|
|
1445
|
+
const cards = [];
|
|
1446
|
+
for (const cardId of directIds) {
|
|
1447
|
+
emittedIds.add(cardId);
|
|
1448
|
+
cards.push({
|
|
1449
|
+
cardId,
|
|
1450
|
+
courseId,
|
|
1451
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1452
|
+
provenance: [
|
|
1453
|
+
{
|
|
1454
|
+
strategy: "prescribed",
|
|
1455
|
+
strategyName: this.strategyName || this.name,
|
|
1456
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1457
|
+
action: "generated",
|
|
1458
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1459
|
+
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};multiplier=${runtime.pressureMultiplier.toFixed(2)}`
|
|
1460
|
+
}
|
|
1461
|
+
]
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
return cards;
|
|
1465
|
+
}
|
|
1466
|
+
buildSupportCards(runtime, courseId, emittedIds) {
|
|
1467
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportCandidates.length === 0) {
|
|
1468
|
+
return [];
|
|
1469
|
+
}
|
|
1470
|
+
const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
|
|
1471
|
+
const supportIds = runtime.supportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
|
|
1472
|
+
const cards = [];
|
|
1473
|
+
for (const cardId of supportIds) {
|
|
1474
|
+
emittedIds.add(cardId);
|
|
1475
|
+
cards.push({
|
|
1476
|
+
cardId,
|
|
1477
|
+
courseId,
|
|
1478
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1479
|
+
provenance: [
|
|
1480
|
+
{
|
|
1481
|
+
strategy: "prescribed",
|
|
1482
|
+
strategyName: this.strategyName || this.name,
|
|
1483
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1484
|
+
action: "generated",
|
|
1485
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1486
|
+
reason: `mode=support;group=${runtime.group.id};blocked=${runtime.blockedTargets.length};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)}`
|
|
1487
|
+
}
|
|
1488
|
+
]
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1195
1491
|
return cards;
|
|
1196
1492
|
}
|
|
1493
|
+
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
1494
|
+
if (supportTags.length === 0) {
|
|
1495
|
+
return [];
|
|
1496
|
+
}
|
|
1497
|
+
const explicitSupportIds = group.supportCardIds ?? [];
|
|
1498
|
+
const explicitPatterns = group.supportTagPatterns ?? [];
|
|
1499
|
+
if (explicitSupportIds.length === 0 && explicitPatterns.length === 0) {
|
|
1500
|
+
return [];
|
|
1501
|
+
}
|
|
1502
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1503
|
+
for (const cardId of explicitSupportIds) {
|
|
1504
|
+
const cardTags = tagsByCard.get(cardId) ?? [];
|
|
1505
|
+
const matchesResolved = supportTags.some((supportTag) => cardTags.includes(supportTag));
|
|
1506
|
+
const matchesPattern = explicitPatterns.some(
|
|
1507
|
+
(pattern) => cardTags.some((tag) => matchesTagPattern(tag, pattern))
|
|
1508
|
+
);
|
|
1509
|
+
if (matchesResolved || matchesPattern) {
|
|
1510
|
+
candidates.add(cardId);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return [...candidates];
|
|
1514
|
+
}
|
|
1515
|
+
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1516
|
+
if (!hierarchyWalkEnabled || targetTags.length === 0 || hierarchyConfigs.length === 0) {
|
|
1517
|
+
return {
|
|
1518
|
+
blocked: false,
|
|
1519
|
+
supportTags: []
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1523
|
+
let blocked = false;
|
|
1524
|
+
for (const targetTag of targetTags) {
|
|
1525
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1526
|
+
const prereqs = hierarchy.prerequisites[targetTag];
|
|
1527
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1528
|
+
const unmet = prereqs.filter(
|
|
1529
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1530
|
+
);
|
|
1531
|
+
if (unmet.length === 0) {
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
blocked = true;
|
|
1535
|
+
for (const prereq of unmet) {
|
|
1536
|
+
this.collectSupportTagsRecursive(
|
|
1537
|
+
prereq.tag,
|
|
1538
|
+
hierarchyConfigs,
|
|
1539
|
+
userTagElo,
|
|
1540
|
+
userGlobalElo,
|
|
1541
|
+
maxDepth,
|
|
1542
|
+
/* @__PURE__ */ new Set(),
|
|
1543
|
+
supportTags
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return { blocked, supportTags: [...supportTags] };
|
|
1549
|
+
}
|
|
1550
|
+
collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
|
|
1551
|
+
if (depth < 0 || visited.has(tag)) return;
|
|
1552
|
+
if (this.isHardGatedTag(tag)) return;
|
|
1553
|
+
visited.add(tag);
|
|
1554
|
+
let walkedFurther = false;
|
|
1555
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1556
|
+
const prereqs = hierarchy.prerequisites[tag];
|
|
1557
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1558
|
+
const unmet = prereqs.filter(
|
|
1559
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1560
|
+
);
|
|
1561
|
+
if (unmet.length > 0 && depth > 0) {
|
|
1562
|
+
walkedFurther = true;
|
|
1563
|
+
for (const prereq of unmet) {
|
|
1564
|
+
this.collectSupportTagsRecursive(
|
|
1565
|
+
prereq.tag,
|
|
1566
|
+
hierarchyConfigs,
|
|
1567
|
+
userTagElo,
|
|
1568
|
+
userGlobalElo,
|
|
1569
|
+
depth - 1,
|
|
1570
|
+
visited,
|
|
1571
|
+
out
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
if (!walkedFurther) {
|
|
1577
|
+
out.add(tag);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
isHardGatedTag(tag) {
|
|
1581
|
+
return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
|
|
1582
|
+
}
|
|
1583
|
+
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1584
|
+
if (!userTagElo) return false;
|
|
1585
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
1586
|
+
if (userTagElo.count < minCount) return false;
|
|
1587
|
+
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1588
|
+
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1589
|
+
}
|
|
1590
|
+
if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1591
|
+
return true;
|
|
1592
|
+
}
|
|
1593
|
+
return userTagElo.score >= userGlobalElo;
|
|
1594
|
+
}
|
|
1197
1595
|
};
|
|
1198
1596
|
}
|
|
1199
1597
|
});
|
|
@@ -1557,12 +1955,13 @@ __export(hierarchyDefinition_exports, {
|
|
|
1557
1955
|
default: () => HierarchyDefinitionNavigator
|
|
1558
1956
|
});
|
|
1559
1957
|
import { toCourseElo as toCourseElo3 } from "@vue-skuilder/common";
|
|
1560
|
-
var
|
|
1958
|
+
var DEFAULT_MIN_COUNT2, HierarchyDefinitionNavigator;
|
|
1561
1959
|
var init_hierarchyDefinition = __esm({
|
|
1562
1960
|
"src/core/navigators/filters/hierarchyDefinition.ts"() {
|
|
1563
1961
|
"use strict";
|
|
1564
1962
|
init_navigators();
|
|
1565
|
-
|
|
1963
|
+
init_logger();
|
|
1964
|
+
DEFAULT_MIN_COUNT2 = 3;
|
|
1566
1965
|
HierarchyDefinitionNavigator = class extends ContentNavigator {
|
|
1567
1966
|
config;
|
|
1568
1967
|
/** Human-readable name for CardFilter interface */
|
|
@@ -1589,7 +1988,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1589
1988
|
*/
|
|
1590
1989
|
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1591
1990
|
if (!userTagElo) return false;
|
|
1592
|
-
const minCount = prereq.masteryThreshold?.minCount ??
|
|
1991
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
|
|
1593
1992
|
if (userTagElo.count < minCount) return false;
|
|
1594
1993
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1595
1994
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
@@ -1690,18 +2089,55 @@ var init_hierarchyDefinition = __esm({
|
|
|
1690
2089
|
}
|
|
1691
2090
|
return boosts;
|
|
1692
2091
|
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Build a map of gated tag → max configured targetBoost for all *open* gates.
|
|
2094
|
+
*
|
|
2095
|
+
* When a gate opens (prereqs met), cards carrying the gated tag get boosted —
|
|
2096
|
+
* ensuring newly-unlocked content surfaces promptly. The boost is a static
|
|
2097
|
+
* multiplier; natural ELO/SRS deprioritization after interaction handles decay.
|
|
2098
|
+
*/
|
|
2099
|
+
getTargetBoosts(unlockedTags) {
|
|
2100
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
2101
|
+
const configKeys = Object.keys(this.config.prerequisites);
|
|
2102
|
+
const unlockedArr = [...unlockedTags];
|
|
2103
|
+
logger.info(
|
|
2104
|
+
`[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(", ")}${unlockedArr.length > 5 ? "..." : ""})`
|
|
2105
|
+
);
|
|
2106
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
2107
|
+
if (!unlockedTags.has(tagId)) continue;
|
|
2108
|
+
logger.info(
|
|
2109
|
+
`[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map((p) => ({ tag: p.tag, tb: p.targetBoost })))}`
|
|
2110
|
+
);
|
|
2111
|
+
for (const prereq of prereqs) {
|
|
2112
|
+
if (!prereq.targetBoost || prereq.targetBoost <= 1) continue;
|
|
2113
|
+
const existing = boosts.get(tagId) ?? 1;
|
|
2114
|
+
boosts.set(tagId, Math.max(existing, prereq.targetBoost));
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (boosts.size > 0) {
|
|
2118
|
+
logger.info(
|
|
2119
|
+
`[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=\xD7${b}`).join(", ")}`
|
|
2120
|
+
);
|
|
2121
|
+
} else {
|
|
2122
|
+
logger.info(
|
|
2123
|
+
`[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
return boosts;
|
|
2127
|
+
}
|
|
1693
2128
|
/**
|
|
1694
2129
|
* CardFilter.transform implementation.
|
|
1695
2130
|
*
|
|
1696
|
-
*
|
|
1697
|
-
* 1. Cards with locked tags receive score * 0.
|
|
1698
|
-
* 2. Cards carrying prereq tags of closed gates receive
|
|
1699
|
-
*
|
|
2131
|
+
* Three effects:
|
|
2132
|
+
* 1. Cards with locked tags receive score * 0.02 (gating penalty)
|
|
2133
|
+
* 2. Cards carrying prereq tags of closed gates receive preReqBoost
|
|
2134
|
+
* 3. Cards carrying gated tags of open gates receive targetBoost
|
|
1700
2135
|
*/
|
|
1701
2136
|
async transform(cards, context) {
|
|
1702
2137
|
const masteredTags = await this.getMasteredTags(context);
|
|
1703
2138
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1704
2139
|
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
2140
|
+
const targetBoosts = this.getTargetBoosts(unlockedTags);
|
|
1705
2141
|
const gated = [];
|
|
1706
2142
|
for (const card of cards) {
|
|
1707
2143
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1729,6 +2165,29 @@ var init_hierarchyDefinition = __esm({
|
|
|
1729
2165
|
finalScore *= maxBoost;
|
|
1730
2166
|
action = "boosted";
|
|
1731
2167
|
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
2168
|
+
logger.info(
|
|
2169
|
+
`[HierarchyDefinition] preReqBoost \xD7${maxBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedPrereqs.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
if (isUnlocked && targetBoosts.size > 0) {
|
|
2174
|
+
const cardTags = card.tags ?? [];
|
|
2175
|
+
let maxTargetBoost = 1;
|
|
2176
|
+
const boostedTargets = [];
|
|
2177
|
+
for (const tag of cardTags) {
|
|
2178
|
+
const boost = targetBoosts.get(tag);
|
|
2179
|
+
if (boost && boost > maxTargetBoost) {
|
|
2180
|
+
maxTargetBoost = boost;
|
|
2181
|
+
boostedTargets.push(tag);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (maxTargetBoost > 1) {
|
|
2185
|
+
finalScore *= maxTargetBoost;
|
|
2186
|
+
action = "boosted";
|
|
2187
|
+
finalReason = `${finalReason} | targetBoost \xD7${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(", ")}`;
|
|
2188
|
+
logger.info(
|
|
2189
|
+
`[HierarchyDefinition] targetBoost \xD7${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2190
|
+
);
|
|
1732
2191
|
}
|
|
1733
2192
|
}
|
|
1734
2193
|
gated.push({
|
|
@@ -1916,12 +2375,12 @@ __export(interferenceMitigator_exports, {
|
|
|
1916
2375
|
default: () => InterferenceMitigatorNavigator
|
|
1917
2376
|
});
|
|
1918
2377
|
import { toCourseElo as toCourseElo4 } from "@vue-skuilder/common";
|
|
1919
|
-
var
|
|
2378
|
+
var DEFAULT_MIN_COUNT3, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
|
|
1920
2379
|
var init_interferenceMitigator = __esm({
|
|
1921
2380
|
"src/core/navigators/filters/interferenceMitigator.ts"() {
|
|
1922
2381
|
"use strict";
|
|
1923
2382
|
init_navigators();
|
|
1924
|
-
|
|
2383
|
+
DEFAULT_MIN_COUNT3 = 10;
|
|
1925
2384
|
DEFAULT_MIN_ELAPSED_DAYS = 3;
|
|
1926
2385
|
DEFAULT_INTERFERENCE_DECAY = 0.8;
|
|
1927
2386
|
InterferenceMitigatorNavigator = class extends ContentNavigator {
|
|
@@ -1946,7 +2405,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1946
2405
|
return {
|
|
1947
2406
|
interferenceSets: sets,
|
|
1948
2407
|
maturityThreshold: {
|
|
1949
|
-
minCount: parsed.maturityThreshold?.minCount ??
|
|
2408
|
+
minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3,
|
|
1950
2409
|
minElo: parsed.maturityThreshold?.minElo,
|
|
1951
2410
|
minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
|
|
1952
2411
|
},
|
|
@@ -1956,7 +2415,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1956
2415
|
return {
|
|
1957
2416
|
interferenceSets: [],
|
|
1958
2417
|
maturityThreshold: {
|
|
1959
|
-
minCount:
|
|
2418
|
+
minCount: DEFAULT_MIN_COUNT3,
|
|
1960
2419
|
minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
|
|
1961
2420
|
},
|
|
1962
2421
|
defaultDecay: DEFAULT_INTERFERENCE_DECAY
|
|
@@ -2003,7 +2462,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2003
2462
|
try {
|
|
2004
2463
|
const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
|
|
2005
2464
|
const userElo = toCourseElo4(courseReg.elo);
|
|
2006
|
-
const minCount = this.config.maturityThreshold?.minCount ??
|
|
2465
|
+
const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3;
|
|
2007
2466
|
const minElo = this.config.maturityThreshold?.minElo;
|
|
2008
2467
|
const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
|
|
2009
2468
|
const minCountForElapsed = minElapsedDays * 2;
|
|
@@ -2238,7 +2697,7 @@ var init_relativePriority = __esm({
|
|
|
2238
2697
|
const cardTags = card.tags ?? [];
|
|
2239
2698
|
const priority = this.computeCardPriority(cardTags);
|
|
2240
2699
|
const boostFactor = this.computeBoostFactor(priority);
|
|
2241
|
-
const finalScore = Math.max(0,
|
|
2700
|
+
const finalScore = Math.max(0, card.score * boostFactor);
|
|
2242
2701
|
const action = boostFactor > 1 ? "boosted" : boostFactor < 1 ? "penalized" : "passed";
|
|
2243
2702
|
const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
|
|
2244
2703
|
return {
|
|
@@ -2258,170 +2717,1728 @@ var init_relativePriority = __esm({
|
|
|
2258
2717
|
};
|
|
2259
2718
|
})
|
|
2260
2719
|
);
|
|
2261
|
-
return adjusted;
|
|
2262
|
-
}
|
|
2720
|
+
return adjusted;
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Legacy getWeightedCards - now throws as filters should not be used as generators.
|
|
2724
|
+
*
|
|
2725
|
+
* Use transform() via Pipeline instead.
|
|
2726
|
+
*/
|
|
2727
|
+
async getWeightedCards(_limit) {
|
|
2728
|
+
throw new Error(
|
|
2729
|
+
"RelativePriorityNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
// src/core/navigators/filters/types.ts
|
|
2737
|
+
var types_exports2 = {};
|
|
2738
|
+
var init_types2 = __esm({
|
|
2739
|
+
"src/core/navigators/filters/types.ts"() {
|
|
2740
|
+
"use strict";
|
|
2741
|
+
}
|
|
2742
|
+
});
|
|
2743
|
+
|
|
2744
|
+
// src/core/navigators/filters/userGoalStub.ts
|
|
2745
|
+
var userGoalStub_exports = {};
|
|
2746
|
+
__export(userGoalStub_exports, {
|
|
2747
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2748
|
+
});
|
|
2749
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2750
|
+
var init_userGoalStub = __esm({
|
|
2751
|
+
"src/core/navigators/filters/userGoalStub.ts"() {
|
|
2752
|
+
"use strict";
|
|
2753
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2754
|
+
}
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2757
|
+
// import("./filters/**/*") in src/core/navigators/index.ts
|
|
2758
|
+
var globImport_filters;
|
|
2759
|
+
var init_2 = __esm({
|
|
2760
|
+
'import("./filters/**/*") in src/core/navigators/index.ts'() {
|
|
2761
|
+
globImport_filters = __glob({
|
|
2762
|
+
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
2763
|
+
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2764
|
+
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2765
|
+
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2766
|
+
"./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
|
|
2767
|
+
"./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2768
|
+
"./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2769
|
+
"./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2770
|
+
"./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
|
|
2771
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
});
|
|
2775
|
+
|
|
2776
|
+
// src/core/orchestration/gradient.ts
|
|
2777
|
+
var init_gradient = __esm({
|
|
2778
|
+
"src/core/orchestration/gradient.ts"() {
|
|
2779
|
+
"use strict";
|
|
2780
|
+
init_logger();
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
// src/core/orchestration/learning.ts
|
|
2785
|
+
var init_learning = __esm({
|
|
2786
|
+
"src/core/orchestration/learning.ts"() {
|
|
2787
|
+
"use strict";
|
|
2788
|
+
init_contentNavigationStrategy();
|
|
2789
|
+
init_types_legacy();
|
|
2790
|
+
init_logger();
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
2793
|
+
|
|
2794
|
+
// src/core/orchestration/signal.ts
|
|
2795
|
+
var init_signal = __esm({
|
|
2796
|
+
"src/core/orchestration/signal.ts"() {
|
|
2797
|
+
"use strict";
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
|
|
2801
|
+
// src/core/orchestration/recording.ts
|
|
2802
|
+
var init_recording = __esm({
|
|
2803
|
+
"src/core/orchestration/recording.ts"() {
|
|
2804
|
+
"use strict";
|
|
2805
|
+
init_signal();
|
|
2806
|
+
init_types_legacy();
|
|
2807
|
+
init_logger();
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
// src/core/orchestration/index.ts
|
|
2812
|
+
function fnv1a(str) {
|
|
2813
|
+
let hash = 2166136261;
|
|
2814
|
+
for (let i = 0; i < str.length; i++) {
|
|
2815
|
+
hash ^= str.charCodeAt(i);
|
|
2816
|
+
hash = Math.imul(hash, 16777619);
|
|
2817
|
+
}
|
|
2818
|
+
return hash >>> 0;
|
|
2819
|
+
}
|
|
2820
|
+
function computeDeviation(userId, strategyId, salt) {
|
|
2821
|
+
const input = `${userId}:${strategyId}:${salt}`;
|
|
2822
|
+
const hash = fnv1a(input);
|
|
2823
|
+
const normalized = hash / 4294967296;
|
|
2824
|
+
return normalized * 2 - 1;
|
|
2825
|
+
}
|
|
2826
|
+
function computeSpread(confidence) {
|
|
2827
|
+
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
2828
|
+
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
2829
|
+
}
|
|
2830
|
+
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
2831
|
+
const deviation = computeDeviation(userId, strategyId, salt);
|
|
2832
|
+
const spread = computeSpread(learnable.confidence);
|
|
2833
|
+
const adjustment = deviation * spread * learnable.weight;
|
|
2834
|
+
const effective = learnable.weight + adjustment;
|
|
2835
|
+
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
2836
|
+
}
|
|
2837
|
+
async function createOrchestrationContext(user, course) {
|
|
2838
|
+
let courseConfig;
|
|
2839
|
+
try {
|
|
2840
|
+
courseConfig = await course.getCourseConfig();
|
|
2841
|
+
} catch (e) {
|
|
2842
|
+
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
2843
|
+
courseConfig = {
|
|
2844
|
+
name: "Unknown",
|
|
2845
|
+
description: "",
|
|
2846
|
+
public: false,
|
|
2847
|
+
deleted: false,
|
|
2848
|
+
creator: "",
|
|
2849
|
+
admins: [],
|
|
2850
|
+
moderators: [],
|
|
2851
|
+
dataShapes: [],
|
|
2852
|
+
questionTypes: [],
|
|
2853
|
+
orchestration: { salt: "default" }
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
const userId = user.getUsername();
|
|
2857
|
+
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
2858
|
+
return {
|
|
2859
|
+
user,
|
|
2860
|
+
course,
|
|
2861
|
+
userId,
|
|
2862
|
+
courseConfig,
|
|
2863
|
+
getEffectiveWeight(strategyId, learnable) {
|
|
2864
|
+
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
2865
|
+
},
|
|
2866
|
+
getDeviation(strategyId) {
|
|
2867
|
+
return computeDeviation(userId, strategyId, salt);
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
2872
|
+
var init_orchestration = __esm({
|
|
2873
|
+
"src/core/orchestration/index.ts"() {
|
|
2874
|
+
"use strict";
|
|
2875
|
+
init_logger();
|
|
2876
|
+
init_gradient();
|
|
2877
|
+
init_learning();
|
|
2878
|
+
init_signal();
|
|
2879
|
+
init_recording();
|
|
2880
|
+
MIN_SPREAD = 0.1;
|
|
2881
|
+
MAX_SPREAD = 0.5;
|
|
2882
|
+
MIN_WEIGHT = 0.1;
|
|
2883
|
+
MAX_WEIGHT = 3;
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
|
|
2887
|
+
// src/study/SpacedRepetition.ts
|
|
2888
|
+
import moment4 from "moment";
|
|
2889
|
+
import { isTaggedPerformance } from "@vue-skuilder/common";
|
|
2890
|
+
var duration;
|
|
2891
|
+
var init_SpacedRepetition = __esm({
|
|
2892
|
+
"src/study/SpacedRepetition.ts"() {
|
|
2893
|
+
"use strict";
|
|
2894
|
+
init_util();
|
|
2895
|
+
init_logger();
|
|
2896
|
+
duration = moment4.duration;
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
|
|
2900
|
+
// src/study/services/SrsService.ts
|
|
2901
|
+
import moment5 from "moment";
|
|
2902
|
+
var init_SrsService = __esm({
|
|
2903
|
+
"src/study/services/SrsService.ts"() {
|
|
2904
|
+
"use strict";
|
|
2905
|
+
init_couch();
|
|
2906
|
+
init_SpacedRepetition();
|
|
2907
|
+
init_logger();
|
|
2908
|
+
}
|
|
2909
|
+
});
|
|
2910
|
+
|
|
2911
|
+
// src/study/services/EloService.ts
|
|
2912
|
+
import {
|
|
2913
|
+
adjustCourseScores,
|
|
2914
|
+
adjustCourseScoresPerTag,
|
|
2915
|
+
toCourseElo as toCourseElo5
|
|
2916
|
+
} from "@vue-skuilder/common";
|
|
2917
|
+
var init_EloService = __esm({
|
|
2918
|
+
"src/study/services/EloService.ts"() {
|
|
2919
|
+
"use strict";
|
|
2920
|
+
init_logger();
|
|
2921
|
+
}
|
|
2922
|
+
});
|
|
2923
|
+
|
|
2924
|
+
// src/study/services/ResponseProcessor.ts
|
|
2925
|
+
import { isTaggedPerformance as isTaggedPerformance2 } from "@vue-skuilder/common";
|
|
2926
|
+
var init_ResponseProcessor = __esm({
|
|
2927
|
+
"src/study/services/ResponseProcessor.ts"() {
|
|
2928
|
+
"use strict";
|
|
2929
|
+
init_core();
|
|
2930
|
+
init_logger();
|
|
2931
|
+
}
|
|
2932
|
+
});
|
|
2933
|
+
|
|
2934
|
+
// src/study/services/CardHydrationService.ts
|
|
2935
|
+
import {
|
|
2936
|
+
displayableDataToViewData,
|
|
2937
|
+
isCourseElo,
|
|
2938
|
+
toCourseElo as toCourseElo6
|
|
2939
|
+
} from "@vue-skuilder/common";
|
|
2940
|
+
var init_CardHydrationService = __esm({
|
|
2941
|
+
"src/study/services/CardHydrationService.ts"() {
|
|
2942
|
+
"use strict";
|
|
2943
|
+
init_logger();
|
|
2944
|
+
}
|
|
2945
|
+
});
|
|
2946
|
+
|
|
2947
|
+
// src/study/ItemQueue.ts
|
|
2948
|
+
var init_ItemQueue = __esm({
|
|
2949
|
+
"src/study/ItemQueue.ts"() {
|
|
2950
|
+
"use strict";
|
|
2951
|
+
}
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2954
|
+
// src/util/packer/types.ts
|
|
2955
|
+
var init_types3 = __esm({
|
|
2956
|
+
"src/util/packer/types.ts"() {
|
|
2957
|
+
"use strict";
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
|
|
2961
|
+
// src/util/packer/CouchDBToStaticPacker.ts
|
|
2962
|
+
var init_CouchDBToStaticPacker = __esm({
|
|
2963
|
+
"src/util/packer/CouchDBToStaticPacker.ts"() {
|
|
2964
|
+
"use strict";
|
|
2965
|
+
init_types_legacy();
|
|
2966
|
+
init_logger();
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2969
|
+
|
|
2970
|
+
// src/util/packer/index.ts
|
|
2971
|
+
var init_packer = __esm({
|
|
2972
|
+
"src/util/packer/index.ts"() {
|
|
2973
|
+
"use strict";
|
|
2974
|
+
init_types3();
|
|
2975
|
+
init_CouchDBToStaticPacker();
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
// src/util/migrator/types.ts
|
|
2980
|
+
var DEFAULT_MIGRATION_OPTIONS;
|
|
2981
|
+
var init_types4 = __esm({
|
|
2982
|
+
"src/util/migrator/types.ts"() {
|
|
2983
|
+
"use strict";
|
|
2984
|
+
DEFAULT_MIGRATION_OPTIONS = {
|
|
2985
|
+
chunkBatchSize: 100,
|
|
2986
|
+
validateRoundTrip: false,
|
|
2987
|
+
cleanupOnFailure: true,
|
|
2988
|
+
timeout: 3e5
|
|
2989
|
+
// 5 minutes
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
|
|
2994
|
+
// src/util/migrator/FileSystemAdapter.ts
|
|
2995
|
+
var FileSystemError;
|
|
2996
|
+
var init_FileSystemAdapter = __esm({
|
|
2997
|
+
"src/util/migrator/FileSystemAdapter.ts"() {
|
|
2998
|
+
"use strict";
|
|
2999
|
+
FileSystemError = class extends Error {
|
|
3000
|
+
constructor(message, operation, filePath, cause) {
|
|
3001
|
+
super(message);
|
|
3002
|
+
this.operation = operation;
|
|
3003
|
+
this.filePath = filePath;
|
|
3004
|
+
this.cause = cause;
|
|
3005
|
+
this.name = "FileSystemError";
|
|
3006
|
+
}
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
|
|
3011
|
+
// src/util/migrator/validation.ts
|
|
3012
|
+
async function validateStaticCourse(staticPath, fs) {
|
|
3013
|
+
const validation = {
|
|
3014
|
+
valid: true,
|
|
3015
|
+
manifestExists: false,
|
|
3016
|
+
chunksExist: false,
|
|
3017
|
+
attachmentsExist: false,
|
|
3018
|
+
errors: [],
|
|
3019
|
+
warnings: []
|
|
3020
|
+
};
|
|
3021
|
+
try {
|
|
3022
|
+
if (fs) {
|
|
3023
|
+
const stats = await fs.stat(staticPath);
|
|
3024
|
+
if (!stats.isDirectory()) {
|
|
3025
|
+
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3026
|
+
validation.valid = false;
|
|
3027
|
+
return validation;
|
|
3028
|
+
}
|
|
3029
|
+
} else if (!nodeFS) {
|
|
3030
|
+
validation.errors.push("File system access not available - validation skipped");
|
|
3031
|
+
validation.valid = false;
|
|
3032
|
+
return validation;
|
|
3033
|
+
} else {
|
|
3034
|
+
const stats = await nodeFS.promises.stat(staticPath);
|
|
3035
|
+
if (!stats.isDirectory()) {
|
|
3036
|
+
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3037
|
+
validation.valid = false;
|
|
3038
|
+
return validation;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
let manifestPath = `${staticPath}/manifest.json`;
|
|
3042
|
+
try {
|
|
3043
|
+
if (fs) {
|
|
3044
|
+
manifestPath = fs.joinPath(staticPath, "manifest.json");
|
|
3045
|
+
if (await fs.exists(manifestPath)) {
|
|
3046
|
+
validation.manifestExists = true;
|
|
3047
|
+
const manifestContent = await fs.readFile(manifestPath);
|
|
3048
|
+
const manifest = JSON.parse(manifestContent);
|
|
3049
|
+
validation.courseId = manifest.courseId;
|
|
3050
|
+
validation.courseName = manifest.courseName;
|
|
3051
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3052
|
+
validation.errors.push("Invalid manifest structure");
|
|
3053
|
+
validation.valid = false;
|
|
3054
|
+
}
|
|
3055
|
+
} else {
|
|
3056
|
+
validation.errors.push(`Manifest not found: ${manifestPath}`);
|
|
3057
|
+
validation.valid = false;
|
|
3058
|
+
}
|
|
3059
|
+
} else {
|
|
3060
|
+
manifestPath = `${staticPath}/manifest.json`;
|
|
3061
|
+
await nodeFS.promises.access(manifestPath);
|
|
3062
|
+
validation.manifestExists = true;
|
|
3063
|
+
const manifestContent = await nodeFS.promises.readFile(manifestPath, "utf8");
|
|
3064
|
+
const manifest = JSON.parse(manifestContent);
|
|
3065
|
+
validation.courseId = manifest.courseId;
|
|
3066
|
+
validation.courseName = manifest.courseName;
|
|
3067
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3068
|
+
validation.errors.push("Invalid manifest structure");
|
|
3069
|
+
validation.valid = false;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Manifest not found or invalid: ${manifestPath}`;
|
|
3074
|
+
validation.errors.push(errorMessage);
|
|
3075
|
+
validation.valid = false;
|
|
3076
|
+
}
|
|
3077
|
+
let chunksPath = `${staticPath}/chunks`;
|
|
3078
|
+
try {
|
|
3079
|
+
if (fs) {
|
|
3080
|
+
chunksPath = fs.joinPath(staticPath, "chunks");
|
|
3081
|
+
if (await fs.exists(chunksPath)) {
|
|
3082
|
+
const chunksStats = await fs.stat(chunksPath);
|
|
3083
|
+
if (chunksStats.isDirectory()) {
|
|
3084
|
+
validation.chunksExist = true;
|
|
3085
|
+
} else {
|
|
3086
|
+
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3087
|
+
validation.valid = false;
|
|
3088
|
+
}
|
|
3089
|
+
} else {
|
|
3090
|
+
validation.errors.push(`Chunks directory not found: ${chunksPath}`);
|
|
3091
|
+
validation.valid = false;
|
|
3092
|
+
}
|
|
3093
|
+
} else {
|
|
3094
|
+
chunksPath = `${staticPath}/chunks`;
|
|
3095
|
+
const chunksStats = await nodeFS.promises.stat(chunksPath);
|
|
3096
|
+
if (chunksStats.isDirectory()) {
|
|
3097
|
+
validation.chunksExist = true;
|
|
3098
|
+
} else {
|
|
3099
|
+
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3100
|
+
validation.valid = false;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
} catch (error) {
|
|
3104
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Chunks directory not found: ${chunksPath}`;
|
|
3105
|
+
validation.errors.push(errorMessage);
|
|
3106
|
+
validation.valid = false;
|
|
3107
|
+
}
|
|
3108
|
+
let attachmentsPath;
|
|
3109
|
+
try {
|
|
3110
|
+
if (fs) {
|
|
3111
|
+
attachmentsPath = fs.joinPath(staticPath, "attachments");
|
|
3112
|
+
if (await fs.exists(attachmentsPath)) {
|
|
3113
|
+
const attachmentsStats = await fs.stat(attachmentsPath);
|
|
3114
|
+
if (attachmentsStats.isDirectory()) {
|
|
3115
|
+
validation.attachmentsExist = true;
|
|
3116
|
+
}
|
|
3117
|
+
} else {
|
|
3118
|
+
validation.warnings.push(
|
|
3119
|
+
`Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
} else {
|
|
3123
|
+
attachmentsPath = `${staticPath}/attachments`;
|
|
3124
|
+
const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
|
|
3125
|
+
if (attachmentsStats.isDirectory()) {
|
|
3126
|
+
validation.attachmentsExist = true;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
attachmentsPath = attachmentsPath || `${staticPath}/attachments`;
|
|
3131
|
+
const warningMessage = error instanceof FileSystemError ? error.message : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
|
|
3132
|
+
validation.warnings.push(warningMessage);
|
|
3133
|
+
}
|
|
3134
|
+
} catch (error) {
|
|
3135
|
+
validation.errors.push(
|
|
3136
|
+
`Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
|
|
3137
|
+
);
|
|
3138
|
+
validation.valid = false;
|
|
3139
|
+
}
|
|
3140
|
+
return validation;
|
|
3141
|
+
}
|
|
3142
|
+
async function validateMigration(targetDB, expectedCounts, manifest) {
|
|
3143
|
+
const validation = {
|
|
3144
|
+
valid: true,
|
|
3145
|
+
documentCountMatch: false,
|
|
3146
|
+
attachmentIntegrity: false,
|
|
3147
|
+
viewFunctionality: false,
|
|
3148
|
+
issues: []
|
|
3149
|
+
};
|
|
3150
|
+
try {
|
|
3151
|
+
logger.info("Starting migration validation...");
|
|
3152
|
+
const actualCounts = await getActualDocumentCounts(targetDB);
|
|
3153
|
+
validation.documentCountMatch = compareDocumentCounts(
|
|
3154
|
+
expectedCounts,
|
|
3155
|
+
actualCounts,
|
|
3156
|
+
validation.issues
|
|
3157
|
+
);
|
|
3158
|
+
await validateCourseConfig(targetDB, manifest, validation.issues);
|
|
3159
|
+
validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
|
|
3160
|
+
validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
|
|
3161
|
+
validation.valid = validation.documentCountMatch && validation.viewFunctionality && validation.attachmentIntegrity;
|
|
3162
|
+
logger.info(`Migration validation completed. Valid: ${validation.valid}`);
|
|
3163
|
+
if (validation.issues.length > 0) {
|
|
3164
|
+
logger.info(`Validation issues: ${validation.issues.length}`);
|
|
3165
|
+
validation.issues.forEach((issue) => {
|
|
3166
|
+
if (issue.type === "error") {
|
|
3167
|
+
logger.error(`${issue.category}: ${issue.message}`);
|
|
3168
|
+
} else {
|
|
3169
|
+
logger.warn(`${issue.category}: ${issue.message}`);
|
|
3170
|
+
}
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
} catch (error) {
|
|
3174
|
+
validation.valid = false;
|
|
3175
|
+
validation.issues.push({
|
|
3176
|
+
type: "error",
|
|
3177
|
+
category: "metadata",
|
|
3178
|
+
message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
return validation;
|
|
3182
|
+
}
|
|
3183
|
+
async function getActualDocumentCounts(db) {
|
|
3184
|
+
const counts = {};
|
|
3185
|
+
try {
|
|
3186
|
+
const allDocs = await db.allDocs({ include_docs: true });
|
|
3187
|
+
for (const row of allDocs.rows) {
|
|
3188
|
+
if (row.id.startsWith("_design/")) {
|
|
3189
|
+
counts["_design"] = (counts["_design"] || 0) + 1;
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
const doc = row.doc;
|
|
3193
|
+
if (doc && doc.docType) {
|
|
3194
|
+
counts[doc.docType] = (counts[doc.docType] || 0) + 1;
|
|
3195
|
+
} else {
|
|
3196
|
+
counts["unknown"] = (counts["unknown"] || 0) + 1;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
logger.error("Failed to get actual document counts:", error);
|
|
3201
|
+
}
|
|
3202
|
+
return counts;
|
|
3203
|
+
}
|
|
3204
|
+
function compareDocumentCounts(expected, actual, issues) {
|
|
3205
|
+
let countsMatch = true;
|
|
3206
|
+
for (const [docType, expectedCount] of Object.entries(expected)) {
|
|
3207
|
+
const actualCount = actual[docType] || 0;
|
|
3208
|
+
if (actualCount !== expectedCount) {
|
|
3209
|
+
countsMatch = false;
|
|
3210
|
+
issues.push({
|
|
3211
|
+
type: "error",
|
|
3212
|
+
category: "documents",
|
|
3213
|
+
message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
for (const [docType, actualCount] of Object.entries(actual)) {
|
|
3218
|
+
if (!expected[docType] && docType !== "_design") {
|
|
3219
|
+
issues.push({
|
|
3220
|
+
type: "warning",
|
|
3221
|
+
category: "documents",
|
|
3222
|
+
message: `Unexpected document type found: ${docType} (${actualCount} documents)`
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
return countsMatch;
|
|
3227
|
+
}
|
|
3228
|
+
async function validateCourseConfig(db, manifest, issues) {
|
|
3229
|
+
try {
|
|
3230
|
+
const courseConfig = await db.get("CourseConfig");
|
|
3231
|
+
if (!courseConfig) {
|
|
3232
|
+
issues.push({
|
|
3233
|
+
type: "error",
|
|
3234
|
+
category: "course_config",
|
|
3235
|
+
message: "CourseConfig document not found after migration"
|
|
3236
|
+
});
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
if (!courseConfig.courseID) {
|
|
3240
|
+
issues.push({
|
|
3241
|
+
type: "warning",
|
|
3242
|
+
category: "course_config",
|
|
3243
|
+
message: "CourseConfig document missing courseID field"
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
if (courseConfig.courseID !== manifest.courseId) {
|
|
3247
|
+
issues.push({
|
|
3248
|
+
type: "warning",
|
|
3249
|
+
category: "course_config",
|
|
3250
|
+
message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${courseConfig.courseID}`
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
logger.debug("CourseConfig document validation passed");
|
|
3254
|
+
} catch (error) {
|
|
3255
|
+
if (error.status === 404) {
|
|
3256
|
+
issues.push({
|
|
3257
|
+
type: "error",
|
|
3258
|
+
category: "course_config",
|
|
3259
|
+
message: "CourseConfig document not found in database"
|
|
3260
|
+
});
|
|
3261
|
+
} else {
|
|
3262
|
+
issues.push({
|
|
3263
|
+
type: "error",
|
|
3264
|
+
category: "course_config",
|
|
3265
|
+
message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
async function validateViews(db, manifest, issues) {
|
|
3271
|
+
let viewsValid = true;
|
|
3272
|
+
try {
|
|
3273
|
+
for (const designDoc of manifest.designDocs) {
|
|
3274
|
+
try {
|
|
3275
|
+
const doc = await db.get(designDoc._id);
|
|
3276
|
+
if (!doc) {
|
|
3277
|
+
viewsValid = false;
|
|
3278
|
+
issues.push({
|
|
3279
|
+
type: "error",
|
|
3280
|
+
category: "views",
|
|
3281
|
+
message: `Design document not found: ${designDoc._id}`
|
|
3282
|
+
});
|
|
3283
|
+
continue;
|
|
3284
|
+
}
|
|
3285
|
+
for (const viewName of Object.keys(designDoc.views)) {
|
|
3286
|
+
try {
|
|
3287
|
+
const viewPath = `${designDoc._id}/${viewName}`;
|
|
3288
|
+
await db.query(viewPath, { limit: 1 });
|
|
3289
|
+
} catch (viewError) {
|
|
3290
|
+
viewsValid = false;
|
|
3291
|
+
issues.push({
|
|
3292
|
+
type: "error",
|
|
3293
|
+
category: "views",
|
|
3294
|
+
message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
} catch (error) {
|
|
3299
|
+
viewsValid = false;
|
|
3300
|
+
issues.push({
|
|
3301
|
+
type: "error",
|
|
3302
|
+
category: "views",
|
|
3303
|
+
message: `Failed to validate design document ${designDoc._id}: ${error}`
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
} catch (error) {
|
|
3308
|
+
viewsValid = false;
|
|
3309
|
+
issues.push({
|
|
3310
|
+
type: "error",
|
|
3311
|
+
category: "views",
|
|
3312
|
+
message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
return viewsValid;
|
|
3316
|
+
}
|
|
3317
|
+
async function validateAttachmentIntegrity(db, issues) {
|
|
3318
|
+
let attachmentsValid = true;
|
|
3319
|
+
try {
|
|
3320
|
+
const allDocs = await db.allDocs({
|
|
3321
|
+
include_docs: true,
|
|
3322
|
+
limit: 10
|
|
3323
|
+
// Sample first 10 documents for performance
|
|
3324
|
+
});
|
|
3325
|
+
let attachmentCount = 0;
|
|
3326
|
+
let validAttachments = 0;
|
|
3327
|
+
for (const row of allDocs.rows) {
|
|
3328
|
+
const doc = row.doc;
|
|
3329
|
+
if (doc && doc._attachments) {
|
|
3330
|
+
for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3331
|
+
attachmentCount++;
|
|
3332
|
+
try {
|
|
3333
|
+
const attachment = await db.getAttachment(doc._id, attachmentName);
|
|
3334
|
+
if (attachment) {
|
|
3335
|
+
validAttachments++;
|
|
3336
|
+
}
|
|
3337
|
+
} catch (attachmentError) {
|
|
3338
|
+
attachmentsValid = false;
|
|
3339
|
+
issues.push({
|
|
3340
|
+
type: "error",
|
|
3341
|
+
category: "attachments",
|
|
3342
|
+
message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
if (attachmentCount === 0) {
|
|
3349
|
+
issues.push({
|
|
3350
|
+
type: "warning",
|
|
3351
|
+
category: "attachments",
|
|
3352
|
+
message: "No attachments found in sampled documents"
|
|
3353
|
+
});
|
|
3354
|
+
} else {
|
|
3355
|
+
logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
|
|
3356
|
+
}
|
|
3357
|
+
} catch (error) {
|
|
3358
|
+
attachmentsValid = false;
|
|
3359
|
+
issues.push({
|
|
3360
|
+
type: "error",
|
|
3361
|
+
category: "attachments",
|
|
3362
|
+
message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
return attachmentsValid;
|
|
3366
|
+
}
|
|
3367
|
+
var nodeFS;
|
|
3368
|
+
var init_validation = __esm({
|
|
3369
|
+
"src/util/migrator/validation.ts"() {
|
|
3370
|
+
"use strict";
|
|
3371
|
+
init_logger();
|
|
3372
|
+
init_FileSystemAdapter();
|
|
3373
|
+
nodeFS = null;
|
|
3374
|
+
try {
|
|
3375
|
+
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3376
|
+
nodeFS = eval("require")("fs");
|
|
3377
|
+
nodeFS.promises = nodeFS.promises || eval("require")("fs").promises;
|
|
3378
|
+
}
|
|
3379
|
+
} catch {
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
});
|
|
3383
|
+
|
|
3384
|
+
// src/util/migrator/StaticToCouchDBMigrator.ts
|
|
3385
|
+
var nodeFS2, nodePath, StaticToCouchDBMigrator;
|
|
3386
|
+
var init_StaticToCouchDBMigrator = __esm({
|
|
3387
|
+
"src/util/migrator/StaticToCouchDBMigrator.ts"() {
|
|
3388
|
+
"use strict";
|
|
3389
|
+
init_logger();
|
|
3390
|
+
init_types4();
|
|
3391
|
+
init_validation();
|
|
3392
|
+
init_FileSystemAdapter();
|
|
3393
|
+
nodeFS2 = null;
|
|
3394
|
+
nodePath = null;
|
|
3395
|
+
try {
|
|
3396
|
+
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3397
|
+
nodeFS2 = eval("require")("fs");
|
|
3398
|
+
nodePath = eval("require")("path");
|
|
3399
|
+
nodeFS2.promises = nodeFS2.promises || eval("require")("fs").promises;
|
|
3400
|
+
}
|
|
3401
|
+
} catch {
|
|
3402
|
+
}
|
|
3403
|
+
StaticToCouchDBMigrator = class {
|
|
3404
|
+
options;
|
|
3405
|
+
progressCallback;
|
|
3406
|
+
fs;
|
|
3407
|
+
constructor(options = {}, fileSystemAdapter) {
|
|
3408
|
+
this.options = {
|
|
3409
|
+
...DEFAULT_MIGRATION_OPTIONS,
|
|
3410
|
+
...options
|
|
3411
|
+
};
|
|
3412
|
+
this.fs = fileSystemAdapter;
|
|
3413
|
+
}
|
|
3414
|
+
/**
|
|
3415
|
+
* Set a progress callback to receive updates during migration
|
|
3416
|
+
*/
|
|
3417
|
+
setProgressCallback(callback) {
|
|
3418
|
+
this.progressCallback = callback;
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Migrate a static course to CouchDB
|
|
3422
|
+
*/
|
|
3423
|
+
async migrateCourse(staticPath, targetDB) {
|
|
3424
|
+
const startTime = Date.now();
|
|
3425
|
+
const result = {
|
|
3426
|
+
success: false,
|
|
3427
|
+
documentsRestored: 0,
|
|
3428
|
+
attachmentsRestored: 0,
|
|
3429
|
+
designDocsRestored: 0,
|
|
3430
|
+
courseConfigRestored: 0,
|
|
3431
|
+
errors: [],
|
|
3432
|
+
warnings: [],
|
|
3433
|
+
migrationTime: 0
|
|
3434
|
+
};
|
|
3435
|
+
try {
|
|
3436
|
+
logger.info(`Starting migration from ${staticPath} to CouchDB`);
|
|
3437
|
+
this.reportProgress("manifest", 0, 1, "Validating static course...");
|
|
3438
|
+
const validation = await validateStaticCourse(staticPath, this.fs);
|
|
3439
|
+
if (!validation.valid) {
|
|
3440
|
+
result.errors.push(...validation.errors);
|
|
3441
|
+
throw new Error(`Static course validation failed: ${validation.errors.join(", ")}`);
|
|
3442
|
+
}
|
|
3443
|
+
result.warnings.push(...validation.warnings);
|
|
3444
|
+
this.reportProgress("manifest", 1, 1, "Loading course manifest...");
|
|
3445
|
+
const manifest = await this.loadManifest(staticPath);
|
|
3446
|
+
logger.info(`Loaded manifest for course: ${manifest.courseId} (${manifest.courseName})`);
|
|
3447
|
+
this.reportProgress(
|
|
3448
|
+
"design_docs",
|
|
3449
|
+
0,
|
|
3450
|
+
manifest.designDocs.length,
|
|
3451
|
+
"Restoring design documents..."
|
|
3452
|
+
);
|
|
3453
|
+
const designDocResults = await this.restoreDesignDocuments(manifest.designDocs, targetDB);
|
|
3454
|
+
result.designDocsRestored = designDocResults.restored;
|
|
3455
|
+
result.errors.push(...designDocResults.errors);
|
|
3456
|
+
result.warnings.push(...designDocResults.warnings);
|
|
3457
|
+
this.reportProgress("course_config", 0, 1, "Restoring CourseConfig document...");
|
|
3458
|
+
const courseConfigResults = await this.restoreCourseConfig(manifest, targetDB);
|
|
3459
|
+
result.courseConfigRestored = courseConfigResults.restored;
|
|
3460
|
+
result.errors.push(...courseConfigResults.errors);
|
|
3461
|
+
result.warnings.push(...courseConfigResults.warnings);
|
|
3462
|
+
this.reportProgress("course_config", 1, 1, "CourseConfig document restored");
|
|
3463
|
+
const expectedCounts = this.calculateExpectedCounts(manifest);
|
|
3464
|
+
this.reportProgress(
|
|
3465
|
+
"documents",
|
|
3466
|
+
0,
|
|
3467
|
+
manifest.documentCount,
|
|
3468
|
+
"Aggregating documents from chunks..."
|
|
3469
|
+
);
|
|
3470
|
+
const documents = await this.aggregateDocuments(staticPath, manifest);
|
|
3471
|
+
const filteredDocuments = documents.filter((doc) => doc._id !== "CourseConfig");
|
|
3472
|
+
if (documents.length !== filteredDocuments.length) {
|
|
3473
|
+
result.warnings.push(
|
|
3474
|
+
`Filtered out ${documents.length - filteredDocuments.length} CourseConfig document(s) from chunks to prevent conflicts`
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
this.reportProgress(
|
|
3478
|
+
"documents",
|
|
3479
|
+
filteredDocuments.length,
|
|
3480
|
+
manifest.documentCount,
|
|
3481
|
+
"Uploading documents to CouchDB..."
|
|
3482
|
+
);
|
|
3483
|
+
const docResults = await this.uploadDocuments(filteredDocuments, targetDB);
|
|
3484
|
+
result.documentsRestored = docResults.restored;
|
|
3485
|
+
result.errors.push(...docResults.errors);
|
|
3486
|
+
result.warnings.push(...docResults.warnings);
|
|
3487
|
+
const docsWithAttachments = documents.filter(
|
|
3488
|
+
(doc) => doc._attachments && Object.keys(doc._attachments).length > 0
|
|
3489
|
+
);
|
|
3490
|
+
this.reportProgress("attachments", 0, docsWithAttachments.length, "Uploading attachments...");
|
|
3491
|
+
const attachmentResults = await this.uploadAttachments(
|
|
3492
|
+
staticPath,
|
|
3493
|
+
docsWithAttachments,
|
|
3494
|
+
targetDB
|
|
3495
|
+
);
|
|
3496
|
+
result.attachmentsRestored = attachmentResults.restored;
|
|
3497
|
+
result.errors.push(...attachmentResults.errors);
|
|
3498
|
+
result.warnings.push(...attachmentResults.warnings);
|
|
3499
|
+
if (this.options.validateRoundTrip) {
|
|
3500
|
+
this.reportProgress("validation", 0, 1, "Validating migration...");
|
|
3501
|
+
const validationResult = await validateMigration(targetDB, expectedCounts, manifest);
|
|
3502
|
+
if (!validationResult.valid) {
|
|
3503
|
+
result.warnings.push("Migration validation found issues");
|
|
3504
|
+
validationResult.issues.forEach((issue) => {
|
|
3505
|
+
if (issue.type === "error") {
|
|
3506
|
+
result.errors.push(`Validation: ${issue.message}`);
|
|
3507
|
+
} else {
|
|
3508
|
+
result.warnings.push(`Validation: ${issue.message}`);
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
this.reportProgress("validation", 1, 1, "Migration validation completed");
|
|
3513
|
+
}
|
|
3514
|
+
result.success = result.errors.length === 0;
|
|
3515
|
+
result.migrationTime = Date.now() - startTime;
|
|
3516
|
+
logger.info(`Migration completed in ${result.migrationTime}ms`);
|
|
3517
|
+
logger.info(`Documents restored: ${result.documentsRestored}`);
|
|
3518
|
+
logger.info(`Attachments restored: ${result.attachmentsRestored}`);
|
|
3519
|
+
logger.info(`Design docs restored: ${result.designDocsRestored}`);
|
|
3520
|
+
logger.info(`CourseConfig restored: ${result.courseConfigRestored}`);
|
|
3521
|
+
if (result.errors.length > 0) {
|
|
3522
|
+
logger.error(`Migration completed with ${result.errors.length} errors`);
|
|
3523
|
+
}
|
|
3524
|
+
if (result.warnings.length > 0) {
|
|
3525
|
+
logger.warn(`Migration completed with ${result.warnings.length} warnings`);
|
|
3526
|
+
}
|
|
3527
|
+
} catch (error) {
|
|
3528
|
+
result.success = false;
|
|
3529
|
+
result.migrationTime = Date.now() - startTime;
|
|
3530
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3531
|
+
result.errors.push(`Migration failed: ${errorMessage}`);
|
|
3532
|
+
logger.error("Migration failed:", error);
|
|
3533
|
+
if (this.options.cleanupOnFailure) {
|
|
3534
|
+
try {
|
|
3535
|
+
await this.cleanupFailedMigration(targetDB);
|
|
3536
|
+
} catch (cleanupError) {
|
|
3537
|
+
logger.error("Failed to cleanup after migration failure:", cleanupError);
|
|
3538
|
+
result.warnings.push("Failed to cleanup after migration failure");
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
return result;
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* Load and parse the manifest file
|
|
3546
|
+
*/
|
|
3547
|
+
async loadManifest(staticPath) {
|
|
3548
|
+
try {
|
|
3549
|
+
let manifestContent;
|
|
3550
|
+
let manifestPath;
|
|
3551
|
+
if (this.fs) {
|
|
3552
|
+
manifestPath = this.fs.joinPath(staticPath, "manifest.json");
|
|
3553
|
+
manifestContent = await this.fs.readFile(manifestPath);
|
|
3554
|
+
} else {
|
|
3555
|
+
manifestPath = nodeFS2 && nodePath ? nodePath.join(staticPath, "manifest.json") : `${staticPath}/manifest.json`;
|
|
3556
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3557
|
+
manifestContent = await nodeFS2.promises.readFile(manifestPath, "utf8");
|
|
3558
|
+
} else {
|
|
3559
|
+
const response = await fetch(manifestPath);
|
|
3560
|
+
if (!response.ok) {
|
|
3561
|
+
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
3562
|
+
}
|
|
3563
|
+
manifestContent = await response.text();
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
const manifest = JSON.parse(manifestContent);
|
|
3567
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks) {
|
|
3568
|
+
throw new Error("Invalid manifest structure");
|
|
3569
|
+
}
|
|
3570
|
+
return manifest;
|
|
3571
|
+
} catch (error) {
|
|
3572
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`;
|
|
3573
|
+
throw new Error(errorMessage);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* Restore design documents to CouchDB
|
|
3578
|
+
*/
|
|
3579
|
+
async restoreDesignDocuments(designDocs, db) {
|
|
3580
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3581
|
+
for (let i = 0; i < designDocs.length; i++) {
|
|
3582
|
+
const designDoc = designDocs[i];
|
|
3583
|
+
this.reportProgress("design_docs", i, designDocs.length, `Restoring ${designDoc._id}...`);
|
|
3584
|
+
try {
|
|
3585
|
+
let existingDoc;
|
|
3586
|
+
try {
|
|
3587
|
+
existingDoc = await db.get(designDoc._id);
|
|
3588
|
+
} catch {
|
|
3589
|
+
}
|
|
3590
|
+
const docToInsert = {
|
|
3591
|
+
_id: designDoc._id,
|
|
3592
|
+
views: designDoc.views
|
|
3593
|
+
};
|
|
3594
|
+
if (existingDoc) {
|
|
3595
|
+
docToInsert._rev = existingDoc._rev;
|
|
3596
|
+
logger.debug(`Updating existing design document: ${designDoc._id}`);
|
|
3597
|
+
} else {
|
|
3598
|
+
logger.debug(`Creating new design document: ${designDoc._id}`);
|
|
3599
|
+
}
|
|
3600
|
+
await db.put(docToInsert);
|
|
3601
|
+
result.restored++;
|
|
3602
|
+
} catch (error) {
|
|
3603
|
+
const errorMessage = `Failed to restore design document ${designDoc._id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3604
|
+
result.errors.push(errorMessage);
|
|
3605
|
+
logger.error(errorMessage);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
this.reportProgress(
|
|
3609
|
+
"design_docs",
|
|
3610
|
+
designDocs.length,
|
|
3611
|
+
designDocs.length,
|
|
3612
|
+
`Restored ${result.restored} design documents`
|
|
3613
|
+
);
|
|
3614
|
+
return result;
|
|
3615
|
+
}
|
|
3616
|
+
/**
|
|
3617
|
+
* Aggregate documents from all chunks
|
|
3618
|
+
*/
|
|
3619
|
+
async aggregateDocuments(staticPath, manifest) {
|
|
3620
|
+
const allDocuments = [];
|
|
3621
|
+
const documentMap = /* @__PURE__ */ new Map();
|
|
3622
|
+
for (let i = 0; i < manifest.chunks.length; i++) {
|
|
3623
|
+
const chunk = manifest.chunks[i];
|
|
3624
|
+
this.reportProgress(
|
|
3625
|
+
"documents",
|
|
3626
|
+
allDocuments.length,
|
|
3627
|
+
manifest.documentCount,
|
|
3628
|
+
`Loading chunk ${chunk.id}...`
|
|
3629
|
+
);
|
|
3630
|
+
try {
|
|
3631
|
+
const documents = await this.loadChunk(staticPath, chunk);
|
|
3632
|
+
for (const doc of documents) {
|
|
3633
|
+
if (!doc._id) {
|
|
3634
|
+
logger.warn(`Document without _id found in chunk ${chunk.id}, skipping`);
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
if (documentMap.has(doc._id)) {
|
|
3638
|
+
logger.warn(`Duplicate document ID found: ${doc._id}, using latest version`);
|
|
3639
|
+
}
|
|
3640
|
+
documentMap.set(doc._id, doc);
|
|
3641
|
+
}
|
|
3642
|
+
} catch (error) {
|
|
3643
|
+
throw new Error(
|
|
3644
|
+
`Failed to load chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
3645
|
+
);
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
allDocuments.push(...documentMap.values());
|
|
3649
|
+
logger.info(
|
|
3650
|
+
`Aggregated ${allDocuments.length} unique documents from ${manifest.chunks.length} chunks`
|
|
3651
|
+
);
|
|
3652
|
+
return allDocuments;
|
|
3653
|
+
}
|
|
3654
|
+
/**
|
|
3655
|
+
* Load documents from a single chunk file
|
|
3656
|
+
*/
|
|
3657
|
+
async loadChunk(staticPath, chunk) {
|
|
3658
|
+
try {
|
|
3659
|
+
let chunkContent;
|
|
3660
|
+
let chunkPath;
|
|
3661
|
+
if (this.fs) {
|
|
3662
|
+
chunkPath = this.fs.joinPath(staticPath, chunk.path);
|
|
3663
|
+
chunkContent = await this.fs.readFile(chunkPath);
|
|
3664
|
+
} else {
|
|
3665
|
+
chunkPath = nodeFS2 && nodePath ? nodePath.join(staticPath, chunk.path) : `${staticPath}/${chunk.path}`;
|
|
3666
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3667
|
+
chunkContent = await nodeFS2.promises.readFile(chunkPath, "utf8");
|
|
3668
|
+
} else {
|
|
3669
|
+
const response = await fetch(chunkPath);
|
|
3670
|
+
if (!response.ok) {
|
|
3671
|
+
throw new Error(`Failed to fetch chunk: ${response.status} ${response.statusText}`);
|
|
3672
|
+
}
|
|
3673
|
+
chunkContent = await response.text();
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
const documents = JSON.parse(chunkContent);
|
|
3677
|
+
if (!Array.isArray(documents)) {
|
|
3678
|
+
throw new Error("Chunk file does not contain an array of documents");
|
|
3679
|
+
}
|
|
3680
|
+
return documents;
|
|
3681
|
+
} catch (error) {
|
|
3682
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load chunk: ${error instanceof Error ? error.message : String(error)}`;
|
|
3683
|
+
throw new Error(errorMessage);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
/**
|
|
3687
|
+
* Upload documents to CouchDB in batches
|
|
3688
|
+
*/
|
|
3689
|
+
async uploadDocuments(documents, db) {
|
|
3690
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3691
|
+
const batchSize = this.options.chunkBatchSize;
|
|
3692
|
+
for (let i = 0; i < documents.length; i += batchSize) {
|
|
3693
|
+
const batch = documents.slice(i, i + batchSize);
|
|
3694
|
+
this.reportProgress(
|
|
3695
|
+
"documents",
|
|
3696
|
+
i,
|
|
3697
|
+
documents.length,
|
|
3698
|
+
`Uploading batch ${Math.floor(i / batchSize) + 1}...`
|
|
3699
|
+
);
|
|
3700
|
+
try {
|
|
3701
|
+
const docsToInsert = batch.map((doc) => {
|
|
3702
|
+
const cleanDoc = { ...doc };
|
|
3703
|
+
delete cleanDoc._rev;
|
|
3704
|
+
delete cleanDoc._attachments;
|
|
3705
|
+
return cleanDoc;
|
|
3706
|
+
});
|
|
3707
|
+
const bulkResult = await db.bulkDocs(docsToInsert);
|
|
3708
|
+
for (let j = 0; j < bulkResult.length; j++) {
|
|
3709
|
+
const docResult = bulkResult[j];
|
|
3710
|
+
const originalDoc = batch[j];
|
|
3711
|
+
if ("error" in docResult) {
|
|
3712
|
+
const errorMessage = `Failed to upload document ${originalDoc._id}: ${docResult.error} - ${docResult.reason}`;
|
|
3713
|
+
result.errors.push(errorMessage);
|
|
3714
|
+
logger.error(errorMessage);
|
|
3715
|
+
} else {
|
|
3716
|
+
result.restored++;
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
} catch (error) {
|
|
3720
|
+
let errorMessage;
|
|
3721
|
+
if (error instanceof Error) {
|
|
3722
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
3723
|
+
} else if (error && typeof error === "object" && "message" in error) {
|
|
3724
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
3725
|
+
} else {
|
|
3726
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${JSON.stringify(error)}`;
|
|
3727
|
+
}
|
|
3728
|
+
result.errors.push(errorMessage);
|
|
3729
|
+
logger.error(errorMessage);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
this.reportProgress(
|
|
3733
|
+
"documents",
|
|
3734
|
+
documents.length,
|
|
3735
|
+
documents.length,
|
|
3736
|
+
`Uploaded ${result.restored} documents`
|
|
3737
|
+
);
|
|
3738
|
+
return result;
|
|
3739
|
+
}
|
|
3740
|
+
/**
|
|
3741
|
+
* Upload attachments from filesystem to CouchDB
|
|
3742
|
+
*/
|
|
3743
|
+
async uploadAttachments(staticPath, documents, db) {
|
|
3744
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3745
|
+
let processedDocs = 0;
|
|
3746
|
+
for (const doc of documents) {
|
|
3747
|
+
this.reportProgress(
|
|
3748
|
+
"attachments",
|
|
3749
|
+
processedDocs,
|
|
3750
|
+
documents.length,
|
|
3751
|
+
`Processing attachments for ${doc._id}...`
|
|
3752
|
+
);
|
|
3753
|
+
processedDocs++;
|
|
3754
|
+
if (!doc._attachments) {
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
for (const [attachmentName, attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3758
|
+
try {
|
|
3759
|
+
const uploadResult = await this.uploadSingleAttachment(
|
|
3760
|
+
staticPath,
|
|
3761
|
+
doc._id,
|
|
3762
|
+
attachmentName,
|
|
3763
|
+
attachmentMeta,
|
|
3764
|
+
db
|
|
3765
|
+
);
|
|
3766
|
+
if (uploadResult.success) {
|
|
3767
|
+
result.restored++;
|
|
3768
|
+
} else {
|
|
3769
|
+
result.errors.push(uploadResult.error || "Unknown attachment upload error");
|
|
3770
|
+
}
|
|
3771
|
+
} catch (error) {
|
|
3772
|
+
const errorMessage = `Failed to upload attachment ${doc._id}/${attachmentName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3773
|
+
result.errors.push(errorMessage);
|
|
3774
|
+
logger.error(errorMessage);
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
this.reportProgress(
|
|
3779
|
+
"attachments",
|
|
3780
|
+
documents.length,
|
|
3781
|
+
documents.length,
|
|
3782
|
+
`Uploaded ${result.restored} attachments`
|
|
3783
|
+
);
|
|
3784
|
+
return result;
|
|
3785
|
+
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Upload a single attachment file
|
|
3788
|
+
*/
|
|
3789
|
+
async uploadSingleAttachment(staticPath, docId, attachmentName, attachmentMeta, db) {
|
|
3790
|
+
const result = {
|
|
3791
|
+
success: false,
|
|
3792
|
+
attachmentName,
|
|
3793
|
+
docId
|
|
3794
|
+
};
|
|
3795
|
+
try {
|
|
3796
|
+
if (!attachmentMeta.path) {
|
|
3797
|
+
result.error = "Attachment metadata missing file path";
|
|
3798
|
+
return result;
|
|
3799
|
+
}
|
|
3800
|
+
let attachmentData;
|
|
3801
|
+
let attachmentPath;
|
|
3802
|
+
if (this.fs) {
|
|
3803
|
+
attachmentPath = this.fs.joinPath(staticPath, attachmentMeta.path);
|
|
3804
|
+
attachmentData = await this.fs.readBinary(attachmentPath);
|
|
3805
|
+
} else {
|
|
3806
|
+
attachmentPath = nodeFS2 && nodePath ? nodePath.join(staticPath, attachmentMeta.path) : `${staticPath}/${attachmentMeta.path}`;
|
|
3807
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3808
|
+
attachmentData = await nodeFS2.promises.readFile(attachmentPath);
|
|
3809
|
+
} else {
|
|
3810
|
+
const response = await fetch(attachmentPath);
|
|
3811
|
+
if (!response.ok) {
|
|
3812
|
+
result.error = `Failed to fetch attachment: ${response.status} ${response.statusText}`;
|
|
3813
|
+
return result;
|
|
3814
|
+
}
|
|
3815
|
+
attachmentData = await response.arrayBuffer();
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
const doc = await db.get(docId);
|
|
3819
|
+
await db.putAttachment(
|
|
3820
|
+
docId,
|
|
3821
|
+
attachmentName,
|
|
3822
|
+
doc._rev,
|
|
3823
|
+
attachmentData,
|
|
3824
|
+
// PouchDB accepts both ArrayBuffer and Buffer
|
|
3825
|
+
attachmentMeta.content_type
|
|
3826
|
+
);
|
|
3827
|
+
result.success = true;
|
|
3828
|
+
} catch (error) {
|
|
3829
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
3830
|
+
}
|
|
3831
|
+
return result;
|
|
3832
|
+
}
|
|
3833
|
+
/**
|
|
3834
|
+
* Restore CourseConfig document from manifest
|
|
3835
|
+
*/
|
|
3836
|
+
async restoreCourseConfig(manifest, targetDB) {
|
|
3837
|
+
const results = {
|
|
3838
|
+
restored: 0,
|
|
3839
|
+
errors: [],
|
|
3840
|
+
warnings: []
|
|
3841
|
+
};
|
|
3842
|
+
try {
|
|
3843
|
+
if (!manifest.courseConfig) {
|
|
3844
|
+
results.warnings.push(
|
|
3845
|
+
"No courseConfig found in manifest, skipping CourseConfig document creation"
|
|
3846
|
+
);
|
|
3847
|
+
return results;
|
|
3848
|
+
}
|
|
3849
|
+
const courseConfigDoc = {
|
|
3850
|
+
_id: "CourseConfig",
|
|
3851
|
+
...manifest.courseConfig,
|
|
3852
|
+
courseID: manifest.courseId
|
|
3853
|
+
};
|
|
3854
|
+
delete courseConfigDoc._rev;
|
|
3855
|
+
await targetDB.put(courseConfigDoc);
|
|
3856
|
+
results.restored = 1;
|
|
3857
|
+
logger.info(`CourseConfig document created for course: ${manifest.courseId}`);
|
|
3858
|
+
} catch (error) {
|
|
3859
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
3860
|
+
results.errors.push(`Failed to restore CourseConfig: ${errorMessage}`);
|
|
3861
|
+
logger.error("CourseConfig restoration failed:", error);
|
|
3862
|
+
}
|
|
3863
|
+
return results;
|
|
3864
|
+
}
|
|
3865
|
+
/**
|
|
3866
|
+
* Calculate expected document counts from manifest
|
|
3867
|
+
*/
|
|
3868
|
+
calculateExpectedCounts(manifest) {
|
|
3869
|
+
const counts = {};
|
|
3870
|
+
for (const chunk of manifest.chunks) {
|
|
3871
|
+
counts[chunk.docType] = (counts[chunk.docType] || 0) + chunk.documentCount;
|
|
3872
|
+
}
|
|
3873
|
+
if (manifest.designDocs.length > 0) {
|
|
3874
|
+
counts["_design"] = manifest.designDocs.length;
|
|
3875
|
+
}
|
|
3876
|
+
return counts;
|
|
3877
|
+
}
|
|
3878
|
+
/**
|
|
3879
|
+
* Clean up database after failed migration
|
|
3880
|
+
*/
|
|
3881
|
+
async cleanupFailedMigration(db) {
|
|
3882
|
+
logger.info("Cleaning up failed migration...");
|
|
3883
|
+
try {
|
|
3884
|
+
const allDocs = await db.allDocs();
|
|
3885
|
+
const docsToDelete = allDocs.rows.map((row) => ({
|
|
3886
|
+
_id: row.id,
|
|
3887
|
+
_rev: row.value.rev,
|
|
3888
|
+
_deleted: true
|
|
3889
|
+
}));
|
|
3890
|
+
if (docsToDelete.length > 0) {
|
|
3891
|
+
await db.bulkDocs(docsToDelete);
|
|
3892
|
+
logger.info(`Cleaned up ${docsToDelete.length} documents from failed migration`);
|
|
3893
|
+
}
|
|
3894
|
+
} catch (error) {
|
|
3895
|
+
logger.error("Failed to cleanup documents:", error);
|
|
3896
|
+
throw error;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Report progress to callback if available
|
|
3901
|
+
*/
|
|
3902
|
+
reportProgress(phase, current, total, message) {
|
|
3903
|
+
if (this.progressCallback) {
|
|
3904
|
+
this.progressCallback({
|
|
3905
|
+
phase,
|
|
3906
|
+
current,
|
|
3907
|
+
total,
|
|
3908
|
+
message
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
/**
|
|
3913
|
+
* Check if a path is a local file path (vs URL)
|
|
3914
|
+
*/
|
|
3915
|
+
isLocalPath(path2) {
|
|
3916
|
+
return !path2.startsWith("http://") && !path2.startsWith("https://");
|
|
3917
|
+
}
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
});
|
|
3921
|
+
|
|
3922
|
+
// src/util/migrator/index.ts
|
|
3923
|
+
var init_migrator = __esm({
|
|
3924
|
+
"src/util/migrator/index.ts"() {
|
|
3925
|
+
"use strict";
|
|
3926
|
+
init_StaticToCouchDBMigrator();
|
|
3927
|
+
init_validation();
|
|
3928
|
+
init_FileSystemAdapter();
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3931
|
+
|
|
3932
|
+
// src/util/index.ts
|
|
3933
|
+
var init_util2 = __esm({
|
|
3934
|
+
"src/util/index.ts"() {
|
|
3935
|
+
"use strict";
|
|
3936
|
+
init_Loggable();
|
|
3937
|
+
init_packer();
|
|
3938
|
+
init_migrator();
|
|
3939
|
+
init_dataDirectory();
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
3942
|
+
|
|
3943
|
+
// src/study/SourceMixer.ts
|
|
3944
|
+
var init_SourceMixer = __esm({
|
|
3945
|
+
"src/study/SourceMixer.ts"() {
|
|
3946
|
+
"use strict";
|
|
3947
|
+
}
|
|
3948
|
+
});
|
|
3949
|
+
|
|
3950
|
+
// src/study/MixerDebugger.ts
|
|
3951
|
+
function printMixerSummary(run) {
|
|
3952
|
+
console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
|
|
3953
|
+
logger.info(`Run ID: ${run.runId}`);
|
|
3954
|
+
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
3955
|
+
logger.info(
|
|
3956
|
+
`Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
|
|
3957
|
+
);
|
|
3958
|
+
console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
|
|
3959
|
+
for (const src of run.sourceSummaries) {
|
|
3960
|
+
logger.info(
|
|
3961
|
+
` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
|
|
3962
|
+
);
|
|
3963
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
|
|
3964
|
+
}
|
|
3965
|
+
console.groupEnd();
|
|
3966
|
+
console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
|
|
3967
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
3968
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
3969
|
+
logger.info(
|
|
3970
|
+
` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
3973
|
+
console.groupEnd();
|
|
3974
|
+
console.groupEnd();
|
|
3975
|
+
}
|
|
3976
|
+
function mountMixerDebugger() {
|
|
3977
|
+
if (typeof window === "undefined") return;
|
|
3978
|
+
const win = window;
|
|
3979
|
+
win.skuilder = win.skuilder || {};
|
|
3980
|
+
win.skuilder.mixer = mixerDebugAPI;
|
|
3981
|
+
}
|
|
3982
|
+
var runHistory2, mixerDebugAPI;
|
|
3983
|
+
var init_MixerDebugger = __esm({
|
|
3984
|
+
"src/study/MixerDebugger.ts"() {
|
|
3985
|
+
"use strict";
|
|
3986
|
+
init_logger();
|
|
3987
|
+
init_navigators();
|
|
3988
|
+
runHistory2 = [];
|
|
3989
|
+
mixerDebugAPI = {
|
|
3990
|
+
/**
|
|
3991
|
+
* Get raw run history for programmatic access.
|
|
3992
|
+
*/
|
|
3993
|
+
get runs() {
|
|
3994
|
+
return [...runHistory2];
|
|
3995
|
+
},
|
|
3996
|
+
/**
|
|
3997
|
+
* Show summary of a specific mixer run.
|
|
3998
|
+
*/
|
|
3999
|
+
showRun(idOrIndex = 0) {
|
|
4000
|
+
if (runHistory2.length === 0) {
|
|
4001
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
let run;
|
|
4005
|
+
if (typeof idOrIndex === "number") {
|
|
4006
|
+
run = runHistory2[idOrIndex];
|
|
4007
|
+
if (!run) {
|
|
4008
|
+
logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
} else {
|
|
4012
|
+
run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
|
|
4013
|
+
if (!run) {
|
|
4014
|
+
logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
printMixerSummary(run);
|
|
4019
|
+
},
|
|
4020
|
+
/**
|
|
4021
|
+
* Show summary of the last mixer run.
|
|
4022
|
+
*/
|
|
4023
|
+
showLastMix() {
|
|
4024
|
+
this.showRun(0);
|
|
4025
|
+
},
|
|
4026
|
+
/**
|
|
4027
|
+
* Explain source balance in the last run.
|
|
4028
|
+
*/
|
|
4029
|
+
explainSourceBalance() {
|
|
4030
|
+
if (runHistory2.length === 0) {
|
|
4031
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
const run = runHistory2[0];
|
|
4035
|
+
console.group("\u2696\uFE0F Source Balance Analysis");
|
|
4036
|
+
logger.info(`Mixer: ${run.mixerType}`);
|
|
4037
|
+
logger.info(`Requested limit: ${run.requestedLimit}`);
|
|
4038
|
+
if (run.quotaPerSource) {
|
|
4039
|
+
logger.info(`Quota per source: ${run.quotaPerSource}`);
|
|
4040
|
+
}
|
|
4041
|
+
console.group("Input Distribution:");
|
|
4042
|
+
for (const src of run.sourceSummaries) {
|
|
4043
|
+
const name = src.sourceName || src.sourceId;
|
|
4044
|
+
logger.info(`${name}:`);
|
|
4045
|
+
logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
|
|
4046
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
|
|
4047
|
+
}
|
|
4048
|
+
console.groupEnd();
|
|
4049
|
+
console.group("Selection Results:");
|
|
4050
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
4051
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4052
|
+
logger.info(`${name}:`);
|
|
4053
|
+
logger.info(
|
|
4054
|
+
` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
|
|
4055
|
+
);
|
|
4056
|
+
logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
|
|
4057
|
+
logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
|
|
4058
|
+
if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
|
|
4059
|
+
logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
|
|
4060
|
+
}
|
|
4061
|
+
if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
|
|
4062
|
+
logger.info(` \u26A0\uFE0F Had cards but none selected!`);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
console.groupEnd();
|
|
4066
|
+
const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
|
|
4067
|
+
const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
|
|
4068
|
+
const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
|
|
4069
|
+
if (maxDeviation > 20) {
|
|
4070
|
+
logger.info(`
|
|
4071
|
+
\u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
|
|
4072
|
+
logger.info("Possible causes:");
|
|
4073
|
+
logger.info(" - Score range differences between sources");
|
|
4074
|
+
logger.info(" - One source has much better quality cards");
|
|
4075
|
+
logger.info(" - Different card availability (reviews vs new)");
|
|
4076
|
+
}
|
|
4077
|
+
console.groupEnd();
|
|
4078
|
+
},
|
|
4079
|
+
/**
|
|
4080
|
+
* Compare score distributions across sources.
|
|
4081
|
+
*/
|
|
4082
|
+
compareScores() {
|
|
4083
|
+
if (runHistory2.length === 0) {
|
|
4084
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
4087
|
+
const run = runHistory2[0];
|
|
4088
|
+
console.group("\u{1F4CA} Score Distribution Comparison");
|
|
4089
|
+
console.table(
|
|
4090
|
+
run.sourceSummaries.map((src) => ({
|
|
4091
|
+
source: src.sourceName || src.sourceId,
|
|
4092
|
+
cards: src.totalCards,
|
|
4093
|
+
min: src.bottomScore.toFixed(3),
|
|
4094
|
+
max: src.topScore.toFixed(3),
|
|
4095
|
+
avg: src.avgScore.toFixed(3),
|
|
4096
|
+
range: (src.topScore - src.bottomScore).toFixed(3)
|
|
4097
|
+
}))
|
|
4098
|
+
);
|
|
4099
|
+
const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
|
|
4100
|
+
const avgScores = run.sourceSummaries.map((s) => s.avgScore);
|
|
4101
|
+
const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
|
|
4102
|
+
const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
|
|
4103
|
+
if (rangeDiff > 0.3 || avgDiff > 0.2) {
|
|
4104
|
+
logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
|
|
4105
|
+
logger.info(
|
|
4106
|
+
"This may cause one source to dominate selection if using global sorting (not quota-based)"
|
|
4107
|
+
);
|
|
4108
|
+
}
|
|
4109
|
+
console.groupEnd();
|
|
4110
|
+
},
|
|
4111
|
+
/**
|
|
4112
|
+
* Show detailed information for a specific card.
|
|
4113
|
+
*/
|
|
4114
|
+
showCard(cardId) {
|
|
4115
|
+
for (const run of runHistory2) {
|
|
4116
|
+
const card = run.cards.find((c) => c.cardId === cardId);
|
|
4117
|
+
if (card) {
|
|
4118
|
+
const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
|
|
4119
|
+
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
4120
|
+
logger.info(`Course: ${card.courseId}`);
|
|
4121
|
+
logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
|
|
4122
|
+
logger.info(`Origin: ${card.origin}`);
|
|
4123
|
+
logger.info(`Score: ${card.score.toFixed(3)}`);
|
|
4124
|
+
if (card.rankInSource) {
|
|
4125
|
+
logger.info(`Rank in source: #${card.rankInSource}`);
|
|
4126
|
+
}
|
|
4127
|
+
if (card.rankInMix) {
|
|
4128
|
+
logger.info(`Rank in mixed results: #${card.rankInMix}`);
|
|
4129
|
+
}
|
|
4130
|
+
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
4131
|
+
if (!card.selected && card.rankInSource) {
|
|
4132
|
+
logger.info("\nWhy not selected:");
|
|
4133
|
+
if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
|
|
4134
|
+
logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
|
|
4135
|
+
}
|
|
4136
|
+
logger.info(" - Check score compared to selected cards using .showRun()");
|
|
4137
|
+
}
|
|
4138
|
+
console.groupEnd();
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
|
|
4143
|
+
},
|
|
4144
|
+
/**
|
|
4145
|
+
* Show all runs in compact format.
|
|
4146
|
+
*/
|
|
4147
|
+
listRuns() {
|
|
4148
|
+
if (runHistory2.length === 0) {
|
|
4149
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
4152
|
+
console.table(
|
|
4153
|
+
runHistory2.map((r) => ({
|
|
4154
|
+
id: r.runId.slice(-8),
|
|
4155
|
+
time: r.timestamp.toLocaleTimeString(),
|
|
4156
|
+
mixer: r.mixerType,
|
|
4157
|
+
sources: r.sourceSummaries.length,
|
|
4158
|
+
selected: r.finalCount,
|
|
4159
|
+
reviews: r.reviewsSelected,
|
|
4160
|
+
new: r.newSelected
|
|
4161
|
+
}))
|
|
4162
|
+
);
|
|
4163
|
+
},
|
|
2263
4164
|
/**
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2266
|
-
* Use transform() via Pipeline instead.
|
|
4165
|
+
* Export run history as JSON for bug reports.
|
|
2267
4166
|
*/
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
);
|
|
4167
|
+
export() {
|
|
4168
|
+
const json = JSON.stringify(runHistory2, null, 2);
|
|
4169
|
+
logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
|
|
4170
|
+
logger.info(" copy(window.skuilder.mixer.export())");
|
|
4171
|
+
return json;
|
|
4172
|
+
},
|
|
4173
|
+
/**
|
|
4174
|
+
* Clear run history.
|
|
4175
|
+
*/
|
|
4176
|
+
clear() {
|
|
4177
|
+
runHistory2.length = 0;
|
|
4178
|
+
logger.info("[Mixer Debug] Run history cleared.");
|
|
4179
|
+
},
|
|
4180
|
+
/**
|
|
4181
|
+
* Show help.
|
|
4182
|
+
*/
|
|
4183
|
+
help() {
|
|
4184
|
+
logger.info(`
|
|
4185
|
+
\u{1F3A8} Mixer Debug API
|
|
4186
|
+
|
|
4187
|
+
Commands:
|
|
4188
|
+
.showLastMix() Show summary of most recent mixer run
|
|
4189
|
+
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
4190
|
+
.explainSourceBalance() Analyze source balance and selection patterns
|
|
4191
|
+
.compareScores() Compare score distributions across sources
|
|
4192
|
+
.showCard(cardId) Show mixer decisions for a specific card
|
|
4193
|
+
.listRuns() List all captured runs in table format
|
|
4194
|
+
.export() Export run history as JSON for bug reports
|
|
4195
|
+
.clear() Clear run history
|
|
4196
|
+
.runs Access raw run history array
|
|
4197
|
+
.help() Show this help message
|
|
4198
|
+
|
|
4199
|
+
Example:
|
|
4200
|
+
window.skuilder.mixer.showLastMix()
|
|
4201
|
+
window.skuilder.mixer.explainSourceBalance()
|
|
4202
|
+
window.skuilder.mixer.compareScores()
|
|
4203
|
+
`);
|
|
2272
4204
|
}
|
|
2273
4205
|
};
|
|
4206
|
+
mountMixerDebugger();
|
|
2274
4207
|
}
|
|
2275
4208
|
});
|
|
2276
4209
|
|
|
2277
|
-
// src/
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
4210
|
+
// src/study/SessionDebugger.ts
|
|
4211
|
+
function showCurrentQueue() {
|
|
4212
|
+
if (!activeSession) {
|
|
4213
|
+
logger.info("[Session Debug] No active session.");
|
|
4214
|
+
return;
|
|
2282
4215
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2289
|
-
});
|
|
2290
|
-
var USER_GOAL_NAVIGATOR_STUB;
|
|
2291
|
-
var init_userGoalStub = __esm({
|
|
2292
|
-
"src/core/navigators/filters/userGoalStub.ts"() {
|
|
2293
|
-
"use strict";
|
|
2294
|
-
USER_GOAL_NAVIGATOR_STUB = true;
|
|
4216
|
+
const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
|
|
4217
|
+
console.group("\u{1F4CA} Current Queue State");
|
|
4218
|
+
logger.info(`Review Queue: ${latest.reviewQLength} cards`);
|
|
4219
|
+
if (latest.reviewQNext3 && latest.reviewQNext3.length > 0) {
|
|
4220
|
+
logger.info(` Next: ${latest.reviewQNext3.join(", ")}`);
|
|
2295
4221
|
}
|
|
2296
|
-
});
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
var globImport_filters;
|
|
2300
|
-
var init_2 = __esm({
|
|
2301
|
-
'import("./filters/**/*") in src/core/navigators/index.ts'() {
|
|
2302
|
-
globImport_filters = __glob({
|
|
2303
|
-
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
2304
|
-
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2305
|
-
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2306
|
-
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2307
|
-
"./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
|
|
2308
|
-
"./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2309
|
-
"./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2310
|
-
"./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2311
|
-
"./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
|
|
2312
|
-
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
|
|
2313
|
-
});
|
|
4222
|
+
logger.info(`New Queue: ${latest.newQLength} cards`);
|
|
4223
|
+
if (latest.newQNext3 && latest.newQNext3.length > 0) {
|
|
4224
|
+
logger.info(` Next: ${latest.newQNext3.join(", ")}`);
|
|
2314
4225
|
}
|
|
2315
|
-
});
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
4226
|
+
logger.info(`Failed Queue: ${latest.failedQLength} cards`);
|
|
4227
|
+
console.groupEnd();
|
|
4228
|
+
}
|
|
4229
|
+
function showPresentationHistory(sessionIndex = 0) {
|
|
4230
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4231
|
+
if (!session) {
|
|
4232
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4233
|
+
return;
|
|
2322
4234
|
}
|
|
2323
|
-
});
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
"src/core/orchestration/learning.ts"() {
|
|
2328
|
-
"use strict";
|
|
2329
|
-
init_contentNavigationStrategy();
|
|
2330
|
-
init_types_legacy();
|
|
2331
|
-
init_logger();
|
|
4235
|
+
console.group(`\u{1F4DC} Session History: ${session.sessionId}`);
|
|
4236
|
+
logger.info(`Started: ${session.startTime.toLocaleTimeString()}`);
|
|
4237
|
+
if (session.endTime) {
|
|
4238
|
+
logger.info(`Ended: ${session.endTime.toLocaleTimeString()}`);
|
|
2332
4239
|
}
|
|
2333
|
-
});
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
4240
|
+
logger.info(`Cards presented: ${session.presentations.length}`);
|
|
4241
|
+
if (session.presentations.length > 0) {
|
|
4242
|
+
console.table(
|
|
4243
|
+
session.presentations.map((p) => ({
|
|
4244
|
+
"#": p.sequenceNumber,
|
|
4245
|
+
course: p.courseName || p.courseId.slice(0, 8),
|
|
4246
|
+
origin: p.origin,
|
|
4247
|
+
queue: p.queueSource,
|
|
4248
|
+
score: p.score?.toFixed(3) || "-",
|
|
4249
|
+
time: p.timestamp.toLocaleTimeString()
|
|
4250
|
+
}))
|
|
4251
|
+
);
|
|
2339
4252
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
init_types_legacy();
|
|
2348
|
-
init_logger();
|
|
4253
|
+
console.groupEnd();
|
|
4254
|
+
}
|
|
4255
|
+
function showInterleaving(sessionIndex = 0) {
|
|
4256
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4257
|
+
if (!session) {
|
|
4258
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4259
|
+
return;
|
|
2349
4260
|
}
|
|
2350
|
-
});
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
4261
|
+
console.group("\u{1F500} Interleaving Analysis");
|
|
4262
|
+
const courseCounts = /* @__PURE__ */ new Map();
|
|
4263
|
+
const courseOrigins = /* @__PURE__ */ new Map();
|
|
4264
|
+
session.presentations.forEach((p) => {
|
|
4265
|
+
const name = p.courseName || p.courseId;
|
|
4266
|
+
courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
|
|
4267
|
+
if (!courseOrigins.has(name)) {
|
|
4268
|
+
courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
|
|
4269
|
+
}
|
|
4270
|
+
const origins = courseOrigins.get(name);
|
|
4271
|
+
origins[p.origin]++;
|
|
4272
|
+
});
|
|
4273
|
+
logger.info("Course distribution:");
|
|
4274
|
+
console.table(
|
|
4275
|
+
Array.from(courseCounts.entries()).map(([course, count]) => {
|
|
4276
|
+
const origins = courseOrigins.get(course);
|
|
4277
|
+
return {
|
|
4278
|
+
course,
|
|
4279
|
+
total: count,
|
|
4280
|
+
reviews: origins.review,
|
|
4281
|
+
new: origins.new,
|
|
4282
|
+
failed: origins.failed,
|
|
4283
|
+
percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
|
|
4284
|
+
};
|
|
4285
|
+
})
|
|
4286
|
+
);
|
|
4287
|
+
if (session.presentations.length > 0) {
|
|
4288
|
+
logger.info("\nPresentation sequence (first 20):");
|
|
4289
|
+
const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
|
|
4290
|
+
logger.info(sequence);
|
|
2358
4291
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
4292
|
+
let maxCluster = 0;
|
|
4293
|
+
let currentCluster = 1;
|
|
4294
|
+
let currentCourse = session.presentations[0]?.courseId;
|
|
4295
|
+
for (let i = 1; i < session.presentations.length; i++) {
|
|
4296
|
+
if (session.presentations[i].courseId === currentCourse) {
|
|
4297
|
+
currentCluster++;
|
|
4298
|
+
maxCluster = Math.max(maxCluster, currentCluster);
|
|
4299
|
+
} else {
|
|
4300
|
+
currentCourse = session.presentations[i].courseId;
|
|
4301
|
+
currentCluster = 1;
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
if (maxCluster > 3) {
|
|
4305
|
+
logger.info(`
|
|
4306
|
+
\u26A0\uFE0F Detected clustering: max ${maxCluster} cards from same course in a row`);
|
|
4307
|
+
logger.info("This suggests cards are sorted by score rather than round-robin by course.");
|
|
4308
|
+
}
|
|
4309
|
+
console.groupEnd();
|
|
2370
4310
|
}
|
|
2371
|
-
function
|
|
2372
|
-
|
|
2373
|
-
const
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
4311
|
+
function mountSessionDebugger() {
|
|
4312
|
+
if (typeof window === "undefined") return;
|
|
4313
|
+
const win = window;
|
|
4314
|
+
win.skuilder = win.skuilder || {};
|
|
4315
|
+
win.skuilder.session = sessionDebugAPI;
|
|
2377
4316
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
4317
|
+
var activeSession, sessionHistory, sessionDebugAPI;
|
|
4318
|
+
var init_SessionDebugger = __esm({
|
|
4319
|
+
"src/study/SessionDebugger.ts"() {
|
|
4320
|
+
"use strict";
|
|
4321
|
+
init_logger();
|
|
4322
|
+
activeSession = null;
|
|
4323
|
+
sessionHistory = [];
|
|
4324
|
+
sessionDebugAPI = {
|
|
4325
|
+
/**
|
|
4326
|
+
* Get raw session history for programmatic access.
|
|
4327
|
+
*/
|
|
4328
|
+
get sessions() {
|
|
4329
|
+
return [...sessionHistory];
|
|
4330
|
+
},
|
|
4331
|
+
/**
|
|
4332
|
+
* Get active session if any.
|
|
4333
|
+
*/
|
|
4334
|
+
get active() {
|
|
4335
|
+
return activeSession;
|
|
4336
|
+
},
|
|
4337
|
+
/**
|
|
4338
|
+
* Show current queue state.
|
|
4339
|
+
*/
|
|
4340
|
+
showQueue() {
|
|
4341
|
+
showCurrentQueue();
|
|
4342
|
+
},
|
|
4343
|
+
/**
|
|
4344
|
+
* Show presentation history for current or past session.
|
|
4345
|
+
*/
|
|
4346
|
+
showHistory(sessionIndex = 0) {
|
|
4347
|
+
showPresentationHistory(sessionIndex);
|
|
4348
|
+
},
|
|
4349
|
+
/**
|
|
4350
|
+
* Analyze course interleaving pattern.
|
|
4351
|
+
*/
|
|
4352
|
+
showInterleaving(sessionIndex = 0) {
|
|
4353
|
+
showInterleaving(sessionIndex);
|
|
4354
|
+
},
|
|
4355
|
+
/**
|
|
4356
|
+
* List all tracked sessions.
|
|
4357
|
+
*/
|
|
4358
|
+
listSessions() {
|
|
4359
|
+
if (activeSession) {
|
|
4360
|
+
logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
|
|
4361
|
+
}
|
|
4362
|
+
if (sessionHistory.length === 0) {
|
|
4363
|
+
logger.info("[Session Debug] No completed sessions in history.");
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
console.table(
|
|
4367
|
+
sessionHistory.map((s, idx) => ({
|
|
4368
|
+
index: idx,
|
|
4369
|
+
id: s.sessionId.slice(-8),
|
|
4370
|
+
started: s.startTime.toLocaleTimeString(),
|
|
4371
|
+
ended: s.endTime?.toLocaleTimeString() || "incomplete",
|
|
4372
|
+
cards: s.presentations.length
|
|
4373
|
+
}))
|
|
4374
|
+
);
|
|
4375
|
+
},
|
|
4376
|
+
/**
|
|
4377
|
+
* Export session history as JSON for bug reports.
|
|
4378
|
+
*/
|
|
4379
|
+
export() {
|
|
4380
|
+
const data = {
|
|
4381
|
+
active: activeSession,
|
|
4382
|
+
history: sessionHistory
|
|
4383
|
+
};
|
|
4384
|
+
const json = JSON.stringify(data, null, 2);
|
|
4385
|
+
logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
|
|
4386
|
+
logger.info(" copy(window.skuilder.session.export())");
|
|
4387
|
+
return json;
|
|
4388
|
+
},
|
|
4389
|
+
/**
|
|
4390
|
+
* Clear session history.
|
|
4391
|
+
*/
|
|
4392
|
+
clear() {
|
|
4393
|
+
sessionHistory.length = 0;
|
|
4394
|
+
logger.info("[Session Debug] Session history cleared.");
|
|
4395
|
+
},
|
|
4396
|
+
/**
|
|
4397
|
+
* Show help.
|
|
4398
|
+
*/
|
|
4399
|
+
help() {
|
|
4400
|
+
logger.info(`
|
|
4401
|
+
\u{1F3AF} Session Debug API
|
|
4402
|
+
|
|
4403
|
+
Commands:
|
|
4404
|
+
.showQueue() Show current queue state (active session only)
|
|
4405
|
+
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
4406
|
+
.showInterleaving(index?) Analyze course interleaving pattern
|
|
4407
|
+
.listSessions() List all tracked sessions
|
|
4408
|
+
.export() Export session data as JSON for bug reports
|
|
4409
|
+
.clear() Clear session history
|
|
4410
|
+
.sessions Access raw session history array
|
|
4411
|
+
.active Access active session (if any)
|
|
4412
|
+
.help() Show this help message
|
|
4413
|
+
|
|
4414
|
+
Example:
|
|
4415
|
+
window.skuilder.session.showHistory()
|
|
4416
|
+
window.skuilder.session.showInterleaving()
|
|
4417
|
+
window.skuilder.session.showQueue()
|
|
4418
|
+
`);
|
|
4419
|
+
}
|
|
2395
4420
|
};
|
|
4421
|
+
mountSessionDebugger();
|
|
2396
4422
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
userId,
|
|
2403
|
-
courseConfig,
|
|
2404
|
-
getEffectiveWeight(strategyId, learnable) {
|
|
2405
|
-
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
2406
|
-
},
|
|
2407
|
-
getDeviation(strategyId) {
|
|
2408
|
-
return computeDeviation(userId, strategyId, salt);
|
|
2409
|
-
}
|
|
2410
|
-
};
|
|
2411
|
-
}
|
|
2412
|
-
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
2413
|
-
var init_orchestration = __esm({
|
|
2414
|
-
"src/core/orchestration/index.ts"() {
|
|
4423
|
+
});
|
|
4424
|
+
|
|
4425
|
+
// src/study/SessionController.ts
|
|
4426
|
+
var init_SessionController = __esm({
|
|
4427
|
+
"src/study/SessionController.ts"() {
|
|
2415
4428
|
"use strict";
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
4429
|
+
init_SrsService();
|
|
4430
|
+
init_EloService();
|
|
4431
|
+
init_ResponseProcessor();
|
|
4432
|
+
init_CardHydrationService();
|
|
4433
|
+
init_ItemQueue();
|
|
4434
|
+
init_couch();
|
|
2420
4435
|
init_recording();
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
4436
|
+
init_util2();
|
|
4437
|
+
init_navigators();
|
|
4438
|
+
init_SourceMixer();
|
|
4439
|
+
init_MixerDebugger();
|
|
4440
|
+
init_SessionDebugger();
|
|
4441
|
+
init_logger();
|
|
2425
4442
|
}
|
|
2426
4443
|
});
|
|
2427
4444
|
|
|
@@ -2430,7 +4447,7 @@ var Pipeline_exports = {};
|
|
|
2430
4447
|
__export(Pipeline_exports, {
|
|
2431
4448
|
Pipeline: () => Pipeline
|
|
2432
4449
|
});
|
|
2433
|
-
import { toCourseElo as
|
|
4450
|
+
import { toCourseElo as toCourseElo7 } from "@vue-skuilder/common";
|
|
2434
4451
|
function globToRegex(pattern) {
|
|
2435
4452
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
2436
4453
|
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
@@ -2514,6 +4531,7 @@ var init_Pipeline = __esm({
|
|
|
2514
4531
|
init_logger();
|
|
2515
4532
|
init_orchestration();
|
|
2516
4533
|
init_PipelineDebugger();
|
|
4534
|
+
init_SessionController();
|
|
2517
4535
|
VERBOSE_RESULTS = true;
|
|
2518
4536
|
Pipeline = class extends ContentNavigator {
|
|
2519
4537
|
generator;
|
|
@@ -2673,8 +4691,9 @@ var init_Pipeline = __esm({
|
|
|
2673
4691
|
generatorSummaries,
|
|
2674
4692
|
generatedCount,
|
|
2675
4693
|
filterImpacts,
|
|
2676
|
-
|
|
2677
|
-
result
|
|
4694
|
+
cards,
|
|
4695
|
+
result,
|
|
4696
|
+
context.userElo
|
|
2678
4697
|
);
|
|
2679
4698
|
captureRun(report);
|
|
2680
4699
|
} catch (e) {
|
|
@@ -2757,7 +4776,7 @@ var init_Pipeline = __esm({
|
|
|
2757
4776
|
card.provenance.push({
|
|
2758
4777
|
strategy: "ephemeralHint",
|
|
2759
4778
|
strategyId: "ephemeral-hint",
|
|
2760
|
-
strategyName: "Replan Hint",
|
|
4779
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
|
|
2761
4780
|
action: "boosted",
|
|
2762
4781
|
score: card.score,
|
|
2763
4782
|
reason: `boostTag ${pattern} \xD7${factor}`
|
|
@@ -2774,7 +4793,7 @@ var init_Pipeline = __esm({
|
|
|
2774
4793
|
card.provenance.push({
|
|
2775
4794
|
strategy: "ephemeralHint",
|
|
2776
4795
|
strategyId: "ephemeral-hint",
|
|
2777
|
-
strategyName: "Replan Hint",
|
|
4796
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
|
|
2778
4797
|
action: "boosted",
|
|
2779
4798
|
score: card.score,
|
|
2780
4799
|
reason: `boostCard ${pattern} \xD7${factor}`
|
|
@@ -2784,6 +4803,7 @@ var init_Pipeline = __esm({
|
|
|
2784
4803
|
}
|
|
2785
4804
|
}
|
|
2786
4805
|
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
4806
|
+
const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
|
|
2787
4807
|
const inject = (card, reason) => {
|
|
2788
4808
|
if (!cardIds.has(card.cardId)) {
|
|
2789
4809
|
const floorScore = Math.max(card.score, 1);
|
|
@@ -2795,7 +4815,7 @@ var init_Pipeline = __esm({
|
|
|
2795
4815
|
{
|
|
2796
4816
|
strategy: "ephemeralHint",
|
|
2797
4817
|
strategyId: "ephemeral-hint",
|
|
2798
|
-
strategyName:
|
|
4818
|
+
strategyName: hintLabel,
|
|
2799
4819
|
action: "boosted",
|
|
2800
4820
|
score: floorScore,
|
|
2801
4821
|
reason
|
|
@@ -2834,7 +4854,7 @@ var init_Pipeline = __esm({
|
|
|
2834
4854
|
let userElo = 1e3;
|
|
2835
4855
|
try {
|
|
2836
4856
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
2837
|
-
const courseElo =
|
|
4857
|
+
const courseElo = toCourseElo7(courseReg.elo);
|
|
2838
4858
|
userElo = courseElo.global.score;
|
|
2839
4859
|
} catch (e) {
|
|
2840
4860
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
@@ -2887,6 +4907,34 @@ var init_Pipeline = __esm({
|
|
|
2887
4907
|
return [...new Set(ids)];
|
|
2888
4908
|
}
|
|
2889
4909
|
// ---------------------------------------------------------------------------
|
|
4910
|
+
// Tag ELO diagnostic
|
|
4911
|
+
// ---------------------------------------------------------------------------
|
|
4912
|
+
/**
|
|
4913
|
+
* Get the user's per-tag ELO data for specified tags (or all tags).
|
|
4914
|
+
* Useful for diagnosing why hierarchy gates are open/closed.
|
|
4915
|
+
*/
|
|
4916
|
+
async getTagEloStatus(tagFilter) {
|
|
4917
|
+
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
4918
|
+
const courseElo = toCourseElo7(courseReg.elo);
|
|
4919
|
+
const result = {};
|
|
4920
|
+
if (!tagFilter) {
|
|
4921
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
4922
|
+
result[tag] = { score: data.score, count: data.count };
|
|
4923
|
+
}
|
|
4924
|
+
} else {
|
|
4925
|
+
const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
|
|
4926
|
+
for (const pattern of patterns) {
|
|
4927
|
+
const regex = globToRegex(pattern);
|
|
4928
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
4929
|
+
if (regex.test(tag)) {
|
|
4930
|
+
result[tag] = { score: data.score, count: data.count };
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
return result;
|
|
4936
|
+
}
|
|
4937
|
+
// ---------------------------------------------------------------------------
|
|
2890
4938
|
// Card-space diagnostic
|
|
2891
4939
|
// ---------------------------------------------------------------------------
|
|
2892
4940
|
/**
|
|
@@ -3469,7 +5517,7 @@ import {
|
|
|
3469
5517
|
EloToNumber,
|
|
3470
5518
|
Status,
|
|
3471
5519
|
blankCourseElo as blankCourseElo2,
|
|
3472
|
-
toCourseElo as
|
|
5520
|
+
toCourseElo as toCourseElo8
|
|
3473
5521
|
} from "@vue-skuilder/common";
|
|
3474
5522
|
var init_courseDB = __esm({
|
|
3475
5523
|
"src/impl/couch/courseDB.ts"() {
|
|
@@ -3488,7 +5536,7 @@ var init_courseDB = __esm({
|
|
|
3488
5536
|
});
|
|
3489
5537
|
|
|
3490
5538
|
// src/impl/couch/classroomDB.ts
|
|
3491
|
-
import
|
|
5539
|
+
import moment6 from "moment";
|
|
3492
5540
|
var init_classroomDB2 = __esm({
|
|
3493
5541
|
"src/impl/couch/classroomDB.ts"() {
|
|
3494
5542
|
"use strict";
|
|
@@ -3550,7 +5598,7 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
3550
5598
|
|
|
3551
5599
|
// src/impl/couch/index.ts
|
|
3552
5600
|
import fetch3 from "cross-fetch";
|
|
3553
|
-
import
|
|
5601
|
+
import moment7 from "moment";
|
|
3554
5602
|
import process2 from "process";
|
|
3555
5603
|
function createPouchDBConfig() {
|
|
3556
5604
|
const hasExplicitCredentials = ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD;
|
|
@@ -3603,7 +5651,7 @@ var init_couch = __esm({
|
|
|
3603
5651
|
|
|
3604
5652
|
// src/impl/common/BaseUserDB.ts
|
|
3605
5653
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
3606
|
-
import
|
|
5654
|
+
import moment8 from "moment";
|
|
3607
5655
|
function accomodateGuest() {
|
|
3608
5656
|
logger.log("[funnel] accomodateGuest() called");
|
|
3609
5657
|
if (typeof localStorage === "undefined") {
|
|
@@ -4046,7 +6094,7 @@ Currently logged-in as ${this._username}.`
|
|
|
4046
6094
|
);
|
|
4047
6095
|
return reviews.rows.filter((r) => {
|
|
4048
6096
|
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
4049
|
-
const date =
|
|
6097
|
+
const date = moment8.utc(
|
|
4050
6098
|
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
4051
6099
|
REVIEW_TIME_FORMAT
|
|
4052
6100
|
);
|
|
@@ -4059,11 +6107,11 @@ Currently logged-in as ${this._username}.`
|
|
|
4059
6107
|
}).map((r) => r.doc);
|
|
4060
6108
|
}
|
|
4061
6109
|
async getReviewsForcast(daysCount) {
|
|
4062
|
-
const time =
|
|
6110
|
+
const time = moment8.utc().add(daysCount, "days");
|
|
4063
6111
|
return this.getReviewstoDate(time);
|
|
4064
6112
|
}
|
|
4065
6113
|
async getPendingReviews(course_id) {
|
|
4066
|
-
const now =
|
|
6114
|
+
const now = moment8.utc();
|
|
4067
6115
|
return this.getReviewstoDate(now, course_id);
|
|
4068
6116
|
}
|
|
4069
6117
|
async getScheduledReviewCount(course_id) {
|
|
@@ -4350,7 +6398,7 @@ Currently logged-in as ${this._username}.`
|
|
|
4350
6398
|
*/
|
|
4351
6399
|
async putCardRecord(record) {
|
|
4352
6400
|
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
4353
|
-
record.timeStamp =
|
|
6401
|
+
record.timeStamp = moment8.utc(record.timeStamp).toString();
|
|
4354
6402
|
try {
|
|
4355
6403
|
const cardHistory = await this.update(
|
|
4356
6404
|
cardHistoryID,
|
|
@@ -4366,7 +6414,7 @@ Currently logged-in as ${this._username}.`
|
|
|
4366
6414
|
const ret = {
|
|
4367
6415
|
...record2
|
|
4368
6416
|
};
|
|
4369
|
-
ret.timeStamp =
|
|
6417
|
+
ret.timeStamp = moment8.utc(record2.timeStamp);
|
|
4370
6418
|
return ret;
|
|
4371
6419
|
});
|
|
4372
6420
|
return cardHistory;
|
|
@@ -4781,7 +6829,7 @@ var init_cardProcessor = __esm({
|
|
|
4781
6829
|
});
|
|
4782
6830
|
|
|
4783
6831
|
// src/core/bulkImport/types.ts
|
|
4784
|
-
var
|
|
6832
|
+
var init_types5 = __esm({
|
|
4785
6833
|
"src/core/bulkImport/types.ts"() {
|
|
4786
6834
|
"use strict";
|
|
4787
6835
|
}
|
|
@@ -4792,7 +6840,7 @@ var init_bulkImport = __esm({
|
|
|
4792
6840
|
"src/core/bulkImport/index.ts"() {
|
|
4793
6841
|
"use strict";
|
|
4794
6842
|
init_cardProcessor();
|
|
4795
|
-
|
|
6843
|
+
init_types5();
|
|
4796
6844
|
}
|
|
4797
6845
|
});
|
|
4798
6846
|
|
|
@@ -5144,7 +7192,7 @@ var init_core = __esm({
|
|
|
5144
7192
|
});
|
|
5145
7193
|
|
|
5146
7194
|
// src/impl/static/StaticDataUnpacker.ts
|
|
5147
|
-
var pathUtils,
|
|
7195
|
+
var pathUtils, nodeFS3, StaticDataUnpacker;
|
|
5148
7196
|
var init_StaticDataUnpacker = __esm({
|
|
5149
7197
|
"src/impl/static/StaticDataUnpacker.ts"() {
|
|
5150
7198
|
"use strict";
|
|
@@ -5161,10 +7209,10 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5161
7209
|
return false;
|
|
5162
7210
|
}
|
|
5163
7211
|
};
|
|
5164
|
-
|
|
7212
|
+
nodeFS3 = null;
|
|
5165
7213
|
try {
|
|
5166
7214
|
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
5167
|
-
|
|
7215
|
+
nodeFS3 = eval("require")("fs");
|
|
5168
7216
|
}
|
|
5169
7217
|
} catch {
|
|
5170
7218
|
}
|
|
@@ -5351,8 +7399,8 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5351
7399
|
const chunkPath = `${this.basePath}/${chunk.path}`;
|
|
5352
7400
|
logger.debug(`Loading chunk from ${chunkPath}`);
|
|
5353
7401
|
let documents;
|
|
5354
|
-
if (this.isLocalPath(chunkPath) &&
|
|
5355
|
-
const fileContent = await
|
|
7402
|
+
if (this.isLocalPath(chunkPath) && nodeFS3) {
|
|
7403
|
+
const fileContent = await nodeFS3.promises.readFile(chunkPath, "utf8");
|
|
5356
7404
|
documents = JSON.parse(fileContent);
|
|
5357
7405
|
} else {
|
|
5358
7406
|
const response = await fetch(chunkPath);
|
|
@@ -5390,8 +7438,8 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5390
7438
|
const indexPath = `${this.basePath}/${indexMeta.path}`;
|
|
5391
7439
|
logger.debug(`Loading index from ${indexPath}`);
|
|
5392
7440
|
let indexData;
|
|
5393
|
-
if (this.isLocalPath(indexPath) &&
|
|
5394
|
-
const fileContent = await
|
|
7441
|
+
if (this.isLocalPath(indexPath) && nodeFS3) {
|
|
7442
|
+
const fileContent = await nodeFS3.promises.readFile(indexPath, "utf8");
|
|
5395
7443
|
indexData = JSON.parse(fileContent);
|
|
5396
7444
|
} else {
|
|
5397
7445
|
const response = await fetch(indexPath);
|
|
@@ -5466,8 +7514,8 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5466
7514
|
return null;
|
|
5467
7515
|
}
|
|
5468
7516
|
try {
|
|
5469
|
-
if (this.isLocalPath(attachmentPath) &&
|
|
5470
|
-
const buffer = await
|
|
7517
|
+
if (this.isLocalPath(attachmentPath) && nodeFS3) {
|
|
7518
|
+
const buffer = await nodeFS3.promises.readFile(attachmentPath);
|
|
5471
7519
|
return buffer;
|
|
5472
7520
|
} else {
|
|
5473
7521
|
const response = await fetch(attachmentPath);
|
|
@@ -5898,9 +7946,17 @@ var init_courseDB3 = __esm({
|
|
|
5898
7946
|
}
|
|
5899
7947
|
}
|
|
5900
7948
|
// Study Content Source implementation
|
|
7949
|
+
_pendingHints = null;
|
|
7950
|
+
setEphemeralHints(hints) {
|
|
7951
|
+
this._pendingHints = hints;
|
|
7952
|
+
}
|
|
5901
7953
|
async getWeightedCards(limit) {
|
|
5902
7954
|
try {
|
|
5903
7955
|
const navigator = await this.createNavigator(this.userDB);
|
|
7956
|
+
if (this._pendingHints) {
|
|
7957
|
+
navigator.setEphemeralHints(this._pendingHints);
|
|
7958
|
+
this._pendingHints = null;
|
|
7959
|
+
}
|
|
5904
7960
|
return navigator.getWeightedCards(limit);
|
|
5905
7961
|
} catch (e) {
|
|
5906
7962
|
logger.error(`[static/courseDB] Error getting weighted cards: ${e}`);
|