@thanh01.pmt/domain-kit 0.1.0 → 0.2.1
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/LICENSE +21 -0
- package/dist/{chunk-GEUDHZE7.mjs → chunk-23TQPTHG.mjs} +173 -4
- package/dist/chunk-23TQPTHG.mjs.map +1 -0
- package/dist/chunk-7XGWW6CB.mjs +3 -0
- package/dist/{chunk-F4RZNBOE.mjs.map → chunk-7XGWW6CB.mjs.map} +1 -1
- package/dist/{chunk-VFART56O.mjs → chunk-BL5NBKPH.mjs} +12 -8
- package/dist/chunk-BL5NBKPH.mjs.map +1 -0
- package/dist/{chunk-3VVPSRAM.mjs → chunk-CRN6D4HG.mjs} +1056 -92
- package/dist/chunk-CRN6D4HG.mjs.map +1 -0
- package/dist/{chunk-3H2HX7PR.mjs → chunk-HUDFN4IX.mjs} +58 -8
- package/dist/chunk-HUDFN4IX.mjs.map +1 -0
- package/dist/chunk-IJZUV7N3.mjs +3 -0
- package/dist/chunk-IJZUV7N3.mjs.map +1 -0
- package/dist/chunk-R57WTNXW.mjs +3 -0
- package/dist/chunk-R57WTNXW.mjs.map +1 -0
- package/dist/chunk-T4Y2DE2W.mjs +187 -0
- package/dist/chunk-T4Y2DE2W.mjs.map +1 -0
- package/dist/{chunk-PM42MMDJ.mjs → chunk-WHBAGJWN.mjs} +48 -3
- package/dist/chunk-WHBAGJWN.mjs.map +1 -0
- package/dist/chunk-ZKLT27T4.mjs +3 -0
- package/dist/chunk-ZKLT27T4.mjs.map +1 -0
- package/dist/concepts/index.d.cts +2 -2
- package/dist/concepts/index.d.ts +2 -2
- package/dist/cpp-CZUPqJt9.d.cts +98 -0
- package/dist/cpp-CZUPqJt9.d.ts +98 -0
- package/dist/curriculumFeedEmitter-E5DAJYUZ.mjs +4 -0
- package/dist/curriculumFeedEmitter-E5DAJYUZ.mjs.map +1 -0
- package/dist/detector/index.cjs +10 -6
- package/dist/detector/index.cjs.map +1 -1
- package/dist/detector/index.d.cts +4 -19
- package/dist/detector/index.d.ts +4 -19
- package/dist/detector/index.mjs +2 -1
- package/dist/domainProfileDetector-DHb1T0zB.d.ts +22 -0
- package/dist/domainProfileDetector-bzugdQmS.d.cts +22 -0
- package/dist/extractors/index.d.cts +3 -3
- package/dist/extractors/index.d.ts +3 -3
- package/dist/feed/index.cjs +56 -6
- package/dist/feed/index.cjs.map +1 -1
- package/dist/feed/index.mjs +2 -1
- package/dist/graph/index.cjs +370 -66
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +7 -67
- package/dist/graph/index.d.ts +7 -67
- package/dist/graph/index.mjs +3 -2
- package/dist/hybridGraphPipeline-DVZYLCAQ.d.ts +159 -0
- package/dist/hybridGraphPipeline-DmWRKYXf.d.cts +159 -0
- package/dist/index.cjs +1303 -700
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.mjs +10 -7
- package/dist/{keywordExtractor-zPAz2isq.d.cts → keywordExtractor-BxlGRMHC.d.cts} +1 -1
- package/dist/{keywordExtractor-CKaXqSku.d.ts → keywordExtractor-DU5XRN8-.d.ts} +1 -1
- package/dist/parsers/index.cjs +185 -0
- package/dist/parsers/index.cjs.map +1 -1
- package/dist/parsers/index.d.cts +2 -98
- package/dist/parsers/index.d.ts +2 -98
- package/dist/parsers/index.mjs +2 -1
- package/dist/pipeline/index.cjs +3142 -8
- package/dist/pipeline/index.cjs.map +1 -1
- package/dist/pipeline/index.d.cts +87 -4
- package/dist/pipeline/index.d.ts +87 -4
- package/dist/pipeline/index.mjs +6 -2
- package/dist/schemas/index.cjs +51 -0
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +222 -3
- package/dist/schemas/index.d.ts +222 -3
- package/dist/schemas/index.mjs +1 -1
- package/dist/syllabusParser-MSX1fEzW.d.cts +40 -0
- package/dist/syllabusParser-MSX1fEzW.d.ts +40 -0
- package/package.json +9 -10
- package/dist/chunk-3H2HX7PR.mjs.map +0 -1
- package/dist/chunk-3VVPSRAM.mjs.map +0 -1
- package/dist/chunk-F4RZNBOE.mjs +0 -3
- package/dist/chunk-GEUDHZE7.mjs.map +0 -1
- package/dist/chunk-MOKNFLUD.mjs +0 -851
- package/dist/chunk-MOKNFLUD.mjs.map +0 -1
- package/dist/chunk-PM42MMDJ.mjs.map +0 -1
- package/dist/chunk-VFART56O.mjs.map +0 -1
- package/dist/graphVerifier-DsTP9uAN.d.ts +0 -37
- package/dist/graphVerifier-QjgDAJce.d.cts +0 -37
package/dist/graph/index.cjs
CHANGED
|
@@ -25,8 +25,8 @@ function resolveProviderChain(config) {
|
|
|
25
25
|
}
|
|
26
26
|
push("legacy", process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || "", process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || "", process.env.LLM_MODEL || process.env.LLM_TIER_FAST || "");
|
|
27
27
|
push("dashscope", process.env.DASHSCOPE_BASE_URL || process.env.ALIBABA_BASE_URL || "", process.env.DASHSCOPE_API_KEY || process.env.ALIBABA_API_KEY || "", process.env.DASHSCOPE_MODEL || process.env.DEFAULT_AI_MODEL || "");
|
|
28
|
-
push("nvidia", "https://integrate.api.nvidia.com/v1", process.env.NVIDIA_API_KEY || "", process.env.NVIDIA_MODEL || "nvidia/nemotron-3-ultra-550b-a55b");
|
|
29
|
-
push("openrouter", "https://openrouter.ai/api/v1", process.env.OPENROUTER_API_KEY || "", process.env.OPENROUTER_MODEL || "nvidia/nemotron-3-ultra-550b-a55b");
|
|
28
|
+
push("nvidia", "https://integrate.api.nvidia.com/v1", process.env.NVIDIA_API_KEY || "", process.env.NVIDIA_MODEL || "nvidia/nemotron-3-ultra-550b-a55b:free");
|
|
29
|
+
push("openrouter", "https://openrouter.ai/api/v1", process.env.OPENROUTER_API_KEY || "", process.env.OPENROUTER_MODEL || "nvidia/nemotron-3-ultra-550b-a55b:free");
|
|
30
30
|
if (process.env.OPENROUTER_MODEL) {
|
|
31
31
|
const or = chain.find((c) => c.label === "openrouter");
|
|
32
32
|
if (or) {
|
|
@@ -968,6 +968,345 @@ var ULO_CODE_RE = /^ULO-[A-Z][A-Z0-9_]*-\d{2}$/;
|
|
|
968
968
|
var CIO_CODE_RE = /^CIO-[A-Z][A-Z0-9_]*-\d{2}-[A-Z][A-Z0-9_]*$/;
|
|
969
969
|
var SIO_CODE_RE = /^SIO-[A-Z0-9]+-[A-Z][A-Z0-9_]*-\d{2}$/;
|
|
970
970
|
|
|
971
|
+
// src/parsers/syllabusParser.ts
|
|
972
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
973
|
+
"and",
|
|
974
|
+
"the",
|
|
975
|
+
"for",
|
|
976
|
+
"with",
|
|
977
|
+
"from",
|
|
978
|
+
"that",
|
|
979
|
+
"this",
|
|
980
|
+
"into",
|
|
981
|
+
"when",
|
|
982
|
+
"while",
|
|
983
|
+
"using",
|
|
984
|
+
"their",
|
|
985
|
+
"your",
|
|
986
|
+
"each",
|
|
987
|
+
"how",
|
|
988
|
+
"why",
|
|
989
|
+
"what",
|
|
990
|
+
"between",
|
|
991
|
+
"through",
|
|
992
|
+
"about",
|
|
993
|
+
"trong",
|
|
994
|
+
"nhung",
|
|
995
|
+
"cac",
|
|
996
|
+
"cua",
|
|
997
|
+
"cho",
|
|
998
|
+
"voi",
|
|
999
|
+
"duoc",
|
|
1000
|
+
"khong",
|
|
1001
|
+
"theo",
|
|
1002
|
+
"tren",
|
|
1003
|
+
"duoi",
|
|
1004
|
+
"va",
|
|
1005
|
+
"hoac",
|
|
1006
|
+
"thi",
|
|
1007
|
+
"la",
|
|
1008
|
+
"nhu"
|
|
1009
|
+
]);
|
|
1010
|
+
function extractTokens(text) {
|
|
1011
|
+
const words = text.toLowerCase().replace(/[^a-z0-9à-ỹ\s-]/gi, " ").split(/\s+/).filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
|
|
1012
|
+
return [...new Set(words)];
|
|
1013
|
+
}
|
|
1014
|
+
function extractGradeBand(text) {
|
|
1015
|
+
const gbMatch = text.match(/(?:grade\s*band|độ\s*tuổi|lớp)\s*[:=]?\s*(?:lớp\s*)?(\d{1,2})\s*(?:-|đến|to)\s*(?:lớp\s*)?(\d{1,2})/i);
|
|
1016
|
+
if (gbMatch && gbMatch[1] && gbMatch[2]) {
|
|
1017
|
+
const min = parseInt(gbMatch[1], 10);
|
|
1018
|
+
const max = parseInt(gbMatch[2], 10);
|
|
1019
|
+
return [Math.min(min, max), Math.max(min, max)];
|
|
1020
|
+
}
|
|
1021
|
+
return void 0;
|
|
1022
|
+
}
|
|
1023
|
+
function extractDomain(text) {
|
|
1024
|
+
const lower = text.toLowerCase();
|
|
1025
|
+
if (lower.includes("computer science") || lower.includes("l\u1EADp tr\xECnh") || lower.includes("algorithm") || lower.includes("software")) return "software";
|
|
1026
|
+
if (lower.includes("to\xE1n") || lower.includes("geometry") || lower.includes("algebra") || lower.includes("h\xECnh h\u1ECDc") || lower.includes("mathematics")) return "math";
|
|
1027
|
+
if (lower.includes("v\u1EADt l\xFD") || lower.includes("h\xF3a h\u1ECDc") || lower.includes("sinh h\u1ECDc") || lower.includes("physics") || lower.includes("chemistry") || lower.includes("biology") || lower.includes("science")) return "science";
|
|
1028
|
+
if (lower.includes("ti\u1EBFng") || lower.includes("ng\u1EEF v\u0103n") || lower.includes("grammar") || lower.includes("language") || lower.includes("writing") || lower.includes("literature")) return "language";
|
|
1029
|
+
return void 0;
|
|
1030
|
+
}
|
|
1031
|
+
function parseSyllabus(rawText) {
|
|
1032
|
+
const lines = rawText.split(/\r?\n/);
|
|
1033
|
+
let subjectTitle = "";
|
|
1034
|
+
let subjectDesc = "";
|
|
1035
|
+
const units = [];
|
|
1036
|
+
const allTopics = [];
|
|
1037
|
+
const globalKeywordsSet = /* @__PURE__ */ new Set();
|
|
1038
|
+
let currentUnit = null;
|
|
1039
|
+
let currentTopic = null;
|
|
1040
|
+
let unitIndex = 0;
|
|
1041
|
+
let topicIndex = 0;
|
|
1042
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1043
|
+
const line = lines[i].trim();
|
|
1044
|
+
const lineNum = i + 1;
|
|
1045
|
+
if (!line) continue;
|
|
1046
|
+
if (line.startsWith("# ") && !subjectTitle) {
|
|
1047
|
+
subjectTitle = line.replace(/^#\s+/, "").trim();
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (!currentUnit) {
|
|
1051
|
+
if (line.toLowerCase().startsWith("m\xF4n h\u1ECDc:") || line.toLowerCase().startsWith("subject:")) {
|
|
1052
|
+
const val = line.split(":")[1]?.trim() || "";
|
|
1053
|
+
if (val && !subjectTitle) subjectTitle = val;
|
|
1054
|
+
}
|
|
1055
|
+
if (line.toLowerCase().startsWith("m\xF4 t\u1EA3:") || line.toLowerCase().startsWith("description:")) {
|
|
1056
|
+
subjectDesc = line.split(":")[1]?.trim() || "";
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const isUnitHeading = /^##\s+(?:chương|module|unit|phần|chủ đề)\s*(\d+|[ivx]+)?[:.-]?\s*(.*)/i.test(line) || /^##\s+([^#].*)/.test(line);
|
|
1060
|
+
if (isUnitHeading) {
|
|
1061
|
+
if (currentTopic) {
|
|
1062
|
+
currentTopic.rawLineEnd = lineNum - 1;
|
|
1063
|
+
currentTopic = null;
|
|
1064
|
+
}
|
|
1065
|
+
unitIndex += 1;
|
|
1066
|
+
const titleMatch = line.replace(/^##\s+/, "").trim();
|
|
1067
|
+
currentUnit = {
|
|
1068
|
+
id: `U${String(unitIndex).padStart(2, "0")}`,
|
|
1069
|
+
title: titleMatch,
|
|
1070
|
+
order: unitIndex,
|
|
1071
|
+
topics: []
|
|
1072
|
+
};
|
|
1073
|
+
units.push(currentUnit);
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
const isTopicItem = /^(?:-\s*\*\*(?:bài|topic|lesson|chủ đề)\s*(\d+(?:\.\d+)?|\w+)?[:.-]?\s*(.*?)\*\*)/i.exec(line) || /^###\s+(?:bài|topic|lesson)?\s*(\d+(?:\.\d+)?|\w+)?[:.-]?\s*(.*)/i.exec(line) || /^-\s+\*\*(.*?)\*\*/.exec(line) || /^(\d+)\.\s+(?:(?:\*\*)?(?:bài|topic|lesson)?\s*(\d+(?:\.\d+)?|\w+)?[:.-]?\s*(.*?)(?:\*\*)?)$/i.exec(line);
|
|
1077
|
+
if (isTopicItem) {
|
|
1078
|
+
if (currentTopic) {
|
|
1079
|
+
currentTopic.rawLineEnd = lineNum - 1;
|
|
1080
|
+
}
|
|
1081
|
+
if (!currentUnit) {
|
|
1082
|
+
unitIndex += 1;
|
|
1083
|
+
currentUnit = {
|
|
1084
|
+
id: `U${String(unitIndex).padStart(2, "0")}`,
|
|
1085
|
+
title: subjectTitle || "General",
|
|
1086
|
+
order: unitIndex,
|
|
1087
|
+
topics: []
|
|
1088
|
+
};
|
|
1089
|
+
units.push(currentUnit);
|
|
1090
|
+
}
|
|
1091
|
+
topicIndex += 1;
|
|
1092
|
+
const rawName = (isTopicItem[3] || isTopicItem[2] || isTopicItem[1] || `Topic ${topicIndex}`).trim();
|
|
1093
|
+
currentTopic = {
|
|
1094
|
+
id: `T${String(topicIndex).padStart(2, "0")}`,
|
|
1095
|
+
title: rawName,
|
|
1096
|
+
unitId: currentUnit.id,
|
|
1097
|
+
unitTitle: currentUnit.title,
|
|
1098
|
+
description: "",
|
|
1099
|
+
learningObjectives: [],
|
|
1100
|
+
keywords: [],
|
|
1101
|
+
rawLineStart: lineNum,
|
|
1102
|
+
rawLineEnd: lineNum
|
|
1103
|
+
};
|
|
1104
|
+
currentUnit.topics.push(currentTopic);
|
|
1105
|
+
allTopics.push(currentTopic);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (currentTopic) {
|
|
1109
|
+
currentTopic.rawLineEnd = lineNum;
|
|
1110
|
+
const stripped = line.replace(/^[-*•\s]+/, "").trim();
|
|
1111
|
+
if (stripped.toLowerCase().startsWith("m\u1EE5c ti\xEAu:") || stripped.toLowerCase().startsWith("objectives:")) {
|
|
1112
|
+
const content = stripped.replace(/^(?:mục tiêu|objectives):\s*/i, "").trim();
|
|
1113
|
+
if (content) currentTopic.learningObjectives.push(content);
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (stripped.toLowerCase().startsWith("t\u1EEB kh\xF3a:") || stripped.toLowerCase().startsWith("keywords:")) {
|
|
1117
|
+
const rawKw = stripped.replace(/^(?:từ khóa|keywords):\s*/i, "").trim();
|
|
1118
|
+
const tokens2 = rawKw.split(/[,;]+/).map((k) => k.trim()).filter(Boolean);
|
|
1119
|
+
for (const tok of tokens2) {
|
|
1120
|
+
currentTopic.keywords.push(tok);
|
|
1121
|
+
globalKeywordsSet.add(tok);
|
|
1122
|
+
}
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (currentTopic.description) {
|
|
1126
|
+
currentTopic.description += " " + stripped;
|
|
1127
|
+
} else {
|
|
1128
|
+
currentTopic.description = stripped;
|
|
1129
|
+
}
|
|
1130
|
+
for (const tok of extractTokens(stripped)) {
|
|
1131
|
+
if (tok.length >= 4) globalKeywordsSet.add(tok);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (currentTopic) {
|
|
1136
|
+
currentTopic.rawLineEnd = lines.length;
|
|
1137
|
+
}
|
|
1138
|
+
if (!subjectTitle && units.length > 0) {
|
|
1139
|
+
subjectTitle = units[0]?.title || "Untitled Subject";
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
subject: {
|
|
1143
|
+
title: subjectTitle || "Untitled Subject",
|
|
1144
|
+
domain: extractDomain(rawText),
|
|
1145
|
+
gradeBand: extractGradeBand(rawText),
|
|
1146
|
+
description: subjectDesc
|
|
1147
|
+
},
|
|
1148
|
+
units,
|
|
1149
|
+
allTopics,
|
|
1150
|
+
extractedKeywords: [...globalKeywordsSet],
|
|
1151
|
+
rawText
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/graph/knowledgeGraphVerifier.ts
|
|
1156
|
+
function detectPrerequisiteCycles(concepts) {
|
|
1157
|
+
const cycles = [];
|
|
1158
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1159
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
1160
|
+
const path = [];
|
|
1161
|
+
const prereqMap = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const c of concepts) {
|
|
1163
|
+
prereqMap.set(c.id, c.prerequisites || []);
|
|
1164
|
+
}
|
|
1165
|
+
function dfs(conceptId) {
|
|
1166
|
+
if (inStack.has(conceptId)) {
|
|
1167
|
+
const cycleStart = path.indexOf(conceptId);
|
|
1168
|
+
if (cycleStart >= 0) {
|
|
1169
|
+
cycles.push([...path.slice(cycleStart), conceptId]);
|
|
1170
|
+
}
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (visited.has(conceptId)) return;
|
|
1174
|
+
visited.add(conceptId);
|
|
1175
|
+
inStack.add(conceptId);
|
|
1176
|
+
path.push(conceptId);
|
|
1177
|
+
for (const prereq of prereqMap.get(conceptId) || []) {
|
|
1178
|
+
dfs(prereq);
|
|
1179
|
+
}
|
|
1180
|
+
path.pop();
|
|
1181
|
+
inStack.delete(conceptId);
|
|
1182
|
+
}
|
|
1183
|
+
for (const c of concepts) {
|
|
1184
|
+
if (!visited.has(c.id)) {
|
|
1185
|
+
dfs(c.id);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return cycles;
|
|
1189
|
+
}
|
|
1190
|
+
function breakPrerequisiteCycles(concepts, cycles) {
|
|
1191
|
+
const removed = [];
|
|
1192
|
+
const fixed = concepts.map((c) => ({ ...c, prerequisites: [...c.prerequisites || []] }));
|
|
1193
|
+
for (const cycle of cycles) {
|
|
1194
|
+
if (cycle.length >= 2) {
|
|
1195
|
+
const breaker = cycle[cycle.length - 2];
|
|
1196
|
+
const target = cycle[cycle.length - 1];
|
|
1197
|
+
for (const concept of fixed) {
|
|
1198
|
+
if (concept.id === breaker && target) {
|
|
1199
|
+
concept.prerequisites = concept.prerequisites.filter((p) => p !== target);
|
|
1200
|
+
removed.push({ from: breaker, to: target });
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return { fixed, removed };
|
|
1206
|
+
}
|
|
1207
|
+
function auditSyllabusCoverage(concepts, syllabus) {
|
|
1208
|
+
if (!syllabus || syllabus.allTopics.length === 0) {
|
|
1209
|
+
return { coverageRatio: 1, totalTopics: 0, coveredTopics: 0, uncovered: [] };
|
|
1210
|
+
}
|
|
1211
|
+
const topics = syllabus.allTopics;
|
|
1212
|
+
const coveredSet = /* @__PURE__ */ new Set();
|
|
1213
|
+
for (const topic of topics) {
|
|
1214
|
+
const topicKeywords = /* @__PURE__ */ new Set([
|
|
1215
|
+
...topic.title.toLowerCase().split(/\s+/),
|
|
1216
|
+
...(topic.keywords || []).map((k) => k.toLowerCase())
|
|
1217
|
+
]);
|
|
1218
|
+
for (const concept of concepts) {
|
|
1219
|
+
concept.name.toLowerCase();
|
|
1220
|
+
(concept.description || "").toLowerCase();
|
|
1221
|
+
(concept.keywords || []).map((k) => k.toLowerCase());
|
|
1222
|
+
if (concept.id.toLowerCase() === topic.id.toLowerCase()) {
|
|
1223
|
+
coveredSet.add(topic.id);
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
const refs = concept.references || [];
|
|
1227
|
+
const hasTopicRef = refs.some(
|
|
1228
|
+
(r) => r.id === topic.id || r.title && r.title.toLowerCase() === topic.title.toLowerCase()
|
|
1229
|
+
);
|
|
1230
|
+
if (hasTopicRef) {
|
|
1231
|
+
coveredSet.add(topic.id);
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
const conceptKeywords = (concept.keywords || []).map((k) => k.toLowerCase());
|
|
1235
|
+
const hasOverlap = conceptKeywords.some((k) => topicKeywords.has(k)) || concept.name.toLowerCase().includes(topic.title.toLowerCase()) || topic.title.toLowerCase().includes(concept.name.toLowerCase());
|
|
1236
|
+
if (hasOverlap) {
|
|
1237
|
+
coveredSet.add(topic.id);
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const totalTopics = topics.length;
|
|
1243
|
+
const coveredTopics = coveredSet.size;
|
|
1244
|
+
const coverageRatio = totalTopics > 0 ? coveredTopics / totalTopics : 1;
|
|
1245
|
+
const uncovered = topics.filter((t) => !coveredSet.has(t.id)).map((t) => `${t.id}: ${t.title}`);
|
|
1246
|
+
return { coverageRatio, totalTopics, coveredTopics, uncovered };
|
|
1247
|
+
}
|
|
1248
|
+
function verifyKnowledgeGraph(kg, syllabusOrOptions) {
|
|
1249
|
+
const syllabus = syllabusOrOptions && "parsedSyllabus" in syllabusOrOptions ? syllabusOrOptions.parsedSyllabus : syllabusOrOptions;
|
|
1250
|
+
const warnings = [];
|
|
1251
|
+
let concepts = [...kg.concepts || []];
|
|
1252
|
+
const conceptIds = new Set(concepts.map((c) => c.id));
|
|
1253
|
+
for (const c of concepts) {
|
|
1254
|
+
if ((c.prerequisites || []).includes(c.id)) {
|
|
1255
|
+
c.prerequisites = (c.prerequisites || []).filter((p) => p !== c.id);
|
|
1256
|
+
warnings.push(`Self-loop removed on concept ${c.id}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const danglingPrerequisites = [];
|
|
1260
|
+
for (const c of concepts) {
|
|
1261
|
+
const validPrereqs = [];
|
|
1262
|
+
for (const p of c.prerequisites || []) {
|
|
1263
|
+
if (!conceptIds.has(p)) {
|
|
1264
|
+
danglingPrerequisites.push({ conceptId: c.id, missingPrereqId: p });
|
|
1265
|
+
warnings.push(`Dangling prerequisite removed: ${c.id} -> ${p}`);
|
|
1266
|
+
} else {
|
|
1267
|
+
validPrereqs.push(p);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
c.prerequisites = validPrereqs;
|
|
1271
|
+
}
|
|
1272
|
+
const initialCycles = detectPrerequisiteCycles(concepts);
|
|
1273
|
+
let removedEdges = [];
|
|
1274
|
+
if (initialCycles.length > 0) {
|
|
1275
|
+
const broken = breakPrerequisiteCycles(concepts, initialCycles);
|
|
1276
|
+
concepts = broken.fixed;
|
|
1277
|
+
removedEdges = broken.removed;
|
|
1278
|
+
for (const r of removedEdges) {
|
|
1279
|
+
warnings.push(`Cycle broken: removed edge ${r.from} -> ${r.to}`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const coverage = auditSyllabusCoverage(concepts, syllabus);
|
|
1283
|
+
if (coverage.coverageRatio < 1) {
|
|
1284
|
+
warnings.push(`Syllabus coverage ${(coverage.coverageRatio * 100).toFixed(1)}% (${coverage.coveredTopics}/${coverage.totalTopics} topics). Uncovered: ${coverage.uncovered.join(", ")}`);
|
|
1285
|
+
}
|
|
1286
|
+
for (const c of concepts) {
|
|
1287
|
+
if (!c.ulo) warnings.push(`Concept ${c.id} missing ULO (WHAT + WHY)`);
|
|
1288
|
+
if (!c.cio) warnings.push(`Concept ${c.id} missing CIO (HOW mechanism)`);
|
|
1289
|
+
if (!c.sio || c.sio.length === 0) warnings.push(`Concept ${c.id} missing SIO implementation items`);
|
|
1290
|
+
}
|
|
1291
|
+
const valid = initialCycles.length === 0 && danglingPrerequisites.length === 0 && coverage.coverageRatio >= 0.8;
|
|
1292
|
+
const verifiedGraph = {
|
|
1293
|
+
...kg,
|
|
1294
|
+
concepts
|
|
1295
|
+
};
|
|
1296
|
+
const report = {
|
|
1297
|
+
valid,
|
|
1298
|
+
cycles: initialCycles,
|
|
1299
|
+
removedEdges,
|
|
1300
|
+
danglingPrerequisites,
|
|
1301
|
+
coverageRatio: coverage.coverageRatio,
|
|
1302
|
+
totalSyllabusTopics: coverage.totalTopics,
|
|
1303
|
+
coveredSyllabusTopics: coverage.coveredTopics,
|
|
1304
|
+
uncoveredTopics: coverage.uncovered,
|
|
1305
|
+
warnings
|
|
1306
|
+
};
|
|
1307
|
+
return { verifiedGraph, report };
|
|
1308
|
+
}
|
|
1309
|
+
|
|
971
1310
|
// src/graph/loExemplars.ts
|
|
972
1311
|
var ULO_EXEMPLARS = [
|
|
973
1312
|
// Calibrated against production learning_objectives (UNIVERSAL rows, neutral)
|
|
@@ -1565,58 +1904,6 @@ async function standardizeConcepts(opts) {
|
|
|
1565
1904
|
}
|
|
1566
1905
|
|
|
1567
1906
|
// src/graph/knowledgeGraphPipeline.ts
|
|
1568
|
-
function detectCycles(concepts) {
|
|
1569
|
-
const cycles = [];
|
|
1570
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1571
|
-
const inStack = /* @__PURE__ */ new Set();
|
|
1572
|
-
const path = [];
|
|
1573
|
-
function dfs(conceptId, prereqMap2) {
|
|
1574
|
-
if (inStack.has(conceptId)) {
|
|
1575
|
-
const cycleStart = path.indexOf(conceptId);
|
|
1576
|
-
if (cycleStart >= 0) {
|
|
1577
|
-
cycles.push([...path.slice(cycleStart), conceptId]);
|
|
1578
|
-
}
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
if (visited.has(conceptId)) return;
|
|
1582
|
-
visited.add(conceptId);
|
|
1583
|
-
inStack.add(conceptId);
|
|
1584
|
-
path.push(conceptId);
|
|
1585
|
-
const prereqs = prereqMap2.get(conceptId) || [];
|
|
1586
|
-
for (const prereq of prereqs) {
|
|
1587
|
-
dfs(prereq, prereqMap2);
|
|
1588
|
-
}
|
|
1589
|
-
path.pop();
|
|
1590
|
-
inStack.delete(conceptId);
|
|
1591
|
-
}
|
|
1592
|
-
const prereqMap = /* @__PURE__ */ new Map();
|
|
1593
|
-
for (const concept of concepts) {
|
|
1594
|
-
prereqMap.set(concept.id, concept.prerequisites || []);
|
|
1595
|
-
}
|
|
1596
|
-
for (const concept of concepts) {
|
|
1597
|
-
if (!visited.has(concept.id)) {
|
|
1598
|
-
dfs(concept.id, prereqMap);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
return cycles;
|
|
1602
|
-
}
|
|
1603
|
-
function breakCycles(concepts, cycles) {
|
|
1604
|
-
const removed = [];
|
|
1605
|
-
const fixed = concepts.map((c) => ({ ...c, prerequisites: [...c.prerequisites || []] }));
|
|
1606
|
-
for (const cycle of cycles) {
|
|
1607
|
-
if (cycle.length >= 2) {
|
|
1608
|
-
const breaker = cycle[cycle.length - 2];
|
|
1609
|
-
const target = cycle[cycle.length - 1];
|
|
1610
|
-
for (const concept of fixed) {
|
|
1611
|
-
if (concept.id === breaker) {
|
|
1612
|
-
concept.prerequisites = concept.prerequisites.filter((p) => p !== target);
|
|
1613
|
-
removed.push({ from: breaker, to: target });
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return { fixed, removed };
|
|
1619
|
-
}
|
|
1620
1907
|
function topologicalSort(concepts) {
|
|
1621
1908
|
const inDegree = /* @__PURE__ */ new Map();
|
|
1622
1909
|
const adjacency = /* @__PURE__ */ new Map();
|
|
@@ -1657,6 +1944,11 @@ async function generateKnowledgeGraph(options) {
|
|
|
1657
1944
|
onProgress?.(step, msg);
|
|
1658
1945
|
};
|
|
1659
1946
|
log("STEP_1", "Decomposing subject into concepts...");
|
|
1947
|
+
let parsedSyllabus = options.parsedSyllabus;
|
|
1948
|
+
if (!parsedSyllabus && syllabusText) {
|
|
1949
|
+
parsedSyllabus = parseSyllabus(syllabusText);
|
|
1950
|
+
log("STEP_1", `Parsed syllabus: ${parsedSyllabus.units.length} units, ${parsedSyllabus.allTopics.length} topics`);
|
|
1951
|
+
}
|
|
1660
1952
|
const client = createLlmClient(llmConfig);
|
|
1661
1953
|
const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "16000", 10);
|
|
1662
1954
|
let truncatedSyllabus = "";
|
|
@@ -1668,10 +1960,13 @@ async function generateKnowledgeGraph(options) {
|
|
|
1668
1960
|
truncatedSyllabus = syllabusText;
|
|
1669
1961
|
}
|
|
1670
1962
|
}
|
|
1963
|
+
const structuredTopicsGuide = parsedSyllabus && parsedSyllabus.allTopics.length > 0 ? `
|
|
1964
|
+
STRUCTURED SYLLABUS TOPICS (Every topic MUST map to at least one concept to ensure 100% syllabus coverage):
|
|
1965
|
+
` + parsedSyllabus.allTopics.map((t) => `- [${t.unitTitle}] ${t.id}: ${t.title} (Keywords: ${t.keywords.join(", ")})`).join("\n") + "\n" : "";
|
|
1671
1966
|
const systemPrompt = "You are an expert curriculum designer. Decompose the given subject into a structured knowledge graph with concepts, prerequisites, and problem types.\n\nCRITICAL RULES:\n1. Each concept MUST have the three depth layers, authored against this CONTRACT:\n - ulo (WHAT + WHY): bound to the CONCEPT's intrinsic NATURE. It explains what the\n concept is and why it exists \u2014 the problem it solves in principle. It must be\n TECHNOLOGY-AGNOSTIC: never name a concrete library, API, function, file format,\n tool or language feature. Typical assessment level: Remember / Understand.\n - cio (HOW): the MECHANISM \u2014 how the concept works and solves that problem, step\n by step. Still bound to the concept's nature: more concrete than the ULO, but\n NOT tied to one specific technology (no concrete API names). CIO capability is\n assessed INDIRECTLY, through completing contextual work, not in isolation.\n - sio (IMPLEMENTATION): an array of 2-4 items. EVERY item MUST reference concrete\n KEYWORDS/technologies (a real API, function, module, file, or named pattern) and\n describe a completable, context-specific implementation task. Application,\n Analysis and Creation skills are assessed INDIRECTLY through completing these\n items. There is NO fixed Bloom ceiling for a layer \u2014 but concreteness MUST\n strictly increase: SIO more technology-bound than CIO, CIO more concrete than ULO.\n - prerequisites: concept IDs that must be learned first (MUST NOT create cycles)\n - problem_types: 2-4 problem types with bloom_level and difficulty. Remember and\n Understand problem types test the ULO/CIO directly; Apply-and-above types are\n proxies for SIO completion in context.\n - techniques: methods, formulas, tools\n2. Prerequisite chains MUST be acyclic (no concept can depend on itself or create a loop)\n3. Concepts should be ordered by prerequisite dependency (topological sort)\n4. Problem types should range from Remember to Apply (not all at same level)\n5. Group concepts into categories (chapters/sections)\n6. ALL text in ENGLISH (even if input is Vietnamese)\n7. NEVER invent concepts not present in the syllabus\n8. Return JSON matching the KnowledgeGraphSchema";
|
|
1672
1967
|
const userPrompt = `Subject: ${subject}
|
|
1673
1968
|
${description ? `Description: ${description}` : ""}
|
|
1674
|
-
${truncatedSyllabus ? `Syllabus:
|
|
1969
|
+
${structuredTopicsGuide}${truncatedSyllabus ? `Syllabus:
|
|
1675
1970
|
${truncatedSyllabus}` : ""}
|
|
1676
1971
|
${gradeBand ? `Grade band: ${gradeBand[0]}-${gradeBand[1]}` : ""}
|
|
1677
1972
|
${domain ? `Domain: ${domain}` : ""}
|
|
@@ -1816,13 +2111,13 @@ Return JSON:
|
|
|
1816
2111
|
log("STEP_2_5", "Standardized: " + std.decisions.filter((d) => d.decision === "standardized").length + ", proposed new: " + std.decisions.filter((d) => d.decision === "proposed_new").length);
|
|
1817
2112
|
}
|
|
1818
2113
|
log("STEP_3", "Checking for prerequisite cycles...");
|
|
1819
|
-
const cycles =
|
|
2114
|
+
const cycles = detectPrerequisiteCycles(concepts);
|
|
1820
2115
|
if (cycles.length > 0) {
|
|
1821
2116
|
log("STEP_3", `Found ${cycles.length} cycle(s) in prerequisite chain \u2014 breaking cycles`);
|
|
1822
2117
|
for (const cycle of cycles) {
|
|
1823
2118
|
warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 breaking at weakest edge`);
|
|
1824
2119
|
}
|
|
1825
|
-
const { fixed, removed } =
|
|
2120
|
+
const { fixed, removed } = breakPrerequisiteCycles(concepts, cycles);
|
|
1826
2121
|
for (let i = 0; i < concepts.length; i++) {
|
|
1827
2122
|
const fixedConcept = fixed.find((f) => f.id === concepts[i].id);
|
|
1828
2123
|
if (fixedConcept) {
|
|
@@ -1873,16 +2168,21 @@ Return JSON:
|
|
|
1873
2168
|
}
|
|
1874
2169
|
}
|
|
1875
2170
|
log("STEP_2", `Validated ${concepts.length} concepts, ${categories.length} categories, ${learningPath.length} learning path steps`);
|
|
2171
|
+
const rawGraph = {
|
|
2172
|
+
schema_version: 1,
|
|
2173
|
+
type: "knowledge_graph",
|
|
2174
|
+
subject: result.subject,
|
|
2175
|
+
concepts,
|
|
2176
|
+
categories,
|
|
2177
|
+
learning_path: learningPath
|
|
2178
|
+
};
|
|
2179
|
+
log("STEP_6", "Running Knowledge Graph verification & reverse coverage audit...");
|
|
2180
|
+
const { verifiedGraph, report } = verifyKnowledgeGraph(rawGraph, parsedSyllabus);
|
|
2181
|
+
warnings.push(...report.warnings);
|
|
1876
2182
|
return {
|
|
1877
|
-
knowledgeGraph:
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
subject: result.subject,
|
|
1881
|
-
concepts,
|
|
1882
|
-
categories,
|
|
1883
|
-
learning_path: learningPath
|
|
1884
|
-
},
|
|
1885
|
-
warnings
|
|
2183
|
+
knowledgeGraph: verifiedGraph,
|
|
2184
|
+
warnings,
|
|
2185
|
+
verificationReport: report
|
|
1886
2186
|
};
|
|
1887
2187
|
}
|
|
1888
2188
|
|
|
@@ -2148,9 +2448,12 @@ exports.SIO_CODE_RE = SIO_CODE_RE;
|
|
|
2148
2448
|
exports.ULO_CODE_RE = ULO_CODE_RE;
|
|
2149
2449
|
exports.assignConceptCodes = assignConceptCodes;
|
|
2150
2450
|
exports.auditDepthLayers = auditDepthLayers;
|
|
2451
|
+
exports.auditSyllabusCoverage = auditSyllabusCoverage;
|
|
2452
|
+
exports.breakPrerequisiteCycles = breakPrerequisiteCycles;
|
|
2151
2453
|
exports.cioActionSlug = cioActionSlug;
|
|
2152
2454
|
exports.cioCode = cioCode;
|
|
2153
2455
|
exports.conceptCodeFromName = conceptCodeFromName;
|
|
2456
|
+
exports.detectPrerequisiteCycles = detectPrerequisiteCycles;
|
|
2154
2457
|
exports.escalateAndMapConcepts = escalateAndMapConcepts;
|
|
2155
2458
|
exports.extractFeatureSteps = extractFeatureSteps;
|
|
2156
2459
|
exports.extractFeatureStepsBatched = extractFeatureStepsBatched;
|
|
@@ -2162,6 +2465,7 @@ exports.sioCode = sioCode;
|
|
|
2162
2465
|
exports.standardStatement = standardStatement;
|
|
2163
2466
|
exports.techTagFor = techTagFor;
|
|
2164
2467
|
exports.uloCode = uloCode;
|
|
2468
|
+
exports.verifyKnowledgeGraph = verifyKnowledgeGraph;
|
|
2165
2469
|
exports.verifyProjectGraph = verifyProjectGraph;
|
|
2166
2470
|
//# sourceMappingURL=index.cjs.map
|
|
2167
2471
|
//# sourceMappingURL=index.cjs.map
|