@vue-skuilder/db 0.1.32-c → 0.1.32-e
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/{dataLayerProvider-BAn-LRh5.d.ts → contentSource-BMlMwSiG.d.cts} +202 -626
- package/dist/{dataLayerProvider-BJqBlMIl.d.cts → contentSource-Ht3N2f-y.d.ts} +202 -626
- package/dist/core/index.d.cts +23 -84
- package/dist/core/index.d.ts +23 -84
- package/dist/core/index.js +476 -1819
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +456 -1803
- package/dist/core/index.mjs.map +1 -1
- package/dist/dataLayerProvider-BEqB8VBR.d.cts +67 -0
- package/dist/dataLayerProvider-DObSXjnf.d.ts +67 -0
- package/dist/impl/couch/index.d.cts +5 -5
- package/dist/impl/couch/index.d.ts +5 -5
- package/dist/impl/couch/index.js +484 -1827
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +460 -1807
- 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 +458 -1801
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +437 -1784
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-X6wHrURm.d.ts → index-BWvO-_rJ.d.ts} +1 -1
- package/dist/{index-m8MMGxxR.d.cts → index-Ba7hYbHj.d.cts} +1 -1
- package/dist/index.d.cts +461 -11
- package/dist/index.d.ts +461 -11
- package/dist/index.js +9239 -9159
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9129 -9049
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DZ5dUqbL.d.ts → types-CJrLM1Ew.d.ts} +1 -1
- package/dist/{types-ZL8tOPQZ.d.cts → types-W8n-B6HG.d.cts} +1 -1
- package/dist/{types-legacy-C7r0T4OV.d.cts → types-legacy-JXDxinpU.d.cts} +1 -1
- package/dist/{types-legacy-C7r0T4OV.d.ts → types-legacy-JXDxinpU.d.ts} +1 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +2 -2
- package/src/core/interfaces/contentSource.ts +2 -3
- package/src/core/navigators/Pipeline.ts +60 -6
- package/src/core/navigators/PipelineDebugger.ts +103 -0
- package/src/core/navigators/filters/hierarchyDefinition.ts +2 -1
- package/src/core/navigators/filters/interferenceMitigator.ts +2 -1
- package/src/core/navigators/filters/relativePriority.ts +2 -1
- package/src/core/navigators/filters/userTagPreference.ts +2 -1
- package/src/core/navigators/generators/CompositeGenerator.ts +58 -5
- package/src/core/navigators/generators/elo.ts +7 -7
- package/src/core/navigators/generators/prescribed.ts +124 -35
- package/src/core/navigators/generators/srs.ts +3 -4
- package/src/core/navigators/generators/types.ts +48 -2
- package/src/core/navigators/index.ts +3 -3
- package/src/impl/couch/classroomDB.ts +4 -3
- package/src/impl/couch/courseDB.ts +3 -3
- package/src/impl/static/courseDB.ts +3 -3
- package/src/study/SessionController.ts +5 -27
- package/src/study/TagFilteredContentSource.ts +4 -3
package/dist/core/index.mjs
CHANGED
|
@@ -916,6 +916,81 @@ var init_PipelineDebugger = __esm({
|
|
|
916
916
|
}
|
|
917
917
|
console.groupEnd();
|
|
918
918
|
},
|
|
919
|
+
/**
|
|
920
|
+
* Show prescribed-related cards from the most recent run.
|
|
921
|
+
*
|
|
922
|
+
* Highlights:
|
|
923
|
+
* - cards directly generated by the prescribed strategy
|
|
924
|
+
* - blocked prescribed targets mentioned in provenance
|
|
925
|
+
* - support tags resolved for blocked targets
|
|
926
|
+
*
|
|
927
|
+
* @param groupId - Optional prescribed group ID filter (e.g. 'intro-core')
|
|
928
|
+
*/
|
|
929
|
+
showPrescribed(groupId) {
|
|
930
|
+
if (runHistory.length === 0) {
|
|
931
|
+
logger.info("[Pipeline Debug] No runs captured yet.");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const run = runHistory[0];
|
|
935
|
+
const prescribedCards = run.cards.filter(
|
|
936
|
+
(c) => c.provenance.some((p) => p.strategy === "prescribed")
|
|
937
|
+
);
|
|
938
|
+
console.group(`\u{1F9ED} Prescribed Debug (${run.courseId})`);
|
|
939
|
+
if (prescribedCards.length === 0) {
|
|
940
|
+
logger.info("No prescribed-generated cards were present in the most recent run.");
|
|
941
|
+
console.groupEnd();
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const rows = prescribedCards.map((card) => {
|
|
945
|
+
const prescribedProv = card.provenance.find((p) => p.strategy === "prescribed");
|
|
946
|
+
const reason = prescribedProv?.reason ?? "";
|
|
947
|
+
const parsedGroup = reason.match(/group=([^;]+)/)?.[1] ?? "unknown";
|
|
948
|
+
const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
|
|
949
|
+
const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
|
|
950
|
+
const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
|
|
951
|
+
const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
|
|
952
|
+
const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
|
|
953
|
+
return {
|
|
954
|
+
group: parsedGroup,
|
|
955
|
+
mode,
|
|
956
|
+
cardId: card.cardId,
|
|
957
|
+
selected: card.selected ? "yes" : "no",
|
|
958
|
+
finalScore: card.finalScore.toFixed(3),
|
|
959
|
+
blocked,
|
|
960
|
+
blockedTargets,
|
|
961
|
+
supportTags,
|
|
962
|
+
multiplier
|
|
963
|
+
};
|
|
964
|
+
}).filter((row) => !groupId || row.group === groupId).sort((a, b) => Number(b.finalScore) - Number(a.finalScore));
|
|
965
|
+
if (rows.length === 0) {
|
|
966
|
+
logger.info(
|
|
967
|
+
`[Pipeline Debug] No prescribed cards matched group '${groupId}' in the most recent run.`
|
|
968
|
+
);
|
|
969
|
+
console.groupEnd();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
console.table(rows);
|
|
973
|
+
const selectedRows = rows.filter((r) => r.selected === "yes");
|
|
974
|
+
const blockedTargetSet = /* @__PURE__ */ new Set();
|
|
975
|
+
const supportTagSet = /* @__PURE__ */ new Set();
|
|
976
|
+
for (const row of rows) {
|
|
977
|
+
if (row.blockedTargets && row.blockedTargets !== "none") {
|
|
978
|
+
row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
|
|
979
|
+
}
|
|
980
|
+
if (row.supportTags && row.supportTags !== "none") {
|
|
981
|
+
row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
logger.info(`Prescribed cards in run: ${rows.length}`);
|
|
985
|
+
logger.info(`Selected prescribed cards: ${selectedRows.length}`);
|
|
986
|
+
logger.info(
|
|
987
|
+
`Blocked prescribed targets referenced: ${blockedTargetSet.size > 0 ? [...blockedTargetSet].join(", ") : "none"}`
|
|
988
|
+
);
|
|
989
|
+
logger.info(
|
|
990
|
+
`Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
|
|
991
|
+
);
|
|
992
|
+
console.groupEnd();
|
|
993
|
+
},
|
|
919
994
|
/**
|
|
920
995
|
* Show all runs in compact format.
|
|
921
996
|
*/
|
|
@@ -1064,6 +1139,7 @@ Commands:
|
|
|
1064
1139
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
1065
1140
|
.showRegistry() Show navigator registry (classes + roles)
|
|
1066
1141
|
.showStrategies() Show registry + strategy mapping from last run
|
|
1142
|
+
.showPrescribed(id?) Show prescribed-generated cards and blocked/support details from last run
|
|
1067
1143
|
.listRuns() List all captured runs in table format
|
|
1068
1144
|
.export() Export run history as JSON for bug reports
|
|
1069
1145
|
.clear() Clear run history
|
|
@@ -1087,6 +1163,44 @@ __export(CompositeGenerator_exports, {
|
|
|
1087
1163
|
AggregationMode: () => AggregationMode,
|
|
1088
1164
|
default: () => CompositeGenerator
|
|
1089
1165
|
});
|
|
1166
|
+
function mergeHints(allHints) {
|
|
1167
|
+
const defined = allHints.filter((h) => h !== void 0);
|
|
1168
|
+
if (defined.length === 0) return void 0;
|
|
1169
|
+
const merged = {};
|
|
1170
|
+
const boostTags = {};
|
|
1171
|
+
for (const hints of defined) {
|
|
1172
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
1173
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (Object.keys(boostTags).length > 0) {
|
|
1177
|
+
merged.boostTags = boostTags;
|
|
1178
|
+
}
|
|
1179
|
+
const boostCards = {};
|
|
1180
|
+
for (const hints of defined) {
|
|
1181
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
1182
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (Object.keys(boostCards).length > 0) {
|
|
1186
|
+
merged.boostCards = boostCards;
|
|
1187
|
+
}
|
|
1188
|
+
const concatUnique = (field) => {
|
|
1189
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
1190
|
+
if (values.length > 0) {
|
|
1191
|
+
merged[field] = [...new Set(values)];
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
concatUnique("requireTags");
|
|
1195
|
+
concatUnique("requireCards");
|
|
1196
|
+
concatUnique("excludeTags");
|
|
1197
|
+
concatUnique("excludeCards");
|
|
1198
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
1199
|
+
if (labels.length > 0) {
|
|
1200
|
+
merged._label = labels.join("; ");
|
|
1201
|
+
}
|
|
1202
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
1203
|
+
}
|
|
1090
1204
|
var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
|
|
1091
1205
|
var init_CompositeGenerator = __esm({
|
|
1092
1206
|
"src/core/navigators/generators/CompositeGenerator.ts"() {
|
|
@@ -1150,17 +1264,18 @@ var init_CompositeGenerator = __esm({
|
|
|
1150
1264
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
1151
1265
|
);
|
|
1152
1266
|
const generatorSummaries = [];
|
|
1153
|
-
results.forEach((
|
|
1267
|
+
results.forEach((result, index) => {
|
|
1268
|
+
const cards2 = result.cards;
|
|
1154
1269
|
const gen = this.generators[index];
|
|
1155
1270
|
const genName = gen.name || `Generator ${index}`;
|
|
1156
|
-
const newCards =
|
|
1157
|
-
const reviewCards =
|
|
1158
|
-
if (
|
|
1159
|
-
const topScore = Math.max(...
|
|
1271
|
+
const newCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("new card"));
|
|
1272
|
+
const reviewCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("review"));
|
|
1273
|
+
if (cards2.length > 0) {
|
|
1274
|
+
const topScore = Math.max(...cards2.map((c) => c.score)).toFixed(2);
|
|
1160
1275
|
const parts = [];
|
|
1161
1276
|
if (newCards.length > 0) parts.push(`${newCards.length} new`);
|
|
1162
1277
|
if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
|
|
1163
|
-
const breakdown = parts.length > 0 ? parts.join(", ") : `${
|
|
1278
|
+
const breakdown = parts.length > 0 ? parts.join(", ") : `${cards2.length} cards`;
|
|
1164
1279
|
generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
|
|
1165
1280
|
} else {
|
|
1166
1281
|
generatorSummaries.push(`${genName}: 0 cards`);
|
|
@@ -1168,7 +1283,8 @@ var init_CompositeGenerator = __esm({
|
|
|
1168
1283
|
});
|
|
1169
1284
|
logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
|
|
1170
1285
|
const byCardId = /* @__PURE__ */ new Map();
|
|
1171
|
-
results.forEach((
|
|
1286
|
+
results.forEach((result, index) => {
|
|
1287
|
+
const cards2 = result.cards;
|
|
1172
1288
|
const gen = this.generators[index];
|
|
1173
1289
|
let weight = gen.learnable?.weight ?? 1;
|
|
1174
1290
|
let deviation;
|
|
@@ -1179,7 +1295,7 @@ var init_CompositeGenerator = __esm({
|
|
|
1179
1295
|
deviation = context.orchestration.getDeviation(strategyId);
|
|
1180
1296
|
}
|
|
1181
1297
|
}
|
|
1182
|
-
for (const card of
|
|
1298
|
+
for (const card of cards2) {
|
|
1183
1299
|
if (card.provenance.length > 0) {
|
|
1184
1300
|
card.provenance[0].effectiveWeight = weight;
|
|
1185
1301
|
card.provenance[0].deviation = deviation;
|
|
@@ -1191,15 +1307,15 @@ var init_CompositeGenerator = __esm({
|
|
|
1191
1307
|
});
|
|
1192
1308
|
const merged = [];
|
|
1193
1309
|
for (const [, items] of byCardId) {
|
|
1194
|
-
const
|
|
1310
|
+
const cards2 = items.map((i) => i.card);
|
|
1195
1311
|
const aggregatedScore = this.aggregateScores(items);
|
|
1196
1312
|
const finalScore = Math.min(1, aggregatedScore);
|
|
1197
|
-
const mergedProvenance =
|
|
1198
|
-
const initialScore =
|
|
1313
|
+
const mergedProvenance = cards2.flatMap((c) => c.provenance);
|
|
1314
|
+
const initialScore = cards2[0].score;
|
|
1199
1315
|
const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
|
|
1200
1316
|
const reason = this.buildAggregationReason(items, finalScore);
|
|
1201
1317
|
merged.push({
|
|
1202
|
-
...
|
|
1318
|
+
...cards2[0],
|
|
1203
1319
|
score: finalScore,
|
|
1204
1320
|
provenance: [
|
|
1205
1321
|
...mergedProvenance,
|
|
@@ -1214,7 +1330,9 @@ var init_CompositeGenerator = __esm({
|
|
|
1214
1330
|
]
|
|
1215
1331
|
});
|
|
1216
1332
|
}
|
|
1217
|
-
|
|
1333
|
+
const cards = merged.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1334
|
+
const hints = mergeHints(results.map((result) => result.hints));
|
|
1335
|
+
return { cards, hints };
|
|
1218
1336
|
}
|
|
1219
1337
|
/**
|
|
1220
1338
|
* Build human-readable reason for score aggregation.
|
|
@@ -1345,16 +1463,16 @@ var init_elo = __esm({
|
|
|
1345
1463
|
};
|
|
1346
1464
|
});
|
|
1347
1465
|
scored.sort((a, b) => b.score - a.score);
|
|
1348
|
-
const
|
|
1349
|
-
if (
|
|
1350
|
-
const topScores =
|
|
1466
|
+
const cards = scored.slice(0, limit);
|
|
1467
|
+
if (cards.length > 0) {
|
|
1468
|
+
const topScores = cards.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
|
|
1351
1469
|
logger.info(
|
|
1352
|
-
`[ELO] Course ${this.course.getCourseID()}: ${
|
|
1470
|
+
`[ELO] Course ${this.course.getCourseID()}: ${cards.length} new cards (top scores: ${topScores})`
|
|
1353
1471
|
);
|
|
1354
1472
|
} else {
|
|
1355
1473
|
logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
|
|
1356
1474
|
}
|
|
1357
|
-
return
|
|
1475
|
+
return { cards };
|
|
1358
1476
|
}
|
|
1359
1477
|
};
|
|
1360
1478
|
}
|
|
@@ -1391,7 +1509,7 @@ function matchesTagPattern(tag, pattern) {
|
|
|
1391
1509
|
function pickTopByScore(cards, limit) {
|
|
1392
1510
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1393
1511
|
}
|
|
1394
|
-
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;
|
|
1512
|
+
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, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1395
1513
|
var init_prescribed = __esm({
|
|
1396
1514
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1397
1515
|
"use strict";
|
|
@@ -1408,6 +1526,7 @@ var init_prescribed = __esm({
|
|
|
1408
1526
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1409
1527
|
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1410
1528
|
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1529
|
+
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
|
|
1411
1530
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1412
1531
|
name;
|
|
1413
1532
|
config;
|
|
@@ -1424,7 +1543,7 @@ var init_prescribed = __esm({
|
|
|
1424
1543
|
}
|
|
1425
1544
|
async getWeightedCards(limit, context) {
|
|
1426
1545
|
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1427
|
-
return [];
|
|
1546
|
+
return { cards: [] };
|
|
1428
1547
|
}
|
|
1429
1548
|
const courseId = this.course.getCourseID();
|
|
1430
1549
|
const activeCards = await this.user.getActiveCards();
|
|
@@ -1449,6 +1568,7 @@ var init_prescribed = __esm({
|
|
|
1449
1568
|
};
|
|
1450
1569
|
const emitted = [];
|
|
1451
1570
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
1571
|
+
const groupRuntimes = [];
|
|
1452
1572
|
for (const group of this.config.groups) {
|
|
1453
1573
|
const runtime = this.buildGroupRuntimeState({
|
|
1454
1574
|
group,
|
|
@@ -1460,6 +1580,7 @@ var init_prescribed = __esm({
|
|
|
1460
1580
|
userTagElo,
|
|
1461
1581
|
userGlobalElo
|
|
1462
1582
|
});
|
|
1583
|
+
groupRuntimes.push(runtime);
|
|
1463
1584
|
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1464
1585
|
const directCards = this.buildDirectTargetCards(
|
|
1465
1586
|
runtime,
|
|
@@ -1473,12 +1594,17 @@ var init_prescribed = __esm({
|
|
|
1473
1594
|
);
|
|
1474
1595
|
emitted.push(...directCards, ...supportCards);
|
|
1475
1596
|
}
|
|
1597
|
+
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1598
|
+
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1599
|
+
boostTags: hintSummary.boostTags,
|
|
1600
|
+
_label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
|
|
1601
|
+
} : void 0;
|
|
1476
1602
|
if (emitted.length === 0) {
|
|
1477
1603
|
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1478
1604
|
await this.putStrategyState(nextState).catch((e) => {
|
|
1479
1605
|
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1480
1606
|
});
|
|
1481
|
-
return [];
|
|
1607
|
+
return hints ? { cards: [], hints } : { cards: [] };
|
|
1482
1608
|
}
|
|
1483
1609
|
const finalCards = pickTopByScore(emitted, limit);
|
|
1484
1610
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
@@ -1509,7 +1635,27 @@ var init_prescribed = __esm({
|
|
|
1509
1635
|
logger.info(
|
|
1510
1636
|
`[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)`
|
|
1511
1637
|
);
|
|
1512
|
-
return finalCards;
|
|
1638
|
+
return hints ? { cards: finalCards, hints } : { cards: finalCards };
|
|
1639
|
+
}
|
|
1640
|
+
buildSupportHintSummary(groupRuntimes) {
|
|
1641
|
+
const boostTags = {};
|
|
1642
|
+
const blockedTargetIds = /* @__PURE__ */ new Set();
|
|
1643
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1644
|
+
for (const runtime of groupRuntimes) {
|
|
1645
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportTags.length === 0) {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
runtime.blockedTargets.forEach((cardId) => blockedTargetIds.add(cardId));
|
|
1649
|
+
for (const tag of runtime.supportTags) {
|
|
1650
|
+
supportTags.add(tag);
|
|
1651
|
+
boostTags[tag] = (boostTags[tag] ?? 1) * runtime.supportMultiplier;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
boostTags,
|
|
1656
|
+
blockedTargetIds: [...blockedTargetIds].sort(),
|
|
1657
|
+
supportTags: [...supportTags].sort()
|
|
1658
|
+
};
|
|
1513
1659
|
}
|
|
1514
1660
|
parseConfig(serializedData) {
|
|
1515
1661
|
try {
|
|
@@ -1592,7 +1738,22 @@ var init_prescribed = __esm({
|
|
|
1592
1738
|
group.hierarchyWalk?.enabled !== false,
|
|
1593
1739
|
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1594
1740
|
);
|
|
1595
|
-
|
|
1741
|
+
const introTags = tags.filter((tag) => tag.startsWith("gpc:intro:"));
|
|
1742
|
+
const exposeTags = new Set(tags.filter((tag) => tag.startsWith("gpc:expose:")));
|
|
1743
|
+
for (const introTag of introTags) {
|
|
1744
|
+
const suffix = introTag.slice("gpc:intro:".length);
|
|
1745
|
+
if (suffix) {
|
|
1746
|
+
exposeTags.add(`gpc:expose:${suffix}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const unmetExposeTags = [...exposeTags].filter((tag) => {
|
|
1750
|
+
const tagElo = userTagElo[tag];
|
|
1751
|
+
return !tagElo || tagElo.count < DEFAULT_MIN_COUNT;
|
|
1752
|
+
});
|
|
1753
|
+
if (unmetExposeTags.length > 0) {
|
|
1754
|
+
unmetExposeTags.forEach((tag) => supportTags.add(tag));
|
|
1755
|
+
}
|
|
1756
|
+
if (resolution.blocked || unmetExposeTags.length > 0) {
|
|
1596
1757
|
blockedTargets.push(cardId);
|
|
1597
1758
|
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1598
1759
|
} else {
|
|
@@ -1622,7 +1783,8 @@ var init_prescribed = __esm({
|
|
|
1622
1783
|
supportCandidates,
|
|
1623
1784
|
supportTags: [...supportTags],
|
|
1624
1785
|
pressureMultiplier,
|
|
1625
|
-
supportMultiplier
|
|
1786
|
+
supportMultiplier,
|
|
1787
|
+
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
1626
1788
|
};
|
|
1627
1789
|
}
|
|
1628
1790
|
buildNextGroupState(runtime, prior) {
|
|
@@ -1630,6 +1792,7 @@ var init_prescribed = __esm({
|
|
|
1630
1792
|
const surfacedThisRun = false;
|
|
1631
1793
|
return {
|
|
1632
1794
|
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1795
|
+
pendingTargetIds: [...runtime.pendingTargets].sort(),
|
|
1633
1796
|
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1634
1797
|
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1635
1798
|
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
@@ -1654,7 +1817,7 @@ var init_prescribed = __esm({
|
|
|
1654
1817
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1655
1818
|
action: "generated",
|
|
1656
1819
|
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1657
|
-
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};multiplier=${runtime.pressureMultiplier.toFixed(2)}`
|
|
1820
|
+
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};blockedTargets=${runtime.blockedTargets.join("|") || "none"};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.pressureMultiplier.toFixed(2)};testversion=${runtime.debugVersion}`
|
|
1658
1821
|
}
|
|
1659
1822
|
]
|
|
1660
1823
|
});
|
|
@@ -1681,7 +1844,7 @@ var init_prescribed = __esm({
|
|
|
1681
1844
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1682
1845
|
action: "generated",
|
|
1683
1846
|
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1684
|
-
reason: `mode=support;group=${runtime.group.id};blocked=${runtime.blockedTargets.length};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)}`
|
|
1847
|
+
reason: `mode=support;group=${runtime.group.id};pending=${runtime.pendingTargets.length};blocked=${runtime.blockedTargets.length};blockedTargets=${runtime.blockedTargets.join("|") || "none"};supportCard=${cardId};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)};testversion=${runtime.debugVersion}`
|
|
1685
1848
|
}
|
|
1686
1849
|
]
|
|
1687
1850
|
});
|
|
@@ -1711,35 +1874,43 @@ var init_prescribed = __esm({
|
|
|
1711
1874
|
return [...candidates];
|
|
1712
1875
|
}
|
|
1713
1876
|
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1714
|
-
if (!hierarchyWalkEnabled || targetTags.length === 0 || hierarchyConfigs.length === 0) {
|
|
1715
|
-
return {
|
|
1716
|
-
blocked: false,
|
|
1717
|
-
supportTags: []
|
|
1718
|
-
};
|
|
1719
|
-
}
|
|
1720
1877
|
const supportTags = /* @__PURE__ */ new Set();
|
|
1721
1878
|
let blocked = false;
|
|
1722
1879
|
for (const targetTag of targetTags) {
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
)
|
|
1729
|
-
|
|
1730
|
-
|
|
1880
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[targetTag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
1881
|
+
if (prereqSets.length === 0) {
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
const tagBlocked = prereqSets.some(
|
|
1885
|
+
(prereqs) => prereqs.some((pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
1886
|
+
);
|
|
1887
|
+
if (!tagBlocked) {
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
blocked = true;
|
|
1891
|
+
if (!hierarchyWalkEnabled) {
|
|
1892
|
+
for (const prereqs of prereqSets) {
|
|
1893
|
+
for (const prereq of prereqs) {
|
|
1894
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1895
|
+
supportTags.add(prereq.tag);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1731
1898
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
for (const prereqs of prereqSets) {
|
|
1902
|
+
for (const prereq of prereqs) {
|
|
1903
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1904
|
+
this.collectSupportTagsRecursive(
|
|
1905
|
+
prereq.tag,
|
|
1906
|
+
hierarchyConfigs,
|
|
1907
|
+
userTagElo,
|
|
1908
|
+
userGlobalElo,
|
|
1909
|
+
maxDepth,
|
|
1910
|
+
/* @__PURE__ */ new Set(),
|
|
1911
|
+
supportTags
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1743
1914
|
}
|
|
1744
1915
|
}
|
|
1745
1916
|
}
|
|
@@ -1894,7 +2065,7 @@ var init_srs = __esm({
|
|
|
1894
2065
|
]
|
|
1895
2066
|
};
|
|
1896
2067
|
});
|
|
1897
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
2068
|
+
return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
1898
2069
|
}
|
|
1899
2070
|
/**
|
|
1900
2071
|
* Compute backlog pressure based on number of due reviews.
|
|
@@ -3137,1752 +3308,194 @@ function runPeriodUpdate(input) {
|
|
|
3137
3308
|
courseId,
|
|
3138
3309
|
strategyId,
|
|
3139
3310
|
newWeight,
|
|
3140
|
-
gradient,
|
|
3141
|
-
existingState
|
|
3142
|
-
);
|
|
3143
|
-
logger.info(
|
|
3144
|
-
`[Orchestration] Period update complete for ${strategyId}: weight ${currentWeight.weight.toFixed(3)} \u2192 ${newWeight.weight.toFixed(3)}, confidence ${currentWeight.confidence.toFixed(3)} \u2192 ${newWeight.confidence.toFixed(3)}`
|
|
3145
|
-
);
|
|
3146
|
-
return {
|
|
3147
|
-
strategyId,
|
|
3148
|
-
previousWeight: currentWeight,
|
|
3149
|
-
newWeight,
|
|
3150
|
-
gradient,
|
|
3151
|
-
learningState,
|
|
3152
|
-
updated
|
|
3153
|
-
};
|
|
3154
|
-
}
|
|
3155
|
-
function getDefaultLearnableWeight() {
|
|
3156
|
-
return { ...DEFAULT_LEARNABLE_WEIGHT };
|
|
3157
|
-
}
|
|
3158
|
-
var MIN_OBSERVATIONS_FOR_UPDATE, LEARNING_RATE, MAX_WEIGHT_DELTA, MIN_R_SQUARED_FOR_GRADIENT, FLAT_GRADIENT_THRESHOLD, MAX_HISTORY_LENGTH;
|
|
3159
|
-
var init_learning = __esm({
|
|
3160
|
-
"src/core/orchestration/learning.ts"() {
|
|
3161
|
-
"use strict";
|
|
3162
|
-
init_contentNavigationStrategy();
|
|
3163
|
-
init_types_legacy();
|
|
3164
|
-
init_logger();
|
|
3165
|
-
MIN_OBSERVATIONS_FOR_UPDATE = 10;
|
|
3166
|
-
LEARNING_RATE = 0.1;
|
|
3167
|
-
MAX_WEIGHT_DELTA = 0.3;
|
|
3168
|
-
MIN_R_SQUARED_FOR_GRADIENT = 0.05;
|
|
3169
|
-
FLAT_GRADIENT_THRESHOLD = 0.02;
|
|
3170
|
-
MAX_HISTORY_LENGTH = 100;
|
|
3171
|
-
}
|
|
3172
|
-
});
|
|
3173
|
-
|
|
3174
|
-
// src/core/orchestration/signal.ts
|
|
3175
|
-
function computeOutcomeSignal(records, config = {}) {
|
|
3176
|
-
if (!records || records.length === 0) {
|
|
3177
|
-
return null;
|
|
3178
|
-
}
|
|
3179
|
-
const target = config.targetAccuracy ?? 0.85;
|
|
3180
|
-
const tolerance = config.tolerance ?? 0.05;
|
|
3181
|
-
let correct = 0;
|
|
3182
|
-
for (const r of records) {
|
|
3183
|
-
if (r.isCorrect) correct++;
|
|
3184
|
-
}
|
|
3185
|
-
const accuracy = correct / records.length;
|
|
3186
|
-
return scoreAccuracyInZone(accuracy, target, tolerance);
|
|
3187
|
-
}
|
|
3188
|
-
function scoreAccuracyInZone(accuracy, target, tolerance) {
|
|
3189
|
-
const dist = Math.abs(accuracy - target);
|
|
3190
|
-
if (dist <= tolerance) {
|
|
3191
|
-
return 1;
|
|
3192
|
-
}
|
|
3193
|
-
const excess = dist - tolerance;
|
|
3194
|
-
const slope = 2.5;
|
|
3195
|
-
return Math.max(0, 1 - excess * slope);
|
|
3196
|
-
}
|
|
3197
|
-
var init_signal = __esm({
|
|
3198
|
-
"src/core/orchestration/signal.ts"() {
|
|
3199
|
-
"use strict";
|
|
3200
|
-
}
|
|
3201
|
-
});
|
|
3202
|
-
|
|
3203
|
-
// src/core/orchestration/recording.ts
|
|
3204
|
-
async function recordUserOutcome(context, periodStart, periodEnd, records, activeStrategyIds, eloStart = 0, eloEnd = 0, config) {
|
|
3205
|
-
const { user, course, userId } = context;
|
|
3206
|
-
const courseId = course.getCourseID();
|
|
3207
|
-
const outcomeValue = computeOutcomeSignal(records, config);
|
|
3208
|
-
if (outcomeValue === null) {
|
|
3209
|
-
logger.debug(
|
|
3210
|
-
`[Orchestration] No outcome signal computed for ${userId} (insufficient data). Skipping record.`
|
|
3211
|
-
);
|
|
3212
|
-
return;
|
|
3213
|
-
}
|
|
3214
|
-
const deviations = {};
|
|
3215
|
-
for (const strategyId of activeStrategyIds) {
|
|
3216
|
-
deviations[strategyId] = context.getDeviation(strategyId);
|
|
3217
|
-
}
|
|
3218
|
-
const id = `USER_OUTCOME::${courseId}::${userId}::${periodEnd}`;
|
|
3219
|
-
const record = {
|
|
3220
|
-
_id: id,
|
|
3221
|
-
docType: "USER_OUTCOME" /* USER_OUTCOME */,
|
|
3222
|
-
courseId,
|
|
3223
|
-
userId,
|
|
3224
|
-
periodStart,
|
|
3225
|
-
periodEnd,
|
|
3226
|
-
outcomeValue,
|
|
3227
|
-
deviations,
|
|
3228
|
-
metadata: {
|
|
3229
|
-
sessionsCount: 1,
|
|
3230
|
-
// Assumes recording is triggered per-session currently
|
|
3231
|
-
cardsSeen: records.length,
|
|
3232
|
-
eloStart,
|
|
3233
|
-
eloEnd,
|
|
3234
|
-
signalType: "accuracy_in_zone"
|
|
3235
|
-
}
|
|
3236
|
-
};
|
|
3237
|
-
try {
|
|
3238
|
-
await user.putUserOutcome(record);
|
|
3239
|
-
logger.debug(
|
|
3240
|
-
`[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
|
|
3241
|
-
);
|
|
3242
|
-
} catch (e) {
|
|
3243
|
-
logger.error(`[Orchestration] Failed to record outcome: ${e}`);
|
|
3244
|
-
}
|
|
3245
|
-
}
|
|
3246
|
-
var init_recording = __esm({
|
|
3247
|
-
"src/core/orchestration/recording.ts"() {
|
|
3248
|
-
"use strict";
|
|
3249
|
-
init_signal();
|
|
3250
|
-
init_types_legacy();
|
|
3251
|
-
init_logger();
|
|
3252
|
-
}
|
|
3253
|
-
});
|
|
3254
|
-
|
|
3255
|
-
// src/core/orchestration/index.ts
|
|
3256
|
-
function fnv1a(str) {
|
|
3257
|
-
let hash = 2166136261;
|
|
3258
|
-
for (let i = 0; i < str.length; i++) {
|
|
3259
|
-
hash ^= str.charCodeAt(i);
|
|
3260
|
-
hash = Math.imul(hash, 16777619);
|
|
3261
|
-
}
|
|
3262
|
-
return hash >>> 0;
|
|
3263
|
-
}
|
|
3264
|
-
function computeDeviation(userId, strategyId, salt) {
|
|
3265
|
-
const input = `${userId}:${strategyId}:${salt}`;
|
|
3266
|
-
const hash = fnv1a(input);
|
|
3267
|
-
const normalized = hash / 4294967296;
|
|
3268
|
-
return normalized * 2 - 1;
|
|
3269
|
-
}
|
|
3270
|
-
function computeSpread(confidence) {
|
|
3271
|
-
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
3272
|
-
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
3273
|
-
}
|
|
3274
|
-
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
3275
|
-
const deviation = computeDeviation(userId, strategyId, salt);
|
|
3276
|
-
const spread = computeSpread(learnable.confidence);
|
|
3277
|
-
const adjustment = deviation * spread * learnable.weight;
|
|
3278
|
-
const effective = learnable.weight + adjustment;
|
|
3279
|
-
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
3280
|
-
}
|
|
3281
|
-
async function createOrchestrationContext(user, course) {
|
|
3282
|
-
let courseConfig;
|
|
3283
|
-
try {
|
|
3284
|
-
courseConfig = await course.getCourseConfig();
|
|
3285
|
-
} catch (e) {
|
|
3286
|
-
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
3287
|
-
courseConfig = {
|
|
3288
|
-
name: "Unknown",
|
|
3289
|
-
description: "",
|
|
3290
|
-
public: false,
|
|
3291
|
-
deleted: false,
|
|
3292
|
-
creator: "",
|
|
3293
|
-
admins: [],
|
|
3294
|
-
moderators: [],
|
|
3295
|
-
dataShapes: [],
|
|
3296
|
-
questionTypes: [],
|
|
3297
|
-
orchestration: { salt: "default" }
|
|
3298
|
-
};
|
|
3299
|
-
}
|
|
3300
|
-
const userId = user.getUsername();
|
|
3301
|
-
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
3302
|
-
return {
|
|
3303
|
-
user,
|
|
3304
|
-
course,
|
|
3305
|
-
userId,
|
|
3306
|
-
courseConfig,
|
|
3307
|
-
getEffectiveWeight(strategyId, learnable) {
|
|
3308
|
-
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
3309
|
-
},
|
|
3310
|
-
getDeviation(strategyId) {
|
|
3311
|
-
return computeDeviation(userId, strategyId, salt);
|
|
3312
|
-
}
|
|
3313
|
-
};
|
|
3314
|
-
}
|
|
3315
|
-
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
3316
|
-
var init_orchestration = __esm({
|
|
3317
|
-
"src/core/orchestration/index.ts"() {
|
|
3318
|
-
"use strict";
|
|
3319
|
-
init_logger();
|
|
3320
|
-
init_gradient();
|
|
3321
|
-
init_learning();
|
|
3322
|
-
init_signal();
|
|
3323
|
-
init_recording();
|
|
3324
|
-
MIN_SPREAD = 0.1;
|
|
3325
|
-
MAX_SPREAD = 0.5;
|
|
3326
|
-
MIN_WEIGHT = 0.1;
|
|
3327
|
-
MAX_WEIGHT = 3;
|
|
3328
|
-
}
|
|
3329
|
-
});
|
|
3330
|
-
|
|
3331
|
-
// src/study/SpacedRepetition.ts
|
|
3332
|
-
import moment4 from "moment";
|
|
3333
|
-
import { isTaggedPerformance } from "@vue-skuilder/common";
|
|
3334
|
-
var duration;
|
|
3335
|
-
var init_SpacedRepetition = __esm({
|
|
3336
|
-
"src/study/SpacedRepetition.ts"() {
|
|
3337
|
-
"use strict";
|
|
3338
|
-
init_util();
|
|
3339
|
-
init_logger();
|
|
3340
|
-
duration = moment4.duration;
|
|
3341
|
-
}
|
|
3342
|
-
});
|
|
3343
|
-
|
|
3344
|
-
// src/study/services/SrsService.ts
|
|
3345
|
-
import moment5 from "moment";
|
|
3346
|
-
var init_SrsService = __esm({
|
|
3347
|
-
"src/study/services/SrsService.ts"() {
|
|
3348
|
-
"use strict";
|
|
3349
|
-
init_couch();
|
|
3350
|
-
init_SpacedRepetition();
|
|
3351
|
-
init_logger();
|
|
3352
|
-
}
|
|
3353
|
-
});
|
|
3354
|
-
|
|
3355
|
-
// src/study/services/EloService.ts
|
|
3356
|
-
import {
|
|
3357
|
-
adjustCourseScores,
|
|
3358
|
-
adjustCourseScoresPerTag,
|
|
3359
|
-
toCourseElo as toCourseElo5
|
|
3360
|
-
} from "@vue-skuilder/common";
|
|
3361
|
-
var init_EloService = __esm({
|
|
3362
|
-
"src/study/services/EloService.ts"() {
|
|
3363
|
-
"use strict";
|
|
3364
|
-
init_logger();
|
|
3365
|
-
}
|
|
3366
|
-
});
|
|
3367
|
-
|
|
3368
|
-
// src/study/services/ResponseProcessor.ts
|
|
3369
|
-
import { isTaggedPerformance as isTaggedPerformance2 } from "@vue-skuilder/common";
|
|
3370
|
-
var init_ResponseProcessor = __esm({
|
|
3371
|
-
"src/study/services/ResponseProcessor.ts"() {
|
|
3372
|
-
"use strict";
|
|
3373
|
-
init_core();
|
|
3374
|
-
init_logger();
|
|
3375
|
-
}
|
|
3376
|
-
});
|
|
3377
|
-
|
|
3378
|
-
// src/study/services/CardHydrationService.ts
|
|
3379
|
-
import {
|
|
3380
|
-
displayableDataToViewData,
|
|
3381
|
-
isCourseElo,
|
|
3382
|
-
toCourseElo as toCourseElo6
|
|
3383
|
-
} from "@vue-skuilder/common";
|
|
3384
|
-
var init_CardHydrationService = __esm({
|
|
3385
|
-
"src/study/services/CardHydrationService.ts"() {
|
|
3386
|
-
"use strict";
|
|
3387
|
-
init_logger();
|
|
3388
|
-
}
|
|
3389
|
-
});
|
|
3390
|
-
|
|
3391
|
-
// src/study/ItemQueue.ts
|
|
3392
|
-
var init_ItemQueue = __esm({
|
|
3393
|
-
"src/study/ItemQueue.ts"() {
|
|
3394
|
-
"use strict";
|
|
3395
|
-
}
|
|
3396
|
-
});
|
|
3397
|
-
|
|
3398
|
-
// src/util/packer/types.ts
|
|
3399
|
-
var init_types3 = __esm({
|
|
3400
|
-
"src/util/packer/types.ts"() {
|
|
3401
|
-
"use strict";
|
|
3402
|
-
}
|
|
3403
|
-
});
|
|
3404
|
-
|
|
3405
|
-
// src/util/packer/CouchDBToStaticPacker.ts
|
|
3406
|
-
var init_CouchDBToStaticPacker = __esm({
|
|
3407
|
-
"src/util/packer/CouchDBToStaticPacker.ts"() {
|
|
3408
|
-
"use strict";
|
|
3409
|
-
init_types_legacy();
|
|
3410
|
-
init_logger();
|
|
3411
|
-
}
|
|
3412
|
-
});
|
|
3413
|
-
|
|
3414
|
-
// src/util/packer/index.ts
|
|
3415
|
-
var init_packer = __esm({
|
|
3416
|
-
"src/util/packer/index.ts"() {
|
|
3417
|
-
"use strict";
|
|
3418
|
-
init_types3();
|
|
3419
|
-
init_CouchDBToStaticPacker();
|
|
3420
|
-
}
|
|
3421
|
-
});
|
|
3422
|
-
|
|
3423
|
-
// src/util/migrator/types.ts
|
|
3424
|
-
var DEFAULT_MIGRATION_OPTIONS;
|
|
3425
|
-
var init_types4 = __esm({
|
|
3426
|
-
"src/util/migrator/types.ts"() {
|
|
3427
|
-
"use strict";
|
|
3428
|
-
DEFAULT_MIGRATION_OPTIONS = {
|
|
3429
|
-
chunkBatchSize: 100,
|
|
3430
|
-
validateRoundTrip: false,
|
|
3431
|
-
cleanupOnFailure: true,
|
|
3432
|
-
timeout: 3e5
|
|
3433
|
-
// 5 minutes
|
|
3434
|
-
};
|
|
3435
|
-
}
|
|
3436
|
-
});
|
|
3437
|
-
|
|
3438
|
-
// src/util/migrator/FileSystemAdapter.ts
|
|
3439
|
-
var FileSystemError;
|
|
3440
|
-
var init_FileSystemAdapter = __esm({
|
|
3441
|
-
"src/util/migrator/FileSystemAdapter.ts"() {
|
|
3442
|
-
"use strict";
|
|
3443
|
-
FileSystemError = class extends Error {
|
|
3444
|
-
constructor(message, operation, filePath, cause) {
|
|
3445
|
-
super(message);
|
|
3446
|
-
this.operation = operation;
|
|
3447
|
-
this.filePath = filePath;
|
|
3448
|
-
this.cause = cause;
|
|
3449
|
-
this.name = "FileSystemError";
|
|
3450
|
-
}
|
|
3451
|
-
};
|
|
3452
|
-
}
|
|
3453
|
-
});
|
|
3454
|
-
|
|
3455
|
-
// src/util/migrator/validation.ts
|
|
3456
|
-
async function validateStaticCourse(staticPath, fs) {
|
|
3457
|
-
const validation = {
|
|
3458
|
-
valid: true,
|
|
3459
|
-
manifestExists: false,
|
|
3460
|
-
chunksExist: false,
|
|
3461
|
-
attachmentsExist: false,
|
|
3462
|
-
errors: [],
|
|
3463
|
-
warnings: []
|
|
3464
|
-
};
|
|
3465
|
-
try {
|
|
3466
|
-
if (fs) {
|
|
3467
|
-
const stats = await fs.stat(staticPath);
|
|
3468
|
-
if (!stats.isDirectory()) {
|
|
3469
|
-
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3470
|
-
validation.valid = false;
|
|
3471
|
-
return validation;
|
|
3472
|
-
}
|
|
3473
|
-
} else if (!nodeFS) {
|
|
3474
|
-
validation.errors.push("File system access not available - validation skipped");
|
|
3475
|
-
validation.valid = false;
|
|
3476
|
-
return validation;
|
|
3477
|
-
} else {
|
|
3478
|
-
const stats = await nodeFS.promises.stat(staticPath);
|
|
3479
|
-
if (!stats.isDirectory()) {
|
|
3480
|
-
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3481
|
-
validation.valid = false;
|
|
3482
|
-
return validation;
|
|
3483
|
-
}
|
|
3484
|
-
}
|
|
3485
|
-
let manifestPath = `${staticPath}/manifest.json`;
|
|
3486
|
-
try {
|
|
3487
|
-
if (fs) {
|
|
3488
|
-
manifestPath = fs.joinPath(staticPath, "manifest.json");
|
|
3489
|
-
if (await fs.exists(manifestPath)) {
|
|
3490
|
-
validation.manifestExists = true;
|
|
3491
|
-
const manifestContent = await fs.readFile(manifestPath);
|
|
3492
|
-
const manifest = JSON.parse(manifestContent);
|
|
3493
|
-
validation.courseId = manifest.courseId;
|
|
3494
|
-
validation.courseName = manifest.courseName;
|
|
3495
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3496
|
-
validation.errors.push("Invalid manifest structure");
|
|
3497
|
-
validation.valid = false;
|
|
3498
|
-
}
|
|
3499
|
-
} else {
|
|
3500
|
-
validation.errors.push(`Manifest not found: ${manifestPath}`);
|
|
3501
|
-
validation.valid = false;
|
|
3502
|
-
}
|
|
3503
|
-
} else {
|
|
3504
|
-
manifestPath = `${staticPath}/manifest.json`;
|
|
3505
|
-
await nodeFS.promises.access(manifestPath);
|
|
3506
|
-
validation.manifestExists = true;
|
|
3507
|
-
const manifestContent = await nodeFS.promises.readFile(manifestPath, "utf8");
|
|
3508
|
-
const manifest = JSON.parse(manifestContent);
|
|
3509
|
-
validation.courseId = manifest.courseId;
|
|
3510
|
-
validation.courseName = manifest.courseName;
|
|
3511
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3512
|
-
validation.errors.push("Invalid manifest structure");
|
|
3513
|
-
validation.valid = false;
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
} catch (error) {
|
|
3517
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Manifest not found or invalid: ${manifestPath}`;
|
|
3518
|
-
validation.errors.push(errorMessage);
|
|
3519
|
-
validation.valid = false;
|
|
3520
|
-
}
|
|
3521
|
-
let chunksPath = `${staticPath}/chunks`;
|
|
3522
|
-
try {
|
|
3523
|
-
if (fs) {
|
|
3524
|
-
chunksPath = fs.joinPath(staticPath, "chunks");
|
|
3525
|
-
if (await fs.exists(chunksPath)) {
|
|
3526
|
-
const chunksStats = await fs.stat(chunksPath);
|
|
3527
|
-
if (chunksStats.isDirectory()) {
|
|
3528
|
-
validation.chunksExist = true;
|
|
3529
|
-
} else {
|
|
3530
|
-
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3531
|
-
validation.valid = false;
|
|
3532
|
-
}
|
|
3533
|
-
} else {
|
|
3534
|
-
validation.errors.push(`Chunks directory not found: ${chunksPath}`);
|
|
3535
|
-
validation.valid = false;
|
|
3536
|
-
}
|
|
3537
|
-
} else {
|
|
3538
|
-
chunksPath = `${staticPath}/chunks`;
|
|
3539
|
-
const chunksStats = await nodeFS.promises.stat(chunksPath);
|
|
3540
|
-
if (chunksStats.isDirectory()) {
|
|
3541
|
-
validation.chunksExist = true;
|
|
3542
|
-
} else {
|
|
3543
|
-
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3544
|
-
validation.valid = false;
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3547
|
-
} catch (error) {
|
|
3548
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Chunks directory not found: ${chunksPath}`;
|
|
3549
|
-
validation.errors.push(errorMessage);
|
|
3550
|
-
validation.valid = false;
|
|
3551
|
-
}
|
|
3552
|
-
let attachmentsPath;
|
|
3553
|
-
try {
|
|
3554
|
-
if (fs) {
|
|
3555
|
-
attachmentsPath = fs.joinPath(staticPath, "attachments");
|
|
3556
|
-
if (await fs.exists(attachmentsPath)) {
|
|
3557
|
-
const attachmentsStats = await fs.stat(attachmentsPath);
|
|
3558
|
-
if (attachmentsStats.isDirectory()) {
|
|
3559
|
-
validation.attachmentsExist = true;
|
|
3560
|
-
}
|
|
3561
|
-
} else {
|
|
3562
|
-
validation.warnings.push(
|
|
3563
|
-
`Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
|
|
3564
|
-
);
|
|
3565
|
-
}
|
|
3566
|
-
} else {
|
|
3567
|
-
attachmentsPath = `${staticPath}/attachments`;
|
|
3568
|
-
const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
|
|
3569
|
-
if (attachmentsStats.isDirectory()) {
|
|
3570
|
-
validation.attachmentsExist = true;
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
} catch (error) {
|
|
3574
|
-
attachmentsPath = attachmentsPath || `${staticPath}/attachments`;
|
|
3575
|
-
const warningMessage = error instanceof FileSystemError ? error.message : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
|
|
3576
|
-
validation.warnings.push(warningMessage);
|
|
3577
|
-
}
|
|
3578
|
-
} catch (error) {
|
|
3579
|
-
validation.errors.push(
|
|
3580
|
-
`Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
|
|
3581
|
-
);
|
|
3582
|
-
validation.valid = false;
|
|
3583
|
-
}
|
|
3584
|
-
return validation;
|
|
3585
|
-
}
|
|
3586
|
-
async function validateMigration(targetDB, expectedCounts, manifest) {
|
|
3587
|
-
const validation = {
|
|
3588
|
-
valid: true,
|
|
3589
|
-
documentCountMatch: false,
|
|
3590
|
-
attachmentIntegrity: false,
|
|
3591
|
-
viewFunctionality: false,
|
|
3592
|
-
issues: []
|
|
3593
|
-
};
|
|
3594
|
-
try {
|
|
3595
|
-
logger.info("Starting migration validation...");
|
|
3596
|
-
const actualCounts = await getActualDocumentCounts(targetDB);
|
|
3597
|
-
validation.documentCountMatch = compareDocumentCounts(
|
|
3598
|
-
expectedCounts,
|
|
3599
|
-
actualCounts,
|
|
3600
|
-
validation.issues
|
|
3601
|
-
);
|
|
3602
|
-
await validateCourseConfig(targetDB, manifest, validation.issues);
|
|
3603
|
-
validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
|
|
3604
|
-
validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
|
|
3605
|
-
validation.valid = validation.documentCountMatch && validation.viewFunctionality && validation.attachmentIntegrity;
|
|
3606
|
-
logger.info(`Migration validation completed. Valid: ${validation.valid}`);
|
|
3607
|
-
if (validation.issues.length > 0) {
|
|
3608
|
-
logger.info(`Validation issues: ${validation.issues.length}`);
|
|
3609
|
-
validation.issues.forEach((issue) => {
|
|
3610
|
-
if (issue.type === "error") {
|
|
3611
|
-
logger.error(`${issue.category}: ${issue.message}`);
|
|
3612
|
-
} else {
|
|
3613
|
-
logger.warn(`${issue.category}: ${issue.message}`);
|
|
3614
|
-
}
|
|
3615
|
-
});
|
|
3616
|
-
}
|
|
3617
|
-
} catch (error) {
|
|
3618
|
-
validation.valid = false;
|
|
3619
|
-
validation.issues.push({
|
|
3620
|
-
type: "error",
|
|
3621
|
-
category: "metadata",
|
|
3622
|
-
message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3623
|
-
});
|
|
3624
|
-
}
|
|
3625
|
-
return validation;
|
|
3626
|
-
}
|
|
3627
|
-
async function getActualDocumentCounts(db) {
|
|
3628
|
-
const counts = {};
|
|
3629
|
-
try {
|
|
3630
|
-
const allDocs = await db.allDocs({ include_docs: true });
|
|
3631
|
-
for (const row of allDocs.rows) {
|
|
3632
|
-
if (row.id.startsWith("_design/")) {
|
|
3633
|
-
counts["_design"] = (counts["_design"] || 0) + 1;
|
|
3634
|
-
continue;
|
|
3635
|
-
}
|
|
3636
|
-
const doc = row.doc;
|
|
3637
|
-
if (doc && doc.docType) {
|
|
3638
|
-
counts[doc.docType] = (counts[doc.docType] || 0) + 1;
|
|
3639
|
-
} else {
|
|
3640
|
-
counts["unknown"] = (counts["unknown"] || 0) + 1;
|
|
3641
|
-
}
|
|
3642
|
-
}
|
|
3643
|
-
} catch (error) {
|
|
3644
|
-
logger.error("Failed to get actual document counts:", error);
|
|
3645
|
-
}
|
|
3646
|
-
return counts;
|
|
3647
|
-
}
|
|
3648
|
-
function compareDocumentCounts(expected, actual, issues) {
|
|
3649
|
-
let countsMatch = true;
|
|
3650
|
-
for (const [docType, expectedCount] of Object.entries(expected)) {
|
|
3651
|
-
const actualCount = actual[docType] || 0;
|
|
3652
|
-
if (actualCount !== expectedCount) {
|
|
3653
|
-
countsMatch = false;
|
|
3654
|
-
issues.push({
|
|
3655
|
-
type: "error",
|
|
3656
|
-
category: "documents",
|
|
3657
|
-
message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`
|
|
3658
|
-
});
|
|
3659
|
-
}
|
|
3660
|
-
}
|
|
3661
|
-
for (const [docType, actualCount] of Object.entries(actual)) {
|
|
3662
|
-
if (!expected[docType] && docType !== "_design") {
|
|
3663
|
-
issues.push({
|
|
3664
|
-
type: "warning",
|
|
3665
|
-
category: "documents",
|
|
3666
|
-
message: `Unexpected document type found: ${docType} (${actualCount} documents)`
|
|
3667
|
-
});
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
return countsMatch;
|
|
3671
|
-
}
|
|
3672
|
-
async function validateCourseConfig(db, manifest, issues) {
|
|
3673
|
-
try {
|
|
3674
|
-
const courseConfig = await db.get("CourseConfig");
|
|
3675
|
-
if (!courseConfig) {
|
|
3676
|
-
issues.push({
|
|
3677
|
-
type: "error",
|
|
3678
|
-
category: "course_config",
|
|
3679
|
-
message: "CourseConfig document not found after migration"
|
|
3680
|
-
});
|
|
3681
|
-
return;
|
|
3682
|
-
}
|
|
3683
|
-
if (!courseConfig.courseID) {
|
|
3684
|
-
issues.push({
|
|
3685
|
-
type: "warning",
|
|
3686
|
-
category: "course_config",
|
|
3687
|
-
message: "CourseConfig document missing courseID field"
|
|
3688
|
-
});
|
|
3689
|
-
}
|
|
3690
|
-
if (courseConfig.courseID !== manifest.courseId) {
|
|
3691
|
-
issues.push({
|
|
3692
|
-
type: "warning",
|
|
3693
|
-
category: "course_config",
|
|
3694
|
-
message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${courseConfig.courseID}`
|
|
3695
|
-
});
|
|
3696
|
-
}
|
|
3697
|
-
logger.debug("CourseConfig document validation passed");
|
|
3698
|
-
} catch (error) {
|
|
3699
|
-
if (error.status === 404) {
|
|
3700
|
-
issues.push({
|
|
3701
|
-
type: "error",
|
|
3702
|
-
category: "course_config",
|
|
3703
|
-
message: "CourseConfig document not found in database"
|
|
3704
|
-
});
|
|
3705
|
-
} else {
|
|
3706
|
-
issues.push({
|
|
3707
|
-
type: "error",
|
|
3708
|
-
category: "course_config",
|
|
3709
|
-
message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`
|
|
3710
|
-
});
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
async function validateViews(db, manifest, issues) {
|
|
3715
|
-
let viewsValid = true;
|
|
3716
|
-
try {
|
|
3717
|
-
for (const designDoc of manifest.designDocs) {
|
|
3718
|
-
try {
|
|
3719
|
-
const doc = await db.get(designDoc._id);
|
|
3720
|
-
if (!doc) {
|
|
3721
|
-
viewsValid = false;
|
|
3722
|
-
issues.push({
|
|
3723
|
-
type: "error",
|
|
3724
|
-
category: "views",
|
|
3725
|
-
message: `Design document not found: ${designDoc._id}`
|
|
3726
|
-
});
|
|
3727
|
-
continue;
|
|
3728
|
-
}
|
|
3729
|
-
for (const viewName of Object.keys(designDoc.views)) {
|
|
3730
|
-
try {
|
|
3731
|
-
const viewPath = `${designDoc._id}/${viewName}`;
|
|
3732
|
-
await db.query(viewPath, { limit: 1 });
|
|
3733
|
-
} catch (viewError) {
|
|
3734
|
-
viewsValid = false;
|
|
3735
|
-
issues.push({
|
|
3736
|
-
type: "error",
|
|
3737
|
-
category: "views",
|
|
3738
|
-
message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`
|
|
3739
|
-
});
|
|
3740
|
-
}
|
|
3741
|
-
}
|
|
3742
|
-
} catch (error) {
|
|
3743
|
-
viewsValid = false;
|
|
3744
|
-
issues.push({
|
|
3745
|
-
type: "error",
|
|
3746
|
-
category: "views",
|
|
3747
|
-
message: `Failed to validate design document ${designDoc._id}: ${error}`
|
|
3748
|
-
});
|
|
3749
|
-
}
|
|
3750
|
-
}
|
|
3751
|
-
} catch (error) {
|
|
3752
|
-
viewsValid = false;
|
|
3753
|
-
issues.push({
|
|
3754
|
-
type: "error",
|
|
3755
|
-
category: "views",
|
|
3756
|
-
message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3757
|
-
});
|
|
3758
|
-
}
|
|
3759
|
-
return viewsValid;
|
|
3760
|
-
}
|
|
3761
|
-
async function validateAttachmentIntegrity(db, issues) {
|
|
3762
|
-
let attachmentsValid = true;
|
|
3763
|
-
try {
|
|
3764
|
-
const allDocs = await db.allDocs({
|
|
3765
|
-
include_docs: true,
|
|
3766
|
-
limit: 10
|
|
3767
|
-
// Sample first 10 documents for performance
|
|
3768
|
-
});
|
|
3769
|
-
let attachmentCount = 0;
|
|
3770
|
-
let validAttachments = 0;
|
|
3771
|
-
for (const row of allDocs.rows) {
|
|
3772
|
-
const doc = row.doc;
|
|
3773
|
-
if (doc && doc._attachments) {
|
|
3774
|
-
for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3775
|
-
attachmentCount++;
|
|
3776
|
-
try {
|
|
3777
|
-
const attachment = await db.getAttachment(doc._id, attachmentName);
|
|
3778
|
-
if (attachment) {
|
|
3779
|
-
validAttachments++;
|
|
3780
|
-
}
|
|
3781
|
-
} catch (attachmentError) {
|
|
3782
|
-
attachmentsValid = false;
|
|
3783
|
-
issues.push({
|
|
3784
|
-
type: "error",
|
|
3785
|
-
category: "attachments",
|
|
3786
|
-
message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`
|
|
3787
|
-
});
|
|
3788
|
-
}
|
|
3789
|
-
}
|
|
3790
|
-
}
|
|
3791
|
-
}
|
|
3792
|
-
if (attachmentCount === 0) {
|
|
3793
|
-
issues.push({
|
|
3794
|
-
type: "warning",
|
|
3795
|
-
category: "attachments",
|
|
3796
|
-
message: "No attachments found in sampled documents"
|
|
3797
|
-
});
|
|
3798
|
-
} else {
|
|
3799
|
-
logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
|
|
3800
|
-
}
|
|
3801
|
-
} catch (error) {
|
|
3802
|
-
attachmentsValid = false;
|
|
3803
|
-
issues.push({
|
|
3804
|
-
type: "error",
|
|
3805
|
-
category: "attachments",
|
|
3806
|
-
message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3807
|
-
});
|
|
3808
|
-
}
|
|
3809
|
-
return attachmentsValid;
|
|
3810
|
-
}
|
|
3811
|
-
var nodeFS;
|
|
3812
|
-
var init_validation = __esm({
|
|
3813
|
-
"src/util/migrator/validation.ts"() {
|
|
3814
|
-
"use strict";
|
|
3815
|
-
init_logger();
|
|
3816
|
-
init_FileSystemAdapter();
|
|
3817
|
-
nodeFS = null;
|
|
3818
|
-
try {
|
|
3819
|
-
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3820
|
-
nodeFS = eval("require")("fs");
|
|
3821
|
-
nodeFS.promises = nodeFS.promises || eval("require")("fs").promises;
|
|
3822
|
-
}
|
|
3823
|
-
} catch {
|
|
3824
|
-
}
|
|
3825
|
-
}
|
|
3826
|
-
});
|
|
3827
|
-
|
|
3828
|
-
// src/util/migrator/StaticToCouchDBMigrator.ts
|
|
3829
|
-
var nodeFS2, nodePath, StaticToCouchDBMigrator;
|
|
3830
|
-
var init_StaticToCouchDBMigrator = __esm({
|
|
3831
|
-
"src/util/migrator/StaticToCouchDBMigrator.ts"() {
|
|
3832
|
-
"use strict";
|
|
3833
|
-
init_logger();
|
|
3834
|
-
init_types4();
|
|
3835
|
-
init_validation();
|
|
3836
|
-
init_FileSystemAdapter();
|
|
3837
|
-
nodeFS2 = null;
|
|
3838
|
-
nodePath = null;
|
|
3839
|
-
try {
|
|
3840
|
-
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3841
|
-
nodeFS2 = eval("require")("fs");
|
|
3842
|
-
nodePath = eval("require")("path");
|
|
3843
|
-
nodeFS2.promises = nodeFS2.promises || eval("require")("fs").promises;
|
|
3844
|
-
}
|
|
3845
|
-
} catch {
|
|
3846
|
-
}
|
|
3847
|
-
StaticToCouchDBMigrator = class {
|
|
3848
|
-
options;
|
|
3849
|
-
progressCallback;
|
|
3850
|
-
fs;
|
|
3851
|
-
constructor(options = {}, fileSystemAdapter) {
|
|
3852
|
-
this.options = {
|
|
3853
|
-
...DEFAULT_MIGRATION_OPTIONS,
|
|
3854
|
-
...options
|
|
3855
|
-
};
|
|
3856
|
-
this.fs = fileSystemAdapter;
|
|
3857
|
-
}
|
|
3858
|
-
/**
|
|
3859
|
-
* Set a progress callback to receive updates during migration
|
|
3860
|
-
*/
|
|
3861
|
-
setProgressCallback(callback) {
|
|
3862
|
-
this.progressCallback = callback;
|
|
3863
|
-
}
|
|
3864
|
-
/**
|
|
3865
|
-
* Migrate a static course to CouchDB
|
|
3866
|
-
*/
|
|
3867
|
-
async migrateCourse(staticPath, targetDB) {
|
|
3868
|
-
const startTime = Date.now();
|
|
3869
|
-
const result = {
|
|
3870
|
-
success: false,
|
|
3871
|
-
documentsRestored: 0,
|
|
3872
|
-
attachmentsRestored: 0,
|
|
3873
|
-
designDocsRestored: 0,
|
|
3874
|
-
courseConfigRestored: 0,
|
|
3875
|
-
errors: [],
|
|
3876
|
-
warnings: [],
|
|
3877
|
-
migrationTime: 0
|
|
3878
|
-
};
|
|
3879
|
-
try {
|
|
3880
|
-
logger.info(`Starting migration from ${staticPath} to CouchDB`);
|
|
3881
|
-
this.reportProgress("manifest", 0, 1, "Validating static course...");
|
|
3882
|
-
const validation = await validateStaticCourse(staticPath, this.fs);
|
|
3883
|
-
if (!validation.valid) {
|
|
3884
|
-
result.errors.push(...validation.errors);
|
|
3885
|
-
throw new Error(`Static course validation failed: ${validation.errors.join(", ")}`);
|
|
3886
|
-
}
|
|
3887
|
-
result.warnings.push(...validation.warnings);
|
|
3888
|
-
this.reportProgress("manifest", 1, 1, "Loading course manifest...");
|
|
3889
|
-
const manifest = await this.loadManifest(staticPath);
|
|
3890
|
-
logger.info(`Loaded manifest for course: ${manifest.courseId} (${manifest.courseName})`);
|
|
3891
|
-
this.reportProgress(
|
|
3892
|
-
"design_docs",
|
|
3893
|
-
0,
|
|
3894
|
-
manifest.designDocs.length,
|
|
3895
|
-
"Restoring design documents..."
|
|
3896
|
-
);
|
|
3897
|
-
const designDocResults = await this.restoreDesignDocuments(manifest.designDocs, targetDB);
|
|
3898
|
-
result.designDocsRestored = designDocResults.restored;
|
|
3899
|
-
result.errors.push(...designDocResults.errors);
|
|
3900
|
-
result.warnings.push(...designDocResults.warnings);
|
|
3901
|
-
this.reportProgress("course_config", 0, 1, "Restoring CourseConfig document...");
|
|
3902
|
-
const courseConfigResults = await this.restoreCourseConfig(manifest, targetDB);
|
|
3903
|
-
result.courseConfigRestored = courseConfigResults.restored;
|
|
3904
|
-
result.errors.push(...courseConfigResults.errors);
|
|
3905
|
-
result.warnings.push(...courseConfigResults.warnings);
|
|
3906
|
-
this.reportProgress("course_config", 1, 1, "CourseConfig document restored");
|
|
3907
|
-
const expectedCounts = this.calculateExpectedCounts(manifest);
|
|
3908
|
-
this.reportProgress(
|
|
3909
|
-
"documents",
|
|
3910
|
-
0,
|
|
3911
|
-
manifest.documentCount,
|
|
3912
|
-
"Aggregating documents from chunks..."
|
|
3913
|
-
);
|
|
3914
|
-
const documents = await this.aggregateDocuments(staticPath, manifest);
|
|
3915
|
-
const filteredDocuments = documents.filter((doc) => doc._id !== "CourseConfig");
|
|
3916
|
-
if (documents.length !== filteredDocuments.length) {
|
|
3917
|
-
result.warnings.push(
|
|
3918
|
-
`Filtered out ${documents.length - filteredDocuments.length} CourseConfig document(s) from chunks to prevent conflicts`
|
|
3919
|
-
);
|
|
3920
|
-
}
|
|
3921
|
-
this.reportProgress(
|
|
3922
|
-
"documents",
|
|
3923
|
-
filteredDocuments.length,
|
|
3924
|
-
manifest.documentCount,
|
|
3925
|
-
"Uploading documents to CouchDB..."
|
|
3926
|
-
);
|
|
3927
|
-
const docResults = await this.uploadDocuments(filteredDocuments, targetDB);
|
|
3928
|
-
result.documentsRestored = docResults.restored;
|
|
3929
|
-
result.errors.push(...docResults.errors);
|
|
3930
|
-
result.warnings.push(...docResults.warnings);
|
|
3931
|
-
const docsWithAttachments = documents.filter(
|
|
3932
|
-
(doc) => doc._attachments && Object.keys(doc._attachments).length > 0
|
|
3933
|
-
);
|
|
3934
|
-
this.reportProgress("attachments", 0, docsWithAttachments.length, "Uploading attachments...");
|
|
3935
|
-
const attachmentResults = await this.uploadAttachments(
|
|
3936
|
-
staticPath,
|
|
3937
|
-
docsWithAttachments,
|
|
3938
|
-
targetDB
|
|
3939
|
-
);
|
|
3940
|
-
result.attachmentsRestored = attachmentResults.restored;
|
|
3941
|
-
result.errors.push(...attachmentResults.errors);
|
|
3942
|
-
result.warnings.push(...attachmentResults.warnings);
|
|
3943
|
-
if (this.options.validateRoundTrip) {
|
|
3944
|
-
this.reportProgress("validation", 0, 1, "Validating migration...");
|
|
3945
|
-
const validationResult = await validateMigration(targetDB, expectedCounts, manifest);
|
|
3946
|
-
if (!validationResult.valid) {
|
|
3947
|
-
result.warnings.push("Migration validation found issues");
|
|
3948
|
-
validationResult.issues.forEach((issue) => {
|
|
3949
|
-
if (issue.type === "error") {
|
|
3950
|
-
result.errors.push(`Validation: ${issue.message}`);
|
|
3951
|
-
} else {
|
|
3952
|
-
result.warnings.push(`Validation: ${issue.message}`);
|
|
3953
|
-
}
|
|
3954
|
-
});
|
|
3955
|
-
}
|
|
3956
|
-
this.reportProgress("validation", 1, 1, "Migration validation completed");
|
|
3957
|
-
}
|
|
3958
|
-
result.success = result.errors.length === 0;
|
|
3959
|
-
result.migrationTime = Date.now() - startTime;
|
|
3960
|
-
logger.info(`Migration completed in ${result.migrationTime}ms`);
|
|
3961
|
-
logger.info(`Documents restored: ${result.documentsRestored}`);
|
|
3962
|
-
logger.info(`Attachments restored: ${result.attachmentsRestored}`);
|
|
3963
|
-
logger.info(`Design docs restored: ${result.designDocsRestored}`);
|
|
3964
|
-
logger.info(`CourseConfig restored: ${result.courseConfigRestored}`);
|
|
3965
|
-
if (result.errors.length > 0) {
|
|
3966
|
-
logger.error(`Migration completed with ${result.errors.length} errors`);
|
|
3967
|
-
}
|
|
3968
|
-
if (result.warnings.length > 0) {
|
|
3969
|
-
logger.warn(`Migration completed with ${result.warnings.length} warnings`);
|
|
3970
|
-
}
|
|
3971
|
-
} catch (error) {
|
|
3972
|
-
result.success = false;
|
|
3973
|
-
result.migrationTime = Date.now() - startTime;
|
|
3974
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3975
|
-
result.errors.push(`Migration failed: ${errorMessage}`);
|
|
3976
|
-
logger.error("Migration failed:", error);
|
|
3977
|
-
if (this.options.cleanupOnFailure) {
|
|
3978
|
-
try {
|
|
3979
|
-
await this.cleanupFailedMigration(targetDB);
|
|
3980
|
-
} catch (cleanupError) {
|
|
3981
|
-
logger.error("Failed to cleanup after migration failure:", cleanupError);
|
|
3982
|
-
result.warnings.push("Failed to cleanup after migration failure");
|
|
3983
|
-
}
|
|
3984
|
-
}
|
|
3985
|
-
}
|
|
3986
|
-
return result;
|
|
3987
|
-
}
|
|
3988
|
-
/**
|
|
3989
|
-
* Load and parse the manifest file
|
|
3990
|
-
*/
|
|
3991
|
-
async loadManifest(staticPath) {
|
|
3992
|
-
try {
|
|
3993
|
-
let manifestContent;
|
|
3994
|
-
let manifestPath;
|
|
3995
|
-
if (this.fs) {
|
|
3996
|
-
manifestPath = this.fs.joinPath(staticPath, "manifest.json");
|
|
3997
|
-
manifestContent = await this.fs.readFile(manifestPath);
|
|
3998
|
-
} else {
|
|
3999
|
-
manifestPath = nodeFS2 && nodePath ? nodePath.join(staticPath, "manifest.json") : `${staticPath}/manifest.json`;
|
|
4000
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4001
|
-
manifestContent = await nodeFS2.promises.readFile(manifestPath, "utf8");
|
|
4002
|
-
} else {
|
|
4003
|
-
const response = await fetch(manifestPath);
|
|
4004
|
-
if (!response.ok) {
|
|
4005
|
-
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
4006
|
-
}
|
|
4007
|
-
manifestContent = await response.text();
|
|
4008
|
-
}
|
|
4009
|
-
}
|
|
4010
|
-
const manifest = JSON.parse(manifestContent);
|
|
4011
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks) {
|
|
4012
|
-
throw new Error("Invalid manifest structure");
|
|
4013
|
-
}
|
|
4014
|
-
return manifest;
|
|
4015
|
-
} catch (error) {
|
|
4016
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`;
|
|
4017
|
-
throw new Error(errorMessage);
|
|
4018
|
-
}
|
|
4019
|
-
}
|
|
4020
|
-
/**
|
|
4021
|
-
* Restore design documents to CouchDB
|
|
4022
|
-
*/
|
|
4023
|
-
async restoreDesignDocuments(designDocs, db) {
|
|
4024
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
4025
|
-
for (let i = 0; i < designDocs.length; i++) {
|
|
4026
|
-
const designDoc = designDocs[i];
|
|
4027
|
-
this.reportProgress("design_docs", i, designDocs.length, `Restoring ${designDoc._id}...`);
|
|
4028
|
-
try {
|
|
4029
|
-
let existingDoc;
|
|
4030
|
-
try {
|
|
4031
|
-
existingDoc = await db.get(designDoc._id);
|
|
4032
|
-
} catch {
|
|
4033
|
-
}
|
|
4034
|
-
const docToInsert = {
|
|
4035
|
-
_id: designDoc._id,
|
|
4036
|
-
views: designDoc.views
|
|
4037
|
-
};
|
|
4038
|
-
if (existingDoc) {
|
|
4039
|
-
docToInsert._rev = existingDoc._rev;
|
|
4040
|
-
logger.debug(`Updating existing design document: ${designDoc._id}`);
|
|
4041
|
-
} else {
|
|
4042
|
-
logger.debug(`Creating new design document: ${designDoc._id}`);
|
|
4043
|
-
}
|
|
4044
|
-
await db.put(docToInsert);
|
|
4045
|
-
result.restored++;
|
|
4046
|
-
} catch (error) {
|
|
4047
|
-
const errorMessage = `Failed to restore design document ${designDoc._id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
4048
|
-
result.errors.push(errorMessage);
|
|
4049
|
-
logger.error(errorMessage);
|
|
4050
|
-
}
|
|
4051
|
-
}
|
|
4052
|
-
this.reportProgress(
|
|
4053
|
-
"design_docs",
|
|
4054
|
-
designDocs.length,
|
|
4055
|
-
designDocs.length,
|
|
4056
|
-
`Restored ${result.restored} design documents`
|
|
4057
|
-
);
|
|
4058
|
-
return result;
|
|
4059
|
-
}
|
|
4060
|
-
/**
|
|
4061
|
-
* Aggregate documents from all chunks
|
|
4062
|
-
*/
|
|
4063
|
-
async aggregateDocuments(staticPath, manifest) {
|
|
4064
|
-
const allDocuments = [];
|
|
4065
|
-
const documentMap = /* @__PURE__ */ new Map();
|
|
4066
|
-
for (let i = 0; i < manifest.chunks.length; i++) {
|
|
4067
|
-
const chunk = manifest.chunks[i];
|
|
4068
|
-
this.reportProgress(
|
|
4069
|
-
"documents",
|
|
4070
|
-
allDocuments.length,
|
|
4071
|
-
manifest.documentCount,
|
|
4072
|
-
`Loading chunk ${chunk.id}...`
|
|
4073
|
-
);
|
|
4074
|
-
try {
|
|
4075
|
-
const documents = await this.loadChunk(staticPath, chunk);
|
|
4076
|
-
for (const doc of documents) {
|
|
4077
|
-
if (!doc._id) {
|
|
4078
|
-
logger.warn(`Document without _id found in chunk ${chunk.id}, skipping`);
|
|
4079
|
-
continue;
|
|
4080
|
-
}
|
|
4081
|
-
if (documentMap.has(doc._id)) {
|
|
4082
|
-
logger.warn(`Duplicate document ID found: ${doc._id}, using latest version`);
|
|
4083
|
-
}
|
|
4084
|
-
documentMap.set(doc._id, doc);
|
|
4085
|
-
}
|
|
4086
|
-
} catch (error) {
|
|
4087
|
-
throw new Error(
|
|
4088
|
-
`Failed to load chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
4089
|
-
);
|
|
4090
|
-
}
|
|
4091
|
-
}
|
|
4092
|
-
allDocuments.push(...documentMap.values());
|
|
4093
|
-
logger.info(
|
|
4094
|
-
`Aggregated ${allDocuments.length} unique documents from ${manifest.chunks.length} chunks`
|
|
4095
|
-
);
|
|
4096
|
-
return allDocuments;
|
|
4097
|
-
}
|
|
4098
|
-
/**
|
|
4099
|
-
* Load documents from a single chunk file
|
|
4100
|
-
*/
|
|
4101
|
-
async loadChunk(staticPath, chunk) {
|
|
4102
|
-
try {
|
|
4103
|
-
let chunkContent;
|
|
4104
|
-
let chunkPath;
|
|
4105
|
-
if (this.fs) {
|
|
4106
|
-
chunkPath = this.fs.joinPath(staticPath, chunk.path);
|
|
4107
|
-
chunkContent = await this.fs.readFile(chunkPath);
|
|
4108
|
-
} else {
|
|
4109
|
-
chunkPath = nodeFS2 && nodePath ? nodePath.join(staticPath, chunk.path) : `${staticPath}/${chunk.path}`;
|
|
4110
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4111
|
-
chunkContent = await nodeFS2.promises.readFile(chunkPath, "utf8");
|
|
4112
|
-
} else {
|
|
4113
|
-
const response = await fetch(chunkPath);
|
|
4114
|
-
if (!response.ok) {
|
|
4115
|
-
throw new Error(`Failed to fetch chunk: ${response.status} ${response.statusText}`);
|
|
4116
|
-
}
|
|
4117
|
-
chunkContent = await response.text();
|
|
4118
|
-
}
|
|
4119
|
-
}
|
|
4120
|
-
const documents = JSON.parse(chunkContent);
|
|
4121
|
-
if (!Array.isArray(documents)) {
|
|
4122
|
-
throw new Error("Chunk file does not contain an array of documents");
|
|
4123
|
-
}
|
|
4124
|
-
return documents;
|
|
4125
|
-
} catch (error) {
|
|
4126
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load chunk: ${error instanceof Error ? error.message : String(error)}`;
|
|
4127
|
-
throw new Error(errorMessage);
|
|
4128
|
-
}
|
|
4129
|
-
}
|
|
4130
|
-
/**
|
|
4131
|
-
* Upload documents to CouchDB in batches
|
|
4132
|
-
*/
|
|
4133
|
-
async uploadDocuments(documents, db) {
|
|
4134
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
4135
|
-
const batchSize = this.options.chunkBatchSize;
|
|
4136
|
-
for (let i = 0; i < documents.length; i += batchSize) {
|
|
4137
|
-
const batch = documents.slice(i, i + batchSize);
|
|
4138
|
-
this.reportProgress(
|
|
4139
|
-
"documents",
|
|
4140
|
-
i,
|
|
4141
|
-
documents.length,
|
|
4142
|
-
`Uploading batch ${Math.floor(i / batchSize) + 1}...`
|
|
4143
|
-
);
|
|
4144
|
-
try {
|
|
4145
|
-
const docsToInsert = batch.map((doc) => {
|
|
4146
|
-
const cleanDoc = { ...doc };
|
|
4147
|
-
delete cleanDoc._rev;
|
|
4148
|
-
delete cleanDoc._attachments;
|
|
4149
|
-
return cleanDoc;
|
|
4150
|
-
});
|
|
4151
|
-
const bulkResult = await db.bulkDocs(docsToInsert);
|
|
4152
|
-
for (let j = 0; j < bulkResult.length; j++) {
|
|
4153
|
-
const docResult = bulkResult[j];
|
|
4154
|
-
const originalDoc = batch[j];
|
|
4155
|
-
if ("error" in docResult) {
|
|
4156
|
-
const errorMessage = `Failed to upload document ${originalDoc._id}: ${docResult.error} - ${docResult.reason}`;
|
|
4157
|
-
result.errors.push(errorMessage);
|
|
4158
|
-
logger.error(errorMessage);
|
|
4159
|
-
} else {
|
|
4160
|
-
result.restored++;
|
|
4161
|
-
}
|
|
4162
|
-
}
|
|
4163
|
-
} catch (error) {
|
|
4164
|
-
let errorMessage;
|
|
4165
|
-
if (error instanceof Error) {
|
|
4166
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
4167
|
-
} else if (error && typeof error === "object" && "message" in error) {
|
|
4168
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
4169
|
-
} else {
|
|
4170
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${JSON.stringify(error)}`;
|
|
4171
|
-
}
|
|
4172
|
-
result.errors.push(errorMessage);
|
|
4173
|
-
logger.error(errorMessage);
|
|
4174
|
-
}
|
|
4175
|
-
}
|
|
4176
|
-
this.reportProgress(
|
|
4177
|
-
"documents",
|
|
4178
|
-
documents.length,
|
|
4179
|
-
documents.length,
|
|
4180
|
-
`Uploaded ${result.restored} documents`
|
|
4181
|
-
);
|
|
4182
|
-
return result;
|
|
4183
|
-
}
|
|
4184
|
-
/**
|
|
4185
|
-
* Upload attachments from filesystem to CouchDB
|
|
4186
|
-
*/
|
|
4187
|
-
async uploadAttachments(staticPath, documents, db) {
|
|
4188
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
4189
|
-
let processedDocs = 0;
|
|
4190
|
-
for (const doc of documents) {
|
|
4191
|
-
this.reportProgress(
|
|
4192
|
-
"attachments",
|
|
4193
|
-
processedDocs,
|
|
4194
|
-
documents.length,
|
|
4195
|
-
`Processing attachments for ${doc._id}...`
|
|
4196
|
-
);
|
|
4197
|
-
processedDocs++;
|
|
4198
|
-
if (!doc._attachments) {
|
|
4199
|
-
continue;
|
|
4200
|
-
}
|
|
4201
|
-
for (const [attachmentName, attachmentMeta] of Object.entries(doc._attachments)) {
|
|
4202
|
-
try {
|
|
4203
|
-
const uploadResult = await this.uploadSingleAttachment(
|
|
4204
|
-
staticPath,
|
|
4205
|
-
doc._id,
|
|
4206
|
-
attachmentName,
|
|
4207
|
-
attachmentMeta,
|
|
4208
|
-
db
|
|
4209
|
-
);
|
|
4210
|
-
if (uploadResult.success) {
|
|
4211
|
-
result.restored++;
|
|
4212
|
-
} else {
|
|
4213
|
-
result.errors.push(uploadResult.error || "Unknown attachment upload error");
|
|
4214
|
-
}
|
|
4215
|
-
} catch (error) {
|
|
4216
|
-
const errorMessage = `Failed to upload attachment ${doc._id}/${attachmentName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
4217
|
-
result.errors.push(errorMessage);
|
|
4218
|
-
logger.error(errorMessage);
|
|
4219
|
-
}
|
|
4220
|
-
}
|
|
4221
|
-
}
|
|
4222
|
-
this.reportProgress(
|
|
4223
|
-
"attachments",
|
|
4224
|
-
documents.length,
|
|
4225
|
-
documents.length,
|
|
4226
|
-
`Uploaded ${result.restored} attachments`
|
|
4227
|
-
);
|
|
4228
|
-
return result;
|
|
4229
|
-
}
|
|
4230
|
-
/**
|
|
4231
|
-
* Upload a single attachment file
|
|
4232
|
-
*/
|
|
4233
|
-
async uploadSingleAttachment(staticPath, docId, attachmentName, attachmentMeta, db) {
|
|
4234
|
-
const result = {
|
|
4235
|
-
success: false,
|
|
4236
|
-
attachmentName,
|
|
4237
|
-
docId
|
|
4238
|
-
};
|
|
4239
|
-
try {
|
|
4240
|
-
if (!attachmentMeta.path) {
|
|
4241
|
-
result.error = "Attachment metadata missing file path";
|
|
4242
|
-
return result;
|
|
4243
|
-
}
|
|
4244
|
-
let attachmentData;
|
|
4245
|
-
let attachmentPath;
|
|
4246
|
-
if (this.fs) {
|
|
4247
|
-
attachmentPath = this.fs.joinPath(staticPath, attachmentMeta.path);
|
|
4248
|
-
attachmentData = await this.fs.readBinary(attachmentPath);
|
|
4249
|
-
} else {
|
|
4250
|
-
attachmentPath = nodeFS2 && nodePath ? nodePath.join(staticPath, attachmentMeta.path) : `${staticPath}/${attachmentMeta.path}`;
|
|
4251
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4252
|
-
attachmentData = await nodeFS2.promises.readFile(attachmentPath);
|
|
4253
|
-
} else {
|
|
4254
|
-
const response = await fetch(attachmentPath);
|
|
4255
|
-
if (!response.ok) {
|
|
4256
|
-
result.error = `Failed to fetch attachment: ${response.status} ${response.statusText}`;
|
|
4257
|
-
return result;
|
|
4258
|
-
}
|
|
4259
|
-
attachmentData = await response.arrayBuffer();
|
|
4260
|
-
}
|
|
4261
|
-
}
|
|
4262
|
-
const doc = await db.get(docId);
|
|
4263
|
-
await db.putAttachment(
|
|
4264
|
-
docId,
|
|
4265
|
-
attachmentName,
|
|
4266
|
-
doc._rev,
|
|
4267
|
-
attachmentData,
|
|
4268
|
-
// PouchDB accepts both ArrayBuffer and Buffer
|
|
4269
|
-
attachmentMeta.content_type
|
|
4270
|
-
);
|
|
4271
|
-
result.success = true;
|
|
4272
|
-
} catch (error) {
|
|
4273
|
-
result.error = error instanceof Error ? error.message : String(error);
|
|
4274
|
-
}
|
|
4275
|
-
return result;
|
|
4276
|
-
}
|
|
4277
|
-
/**
|
|
4278
|
-
* Restore CourseConfig document from manifest
|
|
4279
|
-
*/
|
|
4280
|
-
async restoreCourseConfig(manifest, targetDB) {
|
|
4281
|
-
const results = {
|
|
4282
|
-
restored: 0,
|
|
4283
|
-
errors: [],
|
|
4284
|
-
warnings: []
|
|
4285
|
-
};
|
|
4286
|
-
try {
|
|
4287
|
-
if (!manifest.courseConfig) {
|
|
4288
|
-
results.warnings.push(
|
|
4289
|
-
"No courseConfig found in manifest, skipping CourseConfig document creation"
|
|
4290
|
-
);
|
|
4291
|
-
return results;
|
|
4292
|
-
}
|
|
4293
|
-
const courseConfigDoc = {
|
|
4294
|
-
_id: "CourseConfig",
|
|
4295
|
-
...manifest.courseConfig,
|
|
4296
|
-
courseID: manifest.courseId
|
|
4297
|
-
};
|
|
4298
|
-
delete courseConfigDoc._rev;
|
|
4299
|
-
await targetDB.put(courseConfigDoc);
|
|
4300
|
-
results.restored = 1;
|
|
4301
|
-
logger.info(`CourseConfig document created for course: ${manifest.courseId}`);
|
|
4302
|
-
} catch (error) {
|
|
4303
|
-
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
4304
|
-
results.errors.push(`Failed to restore CourseConfig: ${errorMessage}`);
|
|
4305
|
-
logger.error("CourseConfig restoration failed:", error);
|
|
4306
|
-
}
|
|
4307
|
-
return results;
|
|
4308
|
-
}
|
|
4309
|
-
/**
|
|
4310
|
-
* Calculate expected document counts from manifest
|
|
4311
|
-
*/
|
|
4312
|
-
calculateExpectedCounts(manifest) {
|
|
4313
|
-
const counts = {};
|
|
4314
|
-
for (const chunk of manifest.chunks) {
|
|
4315
|
-
counts[chunk.docType] = (counts[chunk.docType] || 0) + chunk.documentCount;
|
|
4316
|
-
}
|
|
4317
|
-
if (manifest.designDocs.length > 0) {
|
|
4318
|
-
counts["_design"] = manifest.designDocs.length;
|
|
4319
|
-
}
|
|
4320
|
-
return counts;
|
|
4321
|
-
}
|
|
4322
|
-
/**
|
|
4323
|
-
* Clean up database after failed migration
|
|
4324
|
-
*/
|
|
4325
|
-
async cleanupFailedMigration(db) {
|
|
4326
|
-
logger.info("Cleaning up failed migration...");
|
|
4327
|
-
try {
|
|
4328
|
-
const allDocs = await db.allDocs();
|
|
4329
|
-
const docsToDelete = allDocs.rows.map((row) => ({
|
|
4330
|
-
_id: row.id,
|
|
4331
|
-
_rev: row.value.rev,
|
|
4332
|
-
_deleted: true
|
|
4333
|
-
}));
|
|
4334
|
-
if (docsToDelete.length > 0) {
|
|
4335
|
-
await db.bulkDocs(docsToDelete);
|
|
4336
|
-
logger.info(`Cleaned up ${docsToDelete.length} documents from failed migration`);
|
|
4337
|
-
}
|
|
4338
|
-
} catch (error) {
|
|
4339
|
-
logger.error("Failed to cleanup documents:", error);
|
|
4340
|
-
throw error;
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
/**
|
|
4344
|
-
* Report progress to callback if available
|
|
4345
|
-
*/
|
|
4346
|
-
reportProgress(phase, current, total, message) {
|
|
4347
|
-
if (this.progressCallback) {
|
|
4348
|
-
this.progressCallback({
|
|
4349
|
-
phase,
|
|
4350
|
-
current,
|
|
4351
|
-
total,
|
|
4352
|
-
message
|
|
4353
|
-
});
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
/**
|
|
4357
|
-
* Check if a path is a local file path (vs URL)
|
|
4358
|
-
*/
|
|
4359
|
-
isLocalPath(path2) {
|
|
4360
|
-
return !path2.startsWith("http://") && !path2.startsWith("https://");
|
|
4361
|
-
}
|
|
4362
|
-
};
|
|
4363
|
-
}
|
|
4364
|
-
});
|
|
4365
|
-
|
|
4366
|
-
// src/util/migrator/index.ts
|
|
4367
|
-
var init_migrator = __esm({
|
|
4368
|
-
"src/util/migrator/index.ts"() {
|
|
4369
|
-
"use strict";
|
|
4370
|
-
init_StaticToCouchDBMigrator();
|
|
4371
|
-
init_validation();
|
|
4372
|
-
init_FileSystemAdapter();
|
|
4373
|
-
}
|
|
4374
|
-
});
|
|
4375
|
-
|
|
4376
|
-
// src/util/index.ts
|
|
4377
|
-
var init_util2 = __esm({
|
|
4378
|
-
"src/util/index.ts"() {
|
|
4379
|
-
"use strict";
|
|
4380
|
-
init_Loggable();
|
|
4381
|
-
init_packer();
|
|
4382
|
-
init_migrator();
|
|
4383
|
-
init_dataDirectory();
|
|
4384
|
-
}
|
|
4385
|
-
});
|
|
4386
|
-
|
|
4387
|
-
// src/study/SourceMixer.ts
|
|
4388
|
-
var init_SourceMixer = __esm({
|
|
4389
|
-
"src/study/SourceMixer.ts"() {
|
|
4390
|
-
"use strict";
|
|
4391
|
-
}
|
|
4392
|
-
});
|
|
4393
|
-
|
|
4394
|
-
// src/study/MixerDebugger.ts
|
|
4395
|
-
function printMixerSummary(run) {
|
|
4396
|
-
console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
|
|
4397
|
-
logger.info(`Run ID: ${run.runId}`);
|
|
4398
|
-
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
4399
|
-
logger.info(
|
|
4400
|
-
`Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
|
|
4401
|
-
);
|
|
4402
|
-
console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
|
|
4403
|
-
for (const src of run.sourceSummaries) {
|
|
4404
|
-
logger.info(
|
|
4405
|
-
` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
|
|
4406
|
-
);
|
|
4407
|
-
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
|
|
4408
|
-
}
|
|
4409
|
-
console.groupEnd();
|
|
4410
|
-
console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
|
|
4411
|
-
for (const breakdown of run.sourceBreakdowns) {
|
|
4412
|
-
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4413
|
-
logger.info(
|
|
4414
|
-
` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
|
|
4415
|
-
);
|
|
4416
|
-
}
|
|
4417
|
-
console.groupEnd();
|
|
4418
|
-
console.groupEnd();
|
|
4419
|
-
}
|
|
4420
|
-
function mountMixerDebugger() {
|
|
4421
|
-
if (typeof window === "undefined") return;
|
|
4422
|
-
const win = window;
|
|
4423
|
-
win.skuilder = win.skuilder || {};
|
|
4424
|
-
win.skuilder.mixer = mixerDebugAPI;
|
|
4425
|
-
}
|
|
4426
|
-
var runHistory2, mixerDebugAPI;
|
|
4427
|
-
var init_MixerDebugger = __esm({
|
|
4428
|
-
"src/study/MixerDebugger.ts"() {
|
|
4429
|
-
"use strict";
|
|
4430
|
-
init_logger();
|
|
4431
|
-
init_navigators();
|
|
4432
|
-
runHistory2 = [];
|
|
4433
|
-
mixerDebugAPI = {
|
|
4434
|
-
/**
|
|
4435
|
-
* Get raw run history for programmatic access.
|
|
4436
|
-
*/
|
|
4437
|
-
get runs() {
|
|
4438
|
-
return [...runHistory2];
|
|
4439
|
-
},
|
|
4440
|
-
/**
|
|
4441
|
-
* Show summary of a specific mixer run.
|
|
4442
|
-
*/
|
|
4443
|
-
showRun(idOrIndex = 0) {
|
|
4444
|
-
if (runHistory2.length === 0) {
|
|
4445
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4446
|
-
return;
|
|
4447
|
-
}
|
|
4448
|
-
let run;
|
|
4449
|
-
if (typeof idOrIndex === "number") {
|
|
4450
|
-
run = runHistory2[idOrIndex];
|
|
4451
|
-
if (!run) {
|
|
4452
|
-
logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
|
|
4453
|
-
return;
|
|
4454
|
-
}
|
|
4455
|
-
} else {
|
|
4456
|
-
run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
|
|
4457
|
-
if (!run) {
|
|
4458
|
-
logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
|
|
4459
|
-
return;
|
|
4460
|
-
}
|
|
4461
|
-
}
|
|
4462
|
-
printMixerSummary(run);
|
|
4463
|
-
},
|
|
4464
|
-
/**
|
|
4465
|
-
* Show summary of the last mixer run.
|
|
4466
|
-
*/
|
|
4467
|
-
showLastMix() {
|
|
4468
|
-
this.showRun(0);
|
|
4469
|
-
},
|
|
4470
|
-
/**
|
|
4471
|
-
* Explain source balance in the last run.
|
|
4472
|
-
*/
|
|
4473
|
-
explainSourceBalance() {
|
|
4474
|
-
if (runHistory2.length === 0) {
|
|
4475
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4476
|
-
return;
|
|
4477
|
-
}
|
|
4478
|
-
const run = runHistory2[0];
|
|
4479
|
-
console.group("\u2696\uFE0F Source Balance Analysis");
|
|
4480
|
-
logger.info(`Mixer: ${run.mixerType}`);
|
|
4481
|
-
logger.info(`Requested limit: ${run.requestedLimit}`);
|
|
4482
|
-
if (run.quotaPerSource) {
|
|
4483
|
-
logger.info(`Quota per source: ${run.quotaPerSource}`);
|
|
4484
|
-
}
|
|
4485
|
-
console.group("Input Distribution:");
|
|
4486
|
-
for (const src of run.sourceSummaries) {
|
|
4487
|
-
const name = src.sourceName || src.sourceId;
|
|
4488
|
-
logger.info(`${name}:`);
|
|
4489
|
-
logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
|
|
4490
|
-
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
|
|
4491
|
-
}
|
|
4492
|
-
console.groupEnd();
|
|
4493
|
-
console.group("Selection Results:");
|
|
4494
|
-
for (const breakdown of run.sourceBreakdowns) {
|
|
4495
|
-
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4496
|
-
logger.info(`${name}:`);
|
|
4497
|
-
logger.info(
|
|
4498
|
-
` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
|
|
4499
|
-
);
|
|
4500
|
-
logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
|
|
4501
|
-
logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
|
|
4502
|
-
if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
|
|
4503
|
-
logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
|
|
4504
|
-
}
|
|
4505
|
-
if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
|
|
4506
|
-
logger.info(` \u26A0\uFE0F Had cards but none selected!`);
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
console.groupEnd();
|
|
4510
|
-
const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
|
|
4511
|
-
const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
|
|
4512
|
-
const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
|
|
4513
|
-
if (maxDeviation > 20) {
|
|
4514
|
-
logger.info(`
|
|
4515
|
-
\u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
|
|
4516
|
-
logger.info("Possible causes:");
|
|
4517
|
-
logger.info(" - Score range differences between sources");
|
|
4518
|
-
logger.info(" - One source has much better quality cards");
|
|
4519
|
-
logger.info(" - Different card availability (reviews vs new)");
|
|
4520
|
-
}
|
|
4521
|
-
console.groupEnd();
|
|
4522
|
-
},
|
|
4523
|
-
/**
|
|
4524
|
-
* Compare score distributions across sources.
|
|
4525
|
-
*/
|
|
4526
|
-
compareScores() {
|
|
4527
|
-
if (runHistory2.length === 0) {
|
|
4528
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4529
|
-
return;
|
|
4530
|
-
}
|
|
4531
|
-
const run = runHistory2[0];
|
|
4532
|
-
console.group("\u{1F4CA} Score Distribution Comparison");
|
|
4533
|
-
console.table(
|
|
4534
|
-
run.sourceSummaries.map((src) => ({
|
|
4535
|
-
source: src.sourceName || src.sourceId,
|
|
4536
|
-
cards: src.totalCards,
|
|
4537
|
-
min: src.bottomScore.toFixed(3),
|
|
4538
|
-
max: src.topScore.toFixed(3),
|
|
4539
|
-
avg: src.avgScore.toFixed(3),
|
|
4540
|
-
range: (src.topScore - src.bottomScore).toFixed(3)
|
|
4541
|
-
}))
|
|
4542
|
-
);
|
|
4543
|
-
const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
|
|
4544
|
-
const avgScores = run.sourceSummaries.map((s) => s.avgScore);
|
|
4545
|
-
const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
|
|
4546
|
-
const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
|
|
4547
|
-
if (rangeDiff > 0.3 || avgDiff > 0.2) {
|
|
4548
|
-
logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
|
|
4549
|
-
logger.info(
|
|
4550
|
-
"This may cause one source to dominate selection if using global sorting (not quota-based)"
|
|
4551
|
-
);
|
|
4552
|
-
}
|
|
4553
|
-
console.groupEnd();
|
|
4554
|
-
},
|
|
4555
|
-
/**
|
|
4556
|
-
* Show detailed information for a specific card.
|
|
4557
|
-
*/
|
|
4558
|
-
showCard(cardId) {
|
|
4559
|
-
for (const run of runHistory2) {
|
|
4560
|
-
const card = run.cards.find((c) => c.cardId === cardId);
|
|
4561
|
-
if (card) {
|
|
4562
|
-
const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
|
|
4563
|
-
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
4564
|
-
logger.info(`Course: ${card.courseId}`);
|
|
4565
|
-
logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
|
|
4566
|
-
logger.info(`Origin: ${card.origin}`);
|
|
4567
|
-
logger.info(`Score: ${card.score.toFixed(3)}`);
|
|
4568
|
-
if (card.rankInSource) {
|
|
4569
|
-
logger.info(`Rank in source: #${card.rankInSource}`);
|
|
4570
|
-
}
|
|
4571
|
-
if (card.rankInMix) {
|
|
4572
|
-
logger.info(`Rank in mixed results: #${card.rankInMix}`);
|
|
4573
|
-
}
|
|
4574
|
-
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
4575
|
-
if (!card.selected && card.rankInSource) {
|
|
4576
|
-
logger.info("\nWhy not selected:");
|
|
4577
|
-
if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
|
|
4578
|
-
logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
|
|
4579
|
-
}
|
|
4580
|
-
logger.info(" - Check score compared to selected cards using .showRun()");
|
|
4581
|
-
}
|
|
4582
|
-
console.groupEnd();
|
|
4583
|
-
return;
|
|
4584
|
-
}
|
|
4585
|
-
}
|
|
4586
|
-
logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
|
|
4587
|
-
},
|
|
4588
|
-
/**
|
|
4589
|
-
* Show all runs in compact format.
|
|
4590
|
-
*/
|
|
4591
|
-
listRuns() {
|
|
4592
|
-
if (runHistory2.length === 0) {
|
|
4593
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4594
|
-
return;
|
|
4595
|
-
}
|
|
4596
|
-
console.table(
|
|
4597
|
-
runHistory2.map((r) => ({
|
|
4598
|
-
id: r.runId.slice(-8),
|
|
4599
|
-
time: r.timestamp.toLocaleTimeString(),
|
|
4600
|
-
mixer: r.mixerType,
|
|
4601
|
-
sources: r.sourceSummaries.length,
|
|
4602
|
-
selected: r.finalCount,
|
|
4603
|
-
reviews: r.reviewsSelected,
|
|
4604
|
-
new: r.newSelected
|
|
4605
|
-
}))
|
|
4606
|
-
);
|
|
4607
|
-
},
|
|
4608
|
-
/**
|
|
4609
|
-
* Export run history as JSON for bug reports.
|
|
4610
|
-
*/
|
|
4611
|
-
export() {
|
|
4612
|
-
const json = JSON.stringify(runHistory2, null, 2);
|
|
4613
|
-
logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
|
|
4614
|
-
logger.info(" copy(window.skuilder.mixer.export())");
|
|
4615
|
-
return json;
|
|
4616
|
-
},
|
|
4617
|
-
/**
|
|
4618
|
-
* Clear run history.
|
|
4619
|
-
*/
|
|
4620
|
-
clear() {
|
|
4621
|
-
runHistory2.length = 0;
|
|
4622
|
-
logger.info("[Mixer Debug] Run history cleared.");
|
|
4623
|
-
},
|
|
4624
|
-
/**
|
|
4625
|
-
* Show help.
|
|
4626
|
-
*/
|
|
4627
|
-
help() {
|
|
4628
|
-
logger.info(`
|
|
4629
|
-
\u{1F3A8} Mixer Debug API
|
|
4630
|
-
|
|
4631
|
-
Commands:
|
|
4632
|
-
.showLastMix() Show summary of most recent mixer run
|
|
4633
|
-
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
4634
|
-
.explainSourceBalance() Analyze source balance and selection patterns
|
|
4635
|
-
.compareScores() Compare score distributions across sources
|
|
4636
|
-
.showCard(cardId) Show mixer decisions for a specific card
|
|
4637
|
-
.listRuns() List all captured runs in table format
|
|
4638
|
-
.export() Export run history as JSON for bug reports
|
|
4639
|
-
.clear() Clear run history
|
|
4640
|
-
.runs Access raw run history array
|
|
4641
|
-
.help() Show this help message
|
|
4642
|
-
|
|
4643
|
-
Example:
|
|
4644
|
-
window.skuilder.mixer.showLastMix()
|
|
4645
|
-
window.skuilder.mixer.explainSourceBalance()
|
|
4646
|
-
window.skuilder.mixer.compareScores()
|
|
4647
|
-
`);
|
|
4648
|
-
}
|
|
4649
|
-
};
|
|
4650
|
-
mountMixerDebugger();
|
|
3311
|
+
gradient,
|
|
3312
|
+
existingState
|
|
3313
|
+
);
|
|
3314
|
+
logger.info(
|
|
3315
|
+
`[Orchestration] Period update complete for ${strategyId}: weight ${currentWeight.weight.toFixed(3)} \u2192 ${newWeight.weight.toFixed(3)}, confidence ${currentWeight.confidence.toFixed(3)} \u2192 ${newWeight.confidence.toFixed(3)}`
|
|
3316
|
+
);
|
|
3317
|
+
return {
|
|
3318
|
+
strategyId,
|
|
3319
|
+
previousWeight: currentWeight,
|
|
3320
|
+
newWeight,
|
|
3321
|
+
gradient,
|
|
3322
|
+
learningState,
|
|
3323
|
+
updated
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
function getDefaultLearnableWeight() {
|
|
3327
|
+
return { ...DEFAULT_LEARNABLE_WEIGHT };
|
|
3328
|
+
}
|
|
3329
|
+
var MIN_OBSERVATIONS_FOR_UPDATE, LEARNING_RATE, MAX_WEIGHT_DELTA, MIN_R_SQUARED_FOR_GRADIENT, FLAT_GRADIENT_THRESHOLD, MAX_HISTORY_LENGTH;
|
|
3330
|
+
var init_learning = __esm({
|
|
3331
|
+
"src/core/orchestration/learning.ts"() {
|
|
3332
|
+
"use strict";
|
|
3333
|
+
init_contentNavigationStrategy();
|
|
3334
|
+
init_types_legacy();
|
|
3335
|
+
init_logger();
|
|
3336
|
+
MIN_OBSERVATIONS_FOR_UPDATE = 10;
|
|
3337
|
+
LEARNING_RATE = 0.1;
|
|
3338
|
+
MAX_WEIGHT_DELTA = 0.3;
|
|
3339
|
+
MIN_R_SQUARED_FOR_GRADIENT = 0.05;
|
|
3340
|
+
FLAT_GRADIENT_THRESHOLD = 0.02;
|
|
3341
|
+
MAX_HISTORY_LENGTH = 100;
|
|
4651
3342
|
}
|
|
4652
3343
|
});
|
|
4653
3344
|
|
|
4654
|
-
// src/
|
|
4655
|
-
function
|
|
4656
|
-
if (!
|
|
4657
|
-
|
|
4658
|
-
return;
|
|
3345
|
+
// src/core/orchestration/signal.ts
|
|
3346
|
+
function computeOutcomeSignal(records, config = {}) {
|
|
3347
|
+
if (!records || records.length === 0) {
|
|
3348
|
+
return null;
|
|
4659
3349
|
}
|
|
4660
|
-
const
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
3350
|
+
const target = config.targetAccuracy ?? 0.85;
|
|
3351
|
+
const tolerance = config.tolerance ?? 0.05;
|
|
3352
|
+
let correct = 0;
|
|
3353
|
+
for (const r of records) {
|
|
3354
|
+
if (r.isCorrect) correct++;
|
|
4665
3355
|
}
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
3356
|
+
const accuracy = correct / records.length;
|
|
3357
|
+
return scoreAccuracyInZone(accuracy, target, tolerance);
|
|
3358
|
+
}
|
|
3359
|
+
function scoreAccuracyInZone(accuracy, target, tolerance) {
|
|
3360
|
+
const dist = Math.abs(accuracy - target);
|
|
3361
|
+
if (dist <= tolerance) {
|
|
3362
|
+
return 1;
|
|
4669
3363
|
}
|
|
4670
|
-
|
|
4671
|
-
|
|
3364
|
+
const excess = dist - tolerance;
|
|
3365
|
+
const slope = 2.5;
|
|
3366
|
+
return Math.max(0, 1 - excess * slope);
|
|
4672
3367
|
}
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4677
|
-
return;
|
|
3368
|
+
var init_signal = __esm({
|
|
3369
|
+
"src/core/orchestration/signal.ts"() {
|
|
3370
|
+
"use strict";
|
|
4678
3371
|
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
}
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
course: p.courseName || p.courseId.slice(0, 8),
|
|
4690
|
-
origin: p.origin,
|
|
4691
|
-
queue: p.queueSource,
|
|
4692
|
-
score: p.score?.toFixed(3) || "-",
|
|
4693
|
-
time: p.timestamp.toLocaleTimeString()
|
|
4694
|
-
}))
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
// src/core/orchestration/recording.ts
|
|
3375
|
+
async function recordUserOutcome(context, periodStart, periodEnd, records, activeStrategyIds, eloStart = 0, eloEnd = 0, config) {
|
|
3376
|
+
const { user, course, userId } = context;
|
|
3377
|
+
const courseId = course.getCourseID();
|
|
3378
|
+
const outcomeValue = computeOutcomeSignal(records, config);
|
|
3379
|
+
if (outcomeValue === null) {
|
|
3380
|
+
logger.debug(
|
|
3381
|
+
`[Orchestration] No outcome signal computed for ${userId} (insufficient data). Skipping record.`
|
|
4695
3382
|
);
|
|
4696
|
-
}
|
|
4697
|
-
console.groupEnd();
|
|
4698
|
-
}
|
|
4699
|
-
function showInterleaving(sessionIndex = 0) {
|
|
4700
|
-
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4701
|
-
if (!session) {
|
|
4702
|
-
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4703
3383
|
return;
|
|
4704
3384
|
}
|
|
4705
|
-
|
|
4706
|
-
const
|
|
4707
|
-
|
|
4708
|
-
session.presentations.forEach((p) => {
|
|
4709
|
-
const name = p.courseName || p.courseId;
|
|
4710
|
-
courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
|
|
4711
|
-
if (!courseOrigins.has(name)) {
|
|
4712
|
-
courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
|
|
4713
|
-
}
|
|
4714
|
-
const origins = courseOrigins.get(name);
|
|
4715
|
-
origins[p.origin]++;
|
|
4716
|
-
});
|
|
4717
|
-
logger.info("Course distribution:");
|
|
4718
|
-
console.table(
|
|
4719
|
-
Array.from(courseCounts.entries()).map(([course, count]) => {
|
|
4720
|
-
const origins = courseOrigins.get(course);
|
|
4721
|
-
return {
|
|
4722
|
-
course,
|
|
4723
|
-
total: count,
|
|
4724
|
-
reviews: origins.review,
|
|
4725
|
-
new: origins.new,
|
|
4726
|
-
failed: origins.failed,
|
|
4727
|
-
percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
|
|
4728
|
-
};
|
|
4729
|
-
})
|
|
4730
|
-
);
|
|
4731
|
-
if (session.presentations.length > 0) {
|
|
4732
|
-
logger.info("\nPresentation sequence (first 20):");
|
|
4733
|
-
const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
|
|
4734
|
-
logger.info(sequence);
|
|
4735
|
-
}
|
|
4736
|
-
let maxCluster = 0;
|
|
4737
|
-
let currentCluster = 1;
|
|
4738
|
-
let currentCourse = session.presentations[0]?.courseId;
|
|
4739
|
-
for (let i = 1; i < session.presentations.length; i++) {
|
|
4740
|
-
if (session.presentations[i].courseId === currentCourse) {
|
|
4741
|
-
currentCluster++;
|
|
4742
|
-
maxCluster = Math.max(maxCluster, currentCluster);
|
|
4743
|
-
} else {
|
|
4744
|
-
currentCourse = session.presentations[i].courseId;
|
|
4745
|
-
currentCluster = 1;
|
|
4746
|
-
}
|
|
3385
|
+
const deviations = {};
|
|
3386
|
+
for (const strategyId of activeStrategyIds) {
|
|
3387
|
+
deviations[strategyId] = context.getDeviation(strategyId);
|
|
4747
3388
|
}
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
3389
|
+
const id = `USER_OUTCOME::${courseId}::${userId}::${periodEnd}`;
|
|
3390
|
+
const record = {
|
|
3391
|
+
_id: id,
|
|
3392
|
+
docType: "USER_OUTCOME" /* USER_OUTCOME */,
|
|
3393
|
+
courseId,
|
|
3394
|
+
userId,
|
|
3395
|
+
periodStart,
|
|
3396
|
+
periodEnd,
|
|
3397
|
+
outcomeValue,
|
|
3398
|
+
deviations,
|
|
3399
|
+
metadata: {
|
|
3400
|
+
sessionsCount: 1,
|
|
3401
|
+
// Assumes recording is triggered per-session currently
|
|
3402
|
+
cardsSeen: records.length,
|
|
3403
|
+
eloStart,
|
|
3404
|
+
eloEnd,
|
|
3405
|
+
signalType: "accuracy_in_zone"
|
|
3406
|
+
}
|
|
3407
|
+
};
|
|
3408
|
+
try {
|
|
3409
|
+
await user.putUserOutcome(record);
|
|
3410
|
+
logger.debug(
|
|
3411
|
+
`[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
|
|
3412
|
+
);
|
|
3413
|
+
} catch (e) {
|
|
3414
|
+
logger.error(`[Orchestration] Failed to record outcome: ${e}`);
|
|
4752
3415
|
}
|
|
4753
|
-
console.groupEnd();
|
|
4754
|
-
}
|
|
4755
|
-
function mountSessionDebugger() {
|
|
4756
|
-
if (typeof window === "undefined") return;
|
|
4757
|
-
const win = window;
|
|
4758
|
-
win.skuilder = win.skuilder || {};
|
|
4759
|
-
win.skuilder.session = sessionDebugAPI;
|
|
4760
3416
|
}
|
|
4761
|
-
var
|
|
4762
|
-
|
|
4763
|
-
"src/study/SessionDebugger.ts"() {
|
|
3417
|
+
var init_recording = __esm({
|
|
3418
|
+
"src/core/orchestration/recording.ts"() {
|
|
4764
3419
|
"use strict";
|
|
3420
|
+
init_signal();
|
|
3421
|
+
init_types_legacy();
|
|
4765
3422
|
init_logger();
|
|
4766
|
-
activeSession = null;
|
|
4767
|
-
sessionHistory = [];
|
|
4768
|
-
sessionDebugAPI = {
|
|
4769
|
-
/**
|
|
4770
|
-
* Get raw session history for programmatic access.
|
|
4771
|
-
*/
|
|
4772
|
-
get sessions() {
|
|
4773
|
-
return [...sessionHistory];
|
|
4774
|
-
},
|
|
4775
|
-
/**
|
|
4776
|
-
* Get active session if any.
|
|
4777
|
-
*/
|
|
4778
|
-
get active() {
|
|
4779
|
-
return activeSession;
|
|
4780
|
-
},
|
|
4781
|
-
/**
|
|
4782
|
-
* Show current queue state.
|
|
4783
|
-
*/
|
|
4784
|
-
showQueue() {
|
|
4785
|
-
showCurrentQueue();
|
|
4786
|
-
},
|
|
4787
|
-
/**
|
|
4788
|
-
* Show presentation history for current or past session.
|
|
4789
|
-
*/
|
|
4790
|
-
showHistory(sessionIndex = 0) {
|
|
4791
|
-
showPresentationHistory(sessionIndex);
|
|
4792
|
-
},
|
|
4793
|
-
/**
|
|
4794
|
-
* Analyze course interleaving pattern.
|
|
4795
|
-
*/
|
|
4796
|
-
showInterleaving(sessionIndex = 0) {
|
|
4797
|
-
showInterleaving(sessionIndex);
|
|
4798
|
-
},
|
|
4799
|
-
/**
|
|
4800
|
-
* List all tracked sessions.
|
|
4801
|
-
*/
|
|
4802
|
-
listSessions() {
|
|
4803
|
-
if (activeSession) {
|
|
4804
|
-
logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
|
|
4805
|
-
}
|
|
4806
|
-
if (sessionHistory.length === 0) {
|
|
4807
|
-
logger.info("[Session Debug] No completed sessions in history.");
|
|
4808
|
-
return;
|
|
4809
|
-
}
|
|
4810
|
-
console.table(
|
|
4811
|
-
sessionHistory.map((s, idx) => ({
|
|
4812
|
-
index: idx,
|
|
4813
|
-
id: s.sessionId.slice(-8),
|
|
4814
|
-
started: s.startTime.toLocaleTimeString(),
|
|
4815
|
-
ended: s.endTime?.toLocaleTimeString() || "incomplete",
|
|
4816
|
-
cards: s.presentations.length
|
|
4817
|
-
}))
|
|
4818
|
-
);
|
|
4819
|
-
},
|
|
4820
|
-
/**
|
|
4821
|
-
* Export session history as JSON for bug reports.
|
|
4822
|
-
*/
|
|
4823
|
-
export() {
|
|
4824
|
-
const data = {
|
|
4825
|
-
active: activeSession,
|
|
4826
|
-
history: sessionHistory
|
|
4827
|
-
};
|
|
4828
|
-
const json = JSON.stringify(data, null, 2);
|
|
4829
|
-
logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
|
|
4830
|
-
logger.info(" copy(window.skuilder.session.export())");
|
|
4831
|
-
return json;
|
|
4832
|
-
},
|
|
4833
|
-
/**
|
|
4834
|
-
* Clear session history.
|
|
4835
|
-
*/
|
|
4836
|
-
clear() {
|
|
4837
|
-
sessionHistory.length = 0;
|
|
4838
|
-
logger.info("[Session Debug] Session history cleared.");
|
|
4839
|
-
},
|
|
4840
|
-
/**
|
|
4841
|
-
* Show help.
|
|
4842
|
-
*/
|
|
4843
|
-
help() {
|
|
4844
|
-
logger.info(`
|
|
4845
|
-
\u{1F3AF} Session Debug API
|
|
4846
|
-
|
|
4847
|
-
Commands:
|
|
4848
|
-
.showQueue() Show current queue state (active session only)
|
|
4849
|
-
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
4850
|
-
.showInterleaving(index?) Analyze course interleaving pattern
|
|
4851
|
-
.listSessions() List all tracked sessions
|
|
4852
|
-
.export() Export session data as JSON for bug reports
|
|
4853
|
-
.clear() Clear session history
|
|
4854
|
-
.sessions Access raw session history array
|
|
4855
|
-
.active Access active session (if any)
|
|
4856
|
-
.help() Show this help message
|
|
4857
|
-
|
|
4858
|
-
Example:
|
|
4859
|
-
window.skuilder.session.showHistory()
|
|
4860
|
-
window.skuilder.session.showInterleaving()
|
|
4861
|
-
window.skuilder.session.showQueue()
|
|
4862
|
-
`);
|
|
4863
|
-
}
|
|
4864
|
-
};
|
|
4865
|
-
mountSessionDebugger();
|
|
4866
3423
|
}
|
|
4867
3424
|
});
|
|
4868
3425
|
|
|
4869
|
-
// src/
|
|
4870
|
-
|
|
4871
|
-
|
|
3426
|
+
// src/core/orchestration/index.ts
|
|
3427
|
+
function fnv1a(str) {
|
|
3428
|
+
let hash = 2166136261;
|
|
3429
|
+
for (let i = 0; i < str.length; i++) {
|
|
3430
|
+
hash ^= str.charCodeAt(i);
|
|
3431
|
+
hash = Math.imul(hash, 16777619);
|
|
3432
|
+
}
|
|
3433
|
+
return hash >>> 0;
|
|
3434
|
+
}
|
|
3435
|
+
function computeDeviation(userId, strategyId, salt) {
|
|
3436
|
+
const input = `${userId}:${strategyId}:${salt}`;
|
|
3437
|
+
const hash = fnv1a(input);
|
|
3438
|
+
const normalized = hash / 4294967296;
|
|
3439
|
+
return normalized * 2 - 1;
|
|
3440
|
+
}
|
|
3441
|
+
function computeSpread(confidence) {
|
|
3442
|
+
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
3443
|
+
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
3444
|
+
}
|
|
3445
|
+
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
3446
|
+
const deviation = computeDeviation(userId, strategyId, salt);
|
|
3447
|
+
const spread = computeSpread(learnable.confidence);
|
|
3448
|
+
const adjustment = deviation * spread * learnable.weight;
|
|
3449
|
+
const effective = learnable.weight + adjustment;
|
|
3450
|
+
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
3451
|
+
}
|
|
3452
|
+
async function createOrchestrationContext(user, course) {
|
|
3453
|
+
let courseConfig;
|
|
3454
|
+
try {
|
|
3455
|
+
courseConfig = await course.getCourseConfig();
|
|
3456
|
+
} catch (e) {
|
|
3457
|
+
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
3458
|
+
courseConfig = {
|
|
3459
|
+
name: "Unknown",
|
|
3460
|
+
description: "",
|
|
3461
|
+
public: false,
|
|
3462
|
+
deleted: false,
|
|
3463
|
+
creator: "",
|
|
3464
|
+
admins: [],
|
|
3465
|
+
moderators: [],
|
|
3466
|
+
dataShapes: [],
|
|
3467
|
+
questionTypes: [],
|
|
3468
|
+
orchestration: { salt: "default" }
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
const userId = user.getUsername();
|
|
3472
|
+
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
3473
|
+
return {
|
|
3474
|
+
user,
|
|
3475
|
+
course,
|
|
3476
|
+
userId,
|
|
3477
|
+
courseConfig,
|
|
3478
|
+
getEffectiveWeight(strategyId, learnable) {
|
|
3479
|
+
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
3480
|
+
},
|
|
3481
|
+
getDeviation(strategyId) {
|
|
3482
|
+
return computeDeviation(userId, strategyId, salt);
|
|
3483
|
+
}
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
3487
|
+
var init_orchestration = __esm({
|
|
3488
|
+
"src/core/orchestration/index.ts"() {
|
|
4872
3489
|
"use strict";
|
|
4873
|
-
init_SrsService();
|
|
4874
|
-
init_EloService();
|
|
4875
|
-
init_ResponseProcessor();
|
|
4876
|
-
init_CardHydrationService();
|
|
4877
|
-
init_ItemQueue();
|
|
4878
|
-
init_couch();
|
|
4879
|
-
init_recording();
|
|
4880
|
-
init_util2();
|
|
4881
|
-
init_navigators();
|
|
4882
|
-
init_SourceMixer();
|
|
4883
|
-
init_MixerDebugger();
|
|
4884
|
-
init_SessionDebugger();
|
|
4885
3490
|
init_logger();
|
|
3491
|
+
init_gradient();
|
|
3492
|
+
init_learning();
|
|
3493
|
+
init_signal();
|
|
3494
|
+
init_recording();
|
|
3495
|
+
MIN_SPREAD = 0.1;
|
|
3496
|
+
MAX_SPREAD = 0.5;
|
|
3497
|
+
MIN_WEIGHT = 0.1;
|
|
3498
|
+
MAX_WEIGHT = 3;
|
|
4886
3499
|
}
|
|
4887
3500
|
});
|
|
4888
3501
|
|
|
@@ -4891,7 +3504,7 @@ var Pipeline_exports = {};
|
|
|
4891
3504
|
__export(Pipeline_exports, {
|
|
4892
3505
|
Pipeline: () => Pipeline
|
|
4893
3506
|
});
|
|
4894
|
-
import { toCourseElo as
|
|
3507
|
+
import { toCourseElo as toCourseElo5 } from "@vue-skuilder/common";
|
|
4895
3508
|
function globToRegex(pattern) {
|
|
4896
3509
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
4897
3510
|
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
@@ -4904,6 +3517,44 @@ function globMatch(value, pattern) {
|
|
|
4904
3517
|
function cardMatchesTagPattern(card, pattern) {
|
|
4905
3518
|
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
4906
3519
|
}
|
|
3520
|
+
function mergeHints2(allHints) {
|
|
3521
|
+
const defined = allHints.filter((h) => h !== null && h !== void 0);
|
|
3522
|
+
if (defined.length === 0) return void 0;
|
|
3523
|
+
const merged = {};
|
|
3524
|
+
const boostTags = {};
|
|
3525
|
+
for (const hints of defined) {
|
|
3526
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
3527
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
if (Object.keys(boostTags).length > 0) {
|
|
3531
|
+
merged.boostTags = boostTags;
|
|
3532
|
+
}
|
|
3533
|
+
const boostCards = {};
|
|
3534
|
+
for (const hints of defined) {
|
|
3535
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
3536
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
if (Object.keys(boostCards).length > 0) {
|
|
3540
|
+
merged.boostCards = boostCards;
|
|
3541
|
+
}
|
|
3542
|
+
const concatUnique = (field) => {
|
|
3543
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
3544
|
+
if (values.length > 0) {
|
|
3545
|
+
merged[field] = [...new Set(values)];
|
|
3546
|
+
}
|
|
3547
|
+
};
|
|
3548
|
+
concatUnique("requireTags");
|
|
3549
|
+
concatUnique("requireCards");
|
|
3550
|
+
concatUnique("excludeTags");
|
|
3551
|
+
concatUnique("excludeCards");
|
|
3552
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
3553
|
+
if (labels.length > 0) {
|
|
3554
|
+
merged._label = labels.join("; ");
|
|
3555
|
+
}
|
|
3556
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
3557
|
+
}
|
|
4907
3558
|
function logPipelineConfig(generator, filters) {
|
|
4908
3559
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
4909
3560
|
logger.info(
|
|
@@ -4975,7 +3626,6 @@ var init_Pipeline = __esm({
|
|
|
4975
3626
|
init_logger();
|
|
4976
3627
|
init_orchestration();
|
|
4977
3628
|
init_PipelineDebugger();
|
|
4978
|
-
init_SessionController();
|
|
4979
3629
|
VERBOSE_RESULTS = true;
|
|
4980
3630
|
Pipeline = class extends ContentNavigator {
|
|
4981
3631
|
generator;
|
|
@@ -5055,9 +3705,12 @@ var init_Pipeline = __esm({
|
|
|
5055
3705
|
logger.debug(
|
|
5056
3706
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
5057
3707
|
);
|
|
5058
|
-
|
|
3708
|
+
const generatorResult = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3709
|
+
let cards = generatorResult.cards;
|
|
5059
3710
|
const tGenerate = performance.now();
|
|
5060
3711
|
const generatedCount = cards.length;
|
|
3712
|
+
const mergedHints = mergeHints2([this._ephemeralHints, generatorResult.hints]);
|
|
3713
|
+
this._ephemeralHints = mergedHints ?? null;
|
|
5061
3714
|
let generatorSummaries;
|
|
5062
3715
|
if (this.generator.generators) {
|
|
5063
3716
|
const genMap = /* @__PURE__ */ new Map();
|
|
@@ -5143,7 +3796,7 @@ var init_Pipeline = __esm({
|
|
|
5143
3796
|
} catch (e) {
|
|
5144
3797
|
logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
|
|
5145
3798
|
}
|
|
5146
|
-
return result;
|
|
3799
|
+
return { cards: result };
|
|
5147
3800
|
}
|
|
5148
3801
|
/**
|
|
5149
3802
|
* Batch hydrate tags for all cards.
|
|
@@ -5298,7 +3951,7 @@ var init_Pipeline = __esm({
|
|
|
5298
3951
|
let userElo = 1e3;
|
|
5299
3952
|
try {
|
|
5300
3953
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
5301
|
-
const courseElo =
|
|
3954
|
+
const courseElo = toCourseElo5(courseReg.elo);
|
|
5302
3955
|
userElo = courseElo.global.score;
|
|
5303
3956
|
} catch (e) {
|
|
5304
3957
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
@@ -5359,7 +4012,7 @@ var init_Pipeline = __esm({
|
|
|
5359
4012
|
*/
|
|
5360
4013
|
async getTagEloStatus(tagFilter) {
|
|
5361
4014
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
5362
|
-
const courseElo =
|
|
4015
|
+
const courseElo = toCourseElo5(courseReg.elo);
|
|
5363
4016
|
const result = {};
|
|
5364
4017
|
if (!tagFilter) {
|
|
5365
4018
|
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
@@ -5961,7 +4614,7 @@ import {
|
|
|
5961
4614
|
EloToNumber,
|
|
5962
4615
|
Status,
|
|
5963
4616
|
blankCourseElo as blankCourseElo2,
|
|
5964
|
-
toCourseElo as
|
|
4617
|
+
toCourseElo as toCourseElo6
|
|
5965
4618
|
} from "@vue-skuilder/common";
|
|
5966
4619
|
function randIntWeightedTowardZero(n) {
|
|
5967
4620
|
return Math.floor(Math.random() * Math.random() * Math.random() * n);
|
|
@@ -6145,7 +4798,7 @@ var init_courseDB = __esm({
|
|
|
6145
4798
|
docs.rows.forEach((r) => {
|
|
6146
4799
|
if (isSuccessRow(r)) {
|
|
6147
4800
|
if (r.doc && r.doc.elo) {
|
|
6148
|
-
ret.push(
|
|
4801
|
+
ret.push(toCourseElo6(r.doc.elo));
|
|
6149
4802
|
} else {
|
|
6150
4803
|
logger.warn("no elo data for card: " + r.id);
|
|
6151
4804
|
ret.push(blankCourseElo2());
|
|
@@ -6650,7 +5303,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6650
5303
|
});
|
|
6651
5304
|
|
|
6652
5305
|
// src/impl/couch/classroomDB.ts
|
|
6653
|
-
import
|
|
5306
|
+
import moment4 from "moment";
|
|
6654
5307
|
var CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB;
|
|
6655
5308
|
var init_classroomDB2 = __esm({
|
|
6656
5309
|
"src/impl/couch/classroomDB.ts"() {
|
|
@@ -6768,14 +5421,14 @@ var init_classroomDB2 = __esm({
|
|
|
6768
5421
|
}
|
|
6769
5422
|
const activeCards = await this._user.getActiveCards();
|
|
6770
5423
|
const activeCardIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
6771
|
-
const now =
|
|
5424
|
+
const now = moment4.utc();
|
|
6772
5425
|
const assigned = await this.getAssignedContent();
|
|
6773
|
-
const due = assigned.filter((c) => now.isAfter(
|
|
5426
|
+
const due = assigned.filter((c) => now.isAfter(moment4.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
|
|
6774
5427
|
logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(due)}`);
|
|
6775
5428
|
for (const content of due) {
|
|
6776
5429
|
if (content.type === "course") {
|
|
6777
5430
|
const db = new CourseDB(content.courseID, async () => this._user);
|
|
6778
|
-
const courseCards = await db.getWeightedCards(limit);
|
|
5431
|
+
const { cards: courseCards } = await db.getWeightedCards(limit);
|
|
6779
5432
|
for (const card of courseCards) {
|
|
6780
5433
|
if (!activeCardIds.has(card.cardId)) {
|
|
6781
5434
|
weighted.push({
|
|
@@ -6838,7 +5491,7 @@ var init_classroomDB2 = __esm({
|
|
|
6838
5491
|
logger.info(
|
|
6839
5492
|
`[StudentClassroomDB] New cards from classroom ${this._cfg.name}: ${weighted.length} total (reviews + new)`
|
|
6840
5493
|
);
|
|
6841
|
-
return weighted.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5494
|
+
return { cards: weighted.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
6842
5495
|
}
|
|
6843
5496
|
};
|
|
6844
5497
|
}
|
|
@@ -6868,7 +5521,7 @@ var init_CourseSyncService = __esm({
|
|
|
6868
5521
|
});
|
|
6869
5522
|
|
|
6870
5523
|
// src/impl/couch/auth.ts
|
|
6871
|
-
import
|
|
5524
|
+
import fetch from "cross-fetch";
|
|
6872
5525
|
var init_auth = __esm({
|
|
6873
5526
|
"src/impl/couch/auth.ts"() {
|
|
6874
5527
|
"use strict";
|
|
@@ -6893,8 +5546,8 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
6893
5546
|
});
|
|
6894
5547
|
|
|
6895
5548
|
// src/impl/couch/index.ts
|
|
6896
|
-
import
|
|
6897
|
-
import
|
|
5549
|
+
import fetch2 from "cross-fetch";
|
|
5550
|
+
import moment5 from "moment";
|
|
6898
5551
|
import process2 from "process";
|
|
6899
5552
|
function createPouchDBConfig() {
|
|
6900
5553
|
const hasExplicitCredentials = ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD;
|
|
@@ -6971,7 +5624,7 @@ var init_couch = __esm({
|
|
|
6971
5624
|
|
|
6972
5625
|
// src/impl/common/BaseUserDB.ts
|
|
6973
5626
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
6974
|
-
import
|
|
5627
|
+
import moment6 from "moment";
|
|
6975
5628
|
async function getOrCreateClassroomRegistrationsDoc(user) {
|
|
6976
5629
|
let ret;
|
|
6977
5630
|
try {
|
|
@@ -7333,7 +5986,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7333
5986
|
);
|
|
7334
5987
|
return reviews.rows.filter((r) => {
|
|
7335
5988
|
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
7336
|
-
const date =
|
|
5989
|
+
const date = moment6.utc(
|
|
7337
5990
|
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
7338
5991
|
REVIEW_TIME_FORMAT
|
|
7339
5992
|
);
|
|
@@ -7346,11 +5999,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7346
5999
|
}).map((r) => r.doc);
|
|
7347
6000
|
}
|
|
7348
6001
|
async getReviewsForcast(daysCount) {
|
|
7349
|
-
const time =
|
|
6002
|
+
const time = moment6.utc().add(daysCount, "days");
|
|
7350
6003
|
return this.getReviewstoDate(time);
|
|
7351
6004
|
}
|
|
7352
6005
|
async getPendingReviews(course_id) {
|
|
7353
|
-
const now =
|
|
6006
|
+
const now = moment6.utc();
|
|
7354
6007
|
return this.getReviewstoDate(now, course_id);
|
|
7355
6008
|
}
|
|
7356
6009
|
async getScheduledReviewCount(course_id) {
|
|
@@ -7637,7 +6290,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7637
6290
|
*/
|
|
7638
6291
|
async putCardRecord(record) {
|
|
7639
6292
|
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
7640
|
-
record.timeStamp =
|
|
6293
|
+
record.timeStamp = moment6.utc(record.timeStamp).toString();
|
|
7641
6294
|
try {
|
|
7642
6295
|
const cardHistory = await this.update(
|
|
7643
6296
|
cardHistoryID,
|
|
@@ -7653,7 +6306,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7653
6306
|
const ret = {
|
|
7654
6307
|
...record2
|
|
7655
6308
|
};
|
|
7656
|
-
ret.timeStamp =
|
|
6309
|
+
ret.timeStamp = moment6.utc(record2.timeStamp);
|
|
7657
6310
|
return ret;
|
|
7658
6311
|
});
|
|
7659
6312
|
return cardHistory;
|
|
@@ -8072,7 +6725,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
8072
6725
|
async getWeightedCards(limit) {
|
|
8073
6726
|
if (!hasActiveFilter(this.filter)) {
|
|
8074
6727
|
logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
|
|
8075
|
-
return [];
|
|
6728
|
+
return { cards: [] };
|
|
8076
6729
|
}
|
|
8077
6730
|
const eligibleCardIds = await this.resolveFilteredCardIds();
|
|
8078
6731
|
const activeCards = await this.user.getActiveCards();
|
|
@@ -8124,7 +6777,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
8124
6777
|
}
|
|
8125
6778
|
]
|
|
8126
6779
|
}));
|
|
8127
|
-
return [...reviewWeighted, ...newCardWeighted].slice(0, limit);
|
|
6780
|
+
return { cards: [...reviewWeighted, ...newCardWeighted].slice(0, limit) };
|
|
8128
6781
|
}
|
|
8129
6782
|
/**
|
|
8130
6783
|
* Clears the cached resolved card IDs.
|
|
@@ -8353,7 +7006,7 @@ var init_cardProcessor = __esm({
|
|
|
8353
7006
|
});
|
|
8354
7007
|
|
|
8355
7008
|
// src/core/bulkImport/types.ts
|
|
8356
|
-
var
|
|
7009
|
+
var init_types3 = __esm({
|
|
8357
7010
|
"src/core/bulkImport/types.ts"() {
|
|
8358
7011
|
"use strict";
|
|
8359
7012
|
}
|
|
@@ -8364,7 +7017,7 @@ var init_bulkImport = __esm({
|
|
|
8364
7017
|
"src/core/bulkImport/index.ts"() {
|
|
8365
7018
|
"use strict";
|
|
8366
7019
|
init_cardProcessor();
|
|
8367
|
-
|
|
7020
|
+
init_types3();
|
|
8368
7021
|
}
|
|
8369
7022
|
});
|
|
8370
7023
|
|