@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.js
CHANGED
|
@@ -939,6 +939,81 @@ var init_PipelineDebugger = __esm({
|
|
|
939
939
|
}
|
|
940
940
|
console.groupEnd();
|
|
941
941
|
},
|
|
942
|
+
/**
|
|
943
|
+
* Show prescribed-related cards from the most recent run.
|
|
944
|
+
*
|
|
945
|
+
* Highlights:
|
|
946
|
+
* - cards directly generated by the prescribed strategy
|
|
947
|
+
* - blocked prescribed targets mentioned in provenance
|
|
948
|
+
* - support tags resolved for blocked targets
|
|
949
|
+
*
|
|
950
|
+
* @param groupId - Optional prescribed group ID filter (e.g. 'intro-core')
|
|
951
|
+
*/
|
|
952
|
+
showPrescribed(groupId) {
|
|
953
|
+
if (runHistory.length === 0) {
|
|
954
|
+
logger.info("[Pipeline Debug] No runs captured yet.");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const run = runHistory[0];
|
|
958
|
+
const prescribedCards = run.cards.filter(
|
|
959
|
+
(c) => c.provenance.some((p) => p.strategy === "prescribed")
|
|
960
|
+
);
|
|
961
|
+
console.group(`\u{1F9ED} Prescribed Debug (${run.courseId})`);
|
|
962
|
+
if (prescribedCards.length === 0) {
|
|
963
|
+
logger.info("No prescribed-generated cards were present in the most recent run.");
|
|
964
|
+
console.groupEnd();
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const rows = prescribedCards.map((card) => {
|
|
968
|
+
const prescribedProv = card.provenance.find((p) => p.strategy === "prescribed");
|
|
969
|
+
const reason = prescribedProv?.reason ?? "";
|
|
970
|
+
const parsedGroup = reason.match(/group=([^;]+)/)?.[1] ?? "unknown";
|
|
971
|
+
const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
|
|
972
|
+
const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
|
|
973
|
+
const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
|
|
974
|
+
const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
|
|
975
|
+
const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
|
|
976
|
+
return {
|
|
977
|
+
group: parsedGroup,
|
|
978
|
+
mode,
|
|
979
|
+
cardId: card.cardId,
|
|
980
|
+
selected: card.selected ? "yes" : "no",
|
|
981
|
+
finalScore: card.finalScore.toFixed(3),
|
|
982
|
+
blocked,
|
|
983
|
+
blockedTargets,
|
|
984
|
+
supportTags,
|
|
985
|
+
multiplier
|
|
986
|
+
};
|
|
987
|
+
}).filter((row) => !groupId || row.group === groupId).sort((a, b) => Number(b.finalScore) - Number(a.finalScore));
|
|
988
|
+
if (rows.length === 0) {
|
|
989
|
+
logger.info(
|
|
990
|
+
`[Pipeline Debug] No prescribed cards matched group '${groupId}' in the most recent run.`
|
|
991
|
+
);
|
|
992
|
+
console.groupEnd();
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
console.table(rows);
|
|
996
|
+
const selectedRows = rows.filter((r) => r.selected === "yes");
|
|
997
|
+
const blockedTargetSet = /* @__PURE__ */ new Set();
|
|
998
|
+
const supportTagSet = /* @__PURE__ */ new Set();
|
|
999
|
+
for (const row of rows) {
|
|
1000
|
+
if (row.blockedTargets && row.blockedTargets !== "none") {
|
|
1001
|
+
row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
|
|
1002
|
+
}
|
|
1003
|
+
if (row.supportTags && row.supportTags !== "none") {
|
|
1004
|
+
row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
logger.info(`Prescribed cards in run: ${rows.length}`);
|
|
1008
|
+
logger.info(`Selected prescribed cards: ${selectedRows.length}`);
|
|
1009
|
+
logger.info(
|
|
1010
|
+
`Blocked prescribed targets referenced: ${blockedTargetSet.size > 0 ? [...blockedTargetSet].join(", ") : "none"}`
|
|
1011
|
+
);
|
|
1012
|
+
logger.info(
|
|
1013
|
+
`Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
|
|
1014
|
+
);
|
|
1015
|
+
console.groupEnd();
|
|
1016
|
+
},
|
|
942
1017
|
/**
|
|
943
1018
|
* Show all runs in compact format.
|
|
944
1019
|
*/
|
|
@@ -1087,6 +1162,7 @@ Commands:
|
|
|
1087
1162
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
1088
1163
|
.showRegistry() Show navigator registry (classes + roles)
|
|
1089
1164
|
.showStrategies() Show registry + strategy mapping from last run
|
|
1165
|
+
.showPrescribed(id?) Show prescribed-generated cards and blocked/support details from last run
|
|
1090
1166
|
.listRuns() List all captured runs in table format
|
|
1091
1167
|
.export() Export run history as JSON for bug reports
|
|
1092
1168
|
.clear() Clear run history
|
|
@@ -1110,6 +1186,44 @@ __export(CompositeGenerator_exports, {
|
|
|
1110
1186
|
AggregationMode: () => AggregationMode,
|
|
1111
1187
|
default: () => CompositeGenerator
|
|
1112
1188
|
});
|
|
1189
|
+
function mergeHints(allHints) {
|
|
1190
|
+
const defined = allHints.filter((h) => h !== void 0);
|
|
1191
|
+
if (defined.length === 0) return void 0;
|
|
1192
|
+
const merged = {};
|
|
1193
|
+
const boostTags = {};
|
|
1194
|
+
for (const hints of defined) {
|
|
1195
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
1196
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (Object.keys(boostTags).length > 0) {
|
|
1200
|
+
merged.boostTags = boostTags;
|
|
1201
|
+
}
|
|
1202
|
+
const boostCards = {};
|
|
1203
|
+
for (const hints of defined) {
|
|
1204
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
1205
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (Object.keys(boostCards).length > 0) {
|
|
1209
|
+
merged.boostCards = boostCards;
|
|
1210
|
+
}
|
|
1211
|
+
const concatUnique = (field) => {
|
|
1212
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
1213
|
+
if (values.length > 0) {
|
|
1214
|
+
merged[field] = [...new Set(values)];
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
concatUnique("requireTags");
|
|
1218
|
+
concatUnique("requireCards");
|
|
1219
|
+
concatUnique("excludeTags");
|
|
1220
|
+
concatUnique("excludeCards");
|
|
1221
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
1222
|
+
if (labels.length > 0) {
|
|
1223
|
+
merged._label = labels.join("; ");
|
|
1224
|
+
}
|
|
1225
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
1226
|
+
}
|
|
1113
1227
|
var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
|
|
1114
1228
|
var init_CompositeGenerator = __esm({
|
|
1115
1229
|
"src/core/navigators/generators/CompositeGenerator.ts"() {
|
|
@@ -1173,17 +1287,18 @@ var init_CompositeGenerator = __esm({
|
|
|
1173
1287
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
1174
1288
|
);
|
|
1175
1289
|
const generatorSummaries = [];
|
|
1176
|
-
results.forEach((
|
|
1290
|
+
results.forEach((result, index) => {
|
|
1291
|
+
const cards2 = result.cards;
|
|
1177
1292
|
const gen = this.generators[index];
|
|
1178
1293
|
const genName = gen.name || `Generator ${index}`;
|
|
1179
|
-
const newCards =
|
|
1180
|
-
const reviewCards =
|
|
1181
|
-
if (
|
|
1182
|
-
const topScore = Math.max(...
|
|
1294
|
+
const newCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("new card"));
|
|
1295
|
+
const reviewCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("review"));
|
|
1296
|
+
if (cards2.length > 0) {
|
|
1297
|
+
const topScore = Math.max(...cards2.map((c) => c.score)).toFixed(2);
|
|
1183
1298
|
const parts = [];
|
|
1184
1299
|
if (newCards.length > 0) parts.push(`${newCards.length} new`);
|
|
1185
1300
|
if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
|
|
1186
|
-
const breakdown = parts.length > 0 ? parts.join(", ") : `${
|
|
1301
|
+
const breakdown = parts.length > 0 ? parts.join(", ") : `${cards2.length} cards`;
|
|
1187
1302
|
generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
|
|
1188
1303
|
} else {
|
|
1189
1304
|
generatorSummaries.push(`${genName}: 0 cards`);
|
|
@@ -1191,7 +1306,8 @@ var init_CompositeGenerator = __esm({
|
|
|
1191
1306
|
});
|
|
1192
1307
|
logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
|
|
1193
1308
|
const byCardId = /* @__PURE__ */ new Map();
|
|
1194
|
-
results.forEach((
|
|
1309
|
+
results.forEach((result, index) => {
|
|
1310
|
+
const cards2 = result.cards;
|
|
1195
1311
|
const gen = this.generators[index];
|
|
1196
1312
|
let weight = gen.learnable?.weight ?? 1;
|
|
1197
1313
|
let deviation;
|
|
@@ -1202,7 +1318,7 @@ var init_CompositeGenerator = __esm({
|
|
|
1202
1318
|
deviation = context.orchestration.getDeviation(strategyId);
|
|
1203
1319
|
}
|
|
1204
1320
|
}
|
|
1205
|
-
for (const card of
|
|
1321
|
+
for (const card of cards2) {
|
|
1206
1322
|
if (card.provenance.length > 0) {
|
|
1207
1323
|
card.provenance[0].effectiveWeight = weight;
|
|
1208
1324
|
card.provenance[0].deviation = deviation;
|
|
@@ -1214,15 +1330,15 @@ var init_CompositeGenerator = __esm({
|
|
|
1214
1330
|
});
|
|
1215
1331
|
const merged = [];
|
|
1216
1332
|
for (const [, items] of byCardId) {
|
|
1217
|
-
const
|
|
1333
|
+
const cards2 = items.map((i) => i.card);
|
|
1218
1334
|
const aggregatedScore = this.aggregateScores(items);
|
|
1219
1335
|
const finalScore = Math.min(1, aggregatedScore);
|
|
1220
|
-
const mergedProvenance =
|
|
1221
|
-
const initialScore =
|
|
1336
|
+
const mergedProvenance = cards2.flatMap((c) => c.provenance);
|
|
1337
|
+
const initialScore = cards2[0].score;
|
|
1222
1338
|
const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
|
|
1223
1339
|
const reason = this.buildAggregationReason(items, finalScore);
|
|
1224
1340
|
merged.push({
|
|
1225
|
-
...
|
|
1341
|
+
...cards2[0],
|
|
1226
1342
|
score: finalScore,
|
|
1227
1343
|
provenance: [
|
|
1228
1344
|
...mergedProvenance,
|
|
@@ -1237,7 +1353,9 @@ var init_CompositeGenerator = __esm({
|
|
|
1237
1353
|
]
|
|
1238
1354
|
});
|
|
1239
1355
|
}
|
|
1240
|
-
|
|
1356
|
+
const cards = merged.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1357
|
+
const hints = mergeHints(results.map((result) => result.hints));
|
|
1358
|
+
return { cards, hints };
|
|
1241
1359
|
}
|
|
1242
1360
|
/**
|
|
1243
1361
|
* Build human-readable reason for score aggregation.
|
|
@@ -1368,16 +1486,16 @@ var init_elo = __esm({
|
|
|
1368
1486
|
};
|
|
1369
1487
|
});
|
|
1370
1488
|
scored.sort((a, b) => b.score - a.score);
|
|
1371
|
-
const
|
|
1372
|
-
if (
|
|
1373
|
-
const topScores =
|
|
1489
|
+
const cards = scored.slice(0, limit);
|
|
1490
|
+
if (cards.length > 0) {
|
|
1491
|
+
const topScores = cards.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
|
|
1374
1492
|
logger.info(
|
|
1375
|
-
`[ELO] Course ${this.course.getCourseID()}: ${
|
|
1493
|
+
`[ELO] Course ${this.course.getCourseID()}: ${cards.length} new cards (top scores: ${topScores})`
|
|
1376
1494
|
);
|
|
1377
1495
|
} else {
|
|
1378
1496
|
logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
|
|
1379
1497
|
}
|
|
1380
|
-
return
|
|
1498
|
+
return { cards };
|
|
1381
1499
|
}
|
|
1382
1500
|
};
|
|
1383
1501
|
}
|
|
@@ -1414,7 +1532,7 @@ function matchesTagPattern(tag, pattern) {
|
|
|
1414
1532
|
function pickTopByScore(cards, limit) {
|
|
1415
1533
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1416
1534
|
}
|
|
1417
|
-
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;
|
|
1535
|
+
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;
|
|
1418
1536
|
var init_prescribed = __esm({
|
|
1419
1537
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1420
1538
|
"use strict";
|
|
@@ -1431,6 +1549,7 @@ var init_prescribed = __esm({
|
|
|
1431
1549
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1432
1550
|
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1433
1551
|
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1552
|
+
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
|
|
1434
1553
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1435
1554
|
name;
|
|
1436
1555
|
config;
|
|
@@ -1447,7 +1566,7 @@ var init_prescribed = __esm({
|
|
|
1447
1566
|
}
|
|
1448
1567
|
async getWeightedCards(limit, context) {
|
|
1449
1568
|
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1450
|
-
return [];
|
|
1569
|
+
return { cards: [] };
|
|
1451
1570
|
}
|
|
1452
1571
|
const courseId = this.course.getCourseID();
|
|
1453
1572
|
const activeCards = await this.user.getActiveCards();
|
|
@@ -1472,6 +1591,7 @@ var init_prescribed = __esm({
|
|
|
1472
1591
|
};
|
|
1473
1592
|
const emitted = [];
|
|
1474
1593
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
1594
|
+
const groupRuntimes = [];
|
|
1475
1595
|
for (const group of this.config.groups) {
|
|
1476
1596
|
const runtime = this.buildGroupRuntimeState({
|
|
1477
1597
|
group,
|
|
@@ -1483,6 +1603,7 @@ var init_prescribed = __esm({
|
|
|
1483
1603
|
userTagElo,
|
|
1484
1604
|
userGlobalElo
|
|
1485
1605
|
});
|
|
1606
|
+
groupRuntimes.push(runtime);
|
|
1486
1607
|
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1487
1608
|
const directCards = this.buildDirectTargetCards(
|
|
1488
1609
|
runtime,
|
|
@@ -1496,12 +1617,17 @@ var init_prescribed = __esm({
|
|
|
1496
1617
|
);
|
|
1497
1618
|
emitted.push(...directCards, ...supportCards);
|
|
1498
1619
|
}
|
|
1620
|
+
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1621
|
+
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1622
|
+
boostTags: hintSummary.boostTags,
|
|
1623
|
+
_label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
|
|
1624
|
+
} : void 0;
|
|
1499
1625
|
if (emitted.length === 0) {
|
|
1500
1626
|
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1501
1627
|
await this.putStrategyState(nextState).catch((e) => {
|
|
1502
1628
|
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1503
1629
|
});
|
|
1504
|
-
return [];
|
|
1630
|
+
return hints ? { cards: [], hints } : { cards: [] };
|
|
1505
1631
|
}
|
|
1506
1632
|
const finalCards = pickTopByScore(emitted, limit);
|
|
1507
1633
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
@@ -1532,7 +1658,27 @@ var init_prescribed = __esm({
|
|
|
1532
1658
|
logger.info(
|
|
1533
1659
|
`[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)`
|
|
1534
1660
|
);
|
|
1535
|
-
return finalCards;
|
|
1661
|
+
return hints ? { cards: finalCards, hints } : { cards: finalCards };
|
|
1662
|
+
}
|
|
1663
|
+
buildSupportHintSummary(groupRuntimes) {
|
|
1664
|
+
const boostTags = {};
|
|
1665
|
+
const blockedTargetIds = /* @__PURE__ */ new Set();
|
|
1666
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1667
|
+
for (const runtime of groupRuntimes) {
|
|
1668
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportTags.length === 0) {
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
runtime.blockedTargets.forEach((cardId) => blockedTargetIds.add(cardId));
|
|
1672
|
+
for (const tag of runtime.supportTags) {
|
|
1673
|
+
supportTags.add(tag);
|
|
1674
|
+
boostTags[tag] = (boostTags[tag] ?? 1) * runtime.supportMultiplier;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
boostTags,
|
|
1679
|
+
blockedTargetIds: [...blockedTargetIds].sort(),
|
|
1680
|
+
supportTags: [...supportTags].sort()
|
|
1681
|
+
};
|
|
1536
1682
|
}
|
|
1537
1683
|
parseConfig(serializedData) {
|
|
1538
1684
|
try {
|
|
@@ -1615,7 +1761,22 @@ var init_prescribed = __esm({
|
|
|
1615
1761
|
group.hierarchyWalk?.enabled !== false,
|
|
1616
1762
|
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1617
1763
|
);
|
|
1618
|
-
|
|
1764
|
+
const introTags = tags.filter((tag) => tag.startsWith("gpc:intro:"));
|
|
1765
|
+
const exposeTags = new Set(tags.filter((tag) => tag.startsWith("gpc:expose:")));
|
|
1766
|
+
for (const introTag of introTags) {
|
|
1767
|
+
const suffix = introTag.slice("gpc:intro:".length);
|
|
1768
|
+
if (suffix) {
|
|
1769
|
+
exposeTags.add(`gpc:expose:${suffix}`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const unmetExposeTags = [...exposeTags].filter((tag) => {
|
|
1773
|
+
const tagElo = userTagElo[tag];
|
|
1774
|
+
return !tagElo || tagElo.count < DEFAULT_MIN_COUNT;
|
|
1775
|
+
});
|
|
1776
|
+
if (unmetExposeTags.length > 0) {
|
|
1777
|
+
unmetExposeTags.forEach((tag) => supportTags.add(tag));
|
|
1778
|
+
}
|
|
1779
|
+
if (resolution.blocked || unmetExposeTags.length > 0) {
|
|
1619
1780
|
blockedTargets.push(cardId);
|
|
1620
1781
|
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1621
1782
|
} else {
|
|
@@ -1645,7 +1806,8 @@ var init_prescribed = __esm({
|
|
|
1645
1806
|
supportCandidates,
|
|
1646
1807
|
supportTags: [...supportTags],
|
|
1647
1808
|
pressureMultiplier,
|
|
1648
|
-
supportMultiplier
|
|
1809
|
+
supportMultiplier,
|
|
1810
|
+
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
1649
1811
|
};
|
|
1650
1812
|
}
|
|
1651
1813
|
buildNextGroupState(runtime, prior) {
|
|
@@ -1653,6 +1815,7 @@ var init_prescribed = __esm({
|
|
|
1653
1815
|
const surfacedThisRun = false;
|
|
1654
1816
|
return {
|
|
1655
1817
|
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1818
|
+
pendingTargetIds: [...runtime.pendingTargets].sort(),
|
|
1656
1819
|
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1657
1820
|
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1658
1821
|
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
@@ -1677,7 +1840,7 @@ var init_prescribed = __esm({
|
|
|
1677
1840
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1678
1841
|
action: "generated",
|
|
1679
1842
|
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1680
|
-
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};multiplier=${runtime.pressureMultiplier.toFixed(2)}`
|
|
1843
|
+
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}`
|
|
1681
1844
|
}
|
|
1682
1845
|
]
|
|
1683
1846
|
});
|
|
@@ -1704,7 +1867,7 @@ var init_prescribed = __esm({
|
|
|
1704
1867
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1705
1868
|
action: "generated",
|
|
1706
1869
|
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1707
|
-
reason: `mode=support;group=${runtime.group.id};blocked=${runtime.blockedTargets.length};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)}`
|
|
1870
|
+
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}`
|
|
1708
1871
|
}
|
|
1709
1872
|
]
|
|
1710
1873
|
});
|
|
@@ -1734,35 +1897,43 @@ var init_prescribed = __esm({
|
|
|
1734
1897
|
return [...candidates];
|
|
1735
1898
|
}
|
|
1736
1899
|
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1737
|
-
if (!hierarchyWalkEnabled || targetTags.length === 0 || hierarchyConfigs.length === 0) {
|
|
1738
|
-
return {
|
|
1739
|
-
blocked: false,
|
|
1740
|
-
supportTags: []
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
1900
|
const supportTags = /* @__PURE__ */ new Set();
|
|
1744
1901
|
let blocked = false;
|
|
1745
1902
|
for (const targetTag of targetTags) {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
)
|
|
1752
|
-
|
|
1753
|
-
|
|
1903
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[targetTag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
1904
|
+
if (prereqSets.length === 0) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
const tagBlocked = prereqSets.some(
|
|
1908
|
+
(prereqs) => prereqs.some((pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
1909
|
+
);
|
|
1910
|
+
if (!tagBlocked) {
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
blocked = true;
|
|
1914
|
+
if (!hierarchyWalkEnabled) {
|
|
1915
|
+
for (const prereqs of prereqSets) {
|
|
1916
|
+
for (const prereq of prereqs) {
|
|
1917
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1918
|
+
supportTags.add(prereq.tag);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1754
1921
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
for (const prereqs of prereqSets) {
|
|
1925
|
+
for (const prereq of prereqs) {
|
|
1926
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1927
|
+
this.collectSupportTagsRecursive(
|
|
1928
|
+
prereq.tag,
|
|
1929
|
+
hierarchyConfigs,
|
|
1930
|
+
userTagElo,
|
|
1931
|
+
userGlobalElo,
|
|
1932
|
+
maxDepth,
|
|
1933
|
+
/* @__PURE__ */ new Set(),
|
|
1934
|
+
supportTags
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1766
1937
|
}
|
|
1767
1938
|
}
|
|
1768
1939
|
}
|
|
@@ -1917,7 +2088,7 @@ var init_srs = __esm({
|
|
|
1917
2088
|
]
|
|
1918
2089
|
};
|
|
1919
2090
|
});
|
|
1920
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
2091
|
+
return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
1921
2092
|
}
|
|
1922
2093
|
/**
|
|
1923
2094
|
* Compute backlog pressure based on number of due reviews.
|
|
@@ -3167,1741 +3338,187 @@ function runPeriodUpdate(input) {
|
|
|
3167
3338
|
`[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)}`
|
|
3168
3339
|
);
|
|
3169
3340
|
return {
|
|
3170
|
-
strategyId,
|
|
3171
|
-
previousWeight: currentWeight,
|
|
3172
|
-
newWeight,
|
|
3173
|
-
gradient,
|
|
3174
|
-
learningState,
|
|
3175
|
-
updated
|
|
3176
|
-
};
|
|
3177
|
-
}
|
|
3178
|
-
function getDefaultLearnableWeight() {
|
|
3179
|
-
return { ...DEFAULT_LEARNABLE_WEIGHT };
|
|
3180
|
-
}
|
|
3181
|
-
var MIN_OBSERVATIONS_FOR_UPDATE, LEARNING_RATE, MAX_WEIGHT_DELTA, MIN_R_SQUARED_FOR_GRADIENT, FLAT_GRADIENT_THRESHOLD, MAX_HISTORY_LENGTH;
|
|
3182
|
-
var init_learning = __esm({
|
|
3183
|
-
"src/core/orchestration/learning.ts"() {
|
|
3184
|
-
"use strict";
|
|
3185
|
-
init_contentNavigationStrategy();
|
|
3186
|
-
init_types_legacy();
|
|
3187
|
-
init_logger();
|
|
3188
|
-
MIN_OBSERVATIONS_FOR_UPDATE = 10;
|
|
3189
|
-
LEARNING_RATE = 0.1;
|
|
3190
|
-
MAX_WEIGHT_DELTA = 0.3;
|
|
3191
|
-
MIN_R_SQUARED_FOR_GRADIENT = 0.05;
|
|
3192
|
-
FLAT_GRADIENT_THRESHOLD = 0.02;
|
|
3193
|
-
MAX_HISTORY_LENGTH = 100;
|
|
3194
|
-
}
|
|
3195
|
-
});
|
|
3196
|
-
|
|
3197
|
-
// src/core/orchestration/signal.ts
|
|
3198
|
-
function computeOutcomeSignal(records, config = {}) {
|
|
3199
|
-
if (!records || records.length === 0) {
|
|
3200
|
-
return null;
|
|
3201
|
-
}
|
|
3202
|
-
const target = config.targetAccuracy ?? 0.85;
|
|
3203
|
-
const tolerance = config.tolerance ?? 0.05;
|
|
3204
|
-
let correct = 0;
|
|
3205
|
-
for (const r of records) {
|
|
3206
|
-
if (r.isCorrect) correct++;
|
|
3207
|
-
}
|
|
3208
|
-
const accuracy = correct / records.length;
|
|
3209
|
-
return scoreAccuracyInZone(accuracy, target, tolerance);
|
|
3210
|
-
}
|
|
3211
|
-
function scoreAccuracyInZone(accuracy, target, tolerance) {
|
|
3212
|
-
const dist = Math.abs(accuracy - target);
|
|
3213
|
-
if (dist <= tolerance) {
|
|
3214
|
-
return 1;
|
|
3215
|
-
}
|
|
3216
|
-
const excess = dist - tolerance;
|
|
3217
|
-
const slope = 2.5;
|
|
3218
|
-
return Math.max(0, 1 - excess * slope);
|
|
3219
|
-
}
|
|
3220
|
-
var init_signal = __esm({
|
|
3221
|
-
"src/core/orchestration/signal.ts"() {
|
|
3222
|
-
"use strict";
|
|
3223
|
-
}
|
|
3224
|
-
});
|
|
3225
|
-
|
|
3226
|
-
// src/core/orchestration/recording.ts
|
|
3227
|
-
async function recordUserOutcome(context, periodStart, periodEnd, records, activeStrategyIds, eloStart = 0, eloEnd = 0, config) {
|
|
3228
|
-
const { user, course, userId } = context;
|
|
3229
|
-
const courseId = course.getCourseID();
|
|
3230
|
-
const outcomeValue = computeOutcomeSignal(records, config);
|
|
3231
|
-
if (outcomeValue === null) {
|
|
3232
|
-
logger.debug(
|
|
3233
|
-
`[Orchestration] No outcome signal computed for ${userId} (insufficient data). Skipping record.`
|
|
3234
|
-
);
|
|
3235
|
-
return;
|
|
3236
|
-
}
|
|
3237
|
-
const deviations = {};
|
|
3238
|
-
for (const strategyId of activeStrategyIds) {
|
|
3239
|
-
deviations[strategyId] = context.getDeviation(strategyId);
|
|
3240
|
-
}
|
|
3241
|
-
const id = `USER_OUTCOME::${courseId}::${userId}::${periodEnd}`;
|
|
3242
|
-
const record = {
|
|
3243
|
-
_id: id,
|
|
3244
|
-
docType: "USER_OUTCOME" /* USER_OUTCOME */,
|
|
3245
|
-
courseId,
|
|
3246
|
-
userId,
|
|
3247
|
-
periodStart,
|
|
3248
|
-
periodEnd,
|
|
3249
|
-
outcomeValue,
|
|
3250
|
-
deviations,
|
|
3251
|
-
metadata: {
|
|
3252
|
-
sessionsCount: 1,
|
|
3253
|
-
// Assumes recording is triggered per-session currently
|
|
3254
|
-
cardsSeen: records.length,
|
|
3255
|
-
eloStart,
|
|
3256
|
-
eloEnd,
|
|
3257
|
-
signalType: "accuracy_in_zone"
|
|
3258
|
-
}
|
|
3259
|
-
};
|
|
3260
|
-
try {
|
|
3261
|
-
await user.putUserOutcome(record);
|
|
3262
|
-
logger.debug(
|
|
3263
|
-
`[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
|
|
3264
|
-
);
|
|
3265
|
-
} catch (e) {
|
|
3266
|
-
logger.error(`[Orchestration] Failed to record outcome: ${e}`);
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
var init_recording = __esm({
|
|
3270
|
-
"src/core/orchestration/recording.ts"() {
|
|
3271
|
-
"use strict";
|
|
3272
|
-
init_signal();
|
|
3273
|
-
init_types_legacy();
|
|
3274
|
-
init_logger();
|
|
3275
|
-
}
|
|
3276
|
-
});
|
|
3277
|
-
|
|
3278
|
-
// src/core/orchestration/index.ts
|
|
3279
|
-
function fnv1a(str) {
|
|
3280
|
-
let hash = 2166136261;
|
|
3281
|
-
for (let i = 0; i < str.length; i++) {
|
|
3282
|
-
hash ^= str.charCodeAt(i);
|
|
3283
|
-
hash = Math.imul(hash, 16777619);
|
|
3284
|
-
}
|
|
3285
|
-
return hash >>> 0;
|
|
3286
|
-
}
|
|
3287
|
-
function computeDeviation(userId, strategyId, salt) {
|
|
3288
|
-
const input = `${userId}:${strategyId}:${salt}`;
|
|
3289
|
-
const hash = fnv1a(input);
|
|
3290
|
-
const normalized = hash / 4294967296;
|
|
3291
|
-
return normalized * 2 - 1;
|
|
3292
|
-
}
|
|
3293
|
-
function computeSpread(confidence) {
|
|
3294
|
-
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
3295
|
-
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
3296
|
-
}
|
|
3297
|
-
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
3298
|
-
const deviation = computeDeviation(userId, strategyId, salt);
|
|
3299
|
-
const spread = computeSpread(learnable.confidence);
|
|
3300
|
-
const adjustment = deviation * spread * learnable.weight;
|
|
3301
|
-
const effective = learnable.weight + adjustment;
|
|
3302
|
-
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
3303
|
-
}
|
|
3304
|
-
async function createOrchestrationContext(user, course) {
|
|
3305
|
-
let courseConfig;
|
|
3306
|
-
try {
|
|
3307
|
-
courseConfig = await course.getCourseConfig();
|
|
3308
|
-
} catch (e) {
|
|
3309
|
-
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
3310
|
-
courseConfig = {
|
|
3311
|
-
name: "Unknown",
|
|
3312
|
-
description: "",
|
|
3313
|
-
public: false,
|
|
3314
|
-
deleted: false,
|
|
3315
|
-
creator: "",
|
|
3316
|
-
admins: [],
|
|
3317
|
-
moderators: [],
|
|
3318
|
-
dataShapes: [],
|
|
3319
|
-
questionTypes: [],
|
|
3320
|
-
orchestration: { salt: "default" }
|
|
3321
|
-
};
|
|
3322
|
-
}
|
|
3323
|
-
const userId = user.getUsername();
|
|
3324
|
-
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
3325
|
-
return {
|
|
3326
|
-
user,
|
|
3327
|
-
course,
|
|
3328
|
-
userId,
|
|
3329
|
-
courseConfig,
|
|
3330
|
-
getEffectiveWeight(strategyId, learnable) {
|
|
3331
|
-
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
3332
|
-
},
|
|
3333
|
-
getDeviation(strategyId) {
|
|
3334
|
-
return computeDeviation(userId, strategyId, salt);
|
|
3335
|
-
}
|
|
3336
|
-
};
|
|
3337
|
-
}
|
|
3338
|
-
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
3339
|
-
var init_orchestration = __esm({
|
|
3340
|
-
"src/core/orchestration/index.ts"() {
|
|
3341
|
-
"use strict";
|
|
3342
|
-
init_logger();
|
|
3343
|
-
init_gradient();
|
|
3344
|
-
init_learning();
|
|
3345
|
-
init_signal();
|
|
3346
|
-
init_recording();
|
|
3347
|
-
MIN_SPREAD = 0.1;
|
|
3348
|
-
MAX_SPREAD = 0.5;
|
|
3349
|
-
MIN_WEIGHT = 0.1;
|
|
3350
|
-
MAX_WEIGHT = 3;
|
|
3351
|
-
}
|
|
3352
|
-
});
|
|
3353
|
-
|
|
3354
|
-
// src/study/SpacedRepetition.ts
|
|
3355
|
-
var import_moment4, import_common8, duration;
|
|
3356
|
-
var init_SpacedRepetition = __esm({
|
|
3357
|
-
"src/study/SpacedRepetition.ts"() {
|
|
3358
|
-
"use strict";
|
|
3359
|
-
init_util();
|
|
3360
|
-
import_moment4 = __toESM(require("moment"), 1);
|
|
3361
|
-
import_common8 = require("@vue-skuilder/common");
|
|
3362
|
-
init_logger();
|
|
3363
|
-
duration = import_moment4.default.duration;
|
|
3364
|
-
}
|
|
3365
|
-
});
|
|
3366
|
-
|
|
3367
|
-
// src/study/services/SrsService.ts
|
|
3368
|
-
var import_moment5;
|
|
3369
|
-
var init_SrsService = __esm({
|
|
3370
|
-
"src/study/services/SrsService.ts"() {
|
|
3371
|
-
"use strict";
|
|
3372
|
-
import_moment5 = __toESM(require("moment"), 1);
|
|
3373
|
-
init_couch();
|
|
3374
|
-
init_SpacedRepetition();
|
|
3375
|
-
init_logger();
|
|
3376
|
-
}
|
|
3377
|
-
});
|
|
3378
|
-
|
|
3379
|
-
// src/study/services/EloService.ts
|
|
3380
|
-
var import_common9;
|
|
3381
|
-
var init_EloService = __esm({
|
|
3382
|
-
"src/study/services/EloService.ts"() {
|
|
3383
|
-
"use strict";
|
|
3384
|
-
import_common9 = require("@vue-skuilder/common");
|
|
3385
|
-
init_logger();
|
|
3386
|
-
}
|
|
3387
|
-
});
|
|
3388
|
-
|
|
3389
|
-
// src/study/services/ResponseProcessor.ts
|
|
3390
|
-
var import_common10;
|
|
3391
|
-
var init_ResponseProcessor = __esm({
|
|
3392
|
-
"src/study/services/ResponseProcessor.ts"() {
|
|
3393
|
-
"use strict";
|
|
3394
|
-
init_core();
|
|
3395
|
-
init_logger();
|
|
3396
|
-
import_common10 = require("@vue-skuilder/common");
|
|
3397
|
-
}
|
|
3398
|
-
});
|
|
3399
|
-
|
|
3400
|
-
// src/study/services/CardHydrationService.ts
|
|
3401
|
-
var import_common11;
|
|
3402
|
-
var init_CardHydrationService = __esm({
|
|
3403
|
-
"src/study/services/CardHydrationService.ts"() {
|
|
3404
|
-
"use strict";
|
|
3405
|
-
import_common11 = require("@vue-skuilder/common");
|
|
3406
|
-
init_logger();
|
|
3407
|
-
}
|
|
3408
|
-
});
|
|
3409
|
-
|
|
3410
|
-
// src/study/ItemQueue.ts
|
|
3411
|
-
var init_ItemQueue = __esm({
|
|
3412
|
-
"src/study/ItemQueue.ts"() {
|
|
3413
|
-
"use strict";
|
|
3414
|
-
}
|
|
3415
|
-
});
|
|
3416
|
-
|
|
3417
|
-
// src/util/packer/types.ts
|
|
3418
|
-
var init_types3 = __esm({
|
|
3419
|
-
"src/util/packer/types.ts"() {
|
|
3420
|
-
"use strict";
|
|
3421
|
-
}
|
|
3422
|
-
});
|
|
3423
|
-
|
|
3424
|
-
// src/util/packer/CouchDBToStaticPacker.ts
|
|
3425
|
-
var init_CouchDBToStaticPacker = __esm({
|
|
3426
|
-
"src/util/packer/CouchDBToStaticPacker.ts"() {
|
|
3427
|
-
"use strict";
|
|
3428
|
-
init_types_legacy();
|
|
3429
|
-
init_logger();
|
|
3430
|
-
}
|
|
3431
|
-
});
|
|
3432
|
-
|
|
3433
|
-
// src/util/packer/index.ts
|
|
3434
|
-
var init_packer = __esm({
|
|
3435
|
-
"src/util/packer/index.ts"() {
|
|
3436
|
-
"use strict";
|
|
3437
|
-
init_types3();
|
|
3438
|
-
init_CouchDBToStaticPacker();
|
|
3439
|
-
}
|
|
3440
|
-
});
|
|
3441
|
-
|
|
3442
|
-
// src/util/migrator/types.ts
|
|
3443
|
-
var DEFAULT_MIGRATION_OPTIONS;
|
|
3444
|
-
var init_types4 = __esm({
|
|
3445
|
-
"src/util/migrator/types.ts"() {
|
|
3446
|
-
"use strict";
|
|
3447
|
-
DEFAULT_MIGRATION_OPTIONS = {
|
|
3448
|
-
chunkBatchSize: 100,
|
|
3449
|
-
validateRoundTrip: false,
|
|
3450
|
-
cleanupOnFailure: true,
|
|
3451
|
-
timeout: 3e5
|
|
3452
|
-
// 5 minutes
|
|
3453
|
-
};
|
|
3454
|
-
}
|
|
3455
|
-
});
|
|
3456
|
-
|
|
3457
|
-
// src/util/migrator/FileSystemAdapter.ts
|
|
3458
|
-
var FileSystemError;
|
|
3459
|
-
var init_FileSystemAdapter = __esm({
|
|
3460
|
-
"src/util/migrator/FileSystemAdapter.ts"() {
|
|
3461
|
-
"use strict";
|
|
3462
|
-
FileSystemError = class extends Error {
|
|
3463
|
-
constructor(message, operation, filePath, cause) {
|
|
3464
|
-
super(message);
|
|
3465
|
-
this.operation = operation;
|
|
3466
|
-
this.filePath = filePath;
|
|
3467
|
-
this.cause = cause;
|
|
3468
|
-
this.name = "FileSystemError";
|
|
3469
|
-
}
|
|
3470
|
-
};
|
|
3471
|
-
}
|
|
3472
|
-
});
|
|
3473
|
-
|
|
3474
|
-
// src/util/migrator/validation.ts
|
|
3475
|
-
async function validateStaticCourse(staticPath, fs) {
|
|
3476
|
-
const validation = {
|
|
3477
|
-
valid: true,
|
|
3478
|
-
manifestExists: false,
|
|
3479
|
-
chunksExist: false,
|
|
3480
|
-
attachmentsExist: false,
|
|
3481
|
-
errors: [],
|
|
3482
|
-
warnings: []
|
|
3483
|
-
};
|
|
3484
|
-
try {
|
|
3485
|
-
if (fs) {
|
|
3486
|
-
const stats = await fs.stat(staticPath);
|
|
3487
|
-
if (!stats.isDirectory()) {
|
|
3488
|
-
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3489
|
-
validation.valid = false;
|
|
3490
|
-
return validation;
|
|
3491
|
-
}
|
|
3492
|
-
} else if (!nodeFS) {
|
|
3493
|
-
validation.errors.push("File system access not available - validation skipped");
|
|
3494
|
-
validation.valid = false;
|
|
3495
|
-
return validation;
|
|
3496
|
-
} else {
|
|
3497
|
-
const stats = await nodeFS.promises.stat(staticPath);
|
|
3498
|
-
if (!stats.isDirectory()) {
|
|
3499
|
-
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3500
|
-
validation.valid = false;
|
|
3501
|
-
return validation;
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
let manifestPath = `${staticPath}/manifest.json`;
|
|
3505
|
-
try {
|
|
3506
|
-
if (fs) {
|
|
3507
|
-
manifestPath = fs.joinPath(staticPath, "manifest.json");
|
|
3508
|
-
if (await fs.exists(manifestPath)) {
|
|
3509
|
-
validation.manifestExists = true;
|
|
3510
|
-
const manifestContent = await fs.readFile(manifestPath);
|
|
3511
|
-
const manifest = JSON.parse(manifestContent);
|
|
3512
|
-
validation.courseId = manifest.courseId;
|
|
3513
|
-
validation.courseName = manifest.courseName;
|
|
3514
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3515
|
-
validation.errors.push("Invalid manifest structure");
|
|
3516
|
-
validation.valid = false;
|
|
3517
|
-
}
|
|
3518
|
-
} else {
|
|
3519
|
-
validation.errors.push(`Manifest not found: ${manifestPath}`);
|
|
3520
|
-
validation.valid = false;
|
|
3521
|
-
}
|
|
3522
|
-
} else {
|
|
3523
|
-
manifestPath = `${staticPath}/manifest.json`;
|
|
3524
|
-
await nodeFS.promises.access(manifestPath);
|
|
3525
|
-
validation.manifestExists = true;
|
|
3526
|
-
const manifestContent = await nodeFS.promises.readFile(manifestPath, "utf8");
|
|
3527
|
-
const manifest = JSON.parse(manifestContent);
|
|
3528
|
-
validation.courseId = manifest.courseId;
|
|
3529
|
-
validation.courseName = manifest.courseName;
|
|
3530
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3531
|
-
validation.errors.push("Invalid manifest structure");
|
|
3532
|
-
validation.valid = false;
|
|
3533
|
-
}
|
|
3534
|
-
}
|
|
3535
|
-
} catch (error) {
|
|
3536
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Manifest not found or invalid: ${manifestPath}`;
|
|
3537
|
-
validation.errors.push(errorMessage);
|
|
3538
|
-
validation.valid = false;
|
|
3539
|
-
}
|
|
3540
|
-
let chunksPath = `${staticPath}/chunks`;
|
|
3541
|
-
try {
|
|
3542
|
-
if (fs) {
|
|
3543
|
-
chunksPath = fs.joinPath(staticPath, "chunks");
|
|
3544
|
-
if (await fs.exists(chunksPath)) {
|
|
3545
|
-
const chunksStats = await fs.stat(chunksPath);
|
|
3546
|
-
if (chunksStats.isDirectory()) {
|
|
3547
|
-
validation.chunksExist = true;
|
|
3548
|
-
} else {
|
|
3549
|
-
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3550
|
-
validation.valid = false;
|
|
3551
|
-
}
|
|
3552
|
-
} else {
|
|
3553
|
-
validation.errors.push(`Chunks directory not found: ${chunksPath}`);
|
|
3554
|
-
validation.valid = false;
|
|
3555
|
-
}
|
|
3556
|
-
} else {
|
|
3557
|
-
chunksPath = `${staticPath}/chunks`;
|
|
3558
|
-
const chunksStats = await nodeFS.promises.stat(chunksPath);
|
|
3559
|
-
if (chunksStats.isDirectory()) {
|
|
3560
|
-
validation.chunksExist = true;
|
|
3561
|
-
} else {
|
|
3562
|
-
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3563
|
-
validation.valid = false;
|
|
3564
|
-
}
|
|
3565
|
-
}
|
|
3566
|
-
} catch (error) {
|
|
3567
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Chunks directory not found: ${chunksPath}`;
|
|
3568
|
-
validation.errors.push(errorMessage);
|
|
3569
|
-
validation.valid = false;
|
|
3570
|
-
}
|
|
3571
|
-
let attachmentsPath;
|
|
3572
|
-
try {
|
|
3573
|
-
if (fs) {
|
|
3574
|
-
attachmentsPath = fs.joinPath(staticPath, "attachments");
|
|
3575
|
-
if (await fs.exists(attachmentsPath)) {
|
|
3576
|
-
const attachmentsStats = await fs.stat(attachmentsPath);
|
|
3577
|
-
if (attachmentsStats.isDirectory()) {
|
|
3578
|
-
validation.attachmentsExist = true;
|
|
3579
|
-
}
|
|
3580
|
-
} else {
|
|
3581
|
-
validation.warnings.push(
|
|
3582
|
-
`Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
|
|
3583
|
-
);
|
|
3584
|
-
}
|
|
3585
|
-
} else {
|
|
3586
|
-
attachmentsPath = `${staticPath}/attachments`;
|
|
3587
|
-
const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
|
|
3588
|
-
if (attachmentsStats.isDirectory()) {
|
|
3589
|
-
validation.attachmentsExist = true;
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
} catch (error) {
|
|
3593
|
-
attachmentsPath = attachmentsPath || `${staticPath}/attachments`;
|
|
3594
|
-
const warningMessage = error instanceof FileSystemError ? error.message : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
|
|
3595
|
-
validation.warnings.push(warningMessage);
|
|
3596
|
-
}
|
|
3597
|
-
} catch (error) {
|
|
3598
|
-
validation.errors.push(
|
|
3599
|
-
`Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
|
|
3600
|
-
);
|
|
3601
|
-
validation.valid = false;
|
|
3602
|
-
}
|
|
3603
|
-
return validation;
|
|
3604
|
-
}
|
|
3605
|
-
async function validateMigration(targetDB, expectedCounts, manifest) {
|
|
3606
|
-
const validation = {
|
|
3607
|
-
valid: true,
|
|
3608
|
-
documentCountMatch: false,
|
|
3609
|
-
attachmentIntegrity: false,
|
|
3610
|
-
viewFunctionality: false,
|
|
3611
|
-
issues: []
|
|
3612
|
-
};
|
|
3613
|
-
try {
|
|
3614
|
-
logger.info("Starting migration validation...");
|
|
3615
|
-
const actualCounts = await getActualDocumentCounts(targetDB);
|
|
3616
|
-
validation.documentCountMatch = compareDocumentCounts(
|
|
3617
|
-
expectedCounts,
|
|
3618
|
-
actualCounts,
|
|
3619
|
-
validation.issues
|
|
3620
|
-
);
|
|
3621
|
-
await validateCourseConfig(targetDB, manifest, validation.issues);
|
|
3622
|
-
validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
|
|
3623
|
-
validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
|
|
3624
|
-
validation.valid = validation.documentCountMatch && validation.viewFunctionality && validation.attachmentIntegrity;
|
|
3625
|
-
logger.info(`Migration validation completed. Valid: ${validation.valid}`);
|
|
3626
|
-
if (validation.issues.length > 0) {
|
|
3627
|
-
logger.info(`Validation issues: ${validation.issues.length}`);
|
|
3628
|
-
validation.issues.forEach((issue) => {
|
|
3629
|
-
if (issue.type === "error") {
|
|
3630
|
-
logger.error(`${issue.category}: ${issue.message}`);
|
|
3631
|
-
} else {
|
|
3632
|
-
logger.warn(`${issue.category}: ${issue.message}`);
|
|
3633
|
-
}
|
|
3634
|
-
});
|
|
3635
|
-
}
|
|
3636
|
-
} catch (error) {
|
|
3637
|
-
validation.valid = false;
|
|
3638
|
-
validation.issues.push({
|
|
3639
|
-
type: "error",
|
|
3640
|
-
category: "metadata",
|
|
3641
|
-
message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3642
|
-
});
|
|
3643
|
-
}
|
|
3644
|
-
return validation;
|
|
3645
|
-
}
|
|
3646
|
-
async function getActualDocumentCounts(db) {
|
|
3647
|
-
const counts = {};
|
|
3648
|
-
try {
|
|
3649
|
-
const allDocs = await db.allDocs({ include_docs: true });
|
|
3650
|
-
for (const row of allDocs.rows) {
|
|
3651
|
-
if (row.id.startsWith("_design/")) {
|
|
3652
|
-
counts["_design"] = (counts["_design"] || 0) + 1;
|
|
3653
|
-
continue;
|
|
3654
|
-
}
|
|
3655
|
-
const doc = row.doc;
|
|
3656
|
-
if (doc && doc.docType) {
|
|
3657
|
-
counts[doc.docType] = (counts[doc.docType] || 0) + 1;
|
|
3658
|
-
} else {
|
|
3659
|
-
counts["unknown"] = (counts["unknown"] || 0) + 1;
|
|
3660
|
-
}
|
|
3661
|
-
}
|
|
3662
|
-
} catch (error) {
|
|
3663
|
-
logger.error("Failed to get actual document counts:", error);
|
|
3664
|
-
}
|
|
3665
|
-
return counts;
|
|
3666
|
-
}
|
|
3667
|
-
function compareDocumentCounts(expected, actual, issues) {
|
|
3668
|
-
let countsMatch = true;
|
|
3669
|
-
for (const [docType, expectedCount] of Object.entries(expected)) {
|
|
3670
|
-
const actualCount = actual[docType] || 0;
|
|
3671
|
-
if (actualCount !== expectedCount) {
|
|
3672
|
-
countsMatch = false;
|
|
3673
|
-
issues.push({
|
|
3674
|
-
type: "error",
|
|
3675
|
-
category: "documents",
|
|
3676
|
-
message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`
|
|
3677
|
-
});
|
|
3678
|
-
}
|
|
3679
|
-
}
|
|
3680
|
-
for (const [docType, actualCount] of Object.entries(actual)) {
|
|
3681
|
-
if (!expected[docType] && docType !== "_design") {
|
|
3682
|
-
issues.push({
|
|
3683
|
-
type: "warning",
|
|
3684
|
-
category: "documents",
|
|
3685
|
-
message: `Unexpected document type found: ${docType} (${actualCount} documents)`
|
|
3686
|
-
});
|
|
3687
|
-
}
|
|
3688
|
-
}
|
|
3689
|
-
return countsMatch;
|
|
3690
|
-
}
|
|
3691
|
-
async function validateCourseConfig(db, manifest, issues) {
|
|
3692
|
-
try {
|
|
3693
|
-
const courseConfig = await db.get("CourseConfig");
|
|
3694
|
-
if (!courseConfig) {
|
|
3695
|
-
issues.push({
|
|
3696
|
-
type: "error",
|
|
3697
|
-
category: "course_config",
|
|
3698
|
-
message: "CourseConfig document not found after migration"
|
|
3699
|
-
});
|
|
3700
|
-
return;
|
|
3701
|
-
}
|
|
3702
|
-
if (!courseConfig.courseID) {
|
|
3703
|
-
issues.push({
|
|
3704
|
-
type: "warning",
|
|
3705
|
-
category: "course_config",
|
|
3706
|
-
message: "CourseConfig document missing courseID field"
|
|
3707
|
-
});
|
|
3708
|
-
}
|
|
3709
|
-
if (courseConfig.courseID !== manifest.courseId) {
|
|
3710
|
-
issues.push({
|
|
3711
|
-
type: "warning",
|
|
3712
|
-
category: "course_config",
|
|
3713
|
-
message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${courseConfig.courseID}`
|
|
3714
|
-
});
|
|
3715
|
-
}
|
|
3716
|
-
logger.debug("CourseConfig document validation passed");
|
|
3717
|
-
} catch (error) {
|
|
3718
|
-
if (error.status === 404) {
|
|
3719
|
-
issues.push({
|
|
3720
|
-
type: "error",
|
|
3721
|
-
category: "course_config",
|
|
3722
|
-
message: "CourseConfig document not found in database"
|
|
3723
|
-
});
|
|
3724
|
-
} else {
|
|
3725
|
-
issues.push({
|
|
3726
|
-
type: "error",
|
|
3727
|
-
category: "course_config",
|
|
3728
|
-
message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`
|
|
3729
|
-
});
|
|
3730
|
-
}
|
|
3731
|
-
}
|
|
3732
|
-
}
|
|
3733
|
-
async function validateViews(db, manifest, issues) {
|
|
3734
|
-
let viewsValid = true;
|
|
3735
|
-
try {
|
|
3736
|
-
for (const designDoc of manifest.designDocs) {
|
|
3737
|
-
try {
|
|
3738
|
-
const doc = await db.get(designDoc._id);
|
|
3739
|
-
if (!doc) {
|
|
3740
|
-
viewsValid = false;
|
|
3741
|
-
issues.push({
|
|
3742
|
-
type: "error",
|
|
3743
|
-
category: "views",
|
|
3744
|
-
message: `Design document not found: ${designDoc._id}`
|
|
3745
|
-
});
|
|
3746
|
-
continue;
|
|
3747
|
-
}
|
|
3748
|
-
for (const viewName of Object.keys(designDoc.views)) {
|
|
3749
|
-
try {
|
|
3750
|
-
const viewPath = `${designDoc._id}/${viewName}`;
|
|
3751
|
-
await db.query(viewPath, { limit: 1 });
|
|
3752
|
-
} catch (viewError) {
|
|
3753
|
-
viewsValid = false;
|
|
3754
|
-
issues.push({
|
|
3755
|
-
type: "error",
|
|
3756
|
-
category: "views",
|
|
3757
|
-
message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`
|
|
3758
|
-
});
|
|
3759
|
-
}
|
|
3760
|
-
}
|
|
3761
|
-
} catch (error) {
|
|
3762
|
-
viewsValid = false;
|
|
3763
|
-
issues.push({
|
|
3764
|
-
type: "error",
|
|
3765
|
-
category: "views",
|
|
3766
|
-
message: `Failed to validate design document ${designDoc._id}: ${error}`
|
|
3767
|
-
});
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
|
-
} catch (error) {
|
|
3771
|
-
viewsValid = false;
|
|
3772
|
-
issues.push({
|
|
3773
|
-
type: "error",
|
|
3774
|
-
category: "views",
|
|
3775
|
-
message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3776
|
-
});
|
|
3777
|
-
}
|
|
3778
|
-
return viewsValid;
|
|
3779
|
-
}
|
|
3780
|
-
async function validateAttachmentIntegrity(db, issues) {
|
|
3781
|
-
let attachmentsValid = true;
|
|
3782
|
-
try {
|
|
3783
|
-
const allDocs = await db.allDocs({
|
|
3784
|
-
include_docs: true,
|
|
3785
|
-
limit: 10
|
|
3786
|
-
// Sample first 10 documents for performance
|
|
3787
|
-
});
|
|
3788
|
-
let attachmentCount = 0;
|
|
3789
|
-
let validAttachments = 0;
|
|
3790
|
-
for (const row of allDocs.rows) {
|
|
3791
|
-
const doc = row.doc;
|
|
3792
|
-
if (doc && doc._attachments) {
|
|
3793
|
-
for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3794
|
-
attachmentCount++;
|
|
3795
|
-
try {
|
|
3796
|
-
const attachment = await db.getAttachment(doc._id, attachmentName);
|
|
3797
|
-
if (attachment) {
|
|
3798
|
-
validAttachments++;
|
|
3799
|
-
}
|
|
3800
|
-
} catch (attachmentError) {
|
|
3801
|
-
attachmentsValid = false;
|
|
3802
|
-
issues.push({
|
|
3803
|
-
type: "error",
|
|
3804
|
-
category: "attachments",
|
|
3805
|
-
message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`
|
|
3806
|
-
});
|
|
3807
|
-
}
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
}
|
|
3811
|
-
if (attachmentCount === 0) {
|
|
3812
|
-
issues.push({
|
|
3813
|
-
type: "warning",
|
|
3814
|
-
category: "attachments",
|
|
3815
|
-
message: "No attachments found in sampled documents"
|
|
3816
|
-
});
|
|
3817
|
-
} else {
|
|
3818
|
-
logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
|
|
3819
|
-
}
|
|
3820
|
-
} catch (error) {
|
|
3821
|
-
attachmentsValid = false;
|
|
3822
|
-
issues.push({
|
|
3823
|
-
type: "error",
|
|
3824
|
-
category: "attachments",
|
|
3825
|
-
message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3826
|
-
});
|
|
3827
|
-
}
|
|
3828
|
-
return attachmentsValid;
|
|
3829
|
-
}
|
|
3830
|
-
var nodeFS;
|
|
3831
|
-
var init_validation = __esm({
|
|
3832
|
-
"src/util/migrator/validation.ts"() {
|
|
3833
|
-
"use strict";
|
|
3834
|
-
init_logger();
|
|
3835
|
-
init_FileSystemAdapter();
|
|
3836
|
-
nodeFS = null;
|
|
3837
|
-
try {
|
|
3838
|
-
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3839
|
-
nodeFS = eval("require")("fs");
|
|
3840
|
-
nodeFS.promises = nodeFS.promises || eval("require")("fs").promises;
|
|
3841
|
-
}
|
|
3842
|
-
} catch {
|
|
3843
|
-
}
|
|
3844
|
-
}
|
|
3845
|
-
});
|
|
3846
|
-
|
|
3847
|
-
// src/util/migrator/StaticToCouchDBMigrator.ts
|
|
3848
|
-
var nodeFS2, nodePath, StaticToCouchDBMigrator;
|
|
3849
|
-
var init_StaticToCouchDBMigrator = __esm({
|
|
3850
|
-
"src/util/migrator/StaticToCouchDBMigrator.ts"() {
|
|
3851
|
-
"use strict";
|
|
3852
|
-
init_logger();
|
|
3853
|
-
init_types4();
|
|
3854
|
-
init_validation();
|
|
3855
|
-
init_FileSystemAdapter();
|
|
3856
|
-
nodeFS2 = null;
|
|
3857
|
-
nodePath = null;
|
|
3858
|
-
try {
|
|
3859
|
-
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3860
|
-
nodeFS2 = eval("require")("fs");
|
|
3861
|
-
nodePath = eval("require")("path");
|
|
3862
|
-
nodeFS2.promises = nodeFS2.promises || eval("require")("fs").promises;
|
|
3863
|
-
}
|
|
3864
|
-
} catch {
|
|
3865
|
-
}
|
|
3866
|
-
StaticToCouchDBMigrator = class {
|
|
3867
|
-
options;
|
|
3868
|
-
progressCallback;
|
|
3869
|
-
fs;
|
|
3870
|
-
constructor(options = {}, fileSystemAdapter) {
|
|
3871
|
-
this.options = {
|
|
3872
|
-
...DEFAULT_MIGRATION_OPTIONS,
|
|
3873
|
-
...options
|
|
3874
|
-
};
|
|
3875
|
-
this.fs = fileSystemAdapter;
|
|
3876
|
-
}
|
|
3877
|
-
/**
|
|
3878
|
-
* Set a progress callback to receive updates during migration
|
|
3879
|
-
*/
|
|
3880
|
-
setProgressCallback(callback) {
|
|
3881
|
-
this.progressCallback = callback;
|
|
3882
|
-
}
|
|
3883
|
-
/**
|
|
3884
|
-
* Migrate a static course to CouchDB
|
|
3885
|
-
*/
|
|
3886
|
-
async migrateCourse(staticPath, targetDB) {
|
|
3887
|
-
const startTime = Date.now();
|
|
3888
|
-
const result = {
|
|
3889
|
-
success: false,
|
|
3890
|
-
documentsRestored: 0,
|
|
3891
|
-
attachmentsRestored: 0,
|
|
3892
|
-
designDocsRestored: 0,
|
|
3893
|
-
courseConfigRestored: 0,
|
|
3894
|
-
errors: [],
|
|
3895
|
-
warnings: [],
|
|
3896
|
-
migrationTime: 0
|
|
3897
|
-
};
|
|
3898
|
-
try {
|
|
3899
|
-
logger.info(`Starting migration from ${staticPath} to CouchDB`);
|
|
3900
|
-
this.reportProgress("manifest", 0, 1, "Validating static course...");
|
|
3901
|
-
const validation = await validateStaticCourse(staticPath, this.fs);
|
|
3902
|
-
if (!validation.valid) {
|
|
3903
|
-
result.errors.push(...validation.errors);
|
|
3904
|
-
throw new Error(`Static course validation failed: ${validation.errors.join(", ")}`);
|
|
3905
|
-
}
|
|
3906
|
-
result.warnings.push(...validation.warnings);
|
|
3907
|
-
this.reportProgress("manifest", 1, 1, "Loading course manifest...");
|
|
3908
|
-
const manifest = await this.loadManifest(staticPath);
|
|
3909
|
-
logger.info(`Loaded manifest for course: ${manifest.courseId} (${manifest.courseName})`);
|
|
3910
|
-
this.reportProgress(
|
|
3911
|
-
"design_docs",
|
|
3912
|
-
0,
|
|
3913
|
-
manifest.designDocs.length,
|
|
3914
|
-
"Restoring design documents..."
|
|
3915
|
-
);
|
|
3916
|
-
const designDocResults = await this.restoreDesignDocuments(manifest.designDocs, targetDB);
|
|
3917
|
-
result.designDocsRestored = designDocResults.restored;
|
|
3918
|
-
result.errors.push(...designDocResults.errors);
|
|
3919
|
-
result.warnings.push(...designDocResults.warnings);
|
|
3920
|
-
this.reportProgress("course_config", 0, 1, "Restoring CourseConfig document...");
|
|
3921
|
-
const courseConfigResults = await this.restoreCourseConfig(manifest, targetDB);
|
|
3922
|
-
result.courseConfigRestored = courseConfigResults.restored;
|
|
3923
|
-
result.errors.push(...courseConfigResults.errors);
|
|
3924
|
-
result.warnings.push(...courseConfigResults.warnings);
|
|
3925
|
-
this.reportProgress("course_config", 1, 1, "CourseConfig document restored");
|
|
3926
|
-
const expectedCounts = this.calculateExpectedCounts(manifest);
|
|
3927
|
-
this.reportProgress(
|
|
3928
|
-
"documents",
|
|
3929
|
-
0,
|
|
3930
|
-
manifest.documentCount,
|
|
3931
|
-
"Aggregating documents from chunks..."
|
|
3932
|
-
);
|
|
3933
|
-
const documents = await this.aggregateDocuments(staticPath, manifest);
|
|
3934
|
-
const filteredDocuments = documents.filter((doc) => doc._id !== "CourseConfig");
|
|
3935
|
-
if (documents.length !== filteredDocuments.length) {
|
|
3936
|
-
result.warnings.push(
|
|
3937
|
-
`Filtered out ${documents.length - filteredDocuments.length} CourseConfig document(s) from chunks to prevent conflicts`
|
|
3938
|
-
);
|
|
3939
|
-
}
|
|
3940
|
-
this.reportProgress(
|
|
3941
|
-
"documents",
|
|
3942
|
-
filteredDocuments.length,
|
|
3943
|
-
manifest.documentCount,
|
|
3944
|
-
"Uploading documents to CouchDB..."
|
|
3945
|
-
);
|
|
3946
|
-
const docResults = await this.uploadDocuments(filteredDocuments, targetDB);
|
|
3947
|
-
result.documentsRestored = docResults.restored;
|
|
3948
|
-
result.errors.push(...docResults.errors);
|
|
3949
|
-
result.warnings.push(...docResults.warnings);
|
|
3950
|
-
const docsWithAttachments = documents.filter(
|
|
3951
|
-
(doc) => doc._attachments && Object.keys(doc._attachments).length > 0
|
|
3952
|
-
);
|
|
3953
|
-
this.reportProgress("attachments", 0, docsWithAttachments.length, "Uploading attachments...");
|
|
3954
|
-
const attachmentResults = await this.uploadAttachments(
|
|
3955
|
-
staticPath,
|
|
3956
|
-
docsWithAttachments,
|
|
3957
|
-
targetDB
|
|
3958
|
-
);
|
|
3959
|
-
result.attachmentsRestored = attachmentResults.restored;
|
|
3960
|
-
result.errors.push(...attachmentResults.errors);
|
|
3961
|
-
result.warnings.push(...attachmentResults.warnings);
|
|
3962
|
-
if (this.options.validateRoundTrip) {
|
|
3963
|
-
this.reportProgress("validation", 0, 1, "Validating migration...");
|
|
3964
|
-
const validationResult = await validateMigration(targetDB, expectedCounts, manifest);
|
|
3965
|
-
if (!validationResult.valid) {
|
|
3966
|
-
result.warnings.push("Migration validation found issues");
|
|
3967
|
-
validationResult.issues.forEach((issue) => {
|
|
3968
|
-
if (issue.type === "error") {
|
|
3969
|
-
result.errors.push(`Validation: ${issue.message}`);
|
|
3970
|
-
} else {
|
|
3971
|
-
result.warnings.push(`Validation: ${issue.message}`);
|
|
3972
|
-
}
|
|
3973
|
-
});
|
|
3974
|
-
}
|
|
3975
|
-
this.reportProgress("validation", 1, 1, "Migration validation completed");
|
|
3976
|
-
}
|
|
3977
|
-
result.success = result.errors.length === 0;
|
|
3978
|
-
result.migrationTime = Date.now() - startTime;
|
|
3979
|
-
logger.info(`Migration completed in ${result.migrationTime}ms`);
|
|
3980
|
-
logger.info(`Documents restored: ${result.documentsRestored}`);
|
|
3981
|
-
logger.info(`Attachments restored: ${result.attachmentsRestored}`);
|
|
3982
|
-
logger.info(`Design docs restored: ${result.designDocsRestored}`);
|
|
3983
|
-
logger.info(`CourseConfig restored: ${result.courseConfigRestored}`);
|
|
3984
|
-
if (result.errors.length > 0) {
|
|
3985
|
-
logger.error(`Migration completed with ${result.errors.length} errors`);
|
|
3986
|
-
}
|
|
3987
|
-
if (result.warnings.length > 0) {
|
|
3988
|
-
logger.warn(`Migration completed with ${result.warnings.length} warnings`);
|
|
3989
|
-
}
|
|
3990
|
-
} catch (error) {
|
|
3991
|
-
result.success = false;
|
|
3992
|
-
result.migrationTime = Date.now() - startTime;
|
|
3993
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3994
|
-
result.errors.push(`Migration failed: ${errorMessage}`);
|
|
3995
|
-
logger.error("Migration failed:", error);
|
|
3996
|
-
if (this.options.cleanupOnFailure) {
|
|
3997
|
-
try {
|
|
3998
|
-
await this.cleanupFailedMigration(targetDB);
|
|
3999
|
-
} catch (cleanupError) {
|
|
4000
|
-
logger.error("Failed to cleanup after migration failure:", cleanupError);
|
|
4001
|
-
result.warnings.push("Failed to cleanup after migration failure");
|
|
4002
|
-
}
|
|
4003
|
-
}
|
|
4004
|
-
}
|
|
4005
|
-
return result;
|
|
4006
|
-
}
|
|
4007
|
-
/**
|
|
4008
|
-
* Load and parse the manifest file
|
|
4009
|
-
*/
|
|
4010
|
-
async loadManifest(staticPath) {
|
|
4011
|
-
try {
|
|
4012
|
-
let manifestContent;
|
|
4013
|
-
let manifestPath;
|
|
4014
|
-
if (this.fs) {
|
|
4015
|
-
manifestPath = this.fs.joinPath(staticPath, "manifest.json");
|
|
4016
|
-
manifestContent = await this.fs.readFile(manifestPath);
|
|
4017
|
-
} else {
|
|
4018
|
-
manifestPath = nodeFS2 && nodePath ? nodePath.join(staticPath, "manifest.json") : `${staticPath}/manifest.json`;
|
|
4019
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4020
|
-
manifestContent = await nodeFS2.promises.readFile(manifestPath, "utf8");
|
|
4021
|
-
} else {
|
|
4022
|
-
const response = await fetch(manifestPath);
|
|
4023
|
-
if (!response.ok) {
|
|
4024
|
-
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
4025
|
-
}
|
|
4026
|
-
manifestContent = await response.text();
|
|
4027
|
-
}
|
|
4028
|
-
}
|
|
4029
|
-
const manifest = JSON.parse(manifestContent);
|
|
4030
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks) {
|
|
4031
|
-
throw new Error("Invalid manifest structure");
|
|
4032
|
-
}
|
|
4033
|
-
return manifest;
|
|
4034
|
-
} catch (error) {
|
|
4035
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`;
|
|
4036
|
-
throw new Error(errorMessage);
|
|
4037
|
-
}
|
|
4038
|
-
}
|
|
4039
|
-
/**
|
|
4040
|
-
* Restore design documents to CouchDB
|
|
4041
|
-
*/
|
|
4042
|
-
async restoreDesignDocuments(designDocs, db) {
|
|
4043
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
4044
|
-
for (let i = 0; i < designDocs.length; i++) {
|
|
4045
|
-
const designDoc = designDocs[i];
|
|
4046
|
-
this.reportProgress("design_docs", i, designDocs.length, `Restoring ${designDoc._id}...`);
|
|
4047
|
-
try {
|
|
4048
|
-
let existingDoc;
|
|
4049
|
-
try {
|
|
4050
|
-
existingDoc = await db.get(designDoc._id);
|
|
4051
|
-
} catch {
|
|
4052
|
-
}
|
|
4053
|
-
const docToInsert = {
|
|
4054
|
-
_id: designDoc._id,
|
|
4055
|
-
views: designDoc.views
|
|
4056
|
-
};
|
|
4057
|
-
if (existingDoc) {
|
|
4058
|
-
docToInsert._rev = existingDoc._rev;
|
|
4059
|
-
logger.debug(`Updating existing design document: ${designDoc._id}`);
|
|
4060
|
-
} else {
|
|
4061
|
-
logger.debug(`Creating new design document: ${designDoc._id}`);
|
|
4062
|
-
}
|
|
4063
|
-
await db.put(docToInsert);
|
|
4064
|
-
result.restored++;
|
|
4065
|
-
} catch (error) {
|
|
4066
|
-
const errorMessage = `Failed to restore design document ${designDoc._id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
4067
|
-
result.errors.push(errorMessage);
|
|
4068
|
-
logger.error(errorMessage);
|
|
4069
|
-
}
|
|
4070
|
-
}
|
|
4071
|
-
this.reportProgress(
|
|
4072
|
-
"design_docs",
|
|
4073
|
-
designDocs.length,
|
|
4074
|
-
designDocs.length,
|
|
4075
|
-
`Restored ${result.restored} design documents`
|
|
4076
|
-
);
|
|
4077
|
-
return result;
|
|
4078
|
-
}
|
|
4079
|
-
/**
|
|
4080
|
-
* Aggregate documents from all chunks
|
|
4081
|
-
*/
|
|
4082
|
-
async aggregateDocuments(staticPath, manifest) {
|
|
4083
|
-
const allDocuments = [];
|
|
4084
|
-
const documentMap = /* @__PURE__ */ new Map();
|
|
4085
|
-
for (let i = 0; i < manifest.chunks.length; i++) {
|
|
4086
|
-
const chunk = manifest.chunks[i];
|
|
4087
|
-
this.reportProgress(
|
|
4088
|
-
"documents",
|
|
4089
|
-
allDocuments.length,
|
|
4090
|
-
manifest.documentCount,
|
|
4091
|
-
`Loading chunk ${chunk.id}...`
|
|
4092
|
-
);
|
|
4093
|
-
try {
|
|
4094
|
-
const documents = await this.loadChunk(staticPath, chunk);
|
|
4095
|
-
for (const doc of documents) {
|
|
4096
|
-
if (!doc._id) {
|
|
4097
|
-
logger.warn(`Document without _id found in chunk ${chunk.id}, skipping`);
|
|
4098
|
-
continue;
|
|
4099
|
-
}
|
|
4100
|
-
if (documentMap.has(doc._id)) {
|
|
4101
|
-
logger.warn(`Duplicate document ID found: ${doc._id}, using latest version`);
|
|
4102
|
-
}
|
|
4103
|
-
documentMap.set(doc._id, doc);
|
|
4104
|
-
}
|
|
4105
|
-
} catch (error) {
|
|
4106
|
-
throw new Error(
|
|
4107
|
-
`Failed to load chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
4108
|
-
);
|
|
4109
|
-
}
|
|
4110
|
-
}
|
|
4111
|
-
allDocuments.push(...documentMap.values());
|
|
4112
|
-
logger.info(
|
|
4113
|
-
`Aggregated ${allDocuments.length} unique documents from ${manifest.chunks.length} chunks`
|
|
4114
|
-
);
|
|
4115
|
-
return allDocuments;
|
|
4116
|
-
}
|
|
4117
|
-
/**
|
|
4118
|
-
* Load documents from a single chunk file
|
|
4119
|
-
*/
|
|
4120
|
-
async loadChunk(staticPath, chunk) {
|
|
4121
|
-
try {
|
|
4122
|
-
let chunkContent;
|
|
4123
|
-
let chunkPath;
|
|
4124
|
-
if (this.fs) {
|
|
4125
|
-
chunkPath = this.fs.joinPath(staticPath, chunk.path);
|
|
4126
|
-
chunkContent = await this.fs.readFile(chunkPath);
|
|
4127
|
-
} else {
|
|
4128
|
-
chunkPath = nodeFS2 && nodePath ? nodePath.join(staticPath, chunk.path) : `${staticPath}/${chunk.path}`;
|
|
4129
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4130
|
-
chunkContent = await nodeFS2.promises.readFile(chunkPath, "utf8");
|
|
4131
|
-
} else {
|
|
4132
|
-
const response = await fetch(chunkPath);
|
|
4133
|
-
if (!response.ok) {
|
|
4134
|
-
throw new Error(`Failed to fetch chunk: ${response.status} ${response.statusText}`);
|
|
4135
|
-
}
|
|
4136
|
-
chunkContent = await response.text();
|
|
4137
|
-
}
|
|
4138
|
-
}
|
|
4139
|
-
const documents = JSON.parse(chunkContent);
|
|
4140
|
-
if (!Array.isArray(documents)) {
|
|
4141
|
-
throw new Error("Chunk file does not contain an array of documents");
|
|
4142
|
-
}
|
|
4143
|
-
return documents;
|
|
4144
|
-
} catch (error) {
|
|
4145
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load chunk: ${error instanceof Error ? error.message : String(error)}`;
|
|
4146
|
-
throw new Error(errorMessage);
|
|
4147
|
-
}
|
|
4148
|
-
}
|
|
4149
|
-
/**
|
|
4150
|
-
* Upload documents to CouchDB in batches
|
|
4151
|
-
*/
|
|
4152
|
-
async uploadDocuments(documents, db) {
|
|
4153
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
4154
|
-
const batchSize = this.options.chunkBatchSize;
|
|
4155
|
-
for (let i = 0; i < documents.length; i += batchSize) {
|
|
4156
|
-
const batch = documents.slice(i, i + batchSize);
|
|
4157
|
-
this.reportProgress(
|
|
4158
|
-
"documents",
|
|
4159
|
-
i,
|
|
4160
|
-
documents.length,
|
|
4161
|
-
`Uploading batch ${Math.floor(i / batchSize) + 1}...`
|
|
4162
|
-
);
|
|
4163
|
-
try {
|
|
4164
|
-
const docsToInsert = batch.map((doc) => {
|
|
4165
|
-
const cleanDoc = { ...doc };
|
|
4166
|
-
delete cleanDoc._rev;
|
|
4167
|
-
delete cleanDoc._attachments;
|
|
4168
|
-
return cleanDoc;
|
|
4169
|
-
});
|
|
4170
|
-
const bulkResult = await db.bulkDocs(docsToInsert);
|
|
4171
|
-
for (let j = 0; j < bulkResult.length; j++) {
|
|
4172
|
-
const docResult = bulkResult[j];
|
|
4173
|
-
const originalDoc = batch[j];
|
|
4174
|
-
if ("error" in docResult) {
|
|
4175
|
-
const errorMessage = `Failed to upload document ${originalDoc._id}: ${docResult.error} - ${docResult.reason}`;
|
|
4176
|
-
result.errors.push(errorMessage);
|
|
4177
|
-
logger.error(errorMessage);
|
|
4178
|
-
} else {
|
|
4179
|
-
result.restored++;
|
|
4180
|
-
}
|
|
4181
|
-
}
|
|
4182
|
-
} catch (error) {
|
|
4183
|
-
let errorMessage;
|
|
4184
|
-
if (error instanceof Error) {
|
|
4185
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
4186
|
-
} else if (error && typeof error === "object" && "message" in error) {
|
|
4187
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
4188
|
-
} else {
|
|
4189
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${JSON.stringify(error)}`;
|
|
4190
|
-
}
|
|
4191
|
-
result.errors.push(errorMessage);
|
|
4192
|
-
logger.error(errorMessage);
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
|
-
this.reportProgress(
|
|
4196
|
-
"documents",
|
|
4197
|
-
documents.length,
|
|
4198
|
-
documents.length,
|
|
4199
|
-
`Uploaded ${result.restored} documents`
|
|
4200
|
-
);
|
|
4201
|
-
return result;
|
|
4202
|
-
}
|
|
4203
|
-
/**
|
|
4204
|
-
* Upload attachments from filesystem to CouchDB
|
|
4205
|
-
*/
|
|
4206
|
-
async uploadAttachments(staticPath, documents, db) {
|
|
4207
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
4208
|
-
let processedDocs = 0;
|
|
4209
|
-
for (const doc of documents) {
|
|
4210
|
-
this.reportProgress(
|
|
4211
|
-
"attachments",
|
|
4212
|
-
processedDocs,
|
|
4213
|
-
documents.length,
|
|
4214
|
-
`Processing attachments for ${doc._id}...`
|
|
4215
|
-
);
|
|
4216
|
-
processedDocs++;
|
|
4217
|
-
if (!doc._attachments) {
|
|
4218
|
-
continue;
|
|
4219
|
-
}
|
|
4220
|
-
for (const [attachmentName, attachmentMeta] of Object.entries(doc._attachments)) {
|
|
4221
|
-
try {
|
|
4222
|
-
const uploadResult = await this.uploadSingleAttachment(
|
|
4223
|
-
staticPath,
|
|
4224
|
-
doc._id,
|
|
4225
|
-
attachmentName,
|
|
4226
|
-
attachmentMeta,
|
|
4227
|
-
db
|
|
4228
|
-
);
|
|
4229
|
-
if (uploadResult.success) {
|
|
4230
|
-
result.restored++;
|
|
4231
|
-
} else {
|
|
4232
|
-
result.errors.push(uploadResult.error || "Unknown attachment upload error");
|
|
4233
|
-
}
|
|
4234
|
-
} catch (error) {
|
|
4235
|
-
const errorMessage = `Failed to upload attachment ${doc._id}/${attachmentName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
4236
|
-
result.errors.push(errorMessage);
|
|
4237
|
-
logger.error(errorMessage);
|
|
4238
|
-
}
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
|
-
this.reportProgress(
|
|
4242
|
-
"attachments",
|
|
4243
|
-
documents.length,
|
|
4244
|
-
documents.length,
|
|
4245
|
-
`Uploaded ${result.restored} attachments`
|
|
4246
|
-
);
|
|
4247
|
-
return result;
|
|
4248
|
-
}
|
|
4249
|
-
/**
|
|
4250
|
-
* Upload a single attachment file
|
|
4251
|
-
*/
|
|
4252
|
-
async uploadSingleAttachment(staticPath, docId, attachmentName, attachmentMeta, db) {
|
|
4253
|
-
const result = {
|
|
4254
|
-
success: false,
|
|
4255
|
-
attachmentName,
|
|
4256
|
-
docId
|
|
4257
|
-
};
|
|
4258
|
-
try {
|
|
4259
|
-
if (!attachmentMeta.path) {
|
|
4260
|
-
result.error = "Attachment metadata missing file path";
|
|
4261
|
-
return result;
|
|
4262
|
-
}
|
|
4263
|
-
let attachmentData;
|
|
4264
|
-
let attachmentPath;
|
|
4265
|
-
if (this.fs) {
|
|
4266
|
-
attachmentPath = this.fs.joinPath(staticPath, attachmentMeta.path);
|
|
4267
|
-
attachmentData = await this.fs.readBinary(attachmentPath);
|
|
4268
|
-
} else {
|
|
4269
|
-
attachmentPath = nodeFS2 && nodePath ? nodePath.join(staticPath, attachmentMeta.path) : `${staticPath}/${attachmentMeta.path}`;
|
|
4270
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4271
|
-
attachmentData = await nodeFS2.promises.readFile(attachmentPath);
|
|
4272
|
-
} else {
|
|
4273
|
-
const response = await fetch(attachmentPath);
|
|
4274
|
-
if (!response.ok) {
|
|
4275
|
-
result.error = `Failed to fetch attachment: ${response.status} ${response.statusText}`;
|
|
4276
|
-
return result;
|
|
4277
|
-
}
|
|
4278
|
-
attachmentData = await response.arrayBuffer();
|
|
4279
|
-
}
|
|
4280
|
-
}
|
|
4281
|
-
const doc = await db.get(docId);
|
|
4282
|
-
await db.putAttachment(
|
|
4283
|
-
docId,
|
|
4284
|
-
attachmentName,
|
|
4285
|
-
doc._rev,
|
|
4286
|
-
attachmentData,
|
|
4287
|
-
// PouchDB accepts both ArrayBuffer and Buffer
|
|
4288
|
-
attachmentMeta.content_type
|
|
4289
|
-
);
|
|
4290
|
-
result.success = true;
|
|
4291
|
-
} catch (error) {
|
|
4292
|
-
result.error = error instanceof Error ? error.message : String(error);
|
|
4293
|
-
}
|
|
4294
|
-
return result;
|
|
4295
|
-
}
|
|
4296
|
-
/**
|
|
4297
|
-
* Restore CourseConfig document from manifest
|
|
4298
|
-
*/
|
|
4299
|
-
async restoreCourseConfig(manifest, targetDB) {
|
|
4300
|
-
const results = {
|
|
4301
|
-
restored: 0,
|
|
4302
|
-
errors: [],
|
|
4303
|
-
warnings: []
|
|
4304
|
-
};
|
|
4305
|
-
try {
|
|
4306
|
-
if (!manifest.courseConfig) {
|
|
4307
|
-
results.warnings.push(
|
|
4308
|
-
"No courseConfig found in manifest, skipping CourseConfig document creation"
|
|
4309
|
-
);
|
|
4310
|
-
return results;
|
|
4311
|
-
}
|
|
4312
|
-
const courseConfigDoc = {
|
|
4313
|
-
_id: "CourseConfig",
|
|
4314
|
-
...manifest.courseConfig,
|
|
4315
|
-
courseID: manifest.courseId
|
|
4316
|
-
};
|
|
4317
|
-
delete courseConfigDoc._rev;
|
|
4318
|
-
await targetDB.put(courseConfigDoc);
|
|
4319
|
-
results.restored = 1;
|
|
4320
|
-
logger.info(`CourseConfig document created for course: ${manifest.courseId}`);
|
|
4321
|
-
} catch (error) {
|
|
4322
|
-
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
4323
|
-
results.errors.push(`Failed to restore CourseConfig: ${errorMessage}`);
|
|
4324
|
-
logger.error("CourseConfig restoration failed:", error);
|
|
4325
|
-
}
|
|
4326
|
-
return results;
|
|
4327
|
-
}
|
|
4328
|
-
/**
|
|
4329
|
-
* Calculate expected document counts from manifest
|
|
4330
|
-
*/
|
|
4331
|
-
calculateExpectedCounts(manifest) {
|
|
4332
|
-
const counts = {};
|
|
4333
|
-
for (const chunk of manifest.chunks) {
|
|
4334
|
-
counts[chunk.docType] = (counts[chunk.docType] || 0) + chunk.documentCount;
|
|
4335
|
-
}
|
|
4336
|
-
if (manifest.designDocs.length > 0) {
|
|
4337
|
-
counts["_design"] = manifest.designDocs.length;
|
|
4338
|
-
}
|
|
4339
|
-
return counts;
|
|
4340
|
-
}
|
|
4341
|
-
/**
|
|
4342
|
-
* Clean up database after failed migration
|
|
4343
|
-
*/
|
|
4344
|
-
async cleanupFailedMigration(db) {
|
|
4345
|
-
logger.info("Cleaning up failed migration...");
|
|
4346
|
-
try {
|
|
4347
|
-
const allDocs = await db.allDocs();
|
|
4348
|
-
const docsToDelete = allDocs.rows.map((row) => ({
|
|
4349
|
-
_id: row.id,
|
|
4350
|
-
_rev: row.value.rev,
|
|
4351
|
-
_deleted: true
|
|
4352
|
-
}));
|
|
4353
|
-
if (docsToDelete.length > 0) {
|
|
4354
|
-
await db.bulkDocs(docsToDelete);
|
|
4355
|
-
logger.info(`Cleaned up ${docsToDelete.length} documents from failed migration`);
|
|
4356
|
-
}
|
|
4357
|
-
} catch (error) {
|
|
4358
|
-
logger.error("Failed to cleanup documents:", error);
|
|
4359
|
-
throw error;
|
|
4360
|
-
}
|
|
4361
|
-
}
|
|
4362
|
-
/**
|
|
4363
|
-
* Report progress to callback if available
|
|
4364
|
-
*/
|
|
4365
|
-
reportProgress(phase, current, total, message) {
|
|
4366
|
-
if (this.progressCallback) {
|
|
4367
|
-
this.progressCallback({
|
|
4368
|
-
phase,
|
|
4369
|
-
current,
|
|
4370
|
-
total,
|
|
4371
|
-
message
|
|
4372
|
-
});
|
|
4373
|
-
}
|
|
4374
|
-
}
|
|
4375
|
-
/**
|
|
4376
|
-
* Check if a path is a local file path (vs URL)
|
|
4377
|
-
*/
|
|
4378
|
-
isLocalPath(path2) {
|
|
4379
|
-
return !path2.startsWith("http://") && !path2.startsWith("https://");
|
|
4380
|
-
}
|
|
4381
|
-
};
|
|
4382
|
-
}
|
|
4383
|
-
});
|
|
4384
|
-
|
|
4385
|
-
// src/util/migrator/index.ts
|
|
4386
|
-
var init_migrator = __esm({
|
|
4387
|
-
"src/util/migrator/index.ts"() {
|
|
4388
|
-
"use strict";
|
|
4389
|
-
init_StaticToCouchDBMigrator();
|
|
4390
|
-
init_validation();
|
|
4391
|
-
init_FileSystemAdapter();
|
|
4392
|
-
}
|
|
4393
|
-
});
|
|
4394
|
-
|
|
4395
|
-
// src/util/index.ts
|
|
4396
|
-
var init_util2 = __esm({
|
|
4397
|
-
"src/util/index.ts"() {
|
|
4398
|
-
"use strict";
|
|
4399
|
-
init_Loggable();
|
|
4400
|
-
init_packer();
|
|
4401
|
-
init_migrator();
|
|
4402
|
-
init_dataDirectory();
|
|
4403
|
-
}
|
|
4404
|
-
});
|
|
4405
|
-
|
|
4406
|
-
// src/study/SourceMixer.ts
|
|
4407
|
-
var init_SourceMixer = __esm({
|
|
4408
|
-
"src/study/SourceMixer.ts"() {
|
|
4409
|
-
"use strict";
|
|
4410
|
-
}
|
|
4411
|
-
});
|
|
4412
|
-
|
|
4413
|
-
// src/study/MixerDebugger.ts
|
|
4414
|
-
function printMixerSummary(run) {
|
|
4415
|
-
console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
|
|
4416
|
-
logger.info(`Run ID: ${run.runId}`);
|
|
4417
|
-
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
4418
|
-
logger.info(
|
|
4419
|
-
`Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
|
|
4420
|
-
);
|
|
4421
|
-
console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
|
|
4422
|
-
for (const src of run.sourceSummaries) {
|
|
4423
|
-
logger.info(
|
|
4424
|
-
` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
|
|
4425
|
-
);
|
|
4426
|
-
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
|
|
4427
|
-
}
|
|
4428
|
-
console.groupEnd();
|
|
4429
|
-
console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
|
|
4430
|
-
for (const breakdown of run.sourceBreakdowns) {
|
|
4431
|
-
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4432
|
-
logger.info(
|
|
4433
|
-
` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
|
|
4434
|
-
);
|
|
4435
|
-
}
|
|
4436
|
-
console.groupEnd();
|
|
4437
|
-
console.groupEnd();
|
|
4438
|
-
}
|
|
4439
|
-
function mountMixerDebugger() {
|
|
4440
|
-
if (typeof window === "undefined") return;
|
|
4441
|
-
const win = window;
|
|
4442
|
-
win.skuilder = win.skuilder || {};
|
|
4443
|
-
win.skuilder.mixer = mixerDebugAPI;
|
|
4444
|
-
}
|
|
4445
|
-
var runHistory2, mixerDebugAPI;
|
|
4446
|
-
var init_MixerDebugger = __esm({
|
|
4447
|
-
"src/study/MixerDebugger.ts"() {
|
|
4448
|
-
"use strict";
|
|
4449
|
-
init_logger();
|
|
4450
|
-
init_navigators();
|
|
4451
|
-
runHistory2 = [];
|
|
4452
|
-
mixerDebugAPI = {
|
|
4453
|
-
/**
|
|
4454
|
-
* Get raw run history for programmatic access.
|
|
4455
|
-
*/
|
|
4456
|
-
get runs() {
|
|
4457
|
-
return [...runHistory2];
|
|
4458
|
-
},
|
|
4459
|
-
/**
|
|
4460
|
-
* Show summary of a specific mixer run.
|
|
4461
|
-
*/
|
|
4462
|
-
showRun(idOrIndex = 0) {
|
|
4463
|
-
if (runHistory2.length === 0) {
|
|
4464
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4465
|
-
return;
|
|
4466
|
-
}
|
|
4467
|
-
let run;
|
|
4468
|
-
if (typeof idOrIndex === "number") {
|
|
4469
|
-
run = runHistory2[idOrIndex];
|
|
4470
|
-
if (!run) {
|
|
4471
|
-
logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
|
|
4472
|
-
return;
|
|
4473
|
-
}
|
|
4474
|
-
} else {
|
|
4475
|
-
run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
|
|
4476
|
-
if (!run) {
|
|
4477
|
-
logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
|
|
4478
|
-
return;
|
|
4479
|
-
}
|
|
4480
|
-
}
|
|
4481
|
-
printMixerSummary(run);
|
|
4482
|
-
},
|
|
4483
|
-
/**
|
|
4484
|
-
* Show summary of the last mixer run.
|
|
4485
|
-
*/
|
|
4486
|
-
showLastMix() {
|
|
4487
|
-
this.showRun(0);
|
|
4488
|
-
},
|
|
4489
|
-
/**
|
|
4490
|
-
* Explain source balance in the last run.
|
|
4491
|
-
*/
|
|
4492
|
-
explainSourceBalance() {
|
|
4493
|
-
if (runHistory2.length === 0) {
|
|
4494
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4495
|
-
return;
|
|
4496
|
-
}
|
|
4497
|
-
const run = runHistory2[0];
|
|
4498
|
-
console.group("\u2696\uFE0F Source Balance Analysis");
|
|
4499
|
-
logger.info(`Mixer: ${run.mixerType}`);
|
|
4500
|
-
logger.info(`Requested limit: ${run.requestedLimit}`);
|
|
4501
|
-
if (run.quotaPerSource) {
|
|
4502
|
-
logger.info(`Quota per source: ${run.quotaPerSource}`);
|
|
4503
|
-
}
|
|
4504
|
-
console.group("Input Distribution:");
|
|
4505
|
-
for (const src of run.sourceSummaries) {
|
|
4506
|
-
const name = src.sourceName || src.sourceId;
|
|
4507
|
-
logger.info(`${name}:`);
|
|
4508
|
-
logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
|
|
4509
|
-
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
|
|
4510
|
-
}
|
|
4511
|
-
console.groupEnd();
|
|
4512
|
-
console.group("Selection Results:");
|
|
4513
|
-
for (const breakdown of run.sourceBreakdowns) {
|
|
4514
|
-
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4515
|
-
logger.info(`${name}:`);
|
|
4516
|
-
logger.info(
|
|
4517
|
-
` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
|
|
4518
|
-
);
|
|
4519
|
-
logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
|
|
4520
|
-
logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
|
|
4521
|
-
if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
|
|
4522
|
-
logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
|
|
4523
|
-
}
|
|
4524
|
-
if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
|
|
4525
|
-
logger.info(` \u26A0\uFE0F Had cards but none selected!`);
|
|
4526
|
-
}
|
|
4527
|
-
}
|
|
4528
|
-
console.groupEnd();
|
|
4529
|
-
const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
|
|
4530
|
-
const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
|
|
4531
|
-
const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
|
|
4532
|
-
if (maxDeviation > 20) {
|
|
4533
|
-
logger.info(`
|
|
4534
|
-
\u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
|
|
4535
|
-
logger.info("Possible causes:");
|
|
4536
|
-
logger.info(" - Score range differences between sources");
|
|
4537
|
-
logger.info(" - One source has much better quality cards");
|
|
4538
|
-
logger.info(" - Different card availability (reviews vs new)");
|
|
4539
|
-
}
|
|
4540
|
-
console.groupEnd();
|
|
4541
|
-
},
|
|
4542
|
-
/**
|
|
4543
|
-
* Compare score distributions across sources.
|
|
4544
|
-
*/
|
|
4545
|
-
compareScores() {
|
|
4546
|
-
if (runHistory2.length === 0) {
|
|
4547
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4548
|
-
return;
|
|
4549
|
-
}
|
|
4550
|
-
const run = runHistory2[0];
|
|
4551
|
-
console.group("\u{1F4CA} Score Distribution Comparison");
|
|
4552
|
-
console.table(
|
|
4553
|
-
run.sourceSummaries.map((src) => ({
|
|
4554
|
-
source: src.sourceName || src.sourceId,
|
|
4555
|
-
cards: src.totalCards,
|
|
4556
|
-
min: src.bottomScore.toFixed(3),
|
|
4557
|
-
max: src.topScore.toFixed(3),
|
|
4558
|
-
avg: src.avgScore.toFixed(3),
|
|
4559
|
-
range: (src.topScore - src.bottomScore).toFixed(3)
|
|
4560
|
-
}))
|
|
4561
|
-
);
|
|
4562
|
-
const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
|
|
4563
|
-
const avgScores = run.sourceSummaries.map((s) => s.avgScore);
|
|
4564
|
-
const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
|
|
4565
|
-
const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
|
|
4566
|
-
if (rangeDiff > 0.3 || avgDiff > 0.2) {
|
|
4567
|
-
logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
|
|
4568
|
-
logger.info(
|
|
4569
|
-
"This may cause one source to dominate selection if using global sorting (not quota-based)"
|
|
4570
|
-
);
|
|
4571
|
-
}
|
|
4572
|
-
console.groupEnd();
|
|
4573
|
-
},
|
|
4574
|
-
/**
|
|
4575
|
-
* Show detailed information for a specific card.
|
|
4576
|
-
*/
|
|
4577
|
-
showCard(cardId) {
|
|
4578
|
-
for (const run of runHistory2) {
|
|
4579
|
-
const card = run.cards.find((c) => c.cardId === cardId);
|
|
4580
|
-
if (card) {
|
|
4581
|
-
const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
|
|
4582
|
-
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
4583
|
-
logger.info(`Course: ${card.courseId}`);
|
|
4584
|
-
logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
|
|
4585
|
-
logger.info(`Origin: ${card.origin}`);
|
|
4586
|
-
logger.info(`Score: ${card.score.toFixed(3)}`);
|
|
4587
|
-
if (card.rankInSource) {
|
|
4588
|
-
logger.info(`Rank in source: #${card.rankInSource}`);
|
|
4589
|
-
}
|
|
4590
|
-
if (card.rankInMix) {
|
|
4591
|
-
logger.info(`Rank in mixed results: #${card.rankInMix}`);
|
|
4592
|
-
}
|
|
4593
|
-
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
4594
|
-
if (!card.selected && card.rankInSource) {
|
|
4595
|
-
logger.info("\nWhy not selected:");
|
|
4596
|
-
if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
|
|
4597
|
-
logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
|
|
4598
|
-
}
|
|
4599
|
-
logger.info(" - Check score compared to selected cards using .showRun()");
|
|
4600
|
-
}
|
|
4601
|
-
console.groupEnd();
|
|
4602
|
-
return;
|
|
4603
|
-
}
|
|
4604
|
-
}
|
|
4605
|
-
logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
|
|
4606
|
-
},
|
|
4607
|
-
/**
|
|
4608
|
-
* Show all runs in compact format.
|
|
4609
|
-
*/
|
|
4610
|
-
listRuns() {
|
|
4611
|
-
if (runHistory2.length === 0) {
|
|
4612
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4613
|
-
return;
|
|
4614
|
-
}
|
|
4615
|
-
console.table(
|
|
4616
|
-
runHistory2.map((r) => ({
|
|
4617
|
-
id: r.runId.slice(-8),
|
|
4618
|
-
time: r.timestamp.toLocaleTimeString(),
|
|
4619
|
-
mixer: r.mixerType,
|
|
4620
|
-
sources: r.sourceSummaries.length,
|
|
4621
|
-
selected: r.finalCount,
|
|
4622
|
-
reviews: r.reviewsSelected,
|
|
4623
|
-
new: r.newSelected
|
|
4624
|
-
}))
|
|
4625
|
-
);
|
|
4626
|
-
},
|
|
4627
|
-
/**
|
|
4628
|
-
* Export run history as JSON for bug reports.
|
|
4629
|
-
*/
|
|
4630
|
-
export() {
|
|
4631
|
-
const json = JSON.stringify(runHistory2, null, 2);
|
|
4632
|
-
logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
|
|
4633
|
-
logger.info(" copy(window.skuilder.mixer.export())");
|
|
4634
|
-
return json;
|
|
4635
|
-
},
|
|
4636
|
-
/**
|
|
4637
|
-
* Clear run history.
|
|
4638
|
-
*/
|
|
4639
|
-
clear() {
|
|
4640
|
-
runHistory2.length = 0;
|
|
4641
|
-
logger.info("[Mixer Debug] Run history cleared.");
|
|
4642
|
-
},
|
|
4643
|
-
/**
|
|
4644
|
-
* Show help.
|
|
4645
|
-
*/
|
|
4646
|
-
help() {
|
|
4647
|
-
logger.info(`
|
|
4648
|
-
\u{1F3A8} Mixer Debug API
|
|
4649
|
-
|
|
4650
|
-
Commands:
|
|
4651
|
-
.showLastMix() Show summary of most recent mixer run
|
|
4652
|
-
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
4653
|
-
.explainSourceBalance() Analyze source balance and selection patterns
|
|
4654
|
-
.compareScores() Compare score distributions across sources
|
|
4655
|
-
.showCard(cardId) Show mixer decisions for a specific card
|
|
4656
|
-
.listRuns() List all captured runs in table format
|
|
4657
|
-
.export() Export run history as JSON for bug reports
|
|
4658
|
-
.clear() Clear run history
|
|
4659
|
-
.runs Access raw run history array
|
|
4660
|
-
.help() Show this help message
|
|
4661
|
-
|
|
4662
|
-
Example:
|
|
4663
|
-
window.skuilder.mixer.showLastMix()
|
|
4664
|
-
window.skuilder.mixer.explainSourceBalance()
|
|
4665
|
-
window.skuilder.mixer.compareScores()
|
|
4666
|
-
`);
|
|
4667
|
-
}
|
|
4668
|
-
};
|
|
4669
|
-
mountMixerDebugger();
|
|
3341
|
+
strategyId,
|
|
3342
|
+
previousWeight: currentWeight,
|
|
3343
|
+
newWeight,
|
|
3344
|
+
gradient,
|
|
3345
|
+
learningState,
|
|
3346
|
+
updated
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
function getDefaultLearnableWeight() {
|
|
3350
|
+
return { ...DEFAULT_LEARNABLE_WEIGHT };
|
|
3351
|
+
}
|
|
3352
|
+
var MIN_OBSERVATIONS_FOR_UPDATE, LEARNING_RATE, MAX_WEIGHT_DELTA, MIN_R_SQUARED_FOR_GRADIENT, FLAT_GRADIENT_THRESHOLD, MAX_HISTORY_LENGTH;
|
|
3353
|
+
var init_learning = __esm({
|
|
3354
|
+
"src/core/orchestration/learning.ts"() {
|
|
3355
|
+
"use strict";
|
|
3356
|
+
init_contentNavigationStrategy();
|
|
3357
|
+
init_types_legacy();
|
|
3358
|
+
init_logger();
|
|
3359
|
+
MIN_OBSERVATIONS_FOR_UPDATE = 10;
|
|
3360
|
+
LEARNING_RATE = 0.1;
|
|
3361
|
+
MAX_WEIGHT_DELTA = 0.3;
|
|
3362
|
+
MIN_R_SQUARED_FOR_GRADIENT = 0.05;
|
|
3363
|
+
FLAT_GRADIENT_THRESHOLD = 0.02;
|
|
3364
|
+
MAX_HISTORY_LENGTH = 100;
|
|
4670
3365
|
}
|
|
4671
3366
|
});
|
|
4672
3367
|
|
|
4673
|
-
// src/
|
|
4674
|
-
function
|
|
4675
|
-
if (!
|
|
4676
|
-
|
|
4677
|
-
return;
|
|
3368
|
+
// src/core/orchestration/signal.ts
|
|
3369
|
+
function computeOutcomeSignal(records, config = {}) {
|
|
3370
|
+
if (!records || records.length === 0) {
|
|
3371
|
+
return null;
|
|
4678
3372
|
}
|
|
4679
|
-
const
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
3373
|
+
const target = config.targetAccuracy ?? 0.85;
|
|
3374
|
+
const tolerance = config.tolerance ?? 0.05;
|
|
3375
|
+
let correct = 0;
|
|
3376
|
+
for (const r of records) {
|
|
3377
|
+
if (r.isCorrect) correct++;
|
|
4684
3378
|
}
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
3379
|
+
const accuracy = correct / records.length;
|
|
3380
|
+
return scoreAccuracyInZone(accuracy, target, tolerance);
|
|
3381
|
+
}
|
|
3382
|
+
function scoreAccuracyInZone(accuracy, target, tolerance) {
|
|
3383
|
+
const dist = Math.abs(accuracy - target);
|
|
3384
|
+
if (dist <= tolerance) {
|
|
3385
|
+
return 1;
|
|
4688
3386
|
}
|
|
4689
|
-
|
|
4690
|
-
|
|
3387
|
+
const excess = dist - tolerance;
|
|
3388
|
+
const slope = 2.5;
|
|
3389
|
+
return Math.max(0, 1 - excess * slope);
|
|
4691
3390
|
}
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4696
|
-
return;
|
|
3391
|
+
var init_signal = __esm({
|
|
3392
|
+
"src/core/orchestration/signal.ts"() {
|
|
3393
|
+
"use strict";
|
|
4697
3394
|
}
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
}
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
course: p.courseName || p.courseId.slice(0, 8),
|
|
4709
|
-
origin: p.origin,
|
|
4710
|
-
queue: p.queueSource,
|
|
4711
|
-
score: p.score?.toFixed(3) || "-",
|
|
4712
|
-
time: p.timestamp.toLocaleTimeString()
|
|
4713
|
-
}))
|
|
3395
|
+
});
|
|
3396
|
+
|
|
3397
|
+
// src/core/orchestration/recording.ts
|
|
3398
|
+
async function recordUserOutcome(context, periodStart, periodEnd, records, activeStrategyIds, eloStart = 0, eloEnd = 0, config) {
|
|
3399
|
+
const { user, course, userId } = context;
|
|
3400
|
+
const courseId = course.getCourseID();
|
|
3401
|
+
const outcomeValue = computeOutcomeSignal(records, config);
|
|
3402
|
+
if (outcomeValue === null) {
|
|
3403
|
+
logger.debug(
|
|
3404
|
+
`[Orchestration] No outcome signal computed for ${userId} (insufficient data). Skipping record.`
|
|
4714
3405
|
);
|
|
4715
|
-
}
|
|
4716
|
-
console.groupEnd();
|
|
4717
|
-
}
|
|
4718
|
-
function showInterleaving(sessionIndex = 0) {
|
|
4719
|
-
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4720
|
-
if (!session) {
|
|
4721
|
-
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4722
3406
|
return;
|
|
4723
3407
|
}
|
|
4724
|
-
|
|
4725
|
-
const
|
|
4726
|
-
|
|
4727
|
-
session.presentations.forEach((p) => {
|
|
4728
|
-
const name = p.courseName || p.courseId;
|
|
4729
|
-
courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
|
|
4730
|
-
if (!courseOrigins.has(name)) {
|
|
4731
|
-
courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
|
|
4732
|
-
}
|
|
4733
|
-
const origins = courseOrigins.get(name);
|
|
4734
|
-
origins[p.origin]++;
|
|
4735
|
-
});
|
|
4736
|
-
logger.info("Course distribution:");
|
|
4737
|
-
console.table(
|
|
4738
|
-
Array.from(courseCounts.entries()).map(([course, count]) => {
|
|
4739
|
-
const origins = courseOrigins.get(course);
|
|
4740
|
-
return {
|
|
4741
|
-
course,
|
|
4742
|
-
total: count,
|
|
4743
|
-
reviews: origins.review,
|
|
4744
|
-
new: origins.new,
|
|
4745
|
-
failed: origins.failed,
|
|
4746
|
-
percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
|
|
4747
|
-
};
|
|
4748
|
-
})
|
|
4749
|
-
);
|
|
4750
|
-
if (session.presentations.length > 0) {
|
|
4751
|
-
logger.info("\nPresentation sequence (first 20):");
|
|
4752
|
-
const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
|
|
4753
|
-
logger.info(sequence);
|
|
4754
|
-
}
|
|
4755
|
-
let maxCluster = 0;
|
|
4756
|
-
let currentCluster = 1;
|
|
4757
|
-
let currentCourse = session.presentations[0]?.courseId;
|
|
4758
|
-
for (let i = 1; i < session.presentations.length; i++) {
|
|
4759
|
-
if (session.presentations[i].courseId === currentCourse) {
|
|
4760
|
-
currentCluster++;
|
|
4761
|
-
maxCluster = Math.max(maxCluster, currentCluster);
|
|
4762
|
-
} else {
|
|
4763
|
-
currentCourse = session.presentations[i].courseId;
|
|
4764
|
-
currentCluster = 1;
|
|
4765
|
-
}
|
|
3408
|
+
const deviations = {};
|
|
3409
|
+
for (const strategyId of activeStrategyIds) {
|
|
3410
|
+
deviations[strategyId] = context.getDeviation(strategyId);
|
|
4766
3411
|
}
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
3412
|
+
const id = `USER_OUTCOME::${courseId}::${userId}::${periodEnd}`;
|
|
3413
|
+
const record = {
|
|
3414
|
+
_id: id,
|
|
3415
|
+
docType: "USER_OUTCOME" /* USER_OUTCOME */,
|
|
3416
|
+
courseId,
|
|
3417
|
+
userId,
|
|
3418
|
+
periodStart,
|
|
3419
|
+
periodEnd,
|
|
3420
|
+
outcomeValue,
|
|
3421
|
+
deviations,
|
|
3422
|
+
metadata: {
|
|
3423
|
+
sessionsCount: 1,
|
|
3424
|
+
// Assumes recording is triggered per-session currently
|
|
3425
|
+
cardsSeen: records.length,
|
|
3426
|
+
eloStart,
|
|
3427
|
+
eloEnd,
|
|
3428
|
+
signalType: "accuracy_in_zone"
|
|
3429
|
+
}
|
|
3430
|
+
};
|
|
3431
|
+
try {
|
|
3432
|
+
await user.putUserOutcome(record);
|
|
3433
|
+
logger.debug(
|
|
3434
|
+
`[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
|
|
3435
|
+
);
|
|
3436
|
+
} catch (e) {
|
|
3437
|
+
logger.error(`[Orchestration] Failed to record outcome: ${e}`);
|
|
4771
3438
|
}
|
|
4772
|
-
console.groupEnd();
|
|
4773
|
-
}
|
|
4774
|
-
function mountSessionDebugger() {
|
|
4775
|
-
if (typeof window === "undefined") return;
|
|
4776
|
-
const win = window;
|
|
4777
|
-
win.skuilder = win.skuilder || {};
|
|
4778
|
-
win.skuilder.session = sessionDebugAPI;
|
|
4779
3439
|
}
|
|
4780
|
-
var
|
|
4781
|
-
|
|
4782
|
-
"src/study/SessionDebugger.ts"() {
|
|
3440
|
+
var init_recording = __esm({
|
|
3441
|
+
"src/core/orchestration/recording.ts"() {
|
|
4783
3442
|
"use strict";
|
|
3443
|
+
init_signal();
|
|
3444
|
+
init_types_legacy();
|
|
4784
3445
|
init_logger();
|
|
4785
|
-
activeSession = null;
|
|
4786
|
-
sessionHistory = [];
|
|
4787
|
-
sessionDebugAPI = {
|
|
4788
|
-
/**
|
|
4789
|
-
* Get raw session history for programmatic access.
|
|
4790
|
-
*/
|
|
4791
|
-
get sessions() {
|
|
4792
|
-
return [...sessionHistory];
|
|
4793
|
-
},
|
|
4794
|
-
/**
|
|
4795
|
-
* Get active session if any.
|
|
4796
|
-
*/
|
|
4797
|
-
get active() {
|
|
4798
|
-
return activeSession;
|
|
4799
|
-
},
|
|
4800
|
-
/**
|
|
4801
|
-
* Show current queue state.
|
|
4802
|
-
*/
|
|
4803
|
-
showQueue() {
|
|
4804
|
-
showCurrentQueue();
|
|
4805
|
-
},
|
|
4806
|
-
/**
|
|
4807
|
-
* Show presentation history for current or past session.
|
|
4808
|
-
*/
|
|
4809
|
-
showHistory(sessionIndex = 0) {
|
|
4810
|
-
showPresentationHistory(sessionIndex);
|
|
4811
|
-
},
|
|
4812
|
-
/**
|
|
4813
|
-
* Analyze course interleaving pattern.
|
|
4814
|
-
*/
|
|
4815
|
-
showInterleaving(sessionIndex = 0) {
|
|
4816
|
-
showInterleaving(sessionIndex);
|
|
4817
|
-
},
|
|
4818
|
-
/**
|
|
4819
|
-
* List all tracked sessions.
|
|
4820
|
-
*/
|
|
4821
|
-
listSessions() {
|
|
4822
|
-
if (activeSession) {
|
|
4823
|
-
logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
|
|
4824
|
-
}
|
|
4825
|
-
if (sessionHistory.length === 0) {
|
|
4826
|
-
logger.info("[Session Debug] No completed sessions in history.");
|
|
4827
|
-
return;
|
|
4828
|
-
}
|
|
4829
|
-
console.table(
|
|
4830
|
-
sessionHistory.map((s, idx) => ({
|
|
4831
|
-
index: idx,
|
|
4832
|
-
id: s.sessionId.slice(-8),
|
|
4833
|
-
started: s.startTime.toLocaleTimeString(),
|
|
4834
|
-
ended: s.endTime?.toLocaleTimeString() || "incomplete",
|
|
4835
|
-
cards: s.presentations.length
|
|
4836
|
-
}))
|
|
4837
|
-
);
|
|
4838
|
-
},
|
|
4839
|
-
/**
|
|
4840
|
-
* Export session history as JSON for bug reports.
|
|
4841
|
-
*/
|
|
4842
|
-
export() {
|
|
4843
|
-
const data = {
|
|
4844
|
-
active: activeSession,
|
|
4845
|
-
history: sessionHistory
|
|
4846
|
-
};
|
|
4847
|
-
const json = JSON.stringify(data, null, 2);
|
|
4848
|
-
logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
|
|
4849
|
-
logger.info(" copy(window.skuilder.session.export())");
|
|
4850
|
-
return json;
|
|
4851
|
-
},
|
|
4852
|
-
/**
|
|
4853
|
-
* Clear session history.
|
|
4854
|
-
*/
|
|
4855
|
-
clear() {
|
|
4856
|
-
sessionHistory.length = 0;
|
|
4857
|
-
logger.info("[Session Debug] Session history cleared.");
|
|
4858
|
-
},
|
|
4859
|
-
/**
|
|
4860
|
-
* Show help.
|
|
4861
|
-
*/
|
|
4862
|
-
help() {
|
|
4863
|
-
logger.info(`
|
|
4864
|
-
\u{1F3AF} Session Debug API
|
|
4865
|
-
|
|
4866
|
-
Commands:
|
|
4867
|
-
.showQueue() Show current queue state (active session only)
|
|
4868
|
-
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
4869
|
-
.showInterleaving(index?) Analyze course interleaving pattern
|
|
4870
|
-
.listSessions() List all tracked sessions
|
|
4871
|
-
.export() Export session data as JSON for bug reports
|
|
4872
|
-
.clear() Clear session history
|
|
4873
|
-
.sessions Access raw session history array
|
|
4874
|
-
.active Access active session (if any)
|
|
4875
|
-
.help() Show this help message
|
|
4876
|
-
|
|
4877
|
-
Example:
|
|
4878
|
-
window.skuilder.session.showHistory()
|
|
4879
|
-
window.skuilder.session.showInterleaving()
|
|
4880
|
-
window.skuilder.session.showQueue()
|
|
4881
|
-
`);
|
|
4882
|
-
}
|
|
4883
|
-
};
|
|
4884
|
-
mountSessionDebugger();
|
|
4885
3446
|
}
|
|
4886
3447
|
});
|
|
4887
3448
|
|
|
4888
|
-
// src/
|
|
4889
|
-
|
|
4890
|
-
|
|
3449
|
+
// src/core/orchestration/index.ts
|
|
3450
|
+
function fnv1a(str) {
|
|
3451
|
+
let hash = 2166136261;
|
|
3452
|
+
for (let i = 0; i < str.length; i++) {
|
|
3453
|
+
hash ^= str.charCodeAt(i);
|
|
3454
|
+
hash = Math.imul(hash, 16777619);
|
|
3455
|
+
}
|
|
3456
|
+
return hash >>> 0;
|
|
3457
|
+
}
|
|
3458
|
+
function computeDeviation(userId, strategyId, salt) {
|
|
3459
|
+
const input = `${userId}:${strategyId}:${salt}`;
|
|
3460
|
+
const hash = fnv1a(input);
|
|
3461
|
+
const normalized = hash / 4294967296;
|
|
3462
|
+
return normalized * 2 - 1;
|
|
3463
|
+
}
|
|
3464
|
+
function computeSpread(confidence) {
|
|
3465
|
+
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
3466
|
+
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
3467
|
+
}
|
|
3468
|
+
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
3469
|
+
const deviation = computeDeviation(userId, strategyId, salt);
|
|
3470
|
+
const spread = computeSpread(learnable.confidence);
|
|
3471
|
+
const adjustment = deviation * spread * learnable.weight;
|
|
3472
|
+
const effective = learnable.weight + adjustment;
|
|
3473
|
+
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
3474
|
+
}
|
|
3475
|
+
async function createOrchestrationContext(user, course) {
|
|
3476
|
+
let courseConfig;
|
|
3477
|
+
try {
|
|
3478
|
+
courseConfig = await course.getCourseConfig();
|
|
3479
|
+
} catch (e) {
|
|
3480
|
+
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
3481
|
+
courseConfig = {
|
|
3482
|
+
name: "Unknown",
|
|
3483
|
+
description: "",
|
|
3484
|
+
public: false,
|
|
3485
|
+
deleted: false,
|
|
3486
|
+
creator: "",
|
|
3487
|
+
admins: [],
|
|
3488
|
+
moderators: [],
|
|
3489
|
+
dataShapes: [],
|
|
3490
|
+
questionTypes: [],
|
|
3491
|
+
orchestration: { salt: "default" }
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
const userId = user.getUsername();
|
|
3495
|
+
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
3496
|
+
return {
|
|
3497
|
+
user,
|
|
3498
|
+
course,
|
|
3499
|
+
userId,
|
|
3500
|
+
courseConfig,
|
|
3501
|
+
getEffectiveWeight(strategyId, learnable) {
|
|
3502
|
+
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
3503
|
+
},
|
|
3504
|
+
getDeviation(strategyId) {
|
|
3505
|
+
return computeDeviation(userId, strategyId, salt);
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
3510
|
+
var init_orchestration = __esm({
|
|
3511
|
+
"src/core/orchestration/index.ts"() {
|
|
4891
3512
|
"use strict";
|
|
4892
|
-
init_SrsService();
|
|
4893
|
-
init_EloService();
|
|
4894
|
-
init_ResponseProcessor();
|
|
4895
|
-
init_CardHydrationService();
|
|
4896
|
-
init_ItemQueue();
|
|
4897
|
-
init_couch();
|
|
4898
|
-
init_recording();
|
|
4899
|
-
init_util2();
|
|
4900
|
-
init_navigators();
|
|
4901
|
-
init_SourceMixer();
|
|
4902
|
-
init_MixerDebugger();
|
|
4903
|
-
init_SessionDebugger();
|
|
4904
3513
|
init_logger();
|
|
3514
|
+
init_gradient();
|
|
3515
|
+
init_learning();
|
|
3516
|
+
init_signal();
|
|
3517
|
+
init_recording();
|
|
3518
|
+
MIN_SPREAD = 0.1;
|
|
3519
|
+
MAX_SPREAD = 0.5;
|
|
3520
|
+
MIN_WEIGHT = 0.1;
|
|
3521
|
+
MAX_WEIGHT = 3;
|
|
4905
3522
|
}
|
|
4906
3523
|
});
|
|
4907
3524
|
|
|
@@ -4922,6 +3539,44 @@ function globMatch(value, pattern) {
|
|
|
4922
3539
|
function cardMatchesTagPattern(card, pattern) {
|
|
4923
3540
|
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
4924
3541
|
}
|
|
3542
|
+
function mergeHints2(allHints) {
|
|
3543
|
+
const defined = allHints.filter((h) => h !== null && h !== void 0);
|
|
3544
|
+
if (defined.length === 0) return void 0;
|
|
3545
|
+
const merged = {};
|
|
3546
|
+
const boostTags = {};
|
|
3547
|
+
for (const hints of defined) {
|
|
3548
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
3549
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
if (Object.keys(boostTags).length > 0) {
|
|
3553
|
+
merged.boostTags = boostTags;
|
|
3554
|
+
}
|
|
3555
|
+
const boostCards = {};
|
|
3556
|
+
for (const hints of defined) {
|
|
3557
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
3558
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
if (Object.keys(boostCards).length > 0) {
|
|
3562
|
+
merged.boostCards = boostCards;
|
|
3563
|
+
}
|
|
3564
|
+
const concatUnique = (field) => {
|
|
3565
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
3566
|
+
if (values.length > 0) {
|
|
3567
|
+
merged[field] = [...new Set(values)];
|
|
3568
|
+
}
|
|
3569
|
+
};
|
|
3570
|
+
concatUnique("requireTags");
|
|
3571
|
+
concatUnique("requireCards");
|
|
3572
|
+
concatUnique("excludeTags");
|
|
3573
|
+
concatUnique("excludeCards");
|
|
3574
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
3575
|
+
if (labels.length > 0) {
|
|
3576
|
+
merged._label = labels.join("; ");
|
|
3577
|
+
}
|
|
3578
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
3579
|
+
}
|
|
4925
3580
|
function logPipelineConfig(generator, filters) {
|
|
4926
3581
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
4927
3582
|
logger.info(
|
|
@@ -4985,16 +3640,15 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
4985
3640
|
}
|
|
4986
3641
|
}
|
|
4987
3642
|
}
|
|
4988
|
-
var
|
|
3643
|
+
var import_common8, VERBOSE_RESULTS, Pipeline;
|
|
4989
3644
|
var init_Pipeline = __esm({
|
|
4990
3645
|
"src/core/navigators/Pipeline.ts"() {
|
|
4991
3646
|
"use strict";
|
|
4992
|
-
|
|
3647
|
+
import_common8 = require("@vue-skuilder/common");
|
|
4993
3648
|
init_navigators();
|
|
4994
3649
|
init_logger();
|
|
4995
3650
|
init_orchestration();
|
|
4996
3651
|
init_PipelineDebugger();
|
|
4997
|
-
init_SessionController();
|
|
4998
3652
|
VERBOSE_RESULTS = true;
|
|
4999
3653
|
Pipeline = class extends ContentNavigator {
|
|
5000
3654
|
generator;
|
|
@@ -5074,9 +3728,12 @@ var init_Pipeline = __esm({
|
|
|
5074
3728
|
logger.debug(
|
|
5075
3729
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
5076
3730
|
);
|
|
5077
|
-
|
|
3731
|
+
const generatorResult = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3732
|
+
let cards = generatorResult.cards;
|
|
5078
3733
|
const tGenerate = performance.now();
|
|
5079
3734
|
const generatedCount = cards.length;
|
|
3735
|
+
const mergedHints = mergeHints2([this._ephemeralHints, generatorResult.hints]);
|
|
3736
|
+
this._ephemeralHints = mergedHints ?? null;
|
|
5080
3737
|
let generatorSummaries;
|
|
5081
3738
|
if (this.generator.generators) {
|
|
5082
3739
|
const genMap = /* @__PURE__ */ new Map();
|
|
@@ -5162,7 +3819,7 @@ var init_Pipeline = __esm({
|
|
|
5162
3819
|
} catch (e) {
|
|
5163
3820
|
logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
|
|
5164
3821
|
}
|
|
5165
|
-
return result;
|
|
3822
|
+
return { cards: result };
|
|
5166
3823
|
}
|
|
5167
3824
|
/**
|
|
5168
3825
|
* Batch hydrate tags for all cards.
|
|
@@ -5317,7 +3974,7 @@ var init_Pipeline = __esm({
|
|
|
5317
3974
|
let userElo = 1e3;
|
|
5318
3975
|
try {
|
|
5319
3976
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
5320
|
-
const courseElo = (0,
|
|
3977
|
+
const courseElo = (0, import_common8.toCourseElo)(courseReg.elo);
|
|
5321
3978
|
userElo = courseElo.global.score;
|
|
5322
3979
|
} catch (e) {
|
|
5323
3980
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
@@ -5378,7 +4035,7 @@ var init_Pipeline = __esm({
|
|
|
5378
4035
|
*/
|
|
5379
4036
|
async getTagEloStatus(tagFilter) {
|
|
5380
4037
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
5381
|
-
const courseElo = (0,
|
|
4038
|
+
const courseElo = (0, import_common8.toCourseElo)(courseReg.elo);
|
|
5382
4039
|
const result = {};
|
|
5383
4040
|
if (!tagFilter) {
|
|
5384
4041
|
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
@@ -6051,11 +4708,11 @@ ${JSON.stringify(config)}
|
|
|
6051
4708
|
function isSuccessRow(row) {
|
|
6052
4709
|
return "doc" in row && row.doc !== null && row.doc !== void 0;
|
|
6053
4710
|
}
|
|
6054
|
-
var
|
|
4711
|
+
var import_common9, CourseDB;
|
|
6055
4712
|
var init_courseDB = __esm({
|
|
6056
4713
|
"src/impl/couch/courseDB.ts"() {
|
|
6057
4714
|
"use strict";
|
|
6058
|
-
|
|
4715
|
+
import_common9 = require("@vue-skuilder/common");
|
|
6059
4716
|
init_couch();
|
|
6060
4717
|
init_updateQueue();
|
|
6061
4718
|
init_types_legacy();
|
|
@@ -6159,14 +4816,14 @@ var init_courseDB = __esm({
|
|
|
6159
4816
|
docs.rows.forEach((r) => {
|
|
6160
4817
|
if (isSuccessRow(r)) {
|
|
6161
4818
|
if (r.doc && r.doc.elo) {
|
|
6162
|
-
ret.push((0,
|
|
4819
|
+
ret.push((0, import_common9.toCourseElo)(r.doc.elo));
|
|
6163
4820
|
} else {
|
|
6164
4821
|
logger.warn("no elo data for card: " + r.id);
|
|
6165
|
-
ret.push((0,
|
|
4822
|
+
ret.push((0, import_common9.blankCourseElo)());
|
|
6166
4823
|
}
|
|
6167
4824
|
} else {
|
|
6168
4825
|
logger.warn("no elo data for card: " + JSON.stringify(r));
|
|
6169
|
-
ret.push((0,
|
|
4826
|
+
ret.push((0, import_common9.blankCourseElo)());
|
|
6170
4827
|
}
|
|
6171
4828
|
});
|
|
6172
4829
|
return ret;
|
|
@@ -6368,7 +5025,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6368
5025
|
async getCourseTagStubs() {
|
|
6369
5026
|
return getCourseTagStubs(this.id);
|
|
6370
5027
|
}
|
|
6371
|
-
async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0,
|
|
5028
|
+
async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common9.blankCourseElo)()) {
|
|
6372
5029
|
try {
|
|
6373
5030
|
const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
|
|
6374
5031
|
if (resp.ok) {
|
|
@@ -6377,19 +5034,19 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6377
5034
|
`[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
|
|
6378
5035
|
);
|
|
6379
5036
|
return {
|
|
6380
|
-
status:
|
|
5037
|
+
status: import_common9.Status.error,
|
|
6381
5038
|
message: `Note was added but no cards were created: ${resp.cardCreationError}`,
|
|
6382
5039
|
id: resp.id
|
|
6383
5040
|
};
|
|
6384
5041
|
}
|
|
6385
5042
|
return {
|
|
6386
|
-
status:
|
|
5043
|
+
status: import_common9.Status.ok,
|
|
6387
5044
|
message: "",
|
|
6388
5045
|
id: resp.id
|
|
6389
5046
|
};
|
|
6390
5047
|
} else {
|
|
6391
5048
|
return {
|
|
6392
|
-
status:
|
|
5049
|
+
status: import_common9.Status.error,
|
|
6393
5050
|
message: "Unexpected error adding note"
|
|
6394
5051
|
};
|
|
6395
5052
|
}
|
|
@@ -6401,7 +5058,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6401
5058
|
message: ${err.message}`
|
|
6402
5059
|
);
|
|
6403
5060
|
return {
|
|
6404
|
-
status:
|
|
5061
|
+
status: import_common9.Status.error,
|
|
6405
5062
|
message: `Error adding note to course. ${e.reason || err.message}`
|
|
6406
5063
|
};
|
|
6407
5064
|
}
|
|
@@ -6540,7 +5197,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6540
5197
|
const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
|
|
6541
5198
|
return c.courseID === this.id;
|
|
6542
5199
|
});
|
|
6543
|
-
targetElo = (0,
|
|
5200
|
+
targetElo = (0, import_common9.EloToNumber)(courseDoc.elo);
|
|
6544
5201
|
} catch {
|
|
6545
5202
|
targetElo = 1e3;
|
|
6546
5203
|
}
|
|
@@ -6664,13 +5321,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6664
5321
|
});
|
|
6665
5322
|
|
|
6666
5323
|
// src/impl/couch/classroomDB.ts
|
|
6667
|
-
var
|
|
5324
|
+
var import_moment4, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB;
|
|
6668
5325
|
var init_classroomDB2 = __esm({
|
|
6669
5326
|
"src/impl/couch/classroomDB.ts"() {
|
|
6670
5327
|
"use strict";
|
|
6671
5328
|
init_factory();
|
|
6672
5329
|
init_logger();
|
|
6673
|
-
|
|
5330
|
+
import_moment4 = __toESM(require("moment"), 1);
|
|
6674
5331
|
init_pouchdb_setup();
|
|
6675
5332
|
init_couch();
|
|
6676
5333
|
init_courseDB();
|
|
@@ -6782,14 +5439,14 @@ var init_classroomDB2 = __esm({
|
|
|
6782
5439
|
}
|
|
6783
5440
|
const activeCards = await this._user.getActiveCards();
|
|
6784
5441
|
const activeCardIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
6785
|
-
const now =
|
|
5442
|
+
const now = import_moment4.default.utc();
|
|
6786
5443
|
const assigned = await this.getAssignedContent();
|
|
6787
|
-
const due = assigned.filter((c) => now.isAfter(
|
|
5444
|
+
const due = assigned.filter((c) => now.isAfter(import_moment4.default.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
|
|
6788
5445
|
logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(due)}`);
|
|
6789
5446
|
for (const content of due) {
|
|
6790
5447
|
if (content.type === "course") {
|
|
6791
5448
|
const db = new CourseDB(content.courseID, async () => this._user);
|
|
6792
|
-
const courseCards = await db.getWeightedCards(limit);
|
|
5449
|
+
const { cards: courseCards } = await db.getWeightedCards(limit);
|
|
6793
5450
|
for (const card of courseCards) {
|
|
6794
5451
|
if (!activeCardIds.has(card.cardId)) {
|
|
6795
5452
|
weighted.push({
|
|
@@ -6852,7 +5509,7 @@ var init_classroomDB2 = __esm({
|
|
|
6852
5509
|
logger.info(
|
|
6853
5510
|
`[StudentClassroomDB] New cards from classroom ${this._cfg.name}: ${weighted.length} total (reviews + new)`
|
|
6854
5511
|
);
|
|
6855
|
-
return weighted.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5512
|
+
return { cards: weighted.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
6856
5513
|
}
|
|
6857
5514
|
};
|
|
6858
5515
|
}
|
|
@@ -6893,14 +5550,14 @@ var init_auth = __esm({
|
|
|
6893
5550
|
});
|
|
6894
5551
|
|
|
6895
5552
|
// src/impl/couch/CouchDBSyncStrategy.ts
|
|
6896
|
-
var
|
|
5553
|
+
var import_common10;
|
|
6897
5554
|
var init_CouchDBSyncStrategy = __esm({
|
|
6898
5555
|
"src/impl/couch/CouchDBSyncStrategy.ts"() {
|
|
6899
5556
|
"use strict";
|
|
6900
5557
|
init_factory();
|
|
6901
5558
|
init_types_legacy();
|
|
6902
5559
|
init_logger();
|
|
6903
|
-
|
|
5560
|
+
import_common10 = require("@vue-skuilder/common");
|
|
6904
5561
|
init_common();
|
|
6905
5562
|
init_pouchdb_setup();
|
|
6906
5563
|
init_couch();
|
|
@@ -6951,14 +5608,14 @@ function getStartAndEndKeys2(key) {
|
|
|
6951
5608
|
endkey: key + "\uFFF0"
|
|
6952
5609
|
};
|
|
6953
5610
|
}
|
|
6954
|
-
var import_cross_fetch2,
|
|
5611
|
+
var import_cross_fetch2, import_moment5, import_process, isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_TIME_FORMAT2;
|
|
6955
5612
|
var init_couch = __esm({
|
|
6956
5613
|
"src/impl/couch/index.ts"() {
|
|
6957
5614
|
"use strict";
|
|
6958
5615
|
init_factory();
|
|
6959
5616
|
init_types_legacy();
|
|
6960
5617
|
import_cross_fetch2 = __toESM(require("cross-fetch"), 1);
|
|
6961
|
-
|
|
5618
|
+
import_moment5 = __toESM(require("moment"), 1);
|
|
6962
5619
|
init_logger();
|
|
6963
5620
|
init_pouchdb_setup();
|
|
6964
5621
|
import_process = __toESM(require("process"), 1);
|
|
@@ -7078,14 +5735,14 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
7078
5735
|
async function getUserClassrooms(user) {
|
|
7079
5736
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
7080
5737
|
}
|
|
7081
|
-
var
|
|
5738
|
+
var import_common12, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
7082
5739
|
var init_BaseUserDB = __esm({
|
|
7083
5740
|
"src/impl/common/BaseUserDB.ts"() {
|
|
7084
5741
|
"use strict";
|
|
7085
5742
|
init_core();
|
|
7086
5743
|
init_util();
|
|
7087
|
-
|
|
7088
|
-
|
|
5744
|
+
import_common12 = require("@vue-skuilder/common");
|
|
5745
|
+
import_moment6 = __toESM(require("moment"), 1);
|
|
7089
5746
|
init_types_legacy();
|
|
7090
5747
|
init_logger();
|
|
7091
5748
|
init_userDBHelpers();
|
|
@@ -7134,7 +5791,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7134
5791
|
);
|
|
7135
5792
|
}
|
|
7136
5793
|
const result = await this.syncStrategy.createAccount(username, password);
|
|
7137
|
-
if (result.status ===
|
|
5794
|
+
if (result.status === import_common12.Status.ok) {
|
|
7138
5795
|
log3(`Account created successfully, updating username to ${username}`);
|
|
7139
5796
|
this._username = username;
|
|
7140
5797
|
try {
|
|
@@ -7176,7 +5833,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7176
5833
|
async resetUserData() {
|
|
7177
5834
|
if (this.syncStrategy.canAuthenticate()) {
|
|
7178
5835
|
return {
|
|
7179
|
-
status:
|
|
5836
|
+
status: import_common12.Status.error,
|
|
7180
5837
|
error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
|
|
7181
5838
|
};
|
|
7182
5839
|
}
|
|
@@ -7198,11 +5855,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7198
5855
|
await localDB.bulkDocs(docsToDelete);
|
|
7199
5856
|
}
|
|
7200
5857
|
await this.init();
|
|
7201
|
-
return { status:
|
|
5858
|
+
return { status: import_common12.Status.ok };
|
|
7202
5859
|
} catch (error) {
|
|
7203
5860
|
logger.error("Failed to reset user data:", error);
|
|
7204
5861
|
return {
|
|
7205
|
-
status:
|
|
5862
|
+
status: import_common12.Status.error,
|
|
7206
5863
|
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
7207
5864
|
};
|
|
7208
5865
|
}
|
|
@@ -7349,7 +6006,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7349
6006
|
);
|
|
7350
6007
|
return reviews.rows.filter((r) => {
|
|
7351
6008
|
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
7352
|
-
const date =
|
|
6009
|
+
const date = import_moment6.default.utc(
|
|
7353
6010
|
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
7354
6011
|
REVIEW_TIME_FORMAT
|
|
7355
6012
|
);
|
|
@@ -7362,11 +6019,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7362
6019
|
}).map((r) => r.doc);
|
|
7363
6020
|
}
|
|
7364
6021
|
async getReviewsForcast(daysCount) {
|
|
7365
|
-
const time =
|
|
6022
|
+
const time = import_moment6.default.utc().add(daysCount, "days");
|
|
7366
6023
|
return this.getReviewstoDate(time);
|
|
7367
6024
|
}
|
|
7368
6025
|
async getPendingReviews(course_id) {
|
|
7369
|
-
const now =
|
|
6026
|
+
const now = import_moment6.default.utc();
|
|
7370
6027
|
return this.getReviewstoDate(now, course_id);
|
|
7371
6028
|
}
|
|
7372
6029
|
async getScheduledReviewCount(course_id) {
|
|
@@ -7653,7 +6310,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7653
6310
|
*/
|
|
7654
6311
|
async putCardRecord(record) {
|
|
7655
6312
|
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
7656
|
-
record.timeStamp =
|
|
6313
|
+
record.timeStamp = import_moment6.default.utc(record.timeStamp).toString();
|
|
7657
6314
|
try {
|
|
7658
6315
|
const cardHistory = await this.update(
|
|
7659
6316
|
cardHistoryID,
|
|
@@ -7669,7 +6326,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7669
6326
|
const ret = {
|
|
7670
6327
|
...record2
|
|
7671
6328
|
};
|
|
7672
|
-
ret.timeStamp =
|
|
6329
|
+
ret.timeStamp = import_moment6.default.utc(record2.timeStamp);
|
|
7673
6330
|
return ret;
|
|
7674
6331
|
});
|
|
7675
6332
|
return cardHistory;
|
|
@@ -7996,11 +6653,11 @@ var init_factory = __esm({
|
|
|
7996
6653
|
});
|
|
7997
6654
|
|
|
7998
6655
|
// src/study/TagFilteredContentSource.ts
|
|
7999
|
-
var
|
|
6656
|
+
var import_common14, TagFilteredContentSource;
|
|
8000
6657
|
var init_TagFilteredContentSource = __esm({
|
|
8001
6658
|
"src/study/TagFilteredContentSource.ts"() {
|
|
8002
6659
|
"use strict";
|
|
8003
|
-
|
|
6660
|
+
import_common14 = require("@vue-skuilder/common");
|
|
8004
6661
|
init_courseDB();
|
|
8005
6662
|
init_logger();
|
|
8006
6663
|
TagFilteredContentSource = class {
|
|
@@ -8086,9 +6743,9 @@ var init_TagFilteredContentSource = __esm({
|
|
|
8086
6743
|
* @returns Cards sorted by score descending (all scores = 1.0)
|
|
8087
6744
|
*/
|
|
8088
6745
|
async getWeightedCards(limit) {
|
|
8089
|
-
if (!(0,
|
|
6746
|
+
if (!(0, import_common14.hasActiveFilter)(this.filter)) {
|
|
8090
6747
|
logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
|
|
8091
|
-
return [];
|
|
6748
|
+
return { cards: [] };
|
|
8092
6749
|
}
|
|
8093
6750
|
const eligibleCardIds = await this.resolveFilteredCardIds();
|
|
8094
6751
|
const activeCards = await this.user.getActiveCards();
|
|
@@ -8140,7 +6797,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
8140
6797
|
}
|
|
8141
6798
|
]
|
|
8142
6799
|
}));
|
|
8143
|
-
return [...reviewWeighted, ...newCardWeighted].slice(0, limit);
|
|
6800
|
+
return { cards: [...reviewWeighted, ...newCardWeighted].slice(0, limit) };
|
|
8144
6801
|
}
|
|
8145
6802
|
/**
|
|
8146
6803
|
* Clears the cached resolved card IDs.
|
|
@@ -8174,19 +6831,19 @@ async function getStudySource(source, user) {
|
|
|
8174
6831
|
if (source.type === "classroom") {
|
|
8175
6832
|
return await StudentClassroomDB.factory(source.id, user);
|
|
8176
6833
|
} else {
|
|
8177
|
-
if ((0,
|
|
6834
|
+
if ((0, import_common15.hasActiveFilter)(source.tagFilter)) {
|
|
8178
6835
|
return new TagFilteredContentSource(source.id, source.tagFilter, user);
|
|
8179
6836
|
}
|
|
8180
6837
|
return getDataLayer().getCourseDB(source.id);
|
|
8181
6838
|
}
|
|
8182
6839
|
}
|
|
8183
|
-
var
|
|
6840
|
+
var import_common15;
|
|
8184
6841
|
var init_contentSource = __esm({
|
|
8185
6842
|
"src/core/interfaces/contentSource.ts"() {
|
|
8186
6843
|
"use strict";
|
|
8187
6844
|
init_factory();
|
|
8188
6845
|
init_classroomDB2();
|
|
8189
|
-
|
|
6846
|
+
import_common15 = require("@vue-skuilder/common");
|
|
8190
6847
|
init_TagFilteredContentSource();
|
|
8191
6848
|
}
|
|
8192
6849
|
});
|
|
@@ -8317,7 +6974,7 @@ elo: ${elo}`;
|
|
|
8317
6974
|
misc: {}
|
|
8318
6975
|
} : void 0
|
|
8319
6976
|
);
|
|
8320
|
-
if (result.status ===
|
|
6977
|
+
if (result.status === import_common16.Status.ok) {
|
|
8321
6978
|
return {
|
|
8322
6979
|
originalText,
|
|
8323
6980
|
status: "success",
|
|
@@ -8361,17 +7018,17 @@ function validateProcessorConfig(config) {
|
|
|
8361
7018
|
}
|
|
8362
7019
|
return { isValid: true };
|
|
8363
7020
|
}
|
|
8364
|
-
var
|
|
7021
|
+
var import_common16;
|
|
8365
7022
|
var init_cardProcessor = __esm({
|
|
8366
7023
|
"src/core/bulkImport/cardProcessor.ts"() {
|
|
8367
7024
|
"use strict";
|
|
8368
|
-
|
|
7025
|
+
import_common16 = require("@vue-skuilder/common");
|
|
8369
7026
|
init_logger();
|
|
8370
7027
|
}
|
|
8371
7028
|
});
|
|
8372
7029
|
|
|
8373
7030
|
// src/core/bulkImport/types.ts
|
|
8374
|
-
var
|
|
7031
|
+
var init_types3 = __esm({
|
|
8375
7032
|
"src/core/bulkImport/types.ts"() {
|
|
8376
7033
|
"use strict";
|
|
8377
7034
|
}
|
|
@@ -8382,7 +7039,7 @@ var init_bulkImport = __esm({
|
|
|
8382
7039
|
"src/core/bulkImport/index.ts"() {
|
|
8383
7040
|
"use strict";
|
|
8384
7041
|
init_cardProcessor();
|
|
8385
|
-
|
|
7042
|
+
init_types3();
|
|
8386
7043
|
}
|
|
8387
7044
|
});
|
|
8388
7045
|
|