@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
|
@@ -841,6 +841,81 @@ var init_PipelineDebugger = __esm({
|
|
|
841
841
|
}
|
|
842
842
|
console.groupEnd();
|
|
843
843
|
},
|
|
844
|
+
/**
|
|
845
|
+
* Show prescribed-related cards from the most recent run.
|
|
846
|
+
*
|
|
847
|
+
* Highlights:
|
|
848
|
+
* - cards directly generated by the prescribed strategy
|
|
849
|
+
* - blocked prescribed targets mentioned in provenance
|
|
850
|
+
* - support tags resolved for blocked targets
|
|
851
|
+
*
|
|
852
|
+
* @param groupId - Optional prescribed group ID filter (e.g. 'intro-core')
|
|
853
|
+
*/
|
|
854
|
+
showPrescribed(groupId) {
|
|
855
|
+
if (runHistory.length === 0) {
|
|
856
|
+
logger.info("[Pipeline Debug] No runs captured yet.");
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const run = runHistory[0];
|
|
860
|
+
const prescribedCards = run.cards.filter(
|
|
861
|
+
(c) => c.provenance.some((p) => p.strategy === "prescribed")
|
|
862
|
+
);
|
|
863
|
+
console.group(`\u{1F9ED} Prescribed Debug (${run.courseId})`);
|
|
864
|
+
if (prescribedCards.length === 0) {
|
|
865
|
+
logger.info("No prescribed-generated cards were present in the most recent run.");
|
|
866
|
+
console.groupEnd();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const rows = prescribedCards.map((card) => {
|
|
870
|
+
const prescribedProv = card.provenance.find((p) => p.strategy === "prescribed");
|
|
871
|
+
const reason = prescribedProv?.reason ?? "";
|
|
872
|
+
const parsedGroup = reason.match(/group=([^;]+)/)?.[1] ?? "unknown";
|
|
873
|
+
const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
|
|
874
|
+
const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
|
|
875
|
+
const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
|
|
876
|
+
const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
|
|
877
|
+
const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
|
|
878
|
+
return {
|
|
879
|
+
group: parsedGroup,
|
|
880
|
+
mode,
|
|
881
|
+
cardId: card.cardId,
|
|
882
|
+
selected: card.selected ? "yes" : "no",
|
|
883
|
+
finalScore: card.finalScore.toFixed(3),
|
|
884
|
+
blocked,
|
|
885
|
+
blockedTargets,
|
|
886
|
+
supportTags,
|
|
887
|
+
multiplier
|
|
888
|
+
};
|
|
889
|
+
}).filter((row) => !groupId || row.group === groupId).sort((a, b) => Number(b.finalScore) - Number(a.finalScore));
|
|
890
|
+
if (rows.length === 0) {
|
|
891
|
+
logger.info(
|
|
892
|
+
`[Pipeline Debug] No prescribed cards matched group '${groupId}' in the most recent run.`
|
|
893
|
+
);
|
|
894
|
+
console.groupEnd();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
console.table(rows);
|
|
898
|
+
const selectedRows = rows.filter((r) => r.selected === "yes");
|
|
899
|
+
const blockedTargetSet = /* @__PURE__ */ new Set();
|
|
900
|
+
const supportTagSet = /* @__PURE__ */ new Set();
|
|
901
|
+
for (const row of rows) {
|
|
902
|
+
if (row.blockedTargets && row.blockedTargets !== "none") {
|
|
903
|
+
row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
|
|
904
|
+
}
|
|
905
|
+
if (row.supportTags && row.supportTags !== "none") {
|
|
906
|
+
row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
logger.info(`Prescribed cards in run: ${rows.length}`);
|
|
910
|
+
logger.info(`Selected prescribed cards: ${selectedRows.length}`);
|
|
911
|
+
logger.info(
|
|
912
|
+
`Blocked prescribed targets referenced: ${blockedTargetSet.size > 0 ? [...blockedTargetSet].join(", ") : "none"}`
|
|
913
|
+
);
|
|
914
|
+
logger.info(
|
|
915
|
+
`Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
|
|
916
|
+
);
|
|
917
|
+
console.groupEnd();
|
|
918
|
+
},
|
|
844
919
|
/**
|
|
845
920
|
* Show all runs in compact format.
|
|
846
921
|
*/
|
|
@@ -989,6 +1064,7 @@ Commands:
|
|
|
989
1064
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
990
1065
|
.showRegistry() Show navigator registry (classes + roles)
|
|
991
1066
|
.showStrategies() Show registry + strategy mapping from last run
|
|
1067
|
+
.showPrescribed(id?) Show prescribed-generated cards and blocked/support details from last run
|
|
992
1068
|
.listRuns() List all captured runs in table format
|
|
993
1069
|
.export() Export run history as JSON for bug reports
|
|
994
1070
|
.clear() Clear run history
|
|
@@ -1012,6 +1088,44 @@ __export(CompositeGenerator_exports, {
|
|
|
1012
1088
|
AggregationMode: () => AggregationMode,
|
|
1013
1089
|
default: () => CompositeGenerator
|
|
1014
1090
|
});
|
|
1091
|
+
function mergeHints(allHints) {
|
|
1092
|
+
const defined = allHints.filter((h) => h !== void 0);
|
|
1093
|
+
if (defined.length === 0) return void 0;
|
|
1094
|
+
const merged = {};
|
|
1095
|
+
const boostTags = {};
|
|
1096
|
+
for (const hints of defined) {
|
|
1097
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
1098
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (Object.keys(boostTags).length > 0) {
|
|
1102
|
+
merged.boostTags = boostTags;
|
|
1103
|
+
}
|
|
1104
|
+
const boostCards = {};
|
|
1105
|
+
for (const hints of defined) {
|
|
1106
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
1107
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (Object.keys(boostCards).length > 0) {
|
|
1111
|
+
merged.boostCards = boostCards;
|
|
1112
|
+
}
|
|
1113
|
+
const concatUnique = (field) => {
|
|
1114
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
1115
|
+
if (values.length > 0) {
|
|
1116
|
+
merged[field] = [...new Set(values)];
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
concatUnique("requireTags");
|
|
1120
|
+
concatUnique("requireCards");
|
|
1121
|
+
concatUnique("excludeTags");
|
|
1122
|
+
concatUnique("excludeCards");
|
|
1123
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
1124
|
+
if (labels.length > 0) {
|
|
1125
|
+
merged._label = labels.join("; ");
|
|
1126
|
+
}
|
|
1127
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
1128
|
+
}
|
|
1015
1129
|
var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
|
|
1016
1130
|
var init_CompositeGenerator = __esm({
|
|
1017
1131
|
"src/core/navigators/generators/CompositeGenerator.ts"() {
|
|
@@ -1075,17 +1189,18 @@ var init_CompositeGenerator = __esm({
|
|
|
1075
1189
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
1076
1190
|
);
|
|
1077
1191
|
const generatorSummaries = [];
|
|
1078
|
-
results.forEach((
|
|
1192
|
+
results.forEach((result, index) => {
|
|
1193
|
+
const cards2 = result.cards;
|
|
1079
1194
|
const gen = this.generators[index];
|
|
1080
1195
|
const genName = gen.name || `Generator ${index}`;
|
|
1081
|
-
const newCards =
|
|
1082
|
-
const reviewCards =
|
|
1083
|
-
if (
|
|
1084
|
-
const topScore = Math.max(...
|
|
1196
|
+
const newCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("new card"));
|
|
1197
|
+
const reviewCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("review"));
|
|
1198
|
+
if (cards2.length > 0) {
|
|
1199
|
+
const topScore = Math.max(...cards2.map((c) => c.score)).toFixed(2);
|
|
1085
1200
|
const parts = [];
|
|
1086
1201
|
if (newCards.length > 0) parts.push(`${newCards.length} new`);
|
|
1087
1202
|
if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
|
|
1088
|
-
const breakdown = parts.length > 0 ? parts.join(", ") : `${
|
|
1203
|
+
const breakdown = parts.length > 0 ? parts.join(", ") : `${cards2.length} cards`;
|
|
1089
1204
|
generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
|
|
1090
1205
|
} else {
|
|
1091
1206
|
generatorSummaries.push(`${genName}: 0 cards`);
|
|
@@ -1093,7 +1208,8 @@ var init_CompositeGenerator = __esm({
|
|
|
1093
1208
|
});
|
|
1094
1209
|
logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
|
|
1095
1210
|
const byCardId = /* @__PURE__ */ new Map();
|
|
1096
|
-
results.forEach((
|
|
1211
|
+
results.forEach((result, index) => {
|
|
1212
|
+
const cards2 = result.cards;
|
|
1097
1213
|
const gen = this.generators[index];
|
|
1098
1214
|
let weight = gen.learnable?.weight ?? 1;
|
|
1099
1215
|
let deviation;
|
|
@@ -1104,7 +1220,7 @@ var init_CompositeGenerator = __esm({
|
|
|
1104
1220
|
deviation = context.orchestration.getDeviation(strategyId);
|
|
1105
1221
|
}
|
|
1106
1222
|
}
|
|
1107
|
-
for (const card of
|
|
1223
|
+
for (const card of cards2) {
|
|
1108
1224
|
if (card.provenance.length > 0) {
|
|
1109
1225
|
card.provenance[0].effectiveWeight = weight;
|
|
1110
1226
|
card.provenance[0].deviation = deviation;
|
|
@@ -1116,15 +1232,15 @@ var init_CompositeGenerator = __esm({
|
|
|
1116
1232
|
});
|
|
1117
1233
|
const merged = [];
|
|
1118
1234
|
for (const [, items] of byCardId) {
|
|
1119
|
-
const
|
|
1235
|
+
const cards2 = items.map((i) => i.card);
|
|
1120
1236
|
const aggregatedScore = this.aggregateScores(items);
|
|
1121
1237
|
const finalScore = Math.min(1, aggregatedScore);
|
|
1122
|
-
const mergedProvenance =
|
|
1123
|
-
const initialScore =
|
|
1238
|
+
const mergedProvenance = cards2.flatMap((c) => c.provenance);
|
|
1239
|
+
const initialScore = cards2[0].score;
|
|
1124
1240
|
const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
|
|
1125
1241
|
const reason = this.buildAggregationReason(items, finalScore);
|
|
1126
1242
|
merged.push({
|
|
1127
|
-
...
|
|
1243
|
+
...cards2[0],
|
|
1128
1244
|
score: finalScore,
|
|
1129
1245
|
provenance: [
|
|
1130
1246
|
...mergedProvenance,
|
|
@@ -1139,7 +1255,9 @@ var init_CompositeGenerator = __esm({
|
|
|
1139
1255
|
]
|
|
1140
1256
|
});
|
|
1141
1257
|
}
|
|
1142
|
-
|
|
1258
|
+
const cards = merged.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1259
|
+
const hints = mergeHints(results.map((result) => result.hints));
|
|
1260
|
+
return { cards, hints };
|
|
1143
1261
|
}
|
|
1144
1262
|
/**
|
|
1145
1263
|
* Build human-readable reason for score aggregation.
|
|
@@ -1270,16 +1388,16 @@ var init_elo = __esm({
|
|
|
1270
1388
|
};
|
|
1271
1389
|
});
|
|
1272
1390
|
scored.sort((a, b) => b.score - a.score);
|
|
1273
|
-
const
|
|
1274
|
-
if (
|
|
1275
|
-
const topScores =
|
|
1391
|
+
const cards = scored.slice(0, limit);
|
|
1392
|
+
if (cards.length > 0) {
|
|
1393
|
+
const topScores = cards.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
|
|
1276
1394
|
logger.info(
|
|
1277
|
-
`[ELO] Course ${this.course.getCourseID()}: ${
|
|
1395
|
+
`[ELO] Course ${this.course.getCourseID()}: ${cards.length} new cards (top scores: ${topScores})`
|
|
1278
1396
|
);
|
|
1279
1397
|
} else {
|
|
1280
1398
|
logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
|
|
1281
1399
|
}
|
|
1282
|
-
return
|
|
1400
|
+
return { cards };
|
|
1283
1401
|
}
|
|
1284
1402
|
};
|
|
1285
1403
|
}
|
|
@@ -1316,7 +1434,7 @@ function matchesTagPattern(tag, pattern) {
|
|
|
1316
1434
|
function pickTopByScore(cards, limit) {
|
|
1317
1435
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1318
1436
|
}
|
|
1319
|
-
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;
|
|
1437
|
+
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;
|
|
1320
1438
|
var init_prescribed = __esm({
|
|
1321
1439
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1322
1440
|
"use strict";
|
|
@@ -1333,6 +1451,7 @@ var init_prescribed = __esm({
|
|
|
1333
1451
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1334
1452
|
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1335
1453
|
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1454
|
+
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
|
|
1336
1455
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1337
1456
|
name;
|
|
1338
1457
|
config;
|
|
@@ -1349,7 +1468,7 @@ var init_prescribed = __esm({
|
|
|
1349
1468
|
}
|
|
1350
1469
|
async getWeightedCards(limit, context) {
|
|
1351
1470
|
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1352
|
-
return [];
|
|
1471
|
+
return { cards: [] };
|
|
1353
1472
|
}
|
|
1354
1473
|
const courseId = this.course.getCourseID();
|
|
1355
1474
|
const activeCards = await this.user.getActiveCards();
|
|
@@ -1374,6 +1493,7 @@ var init_prescribed = __esm({
|
|
|
1374
1493
|
};
|
|
1375
1494
|
const emitted = [];
|
|
1376
1495
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
1496
|
+
const groupRuntimes = [];
|
|
1377
1497
|
for (const group of this.config.groups) {
|
|
1378
1498
|
const runtime = this.buildGroupRuntimeState({
|
|
1379
1499
|
group,
|
|
@@ -1385,6 +1505,7 @@ var init_prescribed = __esm({
|
|
|
1385
1505
|
userTagElo,
|
|
1386
1506
|
userGlobalElo
|
|
1387
1507
|
});
|
|
1508
|
+
groupRuntimes.push(runtime);
|
|
1388
1509
|
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1389
1510
|
const directCards = this.buildDirectTargetCards(
|
|
1390
1511
|
runtime,
|
|
@@ -1398,12 +1519,17 @@ var init_prescribed = __esm({
|
|
|
1398
1519
|
);
|
|
1399
1520
|
emitted.push(...directCards, ...supportCards);
|
|
1400
1521
|
}
|
|
1522
|
+
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1523
|
+
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1524
|
+
boostTags: hintSummary.boostTags,
|
|
1525
|
+
_label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
|
|
1526
|
+
} : void 0;
|
|
1401
1527
|
if (emitted.length === 0) {
|
|
1402
1528
|
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1403
1529
|
await this.putStrategyState(nextState).catch((e) => {
|
|
1404
1530
|
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1405
1531
|
});
|
|
1406
|
-
return [];
|
|
1532
|
+
return hints ? { cards: [], hints } : { cards: [] };
|
|
1407
1533
|
}
|
|
1408
1534
|
const finalCards = pickTopByScore(emitted, limit);
|
|
1409
1535
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
@@ -1434,7 +1560,27 @@ var init_prescribed = __esm({
|
|
|
1434
1560
|
logger.info(
|
|
1435
1561
|
`[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)`
|
|
1436
1562
|
);
|
|
1437
|
-
return finalCards;
|
|
1563
|
+
return hints ? { cards: finalCards, hints } : { cards: finalCards };
|
|
1564
|
+
}
|
|
1565
|
+
buildSupportHintSummary(groupRuntimes) {
|
|
1566
|
+
const boostTags = {};
|
|
1567
|
+
const blockedTargetIds = /* @__PURE__ */ new Set();
|
|
1568
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1569
|
+
for (const runtime of groupRuntimes) {
|
|
1570
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportTags.length === 0) {
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
runtime.blockedTargets.forEach((cardId) => blockedTargetIds.add(cardId));
|
|
1574
|
+
for (const tag of runtime.supportTags) {
|
|
1575
|
+
supportTags.add(tag);
|
|
1576
|
+
boostTags[tag] = (boostTags[tag] ?? 1) * runtime.supportMultiplier;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
boostTags,
|
|
1581
|
+
blockedTargetIds: [...blockedTargetIds].sort(),
|
|
1582
|
+
supportTags: [...supportTags].sort()
|
|
1583
|
+
};
|
|
1438
1584
|
}
|
|
1439
1585
|
parseConfig(serializedData) {
|
|
1440
1586
|
try {
|
|
@@ -1517,7 +1663,22 @@ var init_prescribed = __esm({
|
|
|
1517
1663
|
group.hierarchyWalk?.enabled !== false,
|
|
1518
1664
|
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1519
1665
|
);
|
|
1520
|
-
|
|
1666
|
+
const introTags = tags.filter((tag) => tag.startsWith("gpc:intro:"));
|
|
1667
|
+
const exposeTags = new Set(tags.filter((tag) => tag.startsWith("gpc:expose:")));
|
|
1668
|
+
for (const introTag of introTags) {
|
|
1669
|
+
const suffix = introTag.slice("gpc:intro:".length);
|
|
1670
|
+
if (suffix) {
|
|
1671
|
+
exposeTags.add(`gpc:expose:${suffix}`);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const unmetExposeTags = [...exposeTags].filter((tag) => {
|
|
1675
|
+
const tagElo = userTagElo[tag];
|
|
1676
|
+
return !tagElo || tagElo.count < DEFAULT_MIN_COUNT;
|
|
1677
|
+
});
|
|
1678
|
+
if (unmetExposeTags.length > 0) {
|
|
1679
|
+
unmetExposeTags.forEach((tag) => supportTags.add(tag));
|
|
1680
|
+
}
|
|
1681
|
+
if (resolution.blocked || unmetExposeTags.length > 0) {
|
|
1521
1682
|
blockedTargets.push(cardId);
|
|
1522
1683
|
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1523
1684
|
} else {
|
|
@@ -1547,7 +1708,8 @@ var init_prescribed = __esm({
|
|
|
1547
1708
|
supportCandidates,
|
|
1548
1709
|
supportTags: [...supportTags],
|
|
1549
1710
|
pressureMultiplier,
|
|
1550
|
-
supportMultiplier
|
|
1711
|
+
supportMultiplier,
|
|
1712
|
+
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
1551
1713
|
};
|
|
1552
1714
|
}
|
|
1553
1715
|
buildNextGroupState(runtime, prior) {
|
|
@@ -1555,6 +1717,7 @@ var init_prescribed = __esm({
|
|
|
1555
1717
|
const surfacedThisRun = false;
|
|
1556
1718
|
return {
|
|
1557
1719
|
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1720
|
+
pendingTargetIds: [...runtime.pendingTargets].sort(),
|
|
1558
1721
|
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1559
1722
|
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1560
1723
|
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
@@ -1579,7 +1742,7 @@ var init_prescribed = __esm({
|
|
|
1579
1742
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1580
1743
|
action: "generated",
|
|
1581
1744
|
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1582
|
-
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};multiplier=${runtime.pressureMultiplier.toFixed(2)}`
|
|
1745
|
+
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}`
|
|
1583
1746
|
}
|
|
1584
1747
|
]
|
|
1585
1748
|
});
|
|
@@ -1606,7 +1769,7 @@ var init_prescribed = __esm({
|
|
|
1606
1769
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1607
1770
|
action: "generated",
|
|
1608
1771
|
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1609
|
-
reason: `mode=support;group=${runtime.group.id};blocked=${runtime.blockedTargets.length};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)}`
|
|
1772
|
+
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}`
|
|
1610
1773
|
}
|
|
1611
1774
|
]
|
|
1612
1775
|
});
|
|
@@ -1636,35 +1799,43 @@ var init_prescribed = __esm({
|
|
|
1636
1799
|
return [...candidates];
|
|
1637
1800
|
}
|
|
1638
1801
|
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1639
|
-
if (!hierarchyWalkEnabled || targetTags.length === 0 || hierarchyConfigs.length === 0) {
|
|
1640
|
-
return {
|
|
1641
|
-
blocked: false,
|
|
1642
|
-
supportTags: []
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
1802
|
const supportTags = /* @__PURE__ */ new Set();
|
|
1646
1803
|
let blocked = false;
|
|
1647
1804
|
for (const targetTag of targetTags) {
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
)
|
|
1654
|
-
|
|
1655
|
-
|
|
1805
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[targetTag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
1806
|
+
if (prereqSets.length === 0) {
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
const tagBlocked = prereqSets.some(
|
|
1810
|
+
(prereqs) => prereqs.some((pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
1811
|
+
);
|
|
1812
|
+
if (!tagBlocked) {
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
blocked = true;
|
|
1816
|
+
if (!hierarchyWalkEnabled) {
|
|
1817
|
+
for (const prereqs of prereqSets) {
|
|
1818
|
+
for (const prereq of prereqs) {
|
|
1819
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1820
|
+
supportTags.add(prereq.tag);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1656
1823
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
for (const prereqs of prereqSets) {
|
|
1827
|
+
for (const prereq of prereqs) {
|
|
1828
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1829
|
+
this.collectSupportTagsRecursive(
|
|
1830
|
+
prereq.tag,
|
|
1831
|
+
hierarchyConfigs,
|
|
1832
|
+
userTagElo,
|
|
1833
|
+
userGlobalElo,
|
|
1834
|
+
maxDepth,
|
|
1835
|
+
/* @__PURE__ */ new Set(),
|
|
1836
|
+
supportTags
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1668
1839
|
}
|
|
1669
1840
|
}
|
|
1670
1841
|
}
|
|
@@ -1819,7 +1990,7 @@ var init_srs = __esm({
|
|
|
1819
1990
|
]
|
|
1820
1991
|
};
|
|
1821
1992
|
});
|
|
1822
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1993
|
+
return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
1823
1994
|
}
|
|
1824
1995
|
/**
|
|
1825
1996
|
* Compute backlog pressure based on number of due reviews.
|
|
@@ -2864,1736 +3035,146 @@ var init_types2 = __esm({
|
|
|
2864
3035
|
}
|
|
2865
3036
|
});
|
|
2866
3037
|
|
|
2867
|
-
// src/core/navigators/filters/userGoalStub.ts
|
|
2868
|
-
var userGoalStub_exports = {};
|
|
2869
|
-
__export(userGoalStub_exports, {
|
|
2870
|
-
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2871
|
-
});
|
|
2872
|
-
var USER_GOAL_NAVIGATOR_STUB;
|
|
2873
|
-
var init_userGoalStub = __esm({
|
|
2874
|
-
"src/core/navigators/filters/userGoalStub.ts"() {
|
|
2875
|
-
"use strict";
|
|
2876
|
-
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2877
|
-
}
|
|
2878
|
-
});
|
|
2879
|
-
|
|
2880
|
-
// import("./filters/**/*") in src/core/navigators/index.ts
|
|
2881
|
-
var globImport_filters;
|
|
2882
|
-
var init_2 = __esm({
|
|
2883
|
-
'import("./filters/**/*") in src/core/navigators/index.ts'() {
|
|
2884
|
-
globImport_filters = __glob({
|
|
2885
|
-
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
2886
|
-
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2887
|
-
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2888
|
-
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2889
|
-
"./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
|
|
2890
|
-
"./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2891
|
-
"./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2892
|
-
"./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2893
|
-
"./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
|
|
2894
|
-
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
|
|
2895
|
-
});
|
|
2896
|
-
}
|
|
2897
|
-
});
|
|
2898
|
-
|
|
2899
|
-
// src/core/orchestration/gradient.ts
|
|
2900
|
-
var init_gradient = __esm({
|
|
2901
|
-
"src/core/orchestration/gradient.ts"() {
|
|
2902
|
-
"use strict";
|
|
2903
|
-
init_logger();
|
|
2904
|
-
}
|
|
2905
|
-
});
|
|
2906
|
-
|
|
2907
|
-
// src/core/orchestration/learning.ts
|
|
2908
|
-
var init_learning = __esm({
|
|
2909
|
-
"src/core/orchestration/learning.ts"() {
|
|
2910
|
-
"use strict";
|
|
2911
|
-
init_contentNavigationStrategy();
|
|
2912
|
-
init_types_legacy();
|
|
2913
|
-
init_logger();
|
|
2914
|
-
}
|
|
2915
|
-
});
|
|
2916
|
-
|
|
2917
|
-
// src/core/orchestration/signal.ts
|
|
2918
|
-
var init_signal = __esm({
|
|
2919
|
-
"src/core/orchestration/signal.ts"() {
|
|
2920
|
-
"use strict";
|
|
2921
|
-
}
|
|
2922
|
-
});
|
|
2923
|
-
|
|
2924
|
-
// src/core/orchestration/recording.ts
|
|
2925
|
-
var init_recording = __esm({
|
|
2926
|
-
"src/core/orchestration/recording.ts"() {
|
|
2927
|
-
"use strict";
|
|
2928
|
-
init_signal();
|
|
2929
|
-
init_types_legacy();
|
|
2930
|
-
init_logger();
|
|
2931
|
-
}
|
|
2932
|
-
});
|
|
2933
|
-
|
|
2934
|
-
// src/core/orchestration/index.ts
|
|
2935
|
-
function fnv1a(str) {
|
|
2936
|
-
let hash = 2166136261;
|
|
2937
|
-
for (let i = 0; i < str.length; i++) {
|
|
2938
|
-
hash ^= str.charCodeAt(i);
|
|
2939
|
-
hash = Math.imul(hash, 16777619);
|
|
2940
|
-
}
|
|
2941
|
-
return hash >>> 0;
|
|
2942
|
-
}
|
|
2943
|
-
function computeDeviation(userId, strategyId, salt) {
|
|
2944
|
-
const input = `${userId}:${strategyId}:${salt}`;
|
|
2945
|
-
const hash = fnv1a(input);
|
|
2946
|
-
const normalized = hash / 4294967296;
|
|
2947
|
-
return normalized * 2 - 1;
|
|
2948
|
-
}
|
|
2949
|
-
function computeSpread(confidence) {
|
|
2950
|
-
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
2951
|
-
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
2952
|
-
}
|
|
2953
|
-
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
2954
|
-
const deviation = computeDeviation(userId, strategyId, salt);
|
|
2955
|
-
const spread = computeSpread(learnable.confidence);
|
|
2956
|
-
const adjustment = deviation * spread * learnable.weight;
|
|
2957
|
-
const effective = learnable.weight + adjustment;
|
|
2958
|
-
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
2959
|
-
}
|
|
2960
|
-
async function createOrchestrationContext(user, course) {
|
|
2961
|
-
let courseConfig;
|
|
2962
|
-
try {
|
|
2963
|
-
courseConfig = await course.getCourseConfig();
|
|
2964
|
-
} catch (e) {
|
|
2965
|
-
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
2966
|
-
courseConfig = {
|
|
2967
|
-
name: "Unknown",
|
|
2968
|
-
description: "",
|
|
2969
|
-
public: false,
|
|
2970
|
-
deleted: false,
|
|
2971
|
-
creator: "",
|
|
2972
|
-
admins: [],
|
|
2973
|
-
moderators: [],
|
|
2974
|
-
dataShapes: [],
|
|
2975
|
-
questionTypes: [],
|
|
2976
|
-
orchestration: { salt: "default" }
|
|
2977
|
-
};
|
|
2978
|
-
}
|
|
2979
|
-
const userId = user.getUsername();
|
|
2980
|
-
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
2981
|
-
return {
|
|
2982
|
-
user,
|
|
2983
|
-
course,
|
|
2984
|
-
userId,
|
|
2985
|
-
courseConfig,
|
|
2986
|
-
getEffectiveWeight(strategyId, learnable) {
|
|
2987
|
-
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
2988
|
-
},
|
|
2989
|
-
getDeviation(strategyId) {
|
|
2990
|
-
return computeDeviation(userId, strategyId, salt);
|
|
2991
|
-
}
|
|
2992
|
-
};
|
|
2993
|
-
}
|
|
2994
|
-
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
2995
|
-
var init_orchestration = __esm({
|
|
2996
|
-
"src/core/orchestration/index.ts"() {
|
|
2997
|
-
"use strict";
|
|
2998
|
-
init_logger();
|
|
2999
|
-
init_gradient();
|
|
3000
|
-
init_learning();
|
|
3001
|
-
init_signal();
|
|
3002
|
-
init_recording();
|
|
3003
|
-
MIN_SPREAD = 0.1;
|
|
3004
|
-
MAX_SPREAD = 0.5;
|
|
3005
|
-
MIN_WEIGHT = 0.1;
|
|
3006
|
-
MAX_WEIGHT = 3;
|
|
3007
|
-
}
|
|
3008
|
-
});
|
|
3009
|
-
|
|
3010
|
-
// src/core/util/index.ts
|
|
3011
|
-
function getCardHistoryID(courseID, cardID) {
|
|
3012
|
-
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
3013
|
-
}
|
|
3014
|
-
var init_util = __esm({
|
|
3015
|
-
"src/core/util/index.ts"() {
|
|
3016
|
-
"use strict";
|
|
3017
|
-
init_types_legacy();
|
|
3018
|
-
}
|
|
3019
|
-
});
|
|
3020
|
-
|
|
3021
|
-
// src/study/SpacedRepetition.ts
|
|
3022
|
-
import moment2 from "moment";
|
|
3023
|
-
import { isTaggedPerformance } from "@vue-skuilder/common";
|
|
3024
|
-
var duration;
|
|
3025
|
-
var init_SpacedRepetition = __esm({
|
|
3026
|
-
"src/study/SpacedRepetition.ts"() {
|
|
3027
|
-
"use strict";
|
|
3028
|
-
init_util();
|
|
3029
|
-
init_logger();
|
|
3030
|
-
duration = moment2.duration;
|
|
3031
|
-
}
|
|
3032
|
-
});
|
|
3033
|
-
|
|
3034
|
-
// src/study/services/SrsService.ts
|
|
3035
|
-
import moment3 from "moment";
|
|
3036
|
-
var init_SrsService = __esm({
|
|
3037
|
-
"src/study/services/SrsService.ts"() {
|
|
3038
|
-
"use strict";
|
|
3039
|
-
init_couch();
|
|
3040
|
-
init_SpacedRepetition();
|
|
3041
|
-
init_logger();
|
|
3042
|
-
}
|
|
3043
|
-
});
|
|
3044
|
-
|
|
3045
|
-
// src/study/services/EloService.ts
|
|
3046
|
-
import {
|
|
3047
|
-
adjustCourseScores,
|
|
3048
|
-
adjustCourseScoresPerTag,
|
|
3049
|
-
toCourseElo as toCourseElo5
|
|
3050
|
-
} from "@vue-skuilder/common";
|
|
3051
|
-
var init_EloService = __esm({
|
|
3052
|
-
"src/study/services/EloService.ts"() {
|
|
3053
|
-
"use strict";
|
|
3054
|
-
init_logger();
|
|
3055
|
-
}
|
|
3056
|
-
});
|
|
3057
|
-
|
|
3058
|
-
// src/study/services/ResponseProcessor.ts
|
|
3059
|
-
import { isTaggedPerformance as isTaggedPerformance2 } from "@vue-skuilder/common";
|
|
3060
|
-
var init_ResponseProcessor = __esm({
|
|
3061
|
-
"src/study/services/ResponseProcessor.ts"() {
|
|
3062
|
-
"use strict";
|
|
3063
|
-
init_core();
|
|
3064
|
-
init_logger();
|
|
3065
|
-
}
|
|
3066
|
-
});
|
|
3067
|
-
|
|
3068
|
-
// src/study/services/CardHydrationService.ts
|
|
3069
|
-
import {
|
|
3070
|
-
displayableDataToViewData,
|
|
3071
|
-
isCourseElo,
|
|
3072
|
-
toCourseElo as toCourseElo6
|
|
3073
|
-
} from "@vue-skuilder/common";
|
|
3074
|
-
var init_CardHydrationService = __esm({
|
|
3075
|
-
"src/study/services/CardHydrationService.ts"() {
|
|
3076
|
-
"use strict";
|
|
3077
|
-
init_logger();
|
|
3078
|
-
}
|
|
3079
|
-
});
|
|
3080
|
-
|
|
3081
|
-
// src/study/ItemQueue.ts
|
|
3082
|
-
var init_ItemQueue = __esm({
|
|
3083
|
-
"src/study/ItemQueue.ts"() {
|
|
3084
|
-
"use strict";
|
|
3085
|
-
}
|
|
3086
|
-
});
|
|
3087
|
-
|
|
3088
|
-
// src/util/packer/types.ts
|
|
3089
|
-
var init_types3 = __esm({
|
|
3090
|
-
"src/util/packer/types.ts"() {
|
|
3091
|
-
"use strict";
|
|
3092
|
-
}
|
|
3093
|
-
});
|
|
3094
|
-
|
|
3095
|
-
// src/util/packer/CouchDBToStaticPacker.ts
|
|
3096
|
-
var init_CouchDBToStaticPacker = __esm({
|
|
3097
|
-
"src/util/packer/CouchDBToStaticPacker.ts"() {
|
|
3098
|
-
"use strict";
|
|
3099
|
-
init_types_legacy();
|
|
3100
|
-
init_logger();
|
|
3101
|
-
}
|
|
3102
|
-
});
|
|
3103
|
-
|
|
3104
|
-
// src/util/packer/index.ts
|
|
3105
|
-
var init_packer = __esm({
|
|
3106
|
-
"src/util/packer/index.ts"() {
|
|
3107
|
-
"use strict";
|
|
3108
|
-
init_types3();
|
|
3109
|
-
init_CouchDBToStaticPacker();
|
|
3110
|
-
}
|
|
3111
|
-
});
|
|
3112
|
-
|
|
3113
|
-
// src/util/migrator/types.ts
|
|
3114
|
-
var DEFAULT_MIGRATION_OPTIONS;
|
|
3115
|
-
var init_types4 = __esm({
|
|
3116
|
-
"src/util/migrator/types.ts"() {
|
|
3117
|
-
"use strict";
|
|
3118
|
-
DEFAULT_MIGRATION_OPTIONS = {
|
|
3119
|
-
chunkBatchSize: 100,
|
|
3120
|
-
validateRoundTrip: false,
|
|
3121
|
-
cleanupOnFailure: true,
|
|
3122
|
-
timeout: 3e5
|
|
3123
|
-
// 5 minutes
|
|
3124
|
-
};
|
|
3125
|
-
}
|
|
3126
|
-
});
|
|
3127
|
-
|
|
3128
|
-
// src/util/migrator/FileSystemAdapter.ts
|
|
3129
|
-
var FileSystemError;
|
|
3130
|
-
var init_FileSystemAdapter = __esm({
|
|
3131
|
-
"src/util/migrator/FileSystemAdapter.ts"() {
|
|
3132
|
-
"use strict";
|
|
3133
|
-
FileSystemError = class extends Error {
|
|
3134
|
-
constructor(message, operation, filePath, cause) {
|
|
3135
|
-
super(message);
|
|
3136
|
-
this.operation = operation;
|
|
3137
|
-
this.filePath = filePath;
|
|
3138
|
-
this.cause = cause;
|
|
3139
|
-
this.name = "FileSystemError";
|
|
3140
|
-
}
|
|
3141
|
-
};
|
|
3142
|
-
}
|
|
3143
|
-
});
|
|
3144
|
-
|
|
3145
|
-
// src/util/migrator/validation.ts
|
|
3146
|
-
async function validateStaticCourse(staticPath, fs) {
|
|
3147
|
-
const validation = {
|
|
3148
|
-
valid: true,
|
|
3149
|
-
manifestExists: false,
|
|
3150
|
-
chunksExist: false,
|
|
3151
|
-
attachmentsExist: false,
|
|
3152
|
-
errors: [],
|
|
3153
|
-
warnings: []
|
|
3154
|
-
};
|
|
3155
|
-
try {
|
|
3156
|
-
if (fs) {
|
|
3157
|
-
const stats = await fs.stat(staticPath);
|
|
3158
|
-
if (!stats.isDirectory()) {
|
|
3159
|
-
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3160
|
-
validation.valid = false;
|
|
3161
|
-
return validation;
|
|
3162
|
-
}
|
|
3163
|
-
} else if (!nodeFS) {
|
|
3164
|
-
validation.errors.push("File system access not available - validation skipped");
|
|
3165
|
-
validation.valid = false;
|
|
3166
|
-
return validation;
|
|
3167
|
-
} else {
|
|
3168
|
-
const stats = await nodeFS.promises.stat(staticPath);
|
|
3169
|
-
if (!stats.isDirectory()) {
|
|
3170
|
-
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3171
|
-
validation.valid = false;
|
|
3172
|
-
return validation;
|
|
3173
|
-
}
|
|
3174
|
-
}
|
|
3175
|
-
let manifestPath = `${staticPath}/manifest.json`;
|
|
3176
|
-
try {
|
|
3177
|
-
if (fs) {
|
|
3178
|
-
manifestPath = fs.joinPath(staticPath, "manifest.json");
|
|
3179
|
-
if (await fs.exists(manifestPath)) {
|
|
3180
|
-
validation.manifestExists = true;
|
|
3181
|
-
const manifestContent = await fs.readFile(manifestPath);
|
|
3182
|
-
const manifest = JSON.parse(manifestContent);
|
|
3183
|
-
validation.courseId = manifest.courseId;
|
|
3184
|
-
validation.courseName = manifest.courseName;
|
|
3185
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3186
|
-
validation.errors.push("Invalid manifest structure");
|
|
3187
|
-
validation.valid = false;
|
|
3188
|
-
}
|
|
3189
|
-
} else {
|
|
3190
|
-
validation.errors.push(`Manifest not found: ${manifestPath}`);
|
|
3191
|
-
validation.valid = false;
|
|
3192
|
-
}
|
|
3193
|
-
} else {
|
|
3194
|
-
manifestPath = `${staticPath}/manifest.json`;
|
|
3195
|
-
await nodeFS.promises.access(manifestPath);
|
|
3196
|
-
validation.manifestExists = true;
|
|
3197
|
-
const manifestContent = await nodeFS.promises.readFile(manifestPath, "utf8");
|
|
3198
|
-
const manifest = JSON.parse(manifestContent);
|
|
3199
|
-
validation.courseId = manifest.courseId;
|
|
3200
|
-
validation.courseName = manifest.courseName;
|
|
3201
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3202
|
-
validation.errors.push("Invalid manifest structure");
|
|
3203
|
-
validation.valid = false;
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
} catch (error) {
|
|
3207
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Manifest not found or invalid: ${manifestPath}`;
|
|
3208
|
-
validation.errors.push(errorMessage);
|
|
3209
|
-
validation.valid = false;
|
|
3210
|
-
}
|
|
3211
|
-
let chunksPath = `${staticPath}/chunks`;
|
|
3212
|
-
try {
|
|
3213
|
-
if (fs) {
|
|
3214
|
-
chunksPath = fs.joinPath(staticPath, "chunks");
|
|
3215
|
-
if (await fs.exists(chunksPath)) {
|
|
3216
|
-
const chunksStats = await fs.stat(chunksPath);
|
|
3217
|
-
if (chunksStats.isDirectory()) {
|
|
3218
|
-
validation.chunksExist = true;
|
|
3219
|
-
} else {
|
|
3220
|
-
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3221
|
-
validation.valid = false;
|
|
3222
|
-
}
|
|
3223
|
-
} else {
|
|
3224
|
-
validation.errors.push(`Chunks directory not found: ${chunksPath}`);
|
|
3225
|
-
validation.valid = false;
|
|
3226
|
-
}
|
|
3227
|
-
} else {
|
|
3228
|
-
chunksPath = `${staticPath}/chunks`;
|
|
3229
|
-
const chunksStats = await nodeFS.promises.stat(chunksPath);
|
|
3230
|
-
if (chunksStats.isDirectory()) {
|
|
3231
|
-
validation.chunksExist = true;
|
|
3232
|
-
} else {
|
|
3233
|
-
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3234
|
-
validation.valid = false;
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
} catch (error) {
|
|
3238
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Chunks directory not found: ${chunksPath}`;
|
|
3239
|
-
validation.errors.push(errorMessage);
|
|
3240
|
-
validation.valid = false;
|
|
3241
|
-
}
|
|
3242
|
-
let attachmentsPath;
|
|
3243
|
-
try {
|
|
3244
|
-
if (fs) {
|
|
3245
|
-
attachmentsPath = fs.joinPath(staticPath, "attachments");
|
|
3246
|
-
if (await fs.exists(attachmentsPath)) {
|
|
3247
|
-
const attachmentsStats = await fs.stat(attachmentsPath);
|
|
3248
|
-
if (attachmentsStats.isDirectory()) {
|
|
3249
|
-
validation.attachmentsExist = true;
|
|
3250
|
-
}
|
|
3251
|
-
} else {
|
|
3252
|
-
validation.warnings.push(
|
|
3253
|
-
`Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
|
|
3254
|
-
);
|
|
3255
|
-
}
|
|
3256
|
-
} else {
|
|
3257
|
-
attachmentsPath = `${staticPath}/attachments`;
|
|
3258
|
-
const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
|
|
3259
|
-
if (attachmentsStats.isDirectory()) {
|
|
3260
|
-
validation.attachmentsExist = true;
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
} catch (error) {
|
|
3264
|
-
attachmentsPath = attachmentsPath || `${staticPath}/attachments`;
|
|
3265
|
-
const warningMessage = error instanceof FileSystemError ? error.message : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
|
|
3266
|
-
validation.warnings.push(warningMessage);
|
|
3267
|
-
}
|
|
3268
|
-
} catch (error) {
|
|
3269
|
-
validation.errors.push(
|
|
3270
|
-
`Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
|
|
3271
|
-
);
|
|
3272
|
-
validation.valid = false;
|
|
3273
|
-
}
|
|
3274
|
-
return validation;
|
|
3275
|
-
}
|
|
3276
|
-
async function validateMigration(targetDB, expectedCounts, manifest) {
|
|
3277
|
-
const validation = {
|
|
3278
|
-
valid: true,
|
|
3279
|
-
documentCountMatch: false,
|
|
3280
|
-
attachmentIntegrity: false,
|
|
3281
|
-
viewFunctionality: false,
|
|
3282
|
-
issues: []
|
|
3283
|
-
};
|
|
3284
|
-
try {
|
|
3285
|
-
logger.info("Starting migration validation...");
|
|
3286
|
-
const actualCounts = await getActualDocumentCounts(targetDB);
|
|
3287
|
-
validation.documentCountMatch = compareDocumentCounts(
|
|
3288
|
-
expectedCounts,
|
|
3289
|
-
actualCounts,
|
|
3290
|
-
validation.issues
|
|
3291
|
-
);
|
|
3292
|
-
await validateCourseConfig(targetDB, manifest, validation.issues);
|
|
3293
|
-
validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
|
|
3294
|
-
validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
|
|
3295
|
-
validation.valid = validation.documentCountMatch && validation.viewFunctionality && validation.attachmentIntegrity;
|
|
3296
|
-
logger.info(`Migration validation completed. Valid: ${validation.valid}`);
|
|
3297
|
-
if (validation.issues.length > 0) {
|
|
3298
|
-
logger.info(`Validation issues: ${validation.issues.length}`);
|
|
3299
|
-
validation.issues.forEach((issue) => {
|
|
3300
|
-
if (issue.type === "error") {
|
|
3301
|
-
logger.error(`${issue.category}: ${issue.message}`);
|
|
3302
|
-
} else {
|
|
3303
|
-
logger.warn(`${issue.category}: ${issue.message}`);
|
|
3304
|
-
}
|
|
3305
|
-
});
|
|
3306
|
-
}
|
|
3307
|
-
} catch (error) {
|
|
3308
|
-
validation.valid = false;
|
|
3309
|
-
validation.issues.push({
|
|
3310
|
-
type: "error",
|
|
3311
|
-
category: "metadata",
|
|
3312
|
-
message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3313
|
-
});
|
|
3314
|
-
}
|
|
3315
|
-
return validation;
|
|
3316
|
-
}
|
|
3317
|
-
async function getActualDocumentCounts(db) {
|
|
3318
|
-
const counts = {};
|
|
3319
|
-
try {
|
|
3320
|
-
const allDocs = await db.allDocs({ include_docs: true });
|
|
3321
|
-
for (const row of allDocs.rows) {
|
|
3322
|
-
if (row.id.startsWith("_design/")) {
|
|
3323
|
-
counts["_design"] = (counts["_design"] || 0) + 1;
|
|
3324
|
-
continue;
|
|
3325
|
-
}
|
|
3326
|
-
const doc = row.doc;
|
|
3327
|
-
if (doc && doc.docType) {
|
|
3328
|
-
counts[doc.docType] = (counts[doc.docType] || 0) + 1;
|
|
3329
|
-
} else {
|
|
3330
|
-
counts["unknown"] = (counts["unknown"] || 0) + 1;
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
} catch (error) {
|
|
3334
|
-
logger.error("Failed to get actual document counts:", error);
|
|
3335
|
-
}
|
|
3336
|
-
return counts;
|
|
3337
|
-
}
|
|
3338
|
-
function compareDocumentCounts(expected, actual, issues) {
|
|
3339
|
-
let countsMatch = true;
|
|
3340
|
-
for (const [docType, expectedCount] of Object.entries(expected)) {
|
|
3341
|
-
const actualCount = actual[docType] || 0;
|
|
3342
|
-
if (actualCount !== expectedCount) {
|
|
3343
|
-
countsMatch = false;
|
|
3344
|
-
issues.push({
|
|
3345
|
-
type: "error",
|
|
3346
|
-
category: "documents",
|
|
3347
|
-
message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`
|
|
3348
|
-
});
|
|
3349
|
-
}
|
|
3350
|
-
}
|
|
3351
|
-
for (const [docType, actualCount] of Object.entries(actual)) {
|
|
3352
|
-
if (!expected[docType] && docType !== "_design") {
|
|
3353
|
-
issues.push({
|
|
3354
|
-
type: "warning",
|
|
3355
|
-
category: "documents",
|
|
3356
|
-
message: `Unexpected document type found: ${docType} (${actualCount} documents)`
|
|
3357
|
-
});
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
return countsMatch;
|
|
3361
|
-
}
|
|
3362
|
-
async function validateCourseConfig(db, manifest, issues) {
|
|
3363
|
-
try {
|
|
3364
|
-
const courseConfig = await db.get("CourseConfig");
|
|
3365
|
-
if (!courseConfig) {
|
|
3366
|
-
issues.push({
|
|
3367
|
-
type: "error",
|
|
3368
|
-
category: "course_config",
|
|
3369
|
-
message: "CourseConfig document not found after migration"
|
|
3370
|
-
});
|
|
3371
|
-
return;
|
|
3372
|
-
}
|
|
3373
|
-
if (!courseConfig.courseID) {
|
|
3374
|
-
issues.push({
|
|
3375
|
-
type: "warning",
|
|
3376
|
-
category: "course_config",
|
|
3377
|
-
message: "CourseConfig document missing courseID field"
|
|
3378
|
-
});
|
|
3379
|
-
}
|
|
3380
|
-
if (courseConfig.courseID !== manifest.courseId) {
|
|
3381
|
-
issues.push({
|
|
3382
|
-
type: "warning",
|
|
3383
|
-
category: "course_config",
|
|
3384
|
-
message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${courseConfig.courseID}`
|
|
3385
|
-
});
|
|
3386
|
-
}
|
|
3387
|
-
logger.debug("CourseConfig document validation passed");
|
|
3388
|
-
} catch (error) {
|
|
3389
|
-
if (error.status === 404) {
|
|
3390
|
-
issues.push({
|
|
3391
|
-
type: "error",
|
|
3392
|
-
category: "course_config",
|
|
3393
|
-
message: "CourseConfig document not found in database"
|
|
3394
|
-
});
|
|
3395
|
-
} else {
|
|
3396
|
-
issues.push({
|
|
3397
|
-
type: "error",
|
|
3398
|
-
category: "course_config",
|
|
3399
|
-
message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`
|
|
3400
|
-
});
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
}
|
|
3404
|
-
async function validateViews(db, manifest, issues) {
|
|
3405
|
-
let viewsValid = true;
|
|
3406
|
-
try {
|
|
3407
|
-
for (const designDoc of manifest.designDocs) {
|
|
3408
|
-
try {
|
|
3409
|
-
const doc = await db.get(designDoc._id);
|
|
3410
|
-
if (!doc) {
|
|
3411
|
-
viewsValid = false;
|
|
3412
|
-
issues.push({
|
|
3413
|
-
type: "error",
|
|
3414
|
-
category: "views",
|
|
3415
|
-
message: `Design document not found: ${designDoc._id}`
|
|
3416
|
-
});
|
|
3417
|
-
continue;
|
|
3418
|
-
}
|
|
3419
|
-
for (const viewName of Object.keys(designDoc.views)) {
|
|
3420
|
-
try {
|
|
3421
|
-
const viewPath = `${designDoc._id}/${viewName}`;
|
|
3422
|
-
await db.query(viewPath, { limit: 1 });
|
|
3423
|
-
} catch (viewError) {
|
|
3424
|
-
viewsValid = false;
|
|
3425
|
-
issues.push({
|
|
3426
|
-
type: "error",
|
|
3427
|
-
category: "views",
|
|
3428
|
-
message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`
|
|
3429
|
-
});
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
} catch (error) {
|
|
3433
|
-
viewsValid = false;
|
|
3434
|
-
issues.push({
|
|
3435
|
-
type: "error",
|
|
3436
|
-
category: "views",
|
|
3437
|
-
message: `Failed to validate design document ${designDoc._id}: ${error}`
|
|
3438
|
-
});
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
} catch (error) {
|
|
3442
|
-
viewsValid = false;
|
|
3443
|
-
issues.push({
|
|
3444
|
-
type: "error",
|
|
3445
|
-
category: "views",
|
|
3446
|
-
message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3447
|
-
});
|
|
3448
|
-
}
|
|
3449
|
-
return viewsValid;
|
|
3450
|
-
}
|
|
3451
|
-
async function validateAttachmentIntegrity(db, issues) {
|
|
3452
|
-
let attachmentsValid = true;
|
|
3453
|
-
try {
|
|
3454
|
-
const allDocs = await db.allDocs({
|
|
3455
|
-
include_docs: true,
|
|
3456
|
-
limit: 10
|
|
3457
|
-
// Sample first 10 documents for performance
|
|
3458
|
-
});
|
|
3459
|
-
let attachmentCount = 0;
|
|
3460
|
-
let validAttachments = 0;
|
|
3461
|
-
for (const row of allDocs.rows) {
|
|
3462
|
-
const doc = row.doc;
|
|
3463
|
-
if (doc && doc._attachments) {
|
|
3464
|
-
for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3465
|
-
attachmentCount++;
|
|
3466
|
-
try {
|
|
3467
|
-
const attachment = await db.getAttachment(doc._id, attachmentName);
|
|
3468
|
-
if (attachment) {
|
|
3469
|
-
validAttachments++;
|
|
3470
|
-
}
|
|
3471
|
-
} catch (attachmentError) {
|
|
3472
|
-
attachmentsValid = false;
|
|
3473
|
-
issues.push({
|
|
3474
|
-
type: "error",
|
|
3475
|
-
category: "attachments",
|
|
3476
|
-
message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`
|
|
3477
|
-
});
|
|
3478
|
-
}
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
}
|
|
3482
|
-
if (attachmentCount === 0) {
|
|
3483
|
-
issues.push({
|
|
3484
|
-
type: "warning",
|
|
3485
|
-
category: "attachments",
|
|
3486
|
-
message: "No attachments found in sampled documents"
|
|
3487
|
-
});
|
|
3488
|
-
} else {
|
|
3489
|
-
logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
|
|
3490
|
-
}
|
|
3491
|
-
} catch (error) {
|
|
3492
|
-
attachmentsValid = false;
|
|
3493
|
-
issues.push({
|
|
3494
|
-
type: "error",
|
|
3495
|
-
category: "attachments",
|
|
3496
|
-
message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3497
|
-
});
|
|
3498
|
-
}
|
|
3499
|
-
return attachmentsValid;
|
|
3500
|
-
}
|
|
3501
|
-
var nodeFS;
|
|
3502
|
-
var init_validation = __esm({
|
|
3503
|
-
"src/util/migrator/validation.ts"() {
|
|
3504
|
-
"use strict";
|
|
3505
|
-
init_logger();
|
|
3506
|
-
init_FileSystemAdapter();
|
|
3507
|
-
nodeFS = null;
|
|
3508
|
-
try {
|
|
3509
|
-
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3510
|
-
nodeFS = eval("require")("fs");
|
|
3511
|
-
nodeFS.promises = nodeFS.promises || eval("require")("fs").promises;
|
|
3512
|
-
}
|
|
3513
|
-
} catch {
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
});
|
|
3517
|
-
|
|
3518
|
-
// src/util/migrator/StaticToCouchDBMigrator.ts
|
|
3519
|
-
var nodeFS2, nodePath, StaticToCouchDBMigrator;
|
|
3520
|
-
var init_StaticToCouchDBMigrator = __esm({
|
|
3521
|
-
"src/util/migrator/StaticToCouchDBMigrator.ts"() {
|
|
3522
|
-
"use strict";
|
|
3523
|
-
init_logger();
|
|
3524
|
-
init_types4();
|
|
3525
|
-
init_validation();
|
|
3526
|
-
init_FileSystemAdapter();
|
|
3527
|
-
nodeFS2 = null;
|
|
3528
|
-
nodePath = null;
|
|
3529
|
-
try {
|
|
3530
|
-
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3531
|
-
nodeFS2 = eval("require")("fs");
|
|
3532
|
-
nodePath = eval("require")("path");
|
|
3533
|
-
nodeFS2.promises = nodeFS2.promises || eval("require")("fs").promises;
|
|
3534
|
-
}
|
|
3535
|
-
} catch {
|
|
3536
|
-
}
|
|
3537
|
-
StaticToCouchDBMigrator = class {
|
|
3538
|
-
options;
|
|
3539
|
-
progressCallback;
|
|
3540
|
-
fs;
|
|
3541
|
-
constructor(options = {}, fileSystemAdapter) {
|
|
3542
|
-
this.options = {
|
|
3543
|
-
...DEFAULT_MIGRATION_OPTIONS,
|
|
3544
|
-
...options
|
|
3545
|
-
};
|
|
3546
|
-
this.fs = fileSystemAdapter;
|
|
3547
|
-
}
|
|
3548
|
-
/**
|
|
3549
|
-
* Set a progress callback to receive updates during migration
|
|
3550
|
-
*/
|
|
3551
|
-
setProgressCallback(callback) {
|
|
3552
|
-
this.progressCallback = callback;
|
|
3553
|
-
}
|
|
3554
|
-
/**
|
|
3555
|
-
* Migrate a static course to CouchDB
|
|
3556
|
-
*/
|
|
3557
|
-
async migrateCourse(staticPath, targetDB) {
|
|
3558
|
-
const startTime = Date.now();
|
|
3559
|
-
const result = {
|
|
3560
|
-
success: false,
|
|
3561
|
-
documentsRestored: 0,
|
|
3562
|
-
attachmentsRestored: 0,
|
|
3563
|
-
designDocsRestored: 0,
|
|
3564
|
-
courseConfigRestored: 0,
|
|
3565
|
-
errors: [],
|
|
3566
|
-
warnings: [],
|
|
3567
|
-
migrationTime: 0
|
|
3568
|
-
};
|
|
3569
|
-
try {
|
|
3570
|
-
logger.info(`Starting migration from ${staticPath} to CouchDB`);
|
|
3571
|
-
this.reportProgress("manifest", 0, 1, "Validating static course...");
|
|
3572
|
-
const validation = await validateStaticCourse(staticPath, this.fs);
|
|
3573
|
-
if (!validation.valid) {
|
|
3574
|
-
result.errors.push(...validation.errors);
|
|
3575
|
-
throw new Error(`Static course validation failed: ${validation.errors.join(", ")}`);
|
|
3576
|
-
}
|
|
3577
|
-
result.warnings.push(...validation.warnings);
|
|
3578
|
-
this.reportProgress("manifest", 1, 1, "Loading course manifest...");
|
|
3579
|
-
const manifest = await this.loadManifest(staticPath);
|
|
3580
|
-
logger.info(`Loaded manifest for course: ${manifest.courseId} (${manifest.courseName})`);
|
|
3581
|
-
this.reportProgress(
|
|
3582
|
-
"design_docs",
|
|
3583
|
-
0,
|
|
3584
|
-
manifest.designDocs.length,
|
|
3585
|
-
"Restoring design documents..."
|
|
3586
|
-
);
|
|
3587
|
-
const designDocResults = await this.restoreDesignDocuments(manifest.designDocs, targetDB);
|
|
3588
|
-
result.designDocsRestored = designDocResults.restored;
|
|
3589
|
-
result.errors.push(...designDocResults.errors);
|
|
3590
|
-
result.warnings.push(...designDocResults.warnings);
|
|
3591
|
-
this.reportProgress("course_config", 0, 1, "Restoring CourseConfig document...");
|
|
3592
|
-
const courseConfigResults = await this.restoreCourseConfig(manifest, targetDB);
|
|
3593
|
-
result.courseConfigRestored = courseConfigResults.restored;
|
|
3594
|
-
result.errors.push(...courseConfigResults.errors);
|
|
3595
|
-
result.warnings.push(...courseConfigResults.warnings);
|
|
3596
|
-
this.reportProgress("course_config", 1, 1, "CourseConfig document restored");
|
|
3597
|
-
const expectedCounts = this.calculateExpectedCounts(manifest);
|
|
3598
|
-
this.reportProgress(
|
|
3599
|
-
"documents",
|
|
3600
|
-
0,
|
|
3601
|
-
manifest.documentCount,
|
|
3602
|
-
"Aggregating documents from chunks..."
|
|
3603
|
-
);
|
|
3604
|
-
const documents = await this.aggregateDocuments(staticPath, manifest);
|
|
3605
|
-
const filteredDocuments = documents.filter((doc) => doc._id !== "CourseConfig");
|
|
3606
|
-
if (documents.length !== filteredDocuments.length) {
|
|
3607
|
-
result.warnings.push(
|
|
3608
|
-
`Filtered out ${documents.length - filteredDocuments.length} CourseConfig document(s) from chunks to prevent conflicts`
|
|
3609
|
-
);
|
|
3610
|
-
}
|
|
3611
|
-
this.reportProgress(
|
|
3612
|
-
"documents",
|
|
3613
|
-
filteredDocuments.length,
|
|
3614
|
-
manifest.documentCount,
|
|
3615
|
-
"Uploading documents to CouchDB..."
|
|
3616
|
-
);
|
|
3617
|
-
const docResults = await this.uploadDocuments(filteredDocuments, targetDB);
|
|
3618
|
-
result.documentsRestored = docResults.restored;
|
|
3619
|
-
result.errors.push(...docResults.errors);
|
|
3620
|
-
result.warnings.push(...docResults.warnings);
|
|
3621
|
-
const docsWithAttachments = documents.filter(
|
|
3622
|
-
(doc) => doc._attachments && Object.keys(doc._attachments).length > 0
|
|
3623
|
-
);
|
|
3624
|
-
this.reportProgress("attachments", 0, docsWithAttachments.length, "Uploading attachments...");
|
|
3625
|
-
const attachmentResults = await this.uploadAttachments(
|
|
3626
|
-
staticPath,
|
|
3627
|
-
docsWithAttachments,
|
|
3628
|
-
targetDB
|
|
3629
|
-
);
|
|
3630
|
-
result.attachmentsRestored = attachmentResults.restored;
|
|
3631
|
-
result.errors.push(...attachmentResults.errors);
|
|
3632
|
-
result.warnings.push(...attachmentResults.warnings);
|
|
3633
|
-
if (this.options.validateRoundTrip) {
|
|
3634
|
-
this.reportProgress("validation", 0, 1, "Validating migration...");
|
|
3635
|
-
const validationResult = await validateMigration(targetDB, expectedCounts, manifest);
|
|
3636
|
-
if (!validationResult.valid) {
|
|
3637
|
-
result.warnings.push("Migration validation found issues");
|
|
3638
|
-
validationResult.issues.forEach((issue) => {
|
|
3639
|
-
if (issue.type === "error") {
|
|
3640
|
-
result.errors.push(`Validation: ${issue.message}`);
|
|
3641
|
-
} else {
|
|
3642
|
-
result.warnings.push(`Validation: ${issue.message}`);
|
|
3643
|
-
}
|
|
3644
|
-
});
|
|
3645
|
-
}
|
|
3646
|
-
this.reportProgress("validation", 1, 1, "Migration validation completed");
|
|
3647
|
-
}
|
|
3648
|
-
result.success = result.errors.length === 0;
|
|
3649
|
-
result.migrationTime = Date.now() - startTime;
|
|
3650
|
-
logger.info(`Migration completed in ${result.migrationTime}ms`);
|
|
3651
|
-
logger.info(`Documents restored: ${result.documentsRestored}`);
|
|
3652
|
-
logger.info(`Attachments restored: ${result.attachmentsRestored}`);
|
|
3653
|
-
logger.info(`Design docs restored: ${result.designDocsRestored}`);
|
|
3654
|
-
logger.info(`CourseConfig restored: ${result.courseConfigRestored}`);
|
|
3655
|
-
if (result.errors.length > 0) {
|
|
3656
|
-
logger.error(`Migration completed with ${result.errors.length} errors`);
|
|
3657
|
-
}
|
|
3658
|
-
if (result.warnings.length > 0) {
|
|
3659
|
-
logger.warn(`Migration completed with ${result.warnings.length} warnings`);
|
|
3660
|
-
}
|
|
3661
|
-
} catch (error) {
|
|
3662
|
-
result.success = false;
|
|
3663
|
-
result.migrationTime = Date.now() - startTime;
|
|
3664
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3665
|
-
result.errors.push(`Migration failed: ${errorMessage}`);
|
|
3666
|
-
logger.error("Migration failed:", error);
|
|
3667
|
-
if (this.options.cleanupOnFailure) {
|
|
3668
|
-
try {
|
|
3669
|
-
await this.cleanupFailedMigration(targetDB);
|
|
3670
|
-
} catch (cleanupError) {
|
|
3671
|
-
logger.error("Failed to cleanup after migration failure:", cleanupError);
|
|
3672
|
-
result.warnings.push("Failed to cleanup after migration failure");
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3676
|
-
return result;
|
|
3677
|
-
}
|
|
3678
|
-
/**
|
|
3679
|
-
* Load and parse the manifest file
|
|
3680
|
-
*/
|
|
3681
|
-
async loadManifest(staticPath) {
|
|
3682
|
-
try {
|
|
3683
|
-
let manifestContent;
|
|
3684
|
-
let manifestPath;
|
|
3685
|
-
if (this.fs) {
|
|
3686
|
-
manifestPath = this.fs.joinPath(staticPath, "manifest.json");
|
|
3687
|
-
manifestContent = await this.fs.readFile(manifestPath);
|
|
3688
|
-
} else {
|
|
3689
|
-
manifestPath = nodeFS2 && nodePath ? nodePath.join(staticPath, "manifest.json") : `${staticPath}/manifest.json`;
|
|
3690
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3691
|
-
manifestContent = await nodeFS2.promises.readFile(manifestPath, "utf8");
|
|
3692
|
-
} else {
|
|
3693
|
-
const response = await fetch(manifestPath);
|
|
3694
|
-
if (!response.ok) {
|
|
3695
|
-
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
3696
|
-
}
|
|
3697
|
-
manifestContent = await response.text();
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
const manifest = JSON.parse(manifestContent);
|
|
3701
|
-
if (!manifest.version || !manifest.courseId || !manifest.chunks) {
|
|
3702
|
-
throw new Error("Invalid manifest structure");
|
|
3703
|
-
}
|
|
3704
|
-
return manifest;
|
|
3705
|
-
} catch (error) {
|
|
3706
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`;
|
|
3707
|
-
throw new Error(errorMessage);
|
|
3708
|
-
}
|
|
3709
|
-
}
|
|
3710
|
-
/**
|
|
3711
|
-
* Restore design documents to CouchDB
|
|
3712
|
-
*/
|
|
3713
|
-
async restoreDesignDocuments(designDocs, db) {
|
|
3714
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
3715
|
-
for (let i = 0; i < designDocs.length; i++) {
|
|
3716
|
-
const designDoc = designDocs[i];
|
|
3717
|
-
this.reportProgress("design_docs", i, designDocs.length, `Restoring ${designDoc._id}...`);
|
|
3718
|
-
try {
|
|
3719
|
-
let existingDoc;
|
|
3720
|
-
try {
|
|
3721
|
-
existingDoc = await db.get(designDoc._id);
|
|
3722
|
-
} catch {
|
|
3723
|
-
}
|
|
3724
|
-
const docToInsert = {
|
|
3725
|
-
_id: designDoc._id,
|
|
3726
|
-
views: designDoc.views
|
|
3727
|
-
};
|
|
3728
|
-
if (existingDoc) {
|
|
3729
|
-
docToInsert._rev = existingDoc._rev;
|
|
3730
|
-
logger.debug(`Updating existing design document: ${designDoc._id}`);
|
|
3731
|
-
} else {
|
|
3732
|
-
logger.debug(`Creating new design document: ${designDoc._id}`);
|
|
3733
|
-
}
|
|
3734
|
-
await db.put(docToInsert);
|
|
3735
|
-
result.restored++;
|
|
3736
|
-
} catch (error) {
|
|
3737
|
-
const errorMessage = `Failed to restore design document ${designDoc._id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3738
|
-
result.errors.push(errorMessage);
|
|
3739
|
-
logger.error(errorMessage);
|
|
3740
|
-
}
|
|
3741
|
-
}
|
|
3742
|
-
this.reportProgress(
|
|
3743
|
-
"design_docs",
|
|
3744
|
-
designDocs.length,
|
|
3745
|
-
designDocs.length,
|
|
3746
|
-
`Restored ${result.restored} design documents`
|
|
3747
|
-
);
|
|
3748
|
-
return result;
|
|
3749
|
-
}
|
|
3750
|
-
/**
|
|
3751
|
-
* Aggregate documents from all chunks
|
|
3752
|
-
*/
|
|
3753
|
-
async aggregateDocuments(staticPath, manifest) {
|
|
3754
|
-
const allDocuments = [];
|
|
3755
|
-
const documentMap = /* @__PURE__ */ new Map();
|
|
3756
|
-
for (let i = 0; i < manifest.chunks.length; i++) {
|
|
3757
|
-
const chunk = manifest.chunks[i];
|
|
3758
|
-
this.reportProgress(
|
|
3759
|
-
"documents",
|
|
3760
|
-
allDocuments.length,
|
|
3761
|
-
manifest.documentCount,
|
|
3762
|
-
`Loading chunk ${chunk.id}...`
|
|
3763
|
-
);
|
|
3764
|
-
try {
|
|
3765
|
-
const documents = await this.loadChunk(staticPath, chunk);
|
|
3766
|
-
for (const doc of documents) {
|
|
3767
|
-
if (!doc._id) {
|
|
3768
|
-
logger.warn(`Document without _id found in chunk ${chunk.id}, skipping`);
|
|
3769
|
-
continue;
|
|
3770
|
-
}
|
|
3771
|
-
if (documentMap.has(doc._id)) {
|
|
3772
|
-
logger.warn(`Duplicate document ID found: ${doc._id}, using latest version`);
|
|
3773
|
-
}
|
|
3774
|
-
documentMap.set(doc._id, doc);
|
|
3775
|
-
}
|
|
3776
|
-
} catch (error) {
|
|
3777
|
-
throw new Error(
|
|
3778
|
-
`Failed to load chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
3779
|
-
);
|
|
3780
|
-
}
|
|
3781
|
-
}
|
|
3782
|
-
allDocuments.push(...documentMap.values());
|
|
3783
|
-
logger.info(
|
|
3784
|
-
`Aggregated ${allDocuments.length} unique documents from ${manifest.chunks.length} chunks`
|
|
3785
|
-
);
|
|
3786
|
-
return allDocuments;
|
|
3787
|
-
}
|
|
3788
|
-
/**
|
|
3789
|
-
* Load documents from a single chunk file
|
|
3790
|
-
*/
|
|
3791
|
-
async loadChunk(staticPath, chunk) {
|
|
3792
|
-
try {
|
|
3793
|
-
let chunkContent;
|
|
3794
|
-
let chunkPath;
|
|
3795
|
-
if (this.fs) {
|
|
3796
|
-
chunkPath = this.fs.joinPath(staticPath, chunk.path);
|
|
3797
|
-
chunkContent = await this.fs.readFile(chunkPath);
|
|
3798
|
-
} else {
|
|
3799
|
-
chunkPath = nodeFS2 && nodePath ? nodePath.join(staticPath, chunk.path) : `${staticPath}/${chunk.path}`;
|
|
3800
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3801
|
-
chunkContent = await nodeFS2.promises.readFile(chunkPath, "utf8");
|
|
3802
|
-
} else {
|
|
3803
|
-
const response = await fetch(chunkPath);
|
|
3804
|
-
if (!response.ok) {
|
|
3805
|
-
throw new Error(`Failed to fetch chunk: ${response.status} ${response.statusText}`);
|
|
3806
|
-
}
|
|
3807
|
-
chunkContent = await response.text();
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
const documents = JSON.parse(chunkContent);
|
|
3811
|
-
if (!Array.isArray(documents)) {
|
|
3812
|
-
throw new Error("Chunk file does not contain an array of documents");
|
|
3813
|
-
}
|
|
3814
|
-
return documents;
|
|
3815
|
-
} catch (error) {
|
|
3816
|
-
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load chunk: ${error instanceof Error ? error.message : String(error)}`;
|
|
3817
|
-
throw new Error(errorMessage);
|
|
3818
|
-
}
|
|
3819
|
-
}
|
|
3820
|
-
/**
|
|
3821
|
-
* Upload documents to CouchDB in batches
|
|
3822
|
-
*/
|
|
3823
|
-
async uploadDocuments(documents, db) {
|
|
3824
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
3825
|
-
const batchSize = this.options.chunkBatchSize;
|
|
3826
|
-
for (let i = 0; i < documents.length; i += batchSize) {
|
|
3827
|
-
const batch = documents.slice(i, i + batchSize);
|
|
3828
|
-
this.reportProgress(
|
|
3829
|
-
"documents",
|
|
3830
|
-
i,
|
|
3831
|
-
documents.length,
|
|
3832
|
-
`Uploading batch ${Math.floor(i / batchSize) + 1}...`
|
|
3833
|
-
);
|
|
3834
|
-
try {
|
|
3835
|
-
const docsToInsert = batch.map((doc) => {
|
|
3836
|
-
const cleanDoc = { ...doc };
|
|
3837
|
-
delete cleanDoc._rev;
|
|
3838
|
-
delete cleanDoc._attachments;
|
|
3839
|
-
return cleanDoc;
|
|
3840
|
-
});
|
|
3841
|
-
const bulkResult = await db.bulkDocs(docsToInsert);
|
|
3842
|
-
for (let j = 0; j < bulkResult.length; j++) {
|
|
3843
|
-
const docResult = bulkResult[j];
|
|
3844
|
-
const originalDoc = batch[j];
|
|
3845
|
-
if ("error" in docResult) {
|
|
3846
|
-
const errorMessage = `Failed to upload document ${originalDoc._id}: ${docResult.error} - ${docResult.reason}`;
|
|
3847
|
-
result.errors.push(errorMessage);
|
|
3848
|
-
logger.error(errorMessage);
|
|
3849
|
-
} else {
|
|
3850
|
-
result.restored++;
|
|
3851
|
-
}
|
|
3852
|
-
}
|
|
3853
|
-
} catch (error) {
|
|
3854
|
-
let errorMessage;
|
|
3855
|
-
if (error instanceof Error) {
|
|
3856
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
3857
|
-
} else if (error && typeof error === "object" && "message" in error) {
|
|
3858
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
3859
|
-
} else {
|
|
3860
|
-
errorMessage = `Failed to upload document batch starting at index ${i}: ${JSON.stringify(error)}`;
|
|
3861
|
-
}
|
|
3862
|
-
result.errors.push(errorMessage);
|
|
3863
|
-
logger.error(errorMessage);
|
|
3864
|
-
}
|
|
3865
|
-
}
|
|
3866
|
-
this.reportProgress(
|
|
3867
|
-
"documents",
|
|
3868
|
-
documents.length,
|
|
3869
|
-
documents.length,
|
|
3870
|
-
`Uploaded ${result.restored} documents`
|
|
3871
|
-
);
|
|
3872
|
-
return result;
|
|
3873
|
-
}
|
|
3874
|
-
/**
|
|
3875
|
-
* Upload attachments from filesystem to CouchDB
|
|
3876
|
-
*/
|
|
3877
|
-
async uploadAttachments(staticPath, documents, db) {
|
|
3878
|
-
const result = { restored: 0, errors: [], warnings: [] };
|
|
3879
|
-
let processedDocs = 0;
|
|
3880
|
-
for (const doc of documents) {
|
|
3881
|
-
this.reportProgress(
|
|
3882
|
-
"attachments",
|
|
3883
|
-
processedDocs,
|
|
3884
|
-
documents.length,
|
|
3885
|
-
`Processing attachments for ${doc._id}...`
|
|
3886
|
-
);
|
|
3887
|
-
processedDocs++;
|
|
3888
|
-
if (!doc._attachments) {
|
|
3889
|
-
continue;
|
|
3890
|
-
}
|
|
3891
|
-
for (const [attachmentName, attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3892
|
-
try {
|
|
3893
|
-
const uploadResult = await this.uploadSingleAttachment(
|
|
3894
|
-
staticPath,
|
|
3895
|
-
doc._id,
|
|
3896
|
-
attachmentName,
|
|
3897
|
-
attachmentMeta,
|
|
3898
|
-
db
|
|
3899
|
-
);
|
|
3900
|
-
if (uploadResult.success) {
|
|
3901
|
-
result.restored++;
|
|
3902
|
-
} else {
|
|
3903
|
-
result.errors.push(uploadResult.error || "Unknown attachment upload error");
|
|
3904
|
-
}
|
|
3905
|
-
} catch (error) {
|
|
3906
|
-
const errorMessage = `Failed to upload attachment ${doc._id}/${attachmentName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3907
|
-
result.errors.push(errorMessage);
|
|
3908
|
-
logger.error(errorMessage);
|
|
3909
|
-
}
|
|
3910
|
-
}
|
|
3911
|
-
}
|
|
3912
|
-
this.reportProgress(
|
|
3913
|
-
"attachments",
|
|
3914
|
-
documents.length,
|
|
3915
|
-
documents.length,
|
|
3916
|
-
`Uploaded ${result.restored} attachments`
|
|
3917
|
-
);
|
|
3918
|
-
return result;
|
|
3919
|
-
}
|
|
3920
|
-
/**
|
|
3921
|
-
* Upload a single attachment file
|
|
3922
|
-
*/
|
|
3923
|
-
async uploadSingleAttachment(staticPath, docId, attachmentName, attachmentMeta, db) {
|
|
3924
|
-
const result = {
|
|
3925
|
-
success: false,
|
|
3926
|
-
attachmentName,
|
|
3927
|
-
docId
|
|
3928
|
-
};
|
|
3929
|
-
try {
|
|
3930
|
-
if (!attachmentMeta.path) {
|
|
3931
|
-
result.error = "Attachment metadata missing file path";
|
|
3932
|
-
return result;
|
|
3933
|
-
}
|
|
3934
|
-
let attachmentData;
|
|
3935
|
-
let attachmentPath;
|
|
3936
|
-
if (this.fs) {
|
|
3937
|
-
attachmentPath = this.fs.joinPath(staticPath, attachmentMeta.path);
|
|
3938
|
-
attachmentData = await this.fs.readBinary(attachmentPath);
|
|
3939
|
-
} else {
|
|
3940
|
-
attachmentPath = nodeFS2 && nodePath ? nodePath.join(staticPath, attachmentMeta.path) : `${staticPath}/${attachmentMeta.path}`;
|
|
3941
|
-
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3942
|
-
attachmentData = await nodeFS2.promises.readFile(attachmentPath);
|
|
3943
|
-
} else {
|
|
3944
|
-
const response = await fetch(attachmentPath);
|
|
3945
|
-
if (!response.ok) {
|
|
3946
|
-
result.error = `Failed to fetch attachment: ${response.status} ${response.statusText}`;
|
|
3947
|
-
return result;
|
|
3948
|
-
}
|
|
3949
|
-
attachmentData = await response.arrayBuffer();
|
|
3950
|
-
}
|
|
3951
|
-
}
|
|
3952
|
-
const doc = await db.get(docId);
|
|
3953
|
-
await db.putAttachment(
|
|
3954
|
-
docId,
|
|
3955
|
-
attachmentName,
|
|
3956
|
-
doc._rev,
|
|
3957
|
-
attachmentData,
|
|
3958
|
-
// PouchDB accepts both ArrayBuffer and Buffer
|
|
3959
|
-
attachmentMeta.content_type
|
|
3960
|
-
);
|
|
3961
|
-
result.success = true;
|
|
3962
|
-
} catch (error) {
|
|
3963
|
-
result.error = error instanceof Error ? error.message : String(error);
|
|
3964
|
-
}
|
|
3965
|
-
return result;
|
|
3966
|
-
}
|
|
3967
|
-
/**
|
|
3968
|
-
* Restore CourseConfig document from manifest
|
|
3969
|
-
*/
|
|
3970
|
-
async restoreCourseConfig(manifest, targetDB) {
|
|
3971
|
-
const results = {
|
|
3972
|
-
restored: 0,
|
|
3973
|
-
errors: [],
|
|
3974
|
-
warnings: []
|
|
3975
|
-
};
|
|
3976
|
-
try {
|
|
3977
|
-
if (!manifest.courseConfig) {
|
|
3978
|
-
results.warnings.push(
|
|
3979
|
-
"No courseConfig found in manifest, skipping CourseConfig document creation"
|
|
3980
|
-
);
|
|
3981
|
-
return results;
|
|
3982
|
-
}
|
|
3983
|
-
const courseConfigDoc = {
|
|
3984
|
-
_id: "CourseConfig",
|
|
3985
|
-
...manifest.courseConfig,
|
|
3986
|
-
courseID: manifest.courseId
|
|
3987
|
-
};
|
|
3988
|
-
delete courseConfigDoc._rev;
|
|
3989
|
-
await targetDB.put(courseConfigDoc);
|
|
3990
|
-
results.restored = 1;
|
|
3991
|
-
logger.info(`CourseConfig document created for course: ${manifest.courseId}`);
|
|
3992
|
-
} catch (error) {
|
|
3993
|
-
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
3994
|
-
results.errors.push(`Failed to restore CourseConfig: ${errorMessage}`);
|
|
3995
|
-
logger.error("CourseConfig restoration failed:", error);
|
|
3996
|
-
}
|
|
3997
|
-
return results;
|
|
3998
|
-
}
|
|
3999
|
-
/**
|
|
4000
|
-
* Calculate expected document counts from manifest
|
|
4001
|
-
*/
|
|
4002
|
-
calculateExpectedCounts(manifest) {
|
|
4003
|
-
const counts = {};
|
|
4004
|
-
for (const chunk of manifest.chunks) {
|
|
4005
|
-
counts[chunk.docType] = (counts[chunk.docType] || 0) + chunk.documentCount;
|
|
4006
|
-
}
|
|
4007
|
-
if (manifest.designDocs.length > 0) {
|
|
4008
|
-
counts["_design"] = manifest.designDocs.length;
|
|
4009
|
-
}
|
|
4010
|
-
return counts;
|
|
4011
|
-
}
|
|
4012
|
-
/**
|
|
4013
|
-
* Clean up database after failed migration
|
|
4014
|
-
*/
|
|
4015
|
-
async cleanupFailedMigration(db) {
|
|
4016
|
-
logger.info("Cleaning up failed migration...");
|
|
4017
|
-
try {
|
|
4018
|
-
const allDocs = await db.allDocs();
|
|
4019
|
-
const docsToDelete = allDocs.rows.map((row) => ({
|
|
4020
|
-
_id: row.id,
|
|
4021
|
-
_rev: row.value.rev,
|
|
4022
|
-
_deleted: true
|
|
4023
|
-
}));
|
|
4024
|
-
if (docsToDelete.length > 0) {
|
|
4025
|
-
await db.bulkDocs(docsToDelete);
|
|
4026
|
-
logger.info(`Cleaned up ${docsToDelete.length} documents from failed migration`);
|
|
4027
|
-
}
|
|
4028
|
-
} catch (error) {
|
|
4029
|
-
logger.error("Failed to cleanup documents:", error);
|
|
4030
|
-
throw error;
|
|
4031
|
-
}
|
|
4032
|
-
}
|
|
4033
|
-
/**
|
|
4034
|
-
* Report progress to callback if available
|
|
4035
|
-
*/
|
|
4036
|
-
reportProgress(phase, current, total, message) {
|
|
4037
|
-
if (this.progressCallback) {
|
|
4038
|
-
this.progressCallback({
|
|
4039
|
-
phase,
|
|
4040
|
-
current,
|
|
4041
|
-
total,
|
|
4042
|
-
message
|
|
4043
|
-
});
|
|
4044
|
-
}
|
|
4045
|
-
}
|
|
4046
|
-
/**
|
|
4047
|
-
* Check if a path is a local file path (vs URL)
|
|
4048
|
-
*/
|
|
4049
|
-
isLocalPath(path2) {
|
|
4050
|
-
return !path2.startsWith("http://") && !path2.startsWith("https://");
|
|
4051
|
-
}
|
|
4052
|
-
};
|
|
4053
|
-
}
|
|
4054
|
-
});
|
|
4055
|
-
|
|
4056
|
-
// src/util/migrator/index.ts
|
|
4057
|
-
var init_migrator = __esm({
|
|
4058
|
-
"src/util/migrator/index.ts"() {
|
|
4059
|
-
"use strict";
|
|
4060
|
-
init_StaticToCouchDBMigrator();
|
|
4061
|
-
init_validation();
|
|
4062
|
-
init_FileSystemAdapter();
|
|
4063
|
-
}
|
|
4064
|
-
});
|
|
4065
|
-
|
|
4066
|
-
// src/util/dataDirectory.ts
|
|
4067
|
-
import * as path from "path";
|
|
4068
|
-
import * as os from "os";
|
|
4069
|
-
function getAppDataDirectory() {
|
|
4070
|
-
if (ENV.LOCAL_STORAGE_PREFIX) {
|
|
4071
|
-
return path.join(os.homedir(), `.tuilder`, ENV.LOCAL_STORAGE_PREFIX);
|
|
4072
|
-
} else {
|
|
4073
|
-
return path.join(os.homedir(), ".tuilder");
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
4076
|
-
function getDbPath(dbName) {
|
|
4077
|
-
return path.join(getAppDataDirectory(), dbName);
|
|
4078
|
-
}
|
|
4079
|
-
var init_dataDirectory = __esm({
|
|
4080
|
-
"src/util/dataDirectory.ts"() {
|
|
4081
|
-
"use strict";
|
|
4082
|
-
init_logger();
|
|
4083
|
-
init_factory();
|
|
4084
|
-
}
|
|
4085
|
-
});
|
|
4086
|
-
|
|
4087
|
-
// src/util/index.ts
|
|
4088
|
-
var init_util2 = __esm({
|
|
4089
|
-
"src/util/index.ts"() {
|
|
4090
|
-
"use strict";
|
|
4091
|
-
init_Loggable();
|
|
4092
|
-
init_packer();
|
|
4093
|
-
init_migrator();
|
|
4094
|
-
init_dataDirectory();
|
|
4095
|
-
}
|
|
4096
|
-
});
|
|
4097
|
-
|
|
4098
|
-
// src/study/SourceMixer.ts
|
|
4099
|
-
var init_SourceMixer = __esm({
|
|
4100
|
-
"src/study/SourceMixer.ts"() {
|
|
4101
|
-
"use strict";
|
|
4102
|
-
}
|
|
4103
|
-
});
|
|
4104
|
-
|
|
4105
|
-
// src/study/MixerDebugger.ts
|
|
4106
|
-
function printMixerSummary(run) {
|
|
4107
|
-
console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
|
|
4108
|
-
logger.info(`Run ID: ${run.runId}`);
|
|
4109
|
-
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
4110
|
-
logger.info(
|
|
4111
|
-
`Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
|
|
4112
|
-
);
|
|
4113
|
-
console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
|
|
4114
|
-
for (const src of run.sourceSummaries) {
|
|
4115
|
-
logger.info(
|
|
4116
|
-
` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
|
|
4117
|
-
);
|
|
4118
|
-
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
|
|
4119
|
-
}
|
|
4120
|
-
console.groupEnd();
|
|
4121
|
-
console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
|
|
4122
|
-
for (const breakdown of run.sourceBreakdowns) {
|
|
4123
|
-
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4124
|
-
logger.info(
|
|
4125
|
-
` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
|
|
4126
|
-
);
|
|
4127
|
-
}
|
|
4128
|
-
console.groupEnd();
|
|
4129
|
-
console.groupEnd();
|
|
4130
|
-
}
|
|
4131
|
-
function mountMixerDebugger() {
|
|
4132
|
-
if (typeof window === "undefined") return;
|
|
4133
|
-
const win = window;
|
|
4134
|
-
win.skuilder = win.skuilder || {};
|
|
4135
|
-
win.skuilder.mixer = mixerDebugAPI;
|
|
4136
|
-
}
|
|
4137
|
-
var runHistory2, mixerDebugAPI;
|
|
4138
|
-
var init_MixerDebugger = __esm({
|
|
4139
|
-
"src/study/MixerDebugger.ts"() {
|
|
4140
|
-
"use strict";
|
|
4141
|
-
init_logger();
|
|
4142
|
-
init_navigators();
|
|
4143
|
-
runHistory2 = [];
|
|
4144
|
-
mixerDebugAPI = {
|
|
4145
|
-
/**
|
|
4146
|
-
* Get raw run history for programmatic access.
|
|
4147
|
-
*/
|
|
4148
|
-
get runs() {
|
|
4149
|
-
return [...runHistory2];
|
|
4150
|
-
},
|
|
4151
|
-
/**
|
|
4152
|
-
* Show summary of a specific mixer run.
|
|
4153
|
-
*/
|
|
4154
|
-
showRun(idOrIndex = 0) {
|
|
4155
|
-
if (runHistory2.length === 0) {
|
|
4156
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4157
|
-
return;
|
|
4158
|
-
}
|
|
4159
|
-
let run;
|
|
4160
|
-
if (typeof idOrIndex === "number") {
|
|
4161
|
-
run = runHistory2[idOrIndex];
|
|
4162
|
-
if (!run) {
|
|
4163
|
-
logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
|
|
4164
|
-
return;
|
|
4165
|
-
}
|
|
4166
|
-
} else {
|
|
4167
|
-
run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
|
|
4168
|
-
if (!run) {
|
|
4169
|
-
logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
|
|
4170
|
-
return;
|
|
4171
|
-
}
|
|
4172
|
-
}
|
|
4173
|
-
printMixerSummary(run);
|
|
4174
|
-
},
|
|
4175
|
-
/**
|
|
4176
|
-
* Show summary of the last mixer run.
|
|
4177
|
-
*/
|
|
4178
|
-
showLastMix() {
|
|
4179
|
-
this.showRun(0);
|
|
4180
|
-
},
|
|
4181
|
-
/**
|
|
4182
|
-
* Explain source balance in the last run.
|
|
4183
|
-
*/
|
|
4184
|
-
explainSourceBalance() {
|
|
4185
|
-
if (runHistory2.length === 0) {
|
|
4186
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4187
|
-
return;
|
|
4188
|
-
}
|
|
4189
|
-
const run = runHistory2[0];
|
|
4190
|
-
console.group("\u2696\uFE0F Source Balance Analysis");
|
|
4191
|
-
logger.info(`Mixer: ${run.mixerType}`);
|
|
4192
|
-
logger.info(`Requested limit: ${run.requestedLimit}`);
|
|
4193
|
-
if (run.quotaPerSource) {
|
|
4194
|
-
logger.info(`Quota per source: ${run.quotaPerSource}`);
|
|
4195
|
-
}
|
|
4196
|
-
console.group("Input Distribution:");
|
|
4197
|
-
for (const src of run.sourceSummaries) {
|
|
4198
|
-
const name = src.sourceName || src.sourceId;
|
|
4199
|
-
logger.info(`${name}:`);
|
|
4200
|
-
logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
|
|
4201
|
-
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
|
|
4202
|
-
}
|
|
4203
|
-
console.groupEnd();
|
|
4204
|
-
console.group("Selection Results:");
|
|
4205
|
-
for (const breakdown of run.sourceBreakdowns) {
|
|
4206
|
-
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4207
|
-
logger.info(`${name}:`);
|
|
4208
|
-
logger.info(
|
|
4209
|
-
` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
|
|
4210
|
-
);
|
|
4211
|
-
logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
|
|
4212
|
-
logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
|
|
4213
|
-
if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
|
|
4214
|
-
logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
|
|
4215
|
-
}
|
|
4216
|
-
if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
|
|
4217
|
-
logger.info(` \u26A0\uFE0F Had cards but none selected!`);
|
|
4218
|
-
}
|
|
4219
|
-
}
|
|
4220
|
-
console.groupEnd();
|
|
4221
|
-
const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
|
|
4222
|
-
const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
|
|
4223
|
-
const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
|
|
4224
|
-
if (maxDeviation > 20) {
|
|
4225
|
-
logger.info(`
|
|
4226
|
-
\u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
|
|
4227
|
-
logger.info("Possible causes:");
|
|
4228
|
-
logger.info(" - Score range differences between sources");
|
|
4229
|
-
logger.info(" - One source has much better quality cards");
|
|
4230
|
-
logger.info(" - Different card availability (reviews vs new)");
|
|
4231
|
-
}
|
|
4232
|
-
console.groupEnd();
|
|
4233
|
-
},
|
|
4234
|
-
/**
|
|
4235
|
-
* Compare score distributions across sources.
|
|
4236
|
-
*/
|
|
4237
|
-
compareScores() {
|
|
4238
|
-
if (runHistory2.length === 0) {
|
|
4239
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4240
|
-
return;
|
|
4241
|
-
}
|
|
4242
|
-
const run = runHistory2[0];
|
|
4243
|
-
console.group("\u{1F4CA} Score Distribution Comparison");
|
|
4244
|
-
console.table(
|
|
4245
|
-
run.sourceSummaries.map((src) => ({
|
|
4246
|
-
source: src.sourceName || src.sourceId,
|
|
4247
|
-
cards: src.totalCards,
|
|
4248
|
-
min: src.bottomScore.toFixed(3),
|
|
4249
|
-
max: src.topScore.toFixed(3),
|
|
4250
|
-
avg: src.avgScore.toFixed(3),
|
|
4251
|
-
range: (src.topScore - src.bottomScore).toFixed(3)
|
|
4252
|
-
}))
|
|
4253
|
-
);
|
|
4254
|
-
const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
|
|
4255
|
-
const avgScores = run.sourceSummaries.map((s) => s.avgScore);
|
|
4256
|
-
const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
|
|
4257
|
-
const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
|
|
4258
|
-
if (rangeDiff > 0.3 || avgDiff > 0.2) {
|
|
4259
|
-
logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
|
|
4260
|
-
logger.info(
|
|
4261
|
-
"This may cause one source to dominate selection if using global sorting (not quota-based)"
|
|
4262
|
-
);
|
|
4263
|
-
}
|
|
4264
|
-
console.groupEnd();
|
|
4265
|
-
},
|
|
4266
|
-
/**
|
|
4267
|
-
* Show detailed information for a specific card.
|
|
4268
|
-
*/
|
|
4269
|
-
showCard(cardId) {
|
|
4270
|
-
for (const run of runHistory2) {
|
|
4271
|
-
const card = run.cards.find((c) => c.cardId === cardId);
|
|
4272
|
-
if (card) {
|
|
4273
|
-
const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
|
|
4274
|
-
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
4275
|
-
logger.info(`Course: ${card.courseId}`);
|
|
4276
|
-
logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
|
|
4277
|
-
logger.info(`Origin: ${card.origin}`);
|
|
4278
|
-
logger.info(`Score: ${card.score.toFixed(3)}`);
|
|
4279
|
-
if (card.rankInSource) {
|
|
4280
|
-
logger.info(`Rank in source: #${card.rankInSource}`);
|
|
4281
|
-
}
|
|
4282
|
-
if (card.rankInMix) {
|
|
4283
|
-
logger.info(`Rank in mixed results: #${card.rankInMix}`);
|
|
4284
|
-
}
|
|
4285
|
-
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
4286
|
-
if (!card.selected && card.rankInSource) {
|
|
4287
|
-
logger.info("\nWhy not selected:");
|
|
4288
|
-
if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
|
|
4289
|
-
logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
|
|
4290
|
-
}
|
|
4291
|
-
logger.info(" - Check score compared to selected cards using .showRun()");
|
|
4292
|
-
}
|
|
4293
|
-
console.groupEnd();
|
|
4294
|
-
return;
|
|
4295
|
-
}
|
|
4296
|
-
}
|
|
4297
|
-
logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
|
|
4298
|
-
},
|
|
4299
|
-
/**
|
|
4300
|
-
* Show all runs in compact format.
|
|
4301
|
-
*/
|
|
4302
|
-
listRuns() {
|
|
4303
|
-
if (runHistory2.length === 0) {
|
|
4304
|
-
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4305
|
-
return;
|
|
4306
|
-
}
|
|
4307
|
-
console.table(
|
|
4308
|
-
runHistory2.map((r) => ({
|
|
4309
|
-
id: r.runId.slice(-8),
|
|
4310
|
-
time: r.timestamp.toLocaleTimeString(),
|
|
4311
|
-
mixer: r.mixerType,
|
|
4312
|
-
sources: r.sourceSummaries.length,
|
|
4313
|
-
selected: r.finalCount,
|
|
4314
|
-
reviews: r.reviewsSelected,
|
|
4315
|
-
new: r.newSelected
|
|
4316
|
-
}))
|
|
4317
|
-
);
|
|
4318
|
-
},
|
|
4319
|
-
/**
|
|
4320
|
-
* Export run history as JSON for bug reports.
|
|
4321
|
-
*/
|
|
4322
|
-
export() {
|
|
4323
|
-
const json = JSON.stringify(runHistory2, null, 2);
|
|
4324
|
-
logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
|
|
4325
|
-
logger.info(" copy(window.skuilder.mixer.export())");
|
|
4326
|
-
return json;
|
|
4327
|
-
},
|
|
4328
|
-
/**
|
|
4329
|
-
* Clear run history.
|
|
4330
|
-
*/
|
|
4331
|
-
clear() {
|
|
4332
|
-
runHistory2.length = 0;
|
|
4333
|
-
logger.info("[Mixer Debug] Run history cleared.");
|
|
4334
|
-
},
|
|
4335
|
-
/**
|
|
4336
|
-
* Show help.
|
|
4337
|
-
*/
|
|
4338
|
-
help() {
|
|
4339
|
-
logger.info(`
|
|
4340
|
-
\u{1F3A8} Mixer Debug API
|
|
4341
|
-
|
|
4342
|
-
Commands:
|
|
4343
|
-
.showLastMix() Show summary of most recent mixer run
|
|
4344
|
-
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
4345
|
-
.explainSourceBalance() Analyze source balance and selection patterns
|
|
4346
|
-
.compareScores() Compare score distributions across sources
|
|
4347
|
-
.showCard(cardId) Show mixer decisions for a specific card
|
|
4348
|
-
.listRuns() List all captured runs in table format
|
|
4349
|
-
.export() Export run history as JSON for bug reports
|
|
4350
|
-
.clear() Clear run history
|
|
4351
|
-
.runs Access raw run history array
|
|
4352
|
-
.help() Show this help message
|
|
4353
|
-
|
|
4354
|
-
Example:
|
|
4355
|
-
window.skuilder.mixer.showLastMix()
|
|
4356
|
-
window.skuilder.mixer.explainSourceBalance()
|
|
4357
|
-
window.skuilder.mixer.compareScores()
|
|
4358
|
-
`);
|
|
4359
|
-
}
|
|
4360
|
-
};
|
|
4361
|
-
mountMixerDebugger();
|
|
3038
|
+
// src/core/navigators/filters/userGoalStub.ts
|
|
3039
|
+
var userGoalStub_exports = {};
|
|
3040
|
+
__export(userGoalStub_exports, {
|
|
3041
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
3042
|
+
});
|
|
3043
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
3044
|
+
var init_userGoalStub = __esm({
|
|
3045
|
+
"src/core/navigators/filters/userGoalStub.ts"() {
|
|
3046
|
+
"use strict";
|
|
3047
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
4362
3048
|
}
|
|
4363
3049
|
});
|
|
4364
3050
|
|
|
4365
|
-
// src/
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
3051
|
+
// import("./filters/**/*") in src/core/navigators/index.ts
|
|
3052
|
+
var globImport_filters;
|
|
3053
|
+
var init_2 = __esm({
|
|
3054
|
+
'import("./filters/**/*") in src/core/navigators/index.ts'() {
|
|
3055
|
+
globImport_filters = __glob({
|
|
3056
|
+
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
3057
|
+
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
3058
|
+
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
3059
|
+
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
3060
|
+
"./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
|
|
3061
|
+
"./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
3062
|
+
"./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
3063
|
+
"./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
3064
|
+
"./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
|
|
3065
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
|
|
3066
|
+
});
|
|
4370
3067
|
}
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
3068
|
+
});
|
|
3069
|
+
|
|
3070
|
+
// src/core/orchestration/gradient.ts
|
|
3071
|
+
var init_gradient = __esm({
|
|
3072
|
+
"src/core/orchestration/gradient.ts"() {
|
|
3073
|
+
"use strict";
|
|
3074
|
+
init_logger();
|
|
4376
3075
|
}
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
3076
|
+
});
|
|
3077
|
+
|
|
3078
|
+
// src/core/orchestration/learning.ts
|
|
3079
|
+
var init_learning = __esm({
|
|
3080
|
+
"src/core/orchestration/learning.ts"() {
|
|
3081
|
+
"use strict";
|
|
3082
|
+
init_contentNavigationStrategy();
|
|
3083
|
+
init_types_legacy();
|
|
3084
|
+
init_logger();
|
|
4380
3085
|
}
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4388
|
-
return;
|
|
3086
|
+
});
|
|
3087
|
+
|
|
3088
|
+
// src/core/orchestration/signal.ts
|
|
3089
|
+
var init_signal = __esm({
|
|
3090
|
+
"src/core/orchestration/signal.ts"() {
|
|
3091
|
+
"use strict";
|
|
4389
3092
|
}
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
3093
|
+
});
|
|
3094
|
+
|
|
3095
|
+
// src/core/orchestration/recording.ts
|
|
3096
|
+
var init_recording = __esm({
|
|
3097
|
+
"src/core/orchestration/recording.ts"() {
|
|
3098
|
+
"use strict";
|
|
3099
|
+
init_signal();
|
|
3100
|
+
init_types_legacy();
|
|
3101
|
+
init_logger();
|
|
4394
3102
|
}
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
score: p.score?.toFixed(3) || "-",
|
|
4404
|
-
time: p.timestamp.toLocaleTimeString()
|
|
4405
|
-
}))
|
|
4406
|
-
);
|
|
3103
|
+
});
|
|
3104
|
+
|
|
3105
|
+
// src/core/orchestration/index.ts
|
|
3106
|
+
function fnv1a(str) {
|
|
3107
|
+
let hash = 2166136261;
|
|
3108
|
+
for (let i = 0; i < str.length; i++) {
|
|
3109
|
+
hash ^= str.charCodeAt(i);
|
|
3110
|
+
hash = Math.imul(hash, 16777619);
|
|
4407
3111
|
}
|
|
4408
|
-
|
|
3112
|
+
return hash >>> 0;
|
|
4409
3113
|
}
|
|
4410
|
-
function
|
|
4411
|
-
const
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
}
|
|
4416
|
-
console.group("\u{1F500} Interleaving Analysis");
|
|
4417
|
-
const courseCounts = /* @__PURE__ */ new Map();
|
|
4418
|
-
const courseOrigins = /* @__PURE__ */ new Map();
|
|
4419
|
-
session.presentations.forEach((p) => {
|
|
4420
|
-
const name = p.courseName || p.courseId;
|
|
4421
|
-
courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
|
|
4422
|
-
if (!courseOrigins.has(name)) {
|
|
4423
|
-
courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
|
|
4424
|
-
}
|
|
4425
|
-
const origins = courseOrigins.get(name);
|
|
4426
|
-
origins[p.origin]++;
|
|
4427
|
-
});
|
|
4428
|
-
logger.info("Course distribution:");
|
|
4429
|
-
console.table(
|
|
4430
|
-
Array.from(courseCounts.entries()).map(([course, count]) => {
|
|
4431
|
-
const origins = courseOrigins.get(course);
|
|
4432
|
-
return {
|
|
4433
|
-
course,
|
|
4434
|
-
total: count,
|
|
4435
|
-
reviews: origins.review,
|
|
4436
|
-
new: origins.new,
|
|
4437
|
-
failed: origins.failed,
|
|
4438
|
-
percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
|
|
4439
|
-
};
|
|
4440
|
-
})
|
|
4441
|
-
);
|
|
4442
|
-
if (session.presentations.length > 0) {
|
|
4443
|
-
logger.info("\nPresentation sequence (first 20):");
|
|
4444
|
-
const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
|
|
4445
|
-
logger.info(sequence);
|
|
4446
|
-
}
|
|
4447
|
-
let maxCluster = 0;
|
|
4448
|
-
let currentCluster = 1;
|
|
4449
|
-
let currentCourse = session.presentations[0]?.courseId;
|
|
4450
|
-
for (let i = 1; i < session.presentations.length; i++) {
|
|
4451
|
-
if (session.presentations[i].courseId === currentCourse) {
|
|
4452
|
-
currentCluster++;
|
|
4453
|
-
maxCluster = Math.max(maxCluster, currentCluster);
|
|
4454
|
-
} else {
|
|
4455
|
-
currentCourse = session.presentations[i].courseId;
|
|
4456
|
-
currentCluster = 1;
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
if (maxCluster > 3) {
|
|
4460
|
-
logger.info(`
|
|
4461
|
-
\u26A0\uFE0F Detected clustering: max ${maxCluster} cards from same course in a row`);
|
|
4462
|
-
logger.info("This suggests cards are sorted by score rather than round-robin by course.");
|
|
4463
|
-
}
|
|
4464
|
-
console.groupEnd();
|
|
3114
|
+
function computeDeviation(userId, strategyId, salt) {
|
|
3115
|
+
const input = `${userId}:${strategyId}:${salt}`;
|
|
3116
|
+
const hash = fnv1a(input);
|
|
3117
|
+
const normalized = hash / 4294967296;
|
|
3118
|
+
return normalized * 2 - 1;
|
|
4465
3119
|
}
|
|
4466
|
-
function
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
win.skuilder = win.skuilder || {};
|
|
4470
|
-
win.skuilder.session = sessionDebugAPI;
|
|
3120
|
+
function computeSpread(confidence) {
|
|
3121
|
+
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
3122
|
+
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
4471
3123
|
}
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
showCurrentQueue();
|
|
4497
|
-
},
|
|
4498
|
-
/**
|
|
4499
|
-
* Show presentation history for current or past session.
|
|
4500
|
-
*/
|
|
4501
|
-
showHistory(sessionIndex = 0) {
|
|
4502
|
-
showPresentationHistory(sessionIndex);
|
|
4503
|
-
},
|
|
4504
|
-
/**
|
|
4505
|
-
* Analyze course interleaving pattern.
|
|
4506
|
-
*/
|
|
4507
|
-
showInterleaving(sessionIndex = 0) {
|
|
4508
|
-
showInterleaving(sessionIndex);
|
|
4509
|
-
},
|
|
4510
|
-
/**
|
|
4511
|
-
* List all tracked sessions.
|
|
4512
|
-
*/
|
|
4513
|
-
listSessions() {
|
|
4514
|
-
if (activeSession) {
|
|
4515
|
-
logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
|
|
4516
|
-
}
|
|
4517
|
-
if (sessionHistory.length === 0) {
|
|
4518
|
-
logger.info("[Session Debug] No completed sessions in history.");
|
|
4519
|
-
return;
|
|
4520
|
-
}
|
|
4521
|
-
console.table(
|
|
4522
|
-
sessionHistory.map((s, idx) => ({
|
|
4523
|
-
index: idx,
|
|
4524
|
-
id: s.sessionId.slice(-8),
|
|
4525
|
-
started: s.startTime.toLocaleTimeString(),
|
|
4526
|
-
ended: s.endTime?.toLocaleTimeString() || "incomplete",
|
|
4527
|
-
cards: s.presentations.length
|
|
4528
|
-
}))
|
|
4529
|
-
);
|
|
4530
|
-
},
|
|
4531
|
-
/**
|
|
4532
|
-
* Export session history as JSON for bug reports.
|
|
4533
|
-
*/
|
|
4534
|
-
export() {
|
|
4535
|
-
const data = {
|
|
4536
|
-
active: activeSession,
|
|
4537
|
-
history: sessionHistory
|
|
4538
|
-
};
|
|
4539
|
-
const json = JSON.stringify(data, null, 2);
|
|
4540
|
-
logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
|
|
4541
|
-
logger.info(" copy(window.skuilder.session.export())");
|
|
4542
|
-
return json;
|
|
4543
|
-
},
|
|
4544
|
-
/**
|
|
4545
|
-
* Clear session history.
|
|
4546
|
-
*/
|
|
4547
|
-
clear() {
|
|
4548
|
-
sessionHistory.length = 0;
|
|
4549
|
-
logger.info("[Session Debug] Session history cleared.");
|
|
4550
|
-
},
|
|
4551
|
-
/**
|
|
4552
|
-
* Show help.
|
|
4553
|
-
*/
|
|
4554
|
-
help() {
|
|
4555
|
-
logger.info(`
|
|
4556
|
-
\u{1F3AF} Session Debug API
|
|
4557
|
-
|
|
4558
|
-
Commands:
|
|
4559
|
-
.showQueue() Show current queue state (active session only)
|
|
4560
|
-
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
4561
|
-
.showInterleaving(index?) Analyze course interleaving pattern
|
|
4562
|
-
.listSessions() List all tracked sessions
|
|
4563
|
-
.export() Export session data as JSON for bug reports
|
|
4564
|
-
.clear() Clear session history
|
|
4565
|
-
.sessions Access raw session history array
|
|
4566
|
-
.active Access active session (if any)
|
|
4567
|
-
.help() Show this help message
|
|
4568
|
-
|
|
4569
|
-
Example:
|
|
4570
|
-
window.skuilder.session.showHistory()
|
|
4571
|
-
window.skuilder.session.showInterleaving()
|
|
4572
|
-
window.skuilder.session.showQueue()
|
|
4573
|
-
`);
|
|
4574
|
-
}
|
|
3124
|
+
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
3125
|
+
const deviation = computeDeviation(userId, strategyId, salt);
|
|
3126
|
+
const spread = computeSpread(learnable.confidence);
|
|
3127
|
+
const adjustment = deviation * spread * learnable.weight;
|
|
3128
|
+
const effective = learnable.weight + adjustment;
|
|
3129
|
+
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
3130
|
+
}
|
|
3131
|
+
async function createOrchestrationContext(user, course) {
|
|
3132
|
+
let courseConfig;
|
|
3133
|
+
try {
|
|
3134
|
+
courseConfig = await course.getCourseConfig();
|
|
3135
|
+
} catch (e) {
|
|
3136
|
+
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
3137
|
+
courseConfig = {
|
|
3138
|
+
name: "Unknown",
|
|
3139
|
+
description: "",
|
|
3140
|
+
public: false,
|
|
3141
|
+
deleted: false,
|
|
3142
|
+
creator: "",
|
|
3143
|
+
admins: [],
|
|
3144
|
+
moderators: [],
|
|
3145
|
+
dataShapes: [],
|
|
3146
|
+
questionTypes: [],
|
|
3147
|
+
orchestration: { salt: "default" }
|
|
4575
3148
|
};
|
|
4576
|
-
mountSessionDebugger();
|
|
4577
3149
|
}
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
3150
|
+
const userId = user.getUsername();
|
|
3151
|
+
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
3152
|
+
return {
|
|
3153
|
+
user,
|
|
3154
|
+
course,
|
|
3155
|
+
userId,
|
|
3156
|
+
courseConfig,
|
|
3157
|
+
getEffectiveWeight(strategyId, learnable) {
|
|
3158
|
+
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
3159
|
+
},
|
|
3160
|
+
getDeviation(strategyId) {
|
|
3161
|
+
return computeDeviation(userId, strategyId, salt);
|
|
3162
|
+
}
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
3166
|
+
var init_orchestration = __esm({
|
|
3167
|
+
"src/core/orchestration/index.ts"() {
|
|
4583
3168
|
"use strict";
|
|
4584
|
-
init_SrsService();
|
|
4585
|
-
init_EloService();
|
|
4586
|
-
init_ResponseProcessor();
|
|
4587
|
-
init_CardHydrationService();
|
|
4588
|
-
init_ItemQueue();
|
|
4589
|
-
init_couch();
|
|
4590
|
-
init_recording();
|
|
4591
|
-
init_util2();
|
|
4592
|
-
init_navigators();
|
|
4593
|
-
init_SourceMixer();
|
|
4594
|
-
init_MixerDebugger();
|
|
4595
|
-
init_SessionDebugger();
|
|
4596
3169
|
init_logger();
|
|
3170
|
+
init_gradient();
|
|
3171
|
+
init_learning();
|
|
3172
|
+
init_signal();
|
|
3173
|
+
init_recording();
|
|
3174
|
+
MIN_SPREAD = 0.1;
|
|
3175
|
+
MAX_SPREAD = 0.5;
|
|
3176
|
+
MIN_WEIGHT = 0.1;
|
|
3177
|
+
MAX_WEIGHT = 3;
|
|
4597
3178
|
}
|
|
4598
3179
|
});
|
|
4599
3180
|
|
|
@@ -4602,7 +3183,7 @@ var Pipeline_exports = {};
|
|
|
4602
3183
|
__export(Pipeline_exports, {
|
|
4603
3184
|
Pipeline: () => Pipeline
|
|
4604
3185
|
});
|
|
4605
|
-
import { toCourseElo as
|
|
3186
|
+
import { toCourseElo as toCourseElo5 } from "@vue-skuilder/common";
|
|
4606
3187
|
function globToRegex(pattern) {
|
|
4607
3188
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
4608
3189
|
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
@@ -4615,6 +3196,44 @@ function globMatch(value, pattern) {
|
|
|
4615
3196
|
function cardMatchesTagPattern(card, pattern) {
|
|
4616
3197
|
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
4617
3198
|
}
|
|
3199
|
+
function mergeHints2(allHints) {
|
|
3200
|
+
const defined = allHints.filter((h) => h !== null && h !== void 0);
|
|
3201
|
+
if (defined.length === 0) return void 0;
|
|
3202
|
+
const merged = {};
|
|
3203
|
+
const boostTags = {};
|
|
3204
|
+
for (const hints of defined) {
|
|
3205
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
3206
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
if (Object.keys(boostTags).length > 0) {
|
|
3210
|
+
merged.boostTags = boostTags;
|
|
3211
|
+
}
|
|
3212
|
+
const boostCards = {};
|
|
3213
|
+
for (const hints of defined) {
|
|
3214
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
3215
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (Object.keys(boostCards).length > 0) {
|
|
3219
|
+
merged.boostCards = boostCards;
|
|
3220
|
+
}
|
|
3221
|
+
const concatUnique = (field) => {
|
|
3222
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
3223
|
+
if (values.length > 0) {
|
|
3224
|
+
merged[field] = [...new Set(values)];
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
concatUnique("requireTags");
|
|
3228
|
+
concatUnique("requireCards");
|
|
3229
|
+
concatUnique("excludeTags");
|
|
3230
|
+
concatUnique("excludeCards");
|
|
3231
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
3232
|
+
if (labels.length > 0) {
|
|
3233
|
+
merged._label = labels.join("; ");
|
|
3234
|
+
}
|
|
3235
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
3236
|
+
}
|
|
4618
3237
|
function logPipelineConfig(generator, filters) {
|
|
4619
3238
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
4620
3239
|
logger.info(
|
|
@@ -4686,7 +3305,6 @@ var init_Pipeline = __esm({
|
|
|
4686
3305
|
init_logger();
|
|
4687
3306
|
init_orchestration();
|
|
4688
3307
|
init_PipelineDebugger();
|
|
4689
|
-
init_SessionController();
|
|
4690
3308
|
VERBOSE_RESULTS = true;
|
|
4691
3309
|
Pipeline = class extends ContentNavigator {
|
|
4692
3310
|
generator;
|
|
@@ -4766,9 +3384,12 @@ var init_Pipeline = __esm({
|
|
|
4766
3384
|
logger.debug(
|
|
4767
3385
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
4768
3386
|
);
|
|
4769
|
-
|
|
3387
|
+
const generatorResult = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3388
|
+
let cards = generatorResult.cards;
|
|
4770
3389
|
const tGenerate = performance.now();
|
|
4771
3390
|
const generatedCount = cards.length;
|
|
3391
|
+
const mergedHints = mergeHints2([this._ephemeralHints, generatorResult.hints]);
|
|
3392
|
+
this._ephemeralHints = mergedHints ?? null;
|
|
4772
3393
|
let generatorSummaries;
|
|
4773
3394
|
if (this.generator.generators) {
|
|
4774
3395
|
const genMap = /* @__PURE__ */ new Map();
|
|
@@ -4854,7 +3475,7 @@ var init_Pipeline = __esm({
|
|
|
4854
3475
|
} catch (e) {
|
|
4855
3476
|
logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
|
|
4856
3477
|
}
|
|
4857
|
-
return result;
|
|
3478
|
+
return { cards: result };
|
|
4858
3479
|
}
|
|
4859
3480
|
/**
|
|
4860
3481
|
* Batch hydrate tags for all cards.
|
|
@@ -5009,7 +3630,7 @@ var init_Pipeline = __esm({
|
|
|
5009
3630
|
let userElo = 1e3;
|
|
5010
3631
|
try {
|
|
5011
3632
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
5012
|
-
const courseElo =
|
|
3633
|
+
const courseElo = toCourseElo5(courseReg.elo);
|
|
5013
3634
|
userElo = courseElo.global.score;
|
|
5014
3635
|
} catch (e) {
|
|
5015
3636
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
@@ -5070,7 +3691,7 @@ var init_Pipeline = __esm({
|
|
|
5070
3691
|
*/
|
|
5071
3692
|
async getTagEloStatus(tagFilter) {
|
|
5072
3693
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
5073
|
-
const courseElo =
|
|
3694
|
+
const courseElo = toCourseElo5(courseReg.elo);
|
|
5074
3695
|
const result = {};
|
|
5075
3696
|
if (!tagFilter) {
|
|
5076
3697
|
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
@@ -5672,7 +4293,7 @@ import {
|
|
|
5672
4293
|
EloToNumber,
|
|
5673
4294
|
Status,
|
|
5674
4295
|
blankCourseElo as blankCourseElo2,
|
|
5675
|
-
toCourseElo as
|
|
4296
|
+
toCourseElo as toCourseElo6
|
|
5676
4297
|
} from "@vue-skuilder/common";
|
|
5677
4298
|
function randIntWeightedTowardZero(n) {
|
|
5678
4299
|
return Math.floor(Math.random() * Math.random() * Math.random() * n);
|
|
@@ -5943,7 +4564,7 @@ var init_courseDB = __esm({
|
|
|
5943
4564
|
docs.rows.forEach((r) => {
|
|
5944
4565
|
if (isSuccessRow(r)) {
|
|
5945
4566
|
if (r.doc && r.doc.elo) {
|
|
5946
|
-
ret.push(
|
|
4567
|
+
ret.push(toCourseElo6(r.doc.elo));
|
|
5947
4568
|
} else {
|
|
5948
4569
|
logger.warn("no elo data for card: " + r.id);
|
|
5949
4570
|
ret.push(blankCourseElo2());
|
|
@@ -6448,7 +5069,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6448
5069
|
});
|
|
6449
5070
|
|
|
6450
5071
|
// src/impl/couch/classroomDB.ts
|
|
6451
|
-
import
|
|
5072
|
+
import moment2 from "moment";
|
|
6452
5073
|
function getClassroomDB(classID, version) {
|
|
6453
5074
|
const dbName = `classdb-${version}-${classID}`;
|
|
6454
5075
|
logger.info(`Retrieving classroom db: ${dbName}`);
|
|
@@ -6578,14 +5199,14 @@ var init_classroomDB2 = __esm({
|
|
|
6578
5199
|
}
|
|
6579
5200
|
const activeCards = await this._user.getActiveCards();
|
|
6580
5201
|
const activeCardIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
6581
|
-
const now =
|
|
5202
|
+
const now = moment2.utc();
|
|
6582
5203
|
const assigned = await this.getAssignedContent();
|
|
6583
|
-
const due = assigned.filter((c) => now.isAfter(
|
|
5204
|
+
const due = assigned.filter((c) => now.isAfter(moment2.utc(c.activeOn, REVIEW_TIME_FORMAT)));
|
|
6584
5205
|
logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(due)}`);
|
|
6585
5206
|
for (const content of due) {
|
|
6586
5207
|
if (content.type === "course") {
|
|
6587
5208
|
const db = new CourseDB(content.courseID, async () => this._user);
|
|
6588
|
-
const courseCards = await db.getWeightedCards(limit);
|
|
5209
|
+
const { cards: courseCards } = await db.getWeightedCards(limit);
|
|
6589
5210
|
for (const card of courseCards) {
|
|
6590
5211
|
if (!activeCardIds.has(card.cardId)) {
|
|
6591
5212
|
weighted.push({
|
|
@@ -6648,7 +5269,7 @@ var init_classroomDB2 = __esm({
|
|
|
6648
5269
|
logger.info(
|
|
6649
5270
|
`[StudentClassroomDB] New cards from classroom ${this._cfg.name}: ${weighted.length} total (reviews + new)`
|
|
6650
5271
|
);
|
|
6651
|
-
return weighted.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5272
|
+
return { cards: weighted.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
6652
5273
|
}
|
|
6653
5274
|
};
|
|
6654
5275
|
TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
|
|
@@ -6706,8 +5327,8 @@ var init_classroomDB2 = __esm({
|
|
|
6706
5327
|
type: "tag",
|
|
6707
5328
|
_id: id,
|
|
6708
5329
|
assignedBy: content.assignedBy,
|
|
6709
|
-
assignedOn:
|
|
6710
|
-
activeOn: content.activeOn ||
|
|
5330
|
+
assignedOn: moment2.utc(),
|
|
5331
|
+
activeOn: content.activeOn || moment2.utc()
|
|
6711
5332
|
});
|
|
6712
5333
|
} else {
|
|
6713
5334
|
put = await this._db.put({
|
|
@@ -6715,8 +5336,8 @@ var init_classroomDB2 = __esm({
|
|
|
6715
5336
|
type: "course",
|
|
6716
5337
|
_id: id,
|
|
6717
5338
|
assignedBy: content.assignedBy,
|
|
6718
|
-
assignedOn:
|
|
6719
|
-
activeOn: content.activeOn ||
|
|
5339
|
+
assignedOn: moment2.utc(),
|
|
5340
|
+
activeOn: content.activeOn || moment2.utc()
|
|
6720
5341
|
});
|
|
6721
5342
|
}
|
|
6722
5343
|
if (put.ok) {
|
|
@@ -6828,7 +5449,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
6828
5449
|
async getWeightedCards(limit) {
|
|
6829
5450
|
if (!hasActiveFilter(this.filter)) {
|
|
6830
5451
|
logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
|
|
6831
|
-
return [];
|
|
5452
|
+
return { cards: [] };
|
|
6832
5453
|
}
|
|
6833
5454
|
const eligibleCardIds = await this.resolveFilteredCardIds();
|
|
6834
5455
|
const activeCards = await this.user.getActiveCards();
|
|
@@ -6880,7 +5501,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
6880
5501
|
}
|
|
6881
5502
|
]
|
|
6882
5503
|
}));
|
|
6883
|
-
return [...reviewWeighted, ...newCardWeighted].slice(0, limit);
|
|
5504
|
+
return { cards: [...reviewWeighted, ...newCardWeighted].slice(0, limit) };
|
|
6884
5505
|
}
|
|
6885
5506
|
/**
|
|
6886
5507
|
* Clears the cached resolved card IDs.
|
|
@@ -6988,6 +5609,17 @@ var init_userOutcome = __esm({
|
|
|
6988
5609
|
}
|
|
6989
5610
|
});
|
|
6990
5611
|
|
|
5612
|
+
// src/core/util/index.ts
|
|
5613
|
+
function getCardHistoryID(courseID, cardID) {
|
|
5614
|
+
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
5615
|
+
}
|
|
5616
|
+
var init_util = __esm({
|
|
5617
|
+
"src/core/util/index.ts"() {
|
|
5618
|
+
"use strict";
|
|
5619
|
+
init_types_legacy();
|
|
5620
|
+
}
|
|
5621
|
+
});
|
|
5622
|
+
|
|
6991
5623
|
// src/core/bulkImport/cardProcessor.ts
|
|
6992
5624
|
import { Status as Status2 } from "@vue-skuilder/common";
|
|
6993
5625
|
var init_cardProcessor = __esm({
|
|
@@ -6998,7 +5630,7 @@ var init_cardProcessor = __esm({
|
|
|
6998
5630
|
});
|
|
6999
5631
|
|
|
7000
5632
|
// src/core/bulkImport/types.ts
|
|
7001
|
-
var
|
|
5633
|
+
var init_types3 = __esm({
|
|
7002
5634
|
"src/core/bulkImport/types.ts"() {
|
|
7003
5635
|
"use strict";
|
|
7004
5636
|
}
|
|
@@ -7009,12 +5641,33 @@ var init_bulkImport = __esm({
|
|
|
7009
5641
|
"src/core/bulkImport/index.ts"() {
|
|
7010
5642
|
"use strict";
|
|
7011
5643
|
init_cardProcessor();
|
|
7012
|
-
|
|
5644
|
+
init_types3();
|
|
5645
|
+
}
|
|
5646
|
+
});
|
|
5647
|
+
|
|
5648
|
+
// src/util/dataDirectory.ts
|
|
5649
|
+
import * as path from "path";
|
|
5650
|
+
import * as os from "os";
|
|
5651
|
+
function getAppDataDirectory() {
|
|
5652
|
+
if (ENV.LOCAL_STORAGE_PREFIX) {
|
|
5653
|
+
return path.join(os.homedir(), `.tuilder`, ENV.LOCAL_STORAGE_PREFIX);
|
|
5654
|
+
} else {
|
|
5655
|
+
return path.join(os.homedir(), ".tuilder");
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
function getDbPath(dbName) {
|
|
5659
|
+
return path.join(getAppDataDirectory(), dbName);
|
|
5660
|
+
}
|
|
5661
|
+
var init_dataDirectory = __esm({
|
|
5662
|
+
"src/util/dataDirectory.ts"() {
|
|
5663
|
+
"use strict";
|
|
5664
|
+
init_logger();
|
|
5665
|
+
init_factory();
|
|
7013
5666
|
}
|
|
7014
5667
|
});
|
|
7015
5668
|
|
|
7016
5669
|
// src/impl/common/userDBHelpers.ts
|
|
7017
|
-
import
|
|
5670
|
+
import moment3 from "moment";
|
|
7018
5671
|
function hexEncode(str) {
|
|
7019
5672
|
let hex;
|
|
7020
5673
|
let returnStr = "";
|
|
@@ -7042,7 +5695,7 @@ function getStartAndEndKeys2(key) {
|
|
|
7042
5695
|
};
|
|
7043
5696
|
}
|
|
7044
5697
|
function updateGuestAccountExpirationDate(guestDB) {
|
|
7045
|
-
const currentTime =
|
|
5698
|
+
const currentTime = moment3.utc();
|
|
7046
5699
|
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
7047
5700
|
const expiryDocID2 = "GuestAccountExpirationDate";
|
|
7048
5701
|
void guestDB.get(expiryDocID2).then((doc) => {
|
|
@@ -7067,7 +5720,7 @@ function getLocalUserDB(username) {
|
|
|
7067
5720
|
}
|
|
7068
5721
|
}
|
|
7069
5722
|
function scheduleCardReviewLocal(userDB, review) {
|
|
7070
|
-
const now =
|
|
5723
|
+
const now = moment3.utc();
|
|
7071
5724
|
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
7072
5725
|
void userDB.put({
|
|
7073
5726
|
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT2),
|
|
@@ -7453,7 +6106,7 @@ var init_core = __esm({
|
|
|
7453
6106
|
});
|
|
7454
6107
|
|
|
7455
6108
|
// src/impl/couch/user-course-relDB.ts
|
|
7456
|
-
import
|
|
6109
|
+
import moment4 from "moment";
|
|
7457
6110
|
var UsrCrsData;
|
|
7458
6111
|
var init_user_course_relDB = __esm({
|
|
7459
6112
|
"src/impl/couch/user-course-relDB.ts"() {
|
|
@@ -7467,11 +6120,11 @@ var init_user_course_relDB = __esm({
|
|
|
7467
6120
|
this._courseId = courseId;
|
|
7468
6121
|
}
|
|
7469
6122
|
async getReviewsForcast(daysCount) {
|
|
7470
|
-
const time =
|
|
6123
|
+
const time = moment4.utc().add(daysCount, "days");
|
|
7471
6124
|
return this.getReviewstoDate(time);
|
|
7472
6125
|
}
|
|
7473
6126
|
async getPendingReviews() {
|
|
7474
|
-
const now =
|
|
6127
|
+
const now = moment4.utc();
|
|
7475
6128
|
return this.getReviewstoDate(now);
|
|
7476
6129
|
}
|
|
7477
6130
|
async getScheduledReviewCount() {
|
|
@@ -7507,7 +6160,7 @@ var init_user_course_relDB = __esm({
|
|
|
7507
6160
|
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
7508
6161
|
);
|
|
7509
6162
|
return allReviews.filter((review) => {
|
|
7510
|
-
const reviewTime =
|
|
6163
|
+
const reviewTime = moment4.utc(review.reviewTime);
|
|
7511
6164
|
return targetDate.isAfter(reviewTime);
|
|
7512
6165
|
});
|
|
7513
6166
|
}
|
|
@@ -7517,7 +6170,7 @@ var init_user_course_relDB = __esm({
|
|
|
7517
6170
|
|
|
7518
6171
|
// src/impl/common/BaseUserDB.ts
|
|
7519
6172
|
import { Status as Status3 } from "@vue-skuilder/common";
|
|
7520
|
-
import
|
|
6173
|
+
import moment5 from "moment";
|
|
7521
6174
|
function accomodateGuest() {
|
|
7522
6175
|
logger.log("[funnel] accomodateGuest() called");
|
|
7523
6176
|
if (typeof localStorage === "undefined") {
|
|
@@ -7960,7 +6613,7 @@ Currently logged-in as ${this._username}.`
|
|
|
7960
6613
|
);
|
|
7961
6614
|
return reviews.rows.filter((r) => {
|
|
7962
6615
|
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
7963
|
-
const date =
|
|
6616
|
+
const date = moment5.utc(
|
|
7964
6617
|
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
7965
6618
|
REVIEW_TIME_FORMAT2
|
|
7966
6619
|
);
|
|
@@ -7973,11 +6626,11 @@ Currently logged-in as ${this._username}.`
|
|
|
7973
6626
|
}).map((r) => r.doc);
|
|
7974
6627
|
}
|
|
7975
6628
|
async getReviewsForcast(daysCount) {
|
|
7976
|
-
const time =
|
|
6629
|
+
const time = moment5.utc().add(daysCount, "days");
|
|
7977
6630
|
return this.getReviewstoDate(time);
|
|
7978
6631
|
}
|
|
7979
6632
|
async getPendingReviews(course_id) {
|
|
7980
|
-
const now =
|
|
6633
|
+
const now = moment5.utc();
|
|
7981
6634
|
return this.getReviewstoDate(now, course_id);
|
|
7982
6635
|
}
|
|
7983
6636
|
async getScheduledReviewCount(course_id) {
|
|
@@ -8264,7 +6917,7 @@ Currently logged-in as ${this._username}.`
|
|
|
8264
6917
|
*/
|
|
8265
6918
|
async putCardRecord(record) {
|
|
8266
6919
|
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
8267
|
-
record.timeStamp =
|
|
6920
|
+
record.timeStamp = moment5.utc(record.timeStamp).toString();
|
|
8268
6921
|
try {
|
|
8269
6922
|
const cardHistory = await this.update(
|
|
8270
6923
|
cardHistoryID,
|
|
@@ -8280,7 +6933,7 @@ Currently logged-in as ${this._username}.`
|
|
|
8280
6933
|
const ret = {
|
|
8281
6934
|
...record2
|
|
8282
6935
|
};
|
|
8283
|
-
ret.timeStamp =
|
|
6936
|
+
ret.timeStamp = moment5.utc(record2.timeStamp);
|
|
8284
6937
|
return ret;
|
|
8285
6938
|
});
|
|
8286
6939
|
return cardHistory;
|
|
@@ -8954,7 +7607,7 @@ var init_CourseSyncService = __esm({
|
|
|
8954
7607
|
});
|
|
8955
7608
|
|
|
8956
7609
|
// src/impl/couch/auth.ts
|
|
8957
|
-
import
|
|
7610
|
+
import fetch from "cross-fetch";
|
|
8958
7611
|
async function getCurrentSession() {
|
|
8959
7612
|
try {
|
|
8960
7613
|
if (ENV.COUCHDB_SERVER_URL === NOT_SET || ENV.COUCHDB_SERVER_PROTOCOL === NOT_SET) {
|
|
@@ -8963,7 +7616,7 @@ async function getCurrentSession() {
|
|
|
8963
7616
|
const baseUrl = ENV.COUCHDB_SERVER_URL.endsWith("/") ? ENV.COUCHDB_SERVER_URL.slice(0, -1) : ENV.COUCHDB_SERVER_URL;
|
|
8964
7617
|
const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${baseUrl}/_session`;
|
|
8965
7618
|
logger.debug(`Attempting session check at: ${url}`);
|
|
8966
|
-
const response = await
|
|
7619
|
+
const response = await fetch(url, {
|
|
8967
7620
|
method: "GET",
|
|
8968
7621
|
credentials: "include"
|
|
8969
7622
|
});
|
|
@@ -9221,8 +7874,8 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
9221
7874
|
});
|
|
9222
7875
|
|
|
9223
7876
|
// src/impl/couch/index.ts
|
|
9224
|
-
import
|
|
9225
|
-
import
|
|
7877
|
+
import fetch2 from "cross-fetch";
|
|
7878
|
+
import moment6 from "moment";
|
|
9226
7879
|
import process2 from "process";
|
|
9227
7880
|
function hexEncode2(str) {
|
|
9228
7881
|
let hex;
|
|
@@ -9283,7 +7936,7 @@ async function usernameIsAvailable(username) {
|
|
|
9283
7936
|
log(`Checking availability of ${username}`);
|
|
9284
7937
|
try {
|
|
9285
7938
|
const url = ENV.COUCHDB_SERVER_URL + "userdb-" + hexEncode2(username);
|
|
9286
|
-
const response = await
|
|
7939
|
+
const response = await fetch2(url, { method: "HEAD" });
|
|
9287
7940
|
return response.status === 404;
|
|
9288
7941
|
} catch (error) {
|
|
9289
7942
|
log(`Error checking username availability: ${error}`);
|
|
@@ -9291,7 +7944,7 @@ async function usernameIsAvailable(username) {
|
|
|
9291
7944
|
}
|
|
9292
7945
|
}
|
|
9293
7946
|
function updateGuestAccountExpirationDate2(guestDB) {
|
|
9294
|
-
const currentTime =
|
|
7947
|
+
const currentTime = moment6.utc();
|
|
9295
7948
|
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
9296
7949
|
void guestDB.get(expiryDocID).then((doc) => {
|
|
9297
7950
|
return guestDB.put({
|
|
@@ -9354,7 +8007,7 @@ function getCouchUserDB(username) {
|
|
|
9354
8007
|
return ret;
|
|
9355
8008
|
}
|
|
9356
8009
|
function scheduleCardReview(review) {
|
|
9357
|
-
const now =
|
|
8010
|
+
const now = moment6.utc();
|
|
9358
8011
|
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
9359
8012
|
void getCouchUserDB(review.user).put({
|
|
9360
8013
|
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
|