@vue-skuilder/db 0.1.32-b → 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/{contentSource-DF1nUbPQ.d.cts → contentSource-BMlMwSiG.d.cts} +124 -5
- package/dist/{contentSource-Bdwkvqa8.d.ts → contentSource-Ht3N2f-y.d.ts} +124 -5
- package/dist/core/index.d.cts +26 -83
- package/dist/core/index.d.ts +26 -83
- package/dist/core/index.js +767 -71
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +767 -71
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BQdfJuBN.d.cts → dataLayerProvider-BEqB8VBR.d.cts} +1 -1
- package/dist/{dataLayerProvider-BKmVoyJR.d.ts → dataLayerProvider-DObSXjnf.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +18 -5
- package/dist/impl/couch/index.d.ts +18 -5
- package/dist/impl/couch/index.js +817 -74
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +817 -74
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -4
- package/dist/impl/static/index.d.ts +4 -4
- package/dist/impl/static/index.js +763 -67
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +763 -67
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +23 -8
- package/dist/index.d.ts +23 -8
- package/dist/index.js +872 -86
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +872 -86
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +2 -2
- package/package.json +2 -2
- package/src/core/interfaces/contentSource.ts +3 -3
- package/src/core/navigators/Pipeline.ts +104 -32
- package/src/core/navigators/PipelineDebugger.ts +152 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +90 -6
- 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 +710 -46
- package/src/core/navigators/generators/srs.ts +3 -4
- package/src/core/navigators/generators/types.ts +48 -2
- package/src/core/navigators/index.ts +4 -3
- package/src/impl/couch/CourseSyncService.ts +72 -4
- package/src/impl/couch/classroomDB.ts +4 -3
- package/src/impl/couch/courseDB.ts +5 -4
- package/src/impl/static/courseDB.ts +5 -4
- package/src/study/SessionController.ts +58 -10
- package/src/study/TagFilteredContentSource.ts +4 -3
- package/src/study/services/EloService.ts +22 -3
- package/src/study/services/ResponseProcessor.ts +7 -3
|
@@ -529,13 +529,20 @@ function captureRun(report) {
|
|
|
529
529
|
runHistory.pop();
|
|
530
530
|
}
|
|
531
531
|
}
|
|
532
|
-
function
|
|
532
|
+
function parseCardElo(provenance) {
|
|
533
|
+
const eloEntry = provenance.find((p) => p.strategy === "elo");
|
|
534
|
+
if (!eloEntry?.reason) return void 0;
|
|
535
|
+
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
536
|
+
return match ? parseInt(match[1], 10) : void 0;
|
|
537
|
+
}
|
|
538
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
|
|
533
539
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
534
540
|
const cards = allCards.map((card) => ({
|
|
535
541
|
cardId: card.cardId,
|
|
536
542
|
courseId: card.courseId,
|
|
537
543
|
origin: getOrigin(card),
|
|
538
544
|
finalScore: card.score,
|
|
545
|
+
cardElo: parseCardElo(card.provenance),
|
|
539
546
|
provenance: card.provenance,
|
|
540
547
|
tags: card.tags,
|
|
541
548
|
selected: selectedIds.has(card.cardId)
|
|
@@ -545,6 +552,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
545
552
|
return {
|
|
546
553
|
courseId,
|
|
547
554
|
courseName,
|
|
555
|
+
userElo,
|
|
548
556
|
generatorName,
|
|
549
557
|
generators,
|
|
550
558
|
generatedCount,
|
|
@@ -565,6 +573,7 @@ function printRunSummary(run) {
|
|
|
565
573
|
console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
|
|
566
574
|
logger.info(`Run ID: ${run.runId}`);
|
|
567
575
|
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
576
|
+
logger.info(`User ELO: ${run.userElo ?? "unknown"}`);
|
|
568
577
|
logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
|
|
569
578
|
if (run.generators && run.generators.length > 0) {
|
|
570
579
|
console.group("Generator breakdown:");
|
|
@@ -651,8 +660,12 @@ var init_PipelineDebugger = __esm({
|
|
|
651
660
|
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
652
661
|
logger.info(`Course: ${card.courseId}`);
|
|
653
662
|
logger.info(`Origin: ${card.origin}`);
|
|
663
|
+
logger.info(`Card ELO: ${card.cardElo ?? "unknown"} | User ELO: ${run.userElo ?? "unknown"}`);
|
|
654
664
|
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
655
665
|
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
666
|
+
if (card.tags && card.tags.length > 0) {
|
|
667
|
+
logger.info(`Tags (${card.tags.length}): ${card.tags.join(", ")}`);
|
|
668
|
+
}
|
|
656
669
|
logger.info("Provenance:");
|
|
657
670
|
logger.info(formatProvenance(card.provenance));
|
|
658
671
|
console.groupEnd();
|
|
@@ -705,6 +718,81 @@ var init_PipelineDebugger = __esm({
|
|
|
705
718
|
}
|
|
706
719
|
console.groupEnd();
|
|
707
720
|
},
|
|
721
|
+
/**
|
|
722
|
+
* Show prescribed-related cards from the most recent run.
|
|
723
|
+
*
|
|
724
|
+
* Highlights:
|
|
725
|
+
* - cards directly generated by the prescribed strategy
|
|
726
|
+
* - blocked prescribed targets mentioned in provenance
|
|
727
|
+
* - support tags resolved for blocked targets
|
|
728
|
+
*
|
|
729
|
+
* @param groupId - Optional prescribed group ID filter (e.g. 'intro-core')
|
|
730
|
+
*/
|
|
731
|
+
showPrescribed(groupId) {
|
|
732
|
+
if (runHistory.length === 0) {
|
|
733
|
+
logger.info("[Pipeline Debug] No runs captured yet.");
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const run = runHistory[0];
|
|
737
|
+
const prescribedCards = run.cards.filter(
|
|
738
|
+
(c) => c.provenance.some((p) => p.strategy === "prescribed")
|
|
739
|
+
);
|
|
740
|
+
console.group(`\u{1F9ED} Prescribed Debug (${run.courseId})`);
|
|
741
|
+
if (prescribedCards.length === 0) {
|
|
742
|
+
logger.info("No prescribed-generated cards were present in the most recent run.");
|
|
743
|
+
console.groupEnd();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const rows = prescribedCards.map((card) => {
|
|
747
|
+
const prescribedProv = card.provenance.find((p) => p.strategy === "prescribed");
|
|
748
|
+
const reason = prescribedProv?.reason ?? "";
|
|
749
|
+
const parsedGroup = reason.match(/group=([^;]+)/)?.[1] ?? "unknown";
|
|
750
|
+
const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
|
|
751
|
+
const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
|
|
752
|
+
const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
|
|
753
|
+
const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
|
|
754
|
+
const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
|
|
755
|
+
return {
|
|
756
|
+
group: parsedGroup,
|
|
757
|
+
mode,
|
|
758
|
+
cardId: card.cardId,
|
|
759
|
+
selected: card.selected ? "yes" : "no",
|
|
760
|
+
finalScore: card.finalScore.toFixed(3),
|
|
761
|
+
blocked,
|
|
762
|
+
blockedTargets,
|
|
763
|
+
supportTags,
|
|
764
|
+
multiplier
|
|
765
|
+
};
|
|
766
|
+
}).filter((row) => !groupId || row.group === groupId).sort((a, b) => Number(b.finalScore) - Number(a.finalScore));
|
|
767
|
+
if (rows.length === 0) {
|
|
768
|
+
logger.info(
|
|
769
|
+
`[Pipeline Debug] No prescribed cards matched group '${groupId}' in the most recent run.`
|
|
770
|
+
);
|
|
771
|
+
console.groupEnd();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
console.table(rows);
|
|
775
|
+
const selectedRows = rows.filter((r) => r.selected === "yes");
|
|
776
|
+
const blockedTargetSet = /* @__PURE__ */ new Set();
|
|
777
|
+
const supportTagSet = /* @__PURE__ */ new Set();
|
|
778
|
+
for (const row of rows) {
|
|
779
|
+
if (row.blockedTargets && row.blockedTargets !== "none") {
|
|
780
|
+
row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
|
|
781
|
+
}
|
|
782
|
+
if (row.supportTags && row.supportTags !== "none") {
|
|
783
|
+
row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
logger.info(`Prescribed cards in run: ${rows.length}`);
|
|
787
|
+
logger.info(`Selected prescribed cards: ${selectedRows.length}`);
|
|
788
|
+
logger.info(
|
|
789
|
+
`Blocked prescribed targets referenced: ${blockedTargetSet.size > 0 ? [...blockedTargetSet].join(", ") : "none"}`
|
|
790
|
+
);
|
|
791
|
+
logger.info(
|
|
792
|
+
`Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
|
|
793
|
+
);
|
|
794
|
+
console.groupEnd();
|
|
795
|
+
},
|
|
708
796
|
/**
|
|
709
797
|
* Show all runs in compact format.
|
|
710
798
|
*/
|
|
@@ -816,6 +904,27 @@ var init_PipelineDebugger = __esm({
|
|
|
816
904
|
}
|
|
817
905
|
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
818
906
|
},
|
|
907
|
+
/**
|
|
908
|
+
* Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
|
|
909
|
+
*
|
|
910
|
+
* @param tagFilter - Optional glob pattern(s) to filter tags.
|
|
911
|
+
* Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
|
|
912
|
+
*/
|
|
913
|
+
async showTagElo(tagFilter) {
|
|
914
|
+
if (!_activePipeline) {
|
|
915
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const status = await _activePipeline.getTagEloStatus(tagFilter);
|
|
919
|
+
const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
|
|
920
|
+
if (entries.length === 0) {
|
|
921
|
+
logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ""}.`);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
console.table(
|
|
925
|
+
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
926
|
+
);
|
|
927
|
+
},
|
|
819
928
|
/**
|
|
820
929
|
* Show help.
|
|
821
930
|
*/
|
|
@@ -827,10 +936,12 @@ Commands:
|
|
|
827
936
|
.showLastRun() Show summary of most recent pipeline run
|
|
828
937
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
829
938
|
.showCard(cardId) Show provenance trail for a specific card
|
|
939
|
+
.showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
|
|
830
940
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
831
941
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
832
942
|
.showRegistry() Show navigator registry (classes + roles)
|
|
833
943
|
.showStrategies() Show registry + strategy mapping from last run
|
|
944
|
+
.showPrescribed(id?) Show prescribed-generated cards and blocked/support details from last run
|
|
834
945
|
.listRuns() List all captured runs in table format
|
|
835
946
|
.export() Export run history as JSON for bug reports
|
|
836
947
|
.clear() Clear run history
|
|
@@ -854,6 +965,44 @@ __export(CompositeGenerator_exports, {
|
|
|
854
965
|
AggregationMode: () => AggregationMode,
|
|
855
966
|
default: () => CompositeGenerator
|
|
856
967
|
});
|
|
968
|
+
function mergeHints(allHints) {
|
|
969
|
+
const defined = allHints.filter((h) => h !== void 0);
|
|
970
|
+
if (defined.length === 0) return void 0;
|
|
971
|
+
const merged = {};
|
|
972
|
+
const boostTags = {};
|
|
973
|
+
for (const hints of defined) {
|
|
974
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
975
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (Object.keys(boostTags).length > 0) {
|
|
979
|
+
merged.boostTags = boostTags;
|
|
980
|
+
}
|
|
981
|
+
const boostCards = {};
|
|
982
|
+
for (const hints of defined) {
|
|
983
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
984
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (Object.keys(boostCards).length > 0) {
|
|
988
|
+
merged.boostCards = boostCards;
|
|
989
|
+
}
|
|
990
|
+
const concatUnique = (field) => {
|
|
991
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
992
|
+
if (values.length > 0) {
|
|
993
|
+
merged[field] = [...new Set(values)];
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
concatUnique("requireTags");
|
|
997
|
+
concatUnique("requireCards");
|
|
998
|
+
concatUnique("excludeTags");
|
|
999
|
+
concatUnique("excludeCards");
|
|
1000
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
1001
|
+
if (labels.length > 0) {
|
|
1002
|
+
merged._label = labels.join("; ");
|
|
1003
|
+
}
|
|
1004
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
1005
|
+
}
|
|
857
1006
|
var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
|
|
858
1007
|
var init_CompositeGenerator = __esm({
|
|
859
1008
|
"src/core/navigators/generators/CompositeGenerator.ts"() {
|
|
@@ -917,17 +1066,18 @@ var init_CompositeGenerator = __esm({
|
|
|
917
1066
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
918
1067
|
);
|
|
919
1068
|
const generatorSummaries = [];
|
|
920
|
-
results.forEach((
|
|
1069
|
+
results.forEach((result, index) => {
|
|
1070
|
+
const cards2 = result.cards;
|
|
921
1071
|
const gen = this.generators[index];
|
|
922
1072
|
const genName = gen.name || `Generator ${index}`;
|
|
923
|
-
const newCards =
|
|
924
|
-
const reviewCards =
|
|
925
|
-
if (
|
|
926
|
-
const topScore = Math.max(...
|
|
1073
|
+
const newCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("new card"));
|
|
1074
|
+
const reviewCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("review"));
|
|
1075
|
+
if (cards2.length > 0) {
|
|
1076
|
+
const topScore = Math.max(...cards2.map((c) => c.score)).toFixed(2);
|
|
927
1077
|
const parts = [];
|
|
928
1078
|
if (newCards.length > 0) parts.push(`${newCards.length} new`);
|
|
929
1079
|
if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
|
|
930
|
-
const breakdown = parts.length > 0 ? parts.join(", ") : `${
|
|
1080
|
+
const breakdown = parts.length > 0 ? parts.join(", ") : `${cards2.length} cards`;
|
|
931
1081
|
generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
|
|
932
1082
|
} else {
|
|
933
1083
|
generatorSummaries.push(`${genName}: 0 cards`);
|
|
@@ -935,7 +1085,8 @@ var init_CompositeGenerator = __esm({
|
|
|
935
1085
|
});
|
|
936
1086
|
logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
|
|
937
1087
|
const byCardId = /* @__PURE__ */ new Map();
|
|
938
|
-
results.forEach((
|
|
1088
|
+
results.forEach((result, index) => {
|
|
1089
|
+
const cards2 = result.cards;
|
|
939
1090
|
const gen = this.generators[index];
|
|
940
1091
|
let weight = gen.learnable?.weight ?? 1;
|
|
941
1092
|
let deviation;
|
|
@@ -946,7 +1097,7 @@ var init_CompositeGenerator = __esm({
|
|
|
946
1097
|
deviation = context.orchestration.getDeviation(strategyId);
|
|
947
1098
|
}
|
|
948
1099
|
}
|
|
949
|
-
for (const card of
|
|
1100
|
+
for (const card of cards2) {
|
|
950
1101
|
if (card.provenance.length > 0) {
|
|
951
1102
|
card.provenance[0].effectiveWeight = weight;
|
|
952
1103
|
card.provenance[0].deviation = deviation;
|
|
@@ -958,15 +1109,15 @@ var init_CompositeGenerator = __esm({
|
|
|
958
1109
|
});
|
|
959
1110
|
const merged = [];
|
|
960
1111
|
for (const [, items] of byCardId) {
|
|
961
|
-
const
|
|
1112
|
+
const cards2 = items.map((i) => i.card);
|
|
962
1113
|
const aggregatedScore = this.aggregateScores(items);
|
|
963
1114
|
const finalScore = Math.min(1, aggregatedScore);
|
|
964
|
-
const mergedProvenance =
|
|
965
|
-
const initialScore =
|
|
1115
|
+
const mergedProvenance = cards2.flatMap((c) => c.provenance);
|
|
1116
|
+
const initialScore = cards2[0].score;
|
|
966
1117
|
const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
|
|
967
1118
|
const reason = this.buildAggregationReason(items, finalScore);
|
|
968
1119
|
merged.push({
|
|
969
|
-
...
|
|
1120
|
+
...cards2[0],
|
|
970
1121
|
score: finalScore,
|
|
971
1122
|
provenance: [
|
|
972
1123
|
...mergedProvenance,
|
|
@@ -981,7 +1132,9 @@ var init_CompositeGenerator = __esm({
|
|
|
981
1132
|
]
|
|
982
1133
|
});
|
|
983
1134
|
}
|
|
984
|
-
|
|
1135
|
+
const cards = merged.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1136
|
+
const hints = mergeHints(results.map((result) => result.hints));
|
|
1137
|
+
return { cards, hints };
|
|
985
1138
|
}
|
|
986
1139
|
/**
|
|
987
1140
|
* Build human-readable reason for score aggregation.
|
|
@@ -1112,16 +1265,16 @@ var init_elo = __esm({
|
|
|
1112
1265
|
};
|
|
1113
1266
|
});
|
|
1114
1267
|
scored.sort((a, b) => b.score - a.score);
|
|
1115
|
-
const
|
|
1116
|
-
if (
|
|
1117
|
-
const topScores =
|
|
1268
|
+
const cards = scored.slice(0, limit);
|
|
1269
|
+
if (cards.length > 0) {
|
|
1270
|
+
const topScores = cards.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
|
|
1118
1271
|
logger.info(
|
|
1119
|
-
`[ELO] Course ${this.course.getCourseID()}: ${
|
|
1272
|
+
`[ELO] Course ${this.course.getCourseID()}: ${cards.length} new cards (top scores: ${topScores})`
|
|
1120
1273
|
);
|
|
1121
1274
|
} else {
|
|
1122
1275
|
logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
|
|
1123
1276
|
}
|
|
1124
|
-
return
|
|
1277
|
+
return { cards };
|
|
1125
1278
|
}
|
|
1126
1279
|
};
|
|
1127
1280
|
}
|
|
@@ -1140,60 +1293,476 @@ var prescribed_exports = {};
|
|
|
1140
1293
|
__export(prescribed_exports, {
|
|
1141
1294
|
default: () => PrescribedCardsGenerator
|
|
1142
1295
|
});
|
|
1143
|
-
|
|
1296
|
+
function dedupe(arr) {
|
|
1297
|
+
return [...new Set(arr)];
|
|
1298
|
+
}
|
|
1299
|
+
function isoNow() {
|
|
1300
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1301
|
+
}
|
|
1302
|
+
function clamp(value, min, max) {
|
|
1303
|
+
return Math.max(min, Math.min(max, value));
|
|
1304
|
+
}
|
|
1305
|
+
function matchesTagPattern(tag, pattern) {
|
|
1306
|
+
if (pattern === "*") return true;
|
|
1307
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1308
|
+
const re = new RegExp(`^${escaped}$`);
|
|
1309
|
+
return re.test(tag);
|
|
1310
|
+
}
|
|
1311
|
+
function pickTopByScore(cards, limit) {
|
|
1312
|
+
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1313
|
+
}
|
|
1314
|
+
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;
|
|
1144
1315
|
var init_prescribed = __esm({
|
|
1145
1316
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1146
1317
|
"use strict";
|
|
1147
1318
|
init_navigators();
|
|
1148
1319
|
init_logger();
|
|
1320
|
+
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1321
|
+
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
1322
|
+
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1323
|
+
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1324
|
+
DEFAULT_MIN_COUNT = 3;
|
|
1325
|
+
BASE_TARGET_SCORE = 1;
|
|
1326
|
+
BASE_SUPPORT_SCORE = 0.8;
|
|
1327
|
+
MAX_TARGET_MULTIPLIER = 8;
|
|
1328
|
+
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1329
|
+
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1330
|
+
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1331
|
+
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
|
|
1149
1332
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1150
1333
|
name;
|
|
1151
1334
|
config;
|
|
1152
1335
|
constructor(user, course, strategyData) {
|
|
1153
1336
|
super(user, course, strategyData);
|
|
1154
1337
|
this.name = strategyData.name || "Prescribed Cards";
|
|
1155
|
-
|
|
1156
|
-
const parsed = JSON.parse(strategyData.serializedData);
|
|
1157
|
-
this.config = { cardIds: parsed.cardIds || [] };
|
|
1158
|
-
} catch {
|
|
1159
|
-
this.config = { cardIds: [] };
|
|
1160
|
-
}
|
|
1338
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
1161
1339
|
logger.debug(
|
|
1162
|
-
`[Prescribed] Initialized with ${this.config.
|
|
1340
|
+
`[Prescribed] Initialized with ${this.config.groups.length} groups and ${this.config.groups.reduce((n, g) => n + g.targetCardIds.length, 0)} targets`
|
|
1163
1341
|
);
|
|
1164
1342
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1343
|
+
get strategyKey() {
|
|
1344
|
+
return "PrescribedProgress";
|
|
1345
|
+
}
|
|
1346
|
+
async getWeightedCards(limit, context) {
|
|
1347
|
+
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1348
|
+
return { cards: [] };
|
|
1168
1349
|
}
|
|
1169
1350
|
const courseId = this.course.getCourseID();
|
|
1170
1351
|
const activeCards = await this.user.getActiveCards();
|
|
1171
1352
|
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1353
|
+
const seenCards = await this.user.getSeenCards(courseId).catch(() => []);
|
|
1354
|
+
const seenIds = new Set(seenCards);
|
|
1355
|
+
const progress = await this.getStrategyState() ?? {
|
|
1356
|
+
updatedAt: isoNow(),
|
|
1357
|
+
groups: {}
|
|
1358
|
+
};
|
|
1359
|
+
const hierarchyConfigs = await this.loadHierarchyConfigs();
|
|
1360
|
+
const courseReg = await this.user.getCourseRegDoc(courseId).catch(() => null);
|
|
1361
|
+
const userGlobalElo = typeof courseReg?.elo === "number" ? courseReg.elo : courseReg?.elo?.global?.score ?? context?.userElo ?? 1e3;
|
|
1362
|
+
const userTagElo = typeof courseReg?.elo === "number" ? {} : courseReg?.elo?.tags ?? {};
|
|
1363
|
+
const allTargetIds = dedupe(this.config.groups.flatMap((g) => g.targetCardIds));
|
|
1364
|
+
const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
|
|
1365
|
+
const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
|
|
1366
|
+
const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
|
|
1367
|
+
const nextState = {
|
|
1368
|
+
updatedAt: isoNow(),
|
|
1369
|
+
groups: {}
|
|
1370
|
+
};
|
|
1371
|
+
const emitted = [];
|
|
1372
|
+
const emittedIds = /* @__PURE__ */ new Set();
|
|
1373
|
+
const groupRuntimes = [];
|
|
1374
|
+
for (const group of this.config.groups) {
|
|
1375
|
+
const runtime = this.buildGroupRuntimeState({
|
|
1376
|
+
group,
|
|
1377
|
+
priorState: progress.groups[group.id],
|
|
1378
|
+
activeIds,
|
|
1379
|
+
seenIds,
|
|
1380
|
+
tagsByCard,
|
|
1381
|
+
hierarchyConfigs,
|
|
1382
|
+
userTagElo,
|
|
1383
|
+
userGlobalElo
|
|
1384
|
+
});
|
|
1385
|
+
groupRuntimes.push(runtime);
|
|
1386
|
+
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1387
|
+
const directCards = this.buildDirectTargetCards(
|
|
1388
|
+
runtime,
|
|
1389
|
+
courseId,
|
|
1390
|
+
emittedIds
|
|
1391
|
+
);
|
|
1392
|
+
const supportCards = this.buildSupportCards(
|
|
1393
|
+
runtime,
|
|
1394
|
+
courseId,
|
|
1395
|
+
emittedIds
|
|
1396
|
+
);
|
|
1397
|
+
emitted.push(...directCards, ...supportCards);
|
|
1398
|
+
}
|
|
1399
|
+
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1400
|
+
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1401
|
+
boostTags: hintSummary.boostTags,
|
|
1402
|
+
_label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
|
|
1403
|
+
} : void 0;
|
|
1404
|
+
if (emitted.length === 0) {
|
|
1405
|
+
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1406
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1407
|
+
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1408
|
+
});
|
|
1409
|
+
return hints ? { cards: [], hints } : { cards: [] };
|
|
1410
|
+
}
|
|
1411
|
+
const finalCards = pickTopByScore(emitted, limit);
|
|
1412
|
+
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1413
|
+
for (const card of finalCards) {
|
|
1414
|
+
const prov = card.provenance[0];
|
|
1415
|
+
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1416
|
+
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1417
|
+
if (!groupId) continue;
|
|
1418
|
+
if (!surfacedByGroup.has(groupId)) {
|
|
1419
|
+
surfacedByGroup.set(groupId, { targetIds: [], supportIds: [] });
|
|
1420
|
+
}
|
|
1421
|
+
surfacedByGroup.get(groupId)[mode].push(card.cardId);
|
|
1422
|
+
}
|
|
1423
|
+
for (const group of this.config.groups) {
|
|
1424
|
+
const groupState = nextState.groups[group.id];
|
|
1425
|
+
const surfaced = surfacedByGroup.get(group.id);
|
|
1426
|
+
if (surfaced && (surfaced.targetIds.length > 0 || surfaced.supportIds.length > 0)) {
|
|
1427
|
+
groupState.lastSurfacedAt = isoNow();
|
|
1428
|
+
groupState.sessionsSinceSurfaced = 0;
|
|
1429
|
+
if (surfaced.supportIds.length > 0) {
|
|
1430
|
+
groupState.lastSupportAt = isoNow();
|
|
1189
1431
|
}
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1435
|
+
logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
|
|
1436
|
+
});
|
|
1192
1437
|
logger.info(
|
|
1193
|
-
`[Prescribed] Emitting ${
|
|
1438
|
+
`[Prescribed] Emitting ${finalCards.length} cards (${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=target")).length} target, ${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=support")).length} support)`
|
|
1194
1439
|
);
|
|
1440
|
+
return hints ? { cards: finalCards, hints } : { cards: finalCards };
|
|
1441
|
+
}
|
|
1442
|
+
buildSupportHintSummary(groupRuntimes) {
|
|
1443
|
+
const boostTags = {};
|
|
1444
|
+
const blockedTargetIds = /* @__PURE__ */ new Set();
|
|
1445
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1446
|
+
for (const runtime of groupRuntimes) {
|
|
1447
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportTags.length === 0) {
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
runtime.blockedTargets.forEach((cardId) => blockedTargetIds.add(cardId));
|
|
1451
|
+
for (const tag of runtime.supportTags) {
|
|
1452
|
+
supportTags.add(tag);
|
|
1453
|
+
boostTags[tag] = (boostTags[tag] ?? 1) * runtime.supportMultiplier;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return {
|
|
1457
|
+
boostTags,
|
|
1458
|
+
blockedTargetIds: [...blockedTargetIds].sort(),
|
|
1459
|
+
supportTags: [...supportTags].sort()
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
parseConfig(serializedData) {
|
|
1463
|
+
try {
|
|
1464
|
+
const parsed = JSON.parse(serializedData);
|
|
1465
|
+
const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
|
|
1466
|
+
const groups = groupsRaw.map((raw, i) => ({
|
|
1467
|
+
id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
|
|
1468
|
+
targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
|
|
1469
|
+
supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
|
|
1470
|
+
supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
|
|
1471
|
+
freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
|
|
1472
|
+
maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
|
|
1473
|
+
maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
|
|
1474
|
+
hierarchyWalk: {
|
|
1475
|
+
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
1476
|
+
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
1477
|
+
},
|
|
1478
|
+
retireOnEncounter: raw.retireOnEncounter !== false
|
|
1479
|
+
})).filter((g) => g.targetCardIds.length > 0);
|
|
1480
|
+
return { groups };
|
|
1481
|
+
} catch {
|
|
1482
|
+
return { groups: [] };
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async loadHierarchyConfigs() {
|
|
1486
|
+
try {
|
|
1487
|
+
const strategies = await this.course.getNavigationStrategies();
|
|
1488
|
+
return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
|
|
1489
|
+
try {
|
|
1490
|
+
const parsed = JSON.parse(s.serializedData);
|
|
1491
|
+
return {
|
|
1492
|
+
prerequisites: parsed.prerequisites || {}
|
|
1493
|
+
};
|
|
1494
|
+
} catch {
|
|
1495
|
+
return { prerequisites: {} };
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
} catch (e) {
|
|
1499
|
+
logger.debug(`[Prescribed] Failed to load hierarchy configs: ${e}`);
|
|
1500
|
+
return [];
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
buildGroupRuntimeState(args) {
|
|
1504
|
+
const {
|
|
1505
|
+
group,
|
|
1506
|
+
priorState,
|
|
1507
|
+
activeIds,
|
|
1508
|
+
seenIds,
|
|
1509
|
+
tagsByCard,
|
|
1510
|
+
hierarchyConfigs,
|
|
1511
|
+
userTagElo,
|
|
1512
|
+
userGlobalElo
|
|
1513
|
+
} = args;
|
|
1514
|
+
const encounteredTargets = /* @__PURE__ */ new Set();
|
|
1515
|
+
for (const cardId of group.targetCardIds) {
|
|
1516
|
+
if (activeIds.has(cardId) || seenIds.has(cardId)) {
|
|
1517
|
+
encounteredTargets.add(cardId);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (priorState?.encounteredCardIds?.length) {
|
|
1521
|
+
for (const cardId of priorState.encounteredCardIds) {
|
|
1522
|
+
encounteredTargets.add(cardId);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const pendingTargets = group.targetCardIds.filter((id) => !encounteredTargets.has(id));
|
|
1526
|
+
const targetTags = /* @__PURE__ */ new Map();
|
|
1527
|
+
for (const cardId of pendingTargets) {
|
|
1528
|
+
targetTags.set(cardId, tagsByCard.get(cardId) ?? []);
|
|
1529
|
+
}
|
|
1530
|
+
const blockedTargets = [];
|
|
1531
|
+
const surfaceableTargets = [];
|
|
1532
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1533
|
+
for (const cardId of pendingTargets) {
|
|
1534
|
+
const tags = targetTags.get(cardId) ?? [];
|
|
1535
|
+
const resolution = this.resolveBlockedSupportTags(
|
|
1536
|
+
tags,
|
|
1537
|
+
hierarchyConfigs,
|
|
1538
|
+
userTagElo,
|
|
1539
|
+
userGlobalElo,
|
|
1540
|
+
group.hierarchyWalk?.enabled !== false,
|
|
1541
|
+
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1542
|
+
);
|
|
1543
|
+
const introTags = tags.filter((tag) => tag.startsWith("gpc:intro:"));
|
|
1544
|
+
const exposeTags = new Set(tags.filter((tag) => tag.startsWith("gpc:expose:")));
|
|
1545
|
+
for (const introTag of introTags) {
|
|
1546
|
+
const suffix = introTag.slice("gpc:intro:".length);
|
|
1547
|
+
if (suffix) {
|
|
1548
|
+
exposeTags.add(`gpc:expose:${suffix}`);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const unmetExposeTags = [...exposeTags].filter((tag) => {
|
|
1552
|
+
const tagElo = userTagElo[tag];
|
|
1553
|
+
return !tagElo || tagElo.count < DEFAULT_MIN_COUNT;
|
|
1554
|
+
});
|
|
1555
|
+
if (unmetExposeTags.length > 0) {
|
|
1556
|
+
unmetExposeTags.forEach((tag) => supportTags.add(tag));
|
|
1557
|
+
}
|
|
1558
|
+
if (resolution.blocked || unmetExposeTags.length > 0) {
|
|
1559
|
+
blockedTargets.push(cardId);
|
|
1560
|
+
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1561
|
+
} else {
|
|
1562
|
+
surfaceableTargets.push(cardId);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const supportCandidates = dedupe([
|
|
1566
|
+
...group.supportCardIds ?? [],
|
|
1567
|
+
...this.findSupportCardsByTags(
|
|
1568
|
+
group,
|
|
1569
|
+
tagsByCard,
|
|
1570
|
+
[...supportTags]
|
|
1571
|
+
)
|
|
1572
|
+
]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
|
|
1573
|
+
const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
|
|
1574
|
+
const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
1575
|
+
const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
|
|
1576
|
+
const pressureMultiplier = pendingTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.75 + Math.min(2, pendingTargets.length * 0.1), 1, MAX_TARGET_MULTIPLIER);
|
|
1577
|
+
const supportMultiplier = blockedTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.5 + Math.min(1.5, blockedTargets.length * 0.15), 1, MAX_SUPPORT_MULTIPLIER);
|
|
1578
|
+
return {
|
|
1579
|
+
group,
|
|
1580
|
+
encounteredTargets,
|
|
1581
|
+
pendingTargets,
|
|
1582
|
+
blockedTargets,
|
|
1583
|
+
surfaceableTargets,
|
|
1584
|
+
targetTags,
|
|
1585
|
+
supportCandidates,
|
|
1586
|
+
supportTags: [...supportTags],
|
|
1587
|
+
pressureMultiplier,
|
|
1588
|
+
supportMultiplier,
|
|
1589
|
+
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
buildNextGroupState(runtime, prior) {
|
|
1593
|
+
const carriedSessions = prior?.sessionsSinceSurfaced ?? 0;
|
|
1594
|
+
const surfacedThisRun = false;
|
|
1595
|
+
return {
|
|
1596
|
+
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1597
|
+
pendingTargetIds: [...runtime.pendingTargets].sort(),
|
|
1598
|
+
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1599
|
+
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1600
|
+
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
1601
|
+
blockedTargetIds: [...runtime.blockedTargets].sort(),
|
|
1602
|
+
lastResolvedSupportTags: [...runtime.supportTags].sort()
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
buildDirectTargetCards(runtime, courseId, emittedIds) {
|
|
1606
|
+
const maxDirect = runtime.group.maxDirectTargetsPerRun ?? DEFAULT_MAX_DIRECT_PER_RUN;
|
|
1607
|
+
const directIds = runtime.surfaceableTargets.filter((id) => !emittedIds.has(id)).slice(0, maxDirect);
|
|
1608
|
+
const cards = [];
|
|
1609
|
+
for (const cardId of directIds) {
|
|
1610
|
+
emittedIds.add(cardId);
|
|
1611
|
+
cards.push({
|
|
1612
|
+
cardId,
|
|
1613
|
+
courseId,
|
|
1614
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1615
|
+
provenance: [
|
|
1616
|
+
{
|
|
1617
|
+
strategy: "prescribed",
|
|
1618
|
+
strategyName: this.strategyName || this.name,
|
|
1619
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1620
|
+
action: "generated",
|
|
1621
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1622
|
+
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}`
|
|
1623
|
+
}
|
|
1624
|
+
]
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1195
1627
|
return cards;
|
|
1196
1628
|
}
|
|
1629
|
+
buildSupportCards(runtime, courseId, emittedIds) {
|
|
1630
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportCandidates.length === 0) {
|
|
1631
|
+
return [];
|
|
1632
|
+
}
|
|
1633
|
+
const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
|
|
1634
|
+
const supportIds = runtime.supportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
|
|
1635
|
+
const cards = [];
|
|
1636
|
+
for (const cardId of supportIds) {
|
|
1637
|
+
emittedIds.add(cardId);
|
|
1638
|
+
cards.push({
|
|
1639
|
+
cardId,
|
|
1640
|
+
courseId,
|
|
1641
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1642
|
+
provenance: [
|
|
1643
|
+
{
|
|
1644
|
+
strategy: "prescribed",
|
|
1645
|
+
strategyName: this.strategyName || this.name,
|
|
1646
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1647
|
+
action: "generated",
|
|
1648
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1649
|
+
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}`
|
|
1650
|
+
}
|
|
1651
|
+
]
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
return cards;
|
|
1655
|
+
}
|
|
1656
|
+
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
1657
|
+
if (supportTags.length === 0) {
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1660
|
+
const explicitSupportIds = group.supportCardIds ?? [];
|
|
1661
|
+
const explicitPatterns = group.supportTagPatterns ?? [];
|
|
1662
|
+
if (explicitSupportIds.length === 0 && explicitPatterns.length === 0) {
|
|
1663
|
+
return [];
|
|
1664
|
+
}
|
|
1665
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1666
|
+
for (const cardId of explicitSupportIds) {
|
|
1667
|
+
const cardTags = tagsByCard.get(cardId) ?? [];
|
|
1668
|
+
const matchesResolved = supportTags.some((supportTag) => cardTags.includes(supportTag));
|
|
1669
|
+
const matchesPattern = explicitPatterns.some(
|
|
1670
|
+
(pattern) => cardTags.some((tag) => matchesTagPattern(tag, pattern))
|
|
1671
|
+
);
|
|
1672
|
+
if (matchesResolved || matchesPattern) {
|
|
1673
|
+
candidates.add(cardId);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return [...candidates];
|
|
1677
|
+
}
|
|
1678
|
+
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1679
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1680
|
+
let blocked = false;
|
|
1681
|
+
for (const targetTag of targetTags) {
|
|
1682
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[targetTag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
1683
|
+
if (prereqSets.length === 0) {
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
const tagBlocked = prereqSets.some(
|
|
1687
|
+
(prereqs) => prereqs.some((pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
1688
|
+
);
|
|
1689
|
+
if (!tagBlocked) {
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
blocked = true;
|
|
1693
|
+
if (!hierarchyWalkEnabled) {
|
|
1694
|
+
for (const prereqs of prereqSets) {
|
|
1695
|
+
for (const prereq of prereqs) {
|
|
1696
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1697
|
+
supportTags.add(prereq.tag);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
for (const prereqs of prereqSets) {
|
|
1704
|
+
for (const prereq of prereqs) {
|
|
1705
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1706
|
+
this.collectSupportTagsRecursive(
|
|
1707
|
+
prereq.tag,
|
|
1708
|
+
hierarchyConfigs,
|
|
1709
|
+
userTagElo,
|
|
1710
|
+
userGlobalElo,
|
|
1711
|
+
maxDepth,
|
|
1712
|
+
/* @__PURE__ */ new Set(),
|
|
1713
|
+
supportTags
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return { blocked, supportTags: [...supportTags] };
|
|
1720
|
+
}
|
|
1721
|
+
collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
|
|
1722
|
+
if (depth < 0 || visited.has(tag)) return;
|
|
1723
|
+
if (this.isHardGatedTag(tag)) return;
|
|
1724
|
+
visited.add(tag);
|
|
1725
|
+
let walkedFurther = false;
|
|
1726
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1727
|
+
const prereqs = hierarchy.prerequisites[tag];
|
|
1728
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1729
|
+
const unmet = prereqs.filter(
|
|
1730
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1731
|
+
);
|
|
1732
|
+
if (unmet.length > 0 && depth > 0) {
|
|
1733
|
+
walkedFurther = true;
|
|
1734
|
+
for (const prereq of unmet) {
|
|
1735
|
+
this.collectSupportTagsRecursive(
|
|
1736
|
+
prereq.tag,
|
|
1737
|
+
hierarchyConfigs,
|
|
1738
|
+
userTagElo,
|
|
1739
|
+
userGlobalElo,
|
|
1740
|
+
depth - 1,
|
|
1741
|
+
visited,
|
|
1742
|
+
out
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
if (!walkedFurther) {
|
|
1748
|
+
out.add(tag);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
isHardGatedTag(tag) {
|
|
1752
|
+
return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
|
|
1753
|
+
}
|
|
1754
|
+
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1755
|
+
if (!userTagElo) return false;
|
|
1756
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
1757
|
+
if (userTagElo.count < minCount) return false;
|
|
1758
|
+
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1759
|
+
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1760
|
+
}
|
|
1761
|
+
if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
return userTagElo.score >= userGlobalElo;
|
|
1765
|
+
}
|
|
1197
1766
|
};
|
|
1198
1767
|
}
|
|
1199
1768
|
});
|
|
@@ -1298,7 +1867,7 @@ var init_srs = __esm({
|
|
|
1298
1867
|
]
|
|
1299
1868
|
};
|
|
1300
1869
|
});
|
|
1301
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1870
|
+
return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
1302
1871
|
}
|
|
1303
1872
|
/**
|
|
1304
1873
|
* Compute backlog pressure based on number of due reviews.
|
|
@@ -1557,13 +2126,13 @@ __export(hierarchyDefinition_exports, {
|
|
|
1557
2126
|
default: () => HierarchyDefinitionNavigator
|
|
1558
2127
|
});
|
|
1559
2128
|
import { toCourseElo as toCourseElo3 } from "@vue-skuilder/common";
|
|
1560
|
-
var
|
|
2129
|
+
var DEFAULT_MIN_COUNT2, HierarchyDefinitionNavigator;
|
|
1561
2130
|
var init_hierarchyDefinition = __esm({
|
|
1562
2131
|
"src/core/navigators/filters/hierarchyDefinition.ts"() {
|
|
1563
2132
|
"use strict";
|
|
1564
2133
|
init_navigators();
|
|
1565
2134
|
init_logger();
|
|
1566
|
-
|
|
2135
|
+
DEFAULT_MIN_COUNT2 = 3;
|
|
1567
2136
|
HierarchyDefinitionNavigator = class extends ContentNavigator {
|
|
1568
2137
|
config;
|
|
1569
2138
|
/** Human-readable name for CardFilter interface */
|
|
@@ -1590,7 +2159,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1590
2159
|
*/
|
|
1591
2160
|
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1592
2161
|
if (!userTagElo) return false;
|
|
1593
|
-
const minCount = prereq.masteryThreshold?.minCount ??
|
|
2162
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
|
|
1594
2163
|
if (userTagElo.count < minCount) return false;
|
|
1595
2164
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1596
2165
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
@@ -1691,18 +2260,55 @@ var init_hierarchyDefinition = __esm({
|
|
|
1691
2260
|
}
|
|
1692
2261
|
return boosts;
|
|
1693
2262
|
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Build a map of gated tag → max configured targetBoost for all *open* gates.
|
|
2265
|
+
*
|
|
2266
|
+
* When a gate opens (prereqs met), cards carrying the gated tag get boosted —
|
|
2267
|
+
* ensuring newly-unlocked content surfaces promptly. The boost is a static
|
|
2268
|
+
* multiplier; natural ELO/SRS deprioritization after interaction handles decay.
|
|
2269
|
+
*/
|
|
2270
|
+
getTargetBoosts(unlockedTags) {
|
|
2271
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
2272
|
+
const configKeys = Object.keys(this.config.prerequisites);
|
|
2273
|
+
const unlockedArr = [...unlockedTags];
|
|
2274
|
+
logger.info(
|
|
2275
|
+
`[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(", ")}${unlockedArr.length > 5 ? "..." : ""})`
|
|
2276
|
+
);
|
|
2277
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
2278
|
+
if (!unlockedTags.has(tagId)) continue;
|
|
2279
|
+
logger.info(
|
|
2280
|
+
`[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map((p) => ({ tag: p.tag, tb: p.targetBoost })))}`
|
|
2281
|
+
);
|
|
2282
|
+
for (const prereq of prereqs) {
|
|
2283
|
+
if (!prereq.targetBoost || prereq.targetBoost <= 1) continue;
|
|
2284
|
+
const existing = boosts.get(tagId) ?? 1;
|
|
2285
|
+
boosts.set(tagId, Math.max(existing, prereq.targetBoost));
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
if (boosts.size > 0) {
|
|
2289
|
+
logger.info(
|
|
2290
|
+
`[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=\xD7${b}`).join(", ")}`
|
|
2291
|
+
);
|
|
2292
|
+
} else {
|
|
2293
|
+
logger.info(
|
|
2294
|
+
`[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
return boosts;
|
|
2298
|
+
}
|
|
1694
2299
|
/**
|
|
1695
2300
|
* CardFilter.transform implementation.
|
|
1696
2301
|
*
|
|
1697
|
-
*
|
|
1698
|
-
* 1. Cards with locked tags receive score * 0.
|
|
1699
|
-
* 2. Cards carrying prereq tags of closed gates receive
|
|
1700
|
-
*
|
|
2302
|
+
* Three effects:
|
|
2303
|
+
* 1. Cards with locked tags receive score * 0.02 (gating penalty)
|
|
2304
|
+
* 2. Cards carrying prereq tags of closed gates receive preReqBoost
|
|
2305
|
+
* 3. Cards carrying gated tags of open gates receive targetBoost
|
|
1701
2306
|
*/
|
|
1702
2307
|
async transform(cards, context) {
|
|
1703
2308
|
const masteredTags = await this.getMasteredTags(context);
|
|
1704
2309
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1705
2310
|
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
2311
|
+
const targetBoosts = this.getTargetBoosts(unlockedTags);
|
|
1706
2312
|
const gated = [];
|
|
1707
2313
|
for (const card of cards) {
|
|
1708
2314
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1735,6 +2341,26 @@ var init_hierarchyDefinition = __esm({
|
|
|
1735
2341
|
);
|
|
1736
2342
|
}
|
|
1737
2343
|
}
|
|
2344
|
+
if (isUnlocked && targetBoosts.size > 0) {
|
|
2345
|
+
const cardTags = card.tags ?? [];
|
|
2346
|
+
let maxTargetBoost = 1;
|
|
2347
|
+
const boostedTargets = [];
|
|
2348
|
+
for (const tag of cardTags) {
|
|
2349
|
+
const boost = targetBoosts.get(tag);
|
|
2350
|
+
if (boost && boost > maxTargetBoost) {
|
|
2351
|
+
maxTargetBoost = boost;
|
|
2352
|
+
boostedTargets.push(tag);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (maxTargetBoost > 1) {
|
|
2356
|
+
finalScore *= maxTargetBoost;
|
|
2357
|
+
action = "boosted";
|
|
2358
|
+
finalReason = `${finalReason} | targetBoost \xD7${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(", ")}`;
|
|
2359
|
+
logger.info(
|
|
2360
|
+
`[HierarchyDefinition] targetBoost \xD7${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
1738
2364
|
gated.push({
|
|
1739
2365
|
...card,
|
|
1740
2366
|
score: finalScore,
|
|
@@ -1920,12 +2546,12 @@ __export(interferenceMitigator_exports, {
|
|
|
1920
2546
|
default: () => InterferenceMitigatorNavigator
|
|
1921
2547
|
});
|
|
1922
2548
|
import { toCourseElo as toCourseElo4 } from "@vue-skuilder/common";
|
|
1923
|
-
var
|
|
2549
|
+
var DEFAULT_MIN_COUNT3, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
|
|
1924
2550
|
var init_interferenceMitigator = __esm({
|
|
1925
2551
|
"src/core/navigators/filters/interferenceMitigator.ts"() {
|
|
1926
2552
|
"use strict";
|
|
1927
2553
|
init_navigators();
|
|
1928
|
-
|
|
2554
|
+
DEFAULT_MIN_COUNT3 = 10;
|
|
1929
2555
|
DEFAULT_MIN_ELAPSED_DAYS = 3;
|
|
1930
2556
|
DEFAULT_INTERFERENCE_DECAY = 0.8;
|
|
1931
2557
|
InterferenceMitigatorNavigator = class extends ContentNavigator {
|
|
@@ -1950,7 +2576,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1950
2576
|
return {
|
|
1951
2577
|
interferenceSets: sets,
|
|
1952
2578
|
maturityThreshold: {
|
|
1953
|
-
minCount: parsed.maturityThreshold?.minCount ??
|
|
2579
|
+
minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3,
|
|
1954
2580
|
minElo: parsed.maturityThreshold?.minElo,
|
|
1955
2581
|
minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
|
|
1956
2582
|
},
|
|
@@ -1960,7 +2586,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1960
2586
|
return {
|
|
1961
2587
|
interferenceSets: [],
|
|
1962
2588
|
maturityThreshold: {
|
|
1963
|
-
minCount:
|
|
2589
|
+
minCount: DEFAULT_MIN_COUNT3,
|
|
1964
2590
|
minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
|
|
1965
2591
|
},
|
|
1966
2592
|
defaultDecay: DEFAULT_INTERFERENCE_DECAY
|
|
@@ -2007,7 +2633,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2007
2633
|
try {
|
|
2008
2634
|
const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
|
|
2009
2635
|
const userElo = toCourseElo4(courseReg.elo);
|
|
2010
|
-
const minCount = this.config.maturityThreshold?.minCount ??
|
|
2636
|
+
const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3;
|
|
2011
2637
|
const minElo = this.config.maturityThreshold?.minElo;
|
|
2012
2638
|
const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
|
|
2013
2639
|
const minCountForElapsed = minElapsedDays * 2;
|
|
@@ -2447,6 +3073,44 @@ function globMatch(value, pattern) {
|
|
|
2447
3073
|
function cardMatchesTagPattern(card, pattern) {
|
|
2448
3074
|
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
2449
3075
|
}
|
|
3076
|
+
function mergeHints2(allHints) {
|
|
3077
|
+
const defined = allHints.filter((h) => h !== null && h !== void 0);
|
|
3078
|
+
if (defined.length === 0) return void 0;
|
|
3079
|
+
const merged = {};
|
|
3080
|
+
const boostTags = {};
|
|
3081
|
+
for (const hints of defined) {
|
|
3082
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
3083
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
if (Object.keys(boostTags).length > 0) {
|
|
3087
|
+
merged.boostTags = boostTags;
|
|
3088
|
+
}
|
|
3089
|
+
const boostCards = {};
|
|
3090
|
+
for (const hints of defined) {
|
|
3091
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
3092
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
if (Object.keys(boostCards).length > 0) {
|
|
3096
|
+
merged.boostCards = boostCards;
|
|
3097
|
+
}
|
|
3098
|
+
const concatUnique = (field) => {
|
|
3099
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
3100
|
+
if (values.length > 0) {
|
|
3101
|
+
merged[field] = [...new Set(values)];
|
|
3102
|
+
}
|
|
3103
|
+
};
|
|
3104
|
+
concatUnique("requireTags");
|
|
3105
|
+
concatUnique("requireCards");
|
|
3106
|
+
concatUnique("excludeTags");
|
|
3107
|
+
concatUnique("excludeCards");
|
|
3108
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
3109
|
+
if (labels.length > 0) {
|
|
3110
|
+
merged._label = labels.join("; ");
|
|
3111
|
+
}
|
|
3112
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
3113
|
+
}
|
|
2450
3114
|
function logPipelineConfig(generator, filters) {
|
|
2451
3115
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2452
3116
|
logger.info(
|
|
@@ -2597,9 +3261,12 @@ var init_Pipeline = __esm({
|
|
|
2597
3261
|
logger.debug(
|
|
2598
3262
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
2599
3263
|
);
|
|
2600
|
-
|
|
3264
|
+
const generatorResult = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3265
|
+
let cards = generatorResult.cards;
|
|
2601
3266
|
const tGenerate = performance.now();
|
|
2602
3267
|
const generatedCount = cards.length;
|
|
3268
|
+
const mergedHints = mergeHints2([this._ephemeralHints, generatorResult.hints]);
|
|
3269
|
+
this._ephemeralHints = mergedHints ?? null;
|
|
2603
3270
|
let generatorSummaries;
|
|
2604
3271
|
if (this.generator.generators) {
|
|
2605
3272
|
const genMap = /* @__PURE__ */ new Map();
|
|
@@ -2677,14 +3344,15 @@ var init_Pipeline = __esm({
|
|
|
2677
3344
|
generatorSummaries,
|
|
2678
3345
|
generatedCount,
|
|
2679
3346
|
filterImpacts,
|
|
2680
|
-
|
|
2681
|
-
result
|
|
3347
|
+
cards,
|
|
3348
|
+
result,
|
|
3349
|
+
context.userElo
|
|
2682
3350
|
);
|
|
2683
3351
|
captureRun(report);
|
|
2684
3352
|
} catch (e) {
|
|
2685
3353
|
logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
|
|
2686
3354
|
}
|
|
2687
|
-
return result;
|
|
3355
|
+
return { cards: result };
|
|
2688
3356
|
}
|
|
2689
3357
|
/**
|
|
2690
3358
|
* Batch hydrate tags for all cards.
|
|
@@ -2892,6 +3560,34 @@ var init_Pipeline = __esm({
|
|
|
2892
3560
|
return [...new Set(ids)];
|
|
2893
3561
|
}
|
|
2894
3562
|
// ---------------------------------------------------------------------------
|
|
3563
|
+
// Tag ELO diagnostic
|
|
3564
|
+
// ---------------------------------------------------------------------------
|
|
3565
|
+
/**
|
|
3566
|
+
* Get the user's per-tag ELO data for specified tags (or all tags).
|
|
3567
|
+
* Useful for diagnosing why hierarchy gates are open/closed.
|
|
3568
|
+
*/
|
|
3569
|
+
async getTagEloStatus(tagFilter) {
|
|
3570
|
+
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
3571
|
+
const courseElo = toCourseElo5(courseReg.elo);
|
|
3572
|
+
const result = {};
|
|
3573
|
+
if (!tagFilter) {
|
|
3574
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
3575
|
+
result[tag] = { score: data.score, count: data.count };
|
|
3576
|
+
}
|
|
3577
|
+
} else {
|
|
3578
|
+
const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
|
|
3579
|
+
for (const pattern of patterns) {
|
|
3580
|
+
const regex = globToRegex(pattern);
|
|
3581
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
3582
|
+
if (regex.test(tag)) {
|
|
3583
|
+
result[tag] = { score: data.score, count: data.count };
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
return result;
|
|
3589
|
+
}
|
|
3590
|
+
// ---------------------------------------------------------------------------
|
|
2895
3591
|
// Card-space diagnostic
|
|
2896
3592
|
// ---------------------------------------------------------------------------
|
|
2897
3593
|
/**
|