@thanh01.pmt/domain-kit 0.4.0 → 0.6.0
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/README.md +682 -168
- package/dist/assembly/index.d.cts +2 -2
- package/dist/assembly/index.d.ts +2 -2
- package/dist/{chunk-HILSUZOF.mjs → chunk-2OGNQUXC.mjs} +171 -31
- package/dist/chunk-2OGNQUXC.mjs.map +1 -0
- package/dist/{chunk-LZQLLAYP.mjs → chunk-5OMSYKQP.mjs} +43 -23
- package/dist/chunk-5OMSYKQP.mjs.map +1 -0
- package/dist/{chunk-BL5NBKPH.mjs → chunk-67GXK3ZE.mjs} +18 -8
- package/dist/chunk-67GXK3ZE.mjs.map +1 -0
- package/dist/{chunk-PKUEOSZ3.mjs → chunk-CGNXGVHV.mjs} +46 -4
- package/dist/chunk-CGNXGVHV.mjs.map +1 -0
- package/dist/{chunk-VCPIWJ7R.mjs → chunk-ERDTHXQA.mjs} +21 -3
- package/dist/chunk-ERDTHXQA.mjs.map +1 -0
- package/dist/chunk-OYSZQVPY.mjs +10 -0
- package/dist/chunk-OYSZQVPY.mjs.map +1 -0
- package/dist/{chunk-QJF4QCBJ.mjs → chunk-WKTH3JPA.mjs} +202 -36
- package/dist/chunk-WKTH3JPA.mjs.map +1 -0
- package/dist/{conceptEscalator-YfMKIKCZ.d.ts → conceptEscalator-BCsw0ys_.d.ts} +10 -1
- package/dist/{conceptEscalator-ltRLtPhf.d.cts → conceptEscalator-DPrs4PwC.d.cts} +10 -1
- package/dist/concepts/index.d.cts +1 -1
- package/dist/concepts/index.d.ts +1 -1
- package/dist/{curriculumFeedSchema-B66H3gEV.d.ts → curriculumFeedSchema-DYayelAA.d.cts} +212 -1
- package/dist/{curriculumFeedSchema-B66H3gEV.d.cts → curriculumFeedSchema-DYayelAA.d.ts} +212 -1
- package/dist/detector/index.cjs +21 -6
- package/dist/detector/index.cjs.map +1 -1
- package/dist/detector/index.mjs +2 -1
- package/dist/feed/index.cjs +243 -33
- package/dist/feed/index.cjs.map +1 -1
- package/dist/feed/index.d.cts +32 -2
- package/dist/feed/index.d.ts +32 -2
- package/dist/feed/index.mjs +2 -2
- package/dist/graph/index.cjs +163 -28
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +14 -5
- package/dist/graph/index.d.ts +14 -5
- package/dist/graph/index.mjs +2 -1
- package/dist/{hybridGraphPipeline-DXQDbBEj.d.ts → hybridGraphPipeline-9dRKJj7r.d.ts} +6 -2
- package/dist/{hybridGraphPipeline-dT-bio-3.d.cts → hybridGraphPipeline-CocfYe4d.d.cts} +6 -2
- package/dist/{hybridGraphSchema-nrUeSwpU.d.ts → hybridGraphSchema-C37qxdTM.d.cts} +16 -3
- package/dist/{hybridGraphSchema-nrUeSwpU.d.cts → hybridGraphSchema-C37qxdTM.d.ts} +16 -3
- package/dist/index.cjs +3742 -3362
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.mjs +7 -6
- package/dist/llmClient-CX5uUiQ1.d.cts +60 -0
- package/dist/llmClient-CX5uUiQ1.d.ts +60 -0
- package/dist/pipeline/index.cjs +1556 -1202
- package/dist/pipeline/index.cjs.map +1 -1
- package/dist/pipeline/index.d.cts +13 -5
- package/dist/pipeline/index.d.ts +13 -5
- package/dist/pipeline/index.mjs +6 -5
- package/dist/schemas/index.cjs +64 -2
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +2 -2
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-BL5NBKPH.mjs.map +0 -1
- package/dist/chunk-HILSUZOF.mjs.map +0 -1
- package/dist/chunk-LZQLLAYP.mjs.map +0 -1
- package/dist/chunk-PKUEOSZ3.mjs.map +0 -1
- package/dist/chunk-QJF4QCBJ.mjs.map +0 -1
- package/dist/chunk-VCPIWJ7R.mjs.map +0 -1
- package/dist/curriculumFeedEmitter-6IK6PFDH.mjs +0 -4
- package/dist/curriculumFeedEmitter-6IK6PFDH.mjs.map +0 -1
- package/dist/llmClient-ysPhLjcH.d.cts +0 -16
- package/dist/llmClient-ysPhLjcH.d.ts +0 -16
package/dist/graph/index.cjs
CHANGED
|
@@ -37,9 +37,10 @@ function resolveProviderChain(config) {
|
|
|
37
37
|
return chain;
|
|
38
38
|
}
|
|
39
39
|
var MIN_REQUEST_INTERVAL_MS = 2500;
|
|
40
|
+
var requestIntervalMs = Number(process.env.LLM_MIN_REQUEST_INTERVAL_MS || MIN_REQUEST_INTERVAL_MS);
|
|
40
41
|
var lastRequestAt = 0;
|
|
41
42
|
async function pacedDelay() {
|
|
42
|
-
const wait = lastRequestAt +
|
|
43
|
+
const wait = lastRequestAt + requestIntervalMs - Date.now();
|
|
43
44
|
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
44
45
|
lastRequestAt = Date.now();
|
|
45
46
|
}
|
|
@@ -54,6 +55,10 @@ function createLlmClient(config) {
|
|
|
54
55
|
primary.apiKey;
|
|
55
56
|
primary.baseUrl;
|
|
56
57
|
const model = primary.model;
|
|
58
|
+
if (config?.minRequestIntervalMs !== void 0) {
|
|
59
|
+
requestIntervalMs = Math.max(0, config.minRequestIntervalMs);
|
|
60
|
+
}
|
|
61
|
+
const callLog = [];
|
|
57
62
|
async function chat(messages, options) {
|
|
58
63
|
const temperature = options?.temperature ?? config?.temperature ?? 0.1;
|
|
59
64
|
const maxTokens = options?.maxTokens ?? config?.maxTokens ?? parseInt(process.env.LLM_MAX_TOKENS || "65536", 10);
|
|
@@ -68,6 +73,7 @@ function createLlmClient(config) {
|
|
|
68
73
|
await new Promise((resolve) => setTimeout(resolve, 2e4));
|
|
69
74
|
}
|
|
70
75
|
for (const provider of chain) {
|
|
76
|
+
const recIdx = callLog.push({ provider: provider.label, model: provider.model, ok: false }) - 1;
|
|
71
77
|
try {
|
|
72
78
|
const headers = {
|
|
73
79
|
"Content-Type": "application/json",
|
|
@@ -128,6 +134,7 @@ function createLlmClient(config) {
|
|
|
128
134
|
const retryData = await retry.json();
|
|
129
135
|
const retryContent2 = retryData.choices?.[0]?.message?.content || "";
|
|
130
136
|
if (!retryContent2.trim()) throw err;
|
|
137
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: true };
|
|
131
138
|
return {
|
|
132
139
|
content: retryContent2,
|
|
133
140
|
model: retryData.model || provider.model,
|
|
@@ -156,6 +163,7 @@ function createLlmClient(config) {
|
|
|
156
163
|
if (!retry.ok) throw err;
|
|
157
164
|
const retryData = await retry.json();
|
|
158
165
|
const retryContent = retryData.choices?.[0]?.message?.content || "";
|
|
166
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: true };
|
|
159
167
|
return {
|
|
160
168
|
content: retryContent,
|
|
161
169
|
model: retryData.model || provider.model,
|
|
@@ -186,6 +194,7 @@ function createLlmClient(config) {
|
|
|
186
194
|
};
|
|
187
195
|
} catch (err) {
|
|
188
196
|
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: false, error: msg.slice(0, 200) };
|
|
189
198
|
attempts.push(`${provider.label}: ${msg}`);
|
|
190
199
|
lastError = err instanceof LlmClientError ? err : new LlmClientError(`[${provider.label}] ${msg}`);
|
|
191
200
|
}
|
|
@@ -195,7 +204,7 @@ function createLlmClient(config) {
|
|
|
195
204
|
}
|
|
196
205
|
throw lastError ?? new LlmClientError("All LLM providers failed after " + MAX_CHAIN_ROUNDS + " rounds.");
|
|
197
206
|
}
|
|
198
|
-
return { chat, model };
|
|
207
|
+
return { chat, model, callLog };
|
|
199
208
|
}
|
|
200
209
|
function parseJsonFromLlm(text) {
|
|
201
210
|
let cleaned = text.trim();
|
|
@@ -259,8 +268,8 @@ async function llmChatJson(client, systemPrompt, userPrompt, options) {
|
|
|
259
268
|
|
|
260
269
|
// src/graph/scaffoldExtractor.ts
|
|
261
270
|
async function extractScaffold(options) {
|
|
262
|
-
const { goal, techStack, fileList, llmConfig } = options;
|
|
263
|
-
const client = createLlmClient(llmConfig);
|
|
271
|
+
const { goal, techStack, fileList, llmConfig, sharedClient } = options;
|
|
272
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
264
273
|
const fileListStr = fileList.length > 0 ? fileList.join("\n") : "(empty)";
|
|
265
274
|
const systemPrompt = "You are a programming pedagogy expert. For ONE concrete project, extract the 'FOUNDATION & SETUP' feature (id F0) \u2014 the COMPLETE set of 'MUST-KNOW' steps the learner needs before studying the first feature. Break it into concrete ACTION steps (5-8 steps), each with completion_level='base'. Include:\n1. Tools required for this tech stack (IDE, simulator/emulator, terminal, git, package manager) + basic operations (open project, build, run, debug, commit).\n2. Create the initial project: from a template OR clone/open the base-project (the provided repo).\n3. MINIMAL programming knowledge of the language (variables, types, functions, if/for, view declaration) + build one small demo (e.g. a single-screen Hello World app) BEFORE touching the real project.\n4. Repo map of the actual project: folder structure, entry point, how to build/run, main architecture (MVVM/Flux...), important packages/modules.\n5. Development loop (build->run->see result->debug), how to read/fix basic compile errors, minimal git workflow (clone/branch/commit/push).\nkeywords[]: real tool/language terms (e.g. 'Xcode', 'Simulator', 'Git', 'Swift', '@main', 'MVVM'). files[]: leave [] or use real paths if the step touches specific files. description: ONE concise sentence, MAX 140 characters. intent: WHY this step matters. outcome: {user_visible: string, technical: string}. acceptance[]: 2 verifiable criteria. effort: {estimated_minutes: number, complexity: low|medium|high}. NEVER invent. ALL text must be in ENGLISH (technical terms stay as-is). Return JSON containing only feature F0.";
|
|
266
275
|
const userPrompt = `App goal: ${goal}
|
|
@@ -345,8 +354,8 @@ ${content}`);
|
|
|
345
354
|
return blocks.join("\n\n");
|
|
346
355
|
}
|
|
347
356
|
async function extractProjectOverview(options) {
|
|
348
|
-
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig } = options;
|
|
349
|
-
const client = createLlmClient(llmConfig);
|
|
357
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig, sharedClient } = options;
|
|
358
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
350
359
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
351
360
|
const allFilesStr = buildSourceText(fileContentsMap);
|
|
352
361
|
let keywordsAnchor = "";
|
|
@@ -415,8 +424,8 @@ ${content}`);
|
|
|
415
424
|
return blocks.join("\n\n");
|
|
416
425
|
}
|
|
417
426
|
async function extractFeatureSteps(options) {
|
|
418
|
-
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig } = options;
|
|
419
|
-
const client = createLlmClient(llmConfig);
|
|
427
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig, sharedClient } = options;
|
|
428
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
420
429
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
421
430
|
const allFilesStr = buildSourceText2(fileContentsMap);
|
|
422
431
|
const fid = feature.id || "F_UNK";
|
|
@@ -483,8 +492,8 @@ Split this feature into 2-6 action steps. Return JSON:
|
|
|
483
492
|
concept_codes: []
|
|
484
493
|
}));
|
|
485
494
|
}
|
|
486
|
-
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig) {
|
|
487
|
-
const client = createLlmClient(llmConfig);
|
|
495
|
+
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig, sharedClient) {
|
|
496
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
488
497
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
489
498
|
const sourceText = buildSourceText2(fileContentsMap);
|
|
490
499
|
const featuresListing = featuresMeta.map(
|
|
@@ -751,7 +760,7 @@ function inferDepthFromContext(keyword, fileContentsMap) {
|
|
|
751
760
|
return { depth: "sio", rationale: `Default: treating '${keyword}' as SIO (implementation-level)` };
|
|
752
761
|
}
|
|
753
762
|
async function escalateAndMapConcepts(options) {
|
|
754
|
-
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig } = options;
|
|
763
|
+
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig, sharedClient } = options;
|
|
755
764
|
const features = projectGraph.features || [];
|
|
756
765
|
const allTerms = /* @__PURE__ */ new Set();
|
|
757
766
|
for (const feature of features) {
|
|
@@ -767,7 +776,7 @@ async function escalateAndMapConcepts(options) {
|
|
|
767
776
|
llmResults = conceptMapOverride;
|
|
768
777
|
} else {
|
|
769
778
|
try {
|
|
770
|
-
const client = createLlmClient(llmConfig);
|
|
779
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
771
780
|
const conceptBank = (availableConcepts || []).sort().join(", ") || "(empty)";
|
|
772
781
|
const termEntries = termList.map((term) => {
|
|
773
782
|
const snippets = extractTermSnippets(term, fileContentsMap);
|
|
@@ -820,13 +829,16 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
|
|
|
820
829
|
const cName = info.concept_name || kw;
|
|
821
830
|
let depth = "sio";
|
|
822
831
|
let depthRationale = "Default: SIO";
|
|
832
|
+
let depthSource = "override";
|
|
823
833
|
if (info.depth && ["ulo", "cio", "sio"].includes(info.depth)) {
|
|
824
834
|
depth = info.depth;
|
|
825
835
|
depthRationale = `LLM classified as ${depth}`;
|
|
836
|
+
depthSource = "llm";
|
|
826
837
|
} else {
|
|
827
838
|
const inferred = inferDepthFromContext(kw, fileContentsMap);
|
|
828
839
|
depth = inferred.depth;
|
|
829
840
|
depthRationale = inferred.rationale;
|
|
841
|
+
depthSource = "heuristic";
|
|
830
842
|
}
|
|
831
843
|
const kwKey = `${cCode}::${kw}`;
|
|
832
844
|
if (seen.has(kwKey)) continue;
|
|
@@ -844,6 +856,7 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
|
|
|
844
856
|
keyword: kw,
|
|
845
857
|
depth,
|
|
846
858
|
depth_rationale: depthRationale,
|
|
859
|
+
depth_source: depthSource,
|
|
847
860
|
evidence_files: evidenceFiles
|
|
848
861
|
});
|
|
849
862
|
}
|
|
@@ -968,6 +981,13 @@ var ULO_CODE_RE = /^ULO-[A-Z][A-Z0-9_]*-\d{2}$/;
|
|
|
968
981
|
var CIO_CODE_RE = /^CIO-[A-Z][A-Z0-9_]*-\d{2}-[A-Z][A-Z0-9_]*$/;
|
|
969
982
|
var SIO_CODE_RE = /^SIO-[A-Z0-9]+-[A-Z][A-Z0-9_]*-\d{2}$/;
|
|
970
983
|
|
|
984
|
+
// src/utils/diacritics.ts
|
|
985
|
+
var COMBINING_MARKS = /[\u0300-\u036f]/g;
|
|
986
|
+
var D_WITH_STROKE = /đ/gi;
|
|
987
|
+
function stripDiacritics(s) {
|
|
988
|
+
return s.normalize("NFD").replace(COMBINING_MARKS, "").replace(D_WITH_STROKE, "d").toLowerCase();
|
|
989
|
+
}
|
|
990
|
+
|
|
971
991
|
// src/parsers/syllabusParser.ts
|
|
972
992
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
973
993
|
"and",
|
|
@@ -1730,7 +1750,7 @@ function loadKnowledgeTreeCatalog(tsvPath) {
|
|
|
1730
1750
|
}
|
|
1731
1751
|
function tokens(s) {
|
|
1732
1752
|
return new Set(
|
|
1733
|
-
s
|
|
1753
|
+
stripDiacritics(s).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4)
|
|
1734
1754
|
);
|
|
1735
1755
|
}
|
|
1736
1756
|
function filterBySelection(catalog, selectedCategories, selectedTopics, analysisConcepts, maxConcepts = 120) {
|
|
@@ -1904,6 +1924,15 @@ async function standardizeConcepts(opts) {
|
|
|
1904
1924
|
}
|
|
1905
1925
|
|
|
1906
1926
|
// src/graph/knowledgeGraphPipeline.ts
|
|
1927
|
+
var MIN_CALIBRATION_MINUTES = 15;
|
|
1928
|
+
var MAX_CALIBRATION_MINUTES = 60;
|
|
1929
|
+
var DEFAULT_CONCEPT_MINUTES = 30;
|
|
1930
|
+
var MIN_CALIBRATION_DIVERGENCE = 0.5;
|
|
1931
|
+
function calibrateConceptMinutes(sioCount, problemTypeCount, prereqCount, hasHighBloom) {
|
|
1932
|
+
const ratio = (value, cap) => Math.min(1, Math.max(0, value) / cap);
|
|
1933
|
+
const score = 0.3 * ratio(sioCount, 4) + 0.25 * ratio(problemTypeCount, 4) + 0.2 * ratio(prereqCount, 4) + 0.25 * (hasHighBloom ? 1 : 0);
|
|
1934
|
+
return Math.round(MIN_CALIBRATION_MINUTES + score * (MAX_CALIBRATION_MINUTES - MIN_CALIBRATION_MINUTES));
|
|
1935
|
+
}
|
|
1907
1936
|
function topologicalSort(concepts) {
|
|
1908
1937
|
const inDegree = /* @__PURE__ */ new Map();
|
|
1909
1938
|
const adjacency = /* @__PURE__ */ new Map();
|
|
@@ -1963,7 +1992,42 @@ async function generateKnowledgeGraph(options) {
|
|
|
1963
1992
|
const structuredTopicsGuide = parsedSyllabus && parsedSyllabus.allTopics.length > 0 ? `
|
|
1964
1993
|
STRUCTURED SYLLABUS TOPICS (Every topic MUST map to at least one concept to ensure 100% syllabus coverage):
|
|
1965
1994
|
` + parsedSyllabus.allTopics.map((t) => `- [${t.unitTitle}] ${t.id}: ${t.title} (Keywords: ${t.keywords.join(", ")})`).join("\n") + "\n" : "";
|
|
1966
|
-
const systemPrompt =
|
|
1995
|
+
const systemPrompt = `You are an expert curriculum designer. Decompose the given subject into a structured knowledge graph with concepts, prerequisites, and problem types.
|
|
1996
|
+
|
|
1997
|
+
CRITICAL RULES:
|
|
1998
|
+
1. Each concept MUST have the three depth layers, authored against this CONTRACT:
|
|
1999
|
+
- ulo (WHAT + WHY): bound to the CONCEPT's intrinsic NATURE. It explains what the
|
|
2000
|
+
concept is and why it exists \u2014 the problem it solves in principle. It must be
|
|
2001
|
+
TECHNOLOGY-AGNOSTIC: never name a concrete library, API, function, file format,
|
|
2002
|
+
tool or language feature. Typical assessment level: Remember / Understand.
|
|
2003
|
+
- cio (HOW): the MECHANISM \u2014 how the concept works and solves that problem, step
|
|
2004
|
+
by step. Still bound to the concept's nature: more concrete than the ULO, but
|
|
2005
|
+
NOT tied to one specific technology (no concrete API names). CIO capability is
|
|
2006
|
+
assessed INDIRECTLY, through completing contextual work, not in isolation.
|
|
2007
|
+
- sio (IMPLEMENTATION): an array of 2-4 items. EVERY item MUST reference concrete
|
|
2008
|
+
KEYWORDS/technologies (a real API, function, module, file, or named pattern) and
|
|
2009
|
+
describe a completable, context-specific implementation task. Application,
|
|
2010
|
+
Analysis and Creation skills are assessed INDIRECTLY through completing these
|
|
2011
|
+
items. There is NO fixed Bloom ceiling for a layer \u2014 but concreteness MUST
|
|
2012
|
+
strictly increase: SIO more technology-bound than CIO, CIO more concrete than ULO.
|
|
2013
|
+
- prerequisites: concept IDs that must be learned first (MUST NOT create cycles)
|
|
2014
|
+
- problem_types: 2-4 problem types with bloom_level and difficulty. Remember and
|
|
2015
|
+
Understand problem types test the ULO/CIO directly; Apply-and-above types are
|
|
2016
|
+
proxies for SIO completion in context.
|
|
2017
|
+
- techniques: methods, formulas, tools
|
|
2018
|
+
2. Prerequisite chains MUST be acyclic (no concept can depend on itself or create a loop)
|
|
2019
|
+
3. Concepts should be ordered by prerequisite dependency (topological sort)
|
|
2020
|
+
4. Problem types should range from Remember to Apply (not all at same level)
|
|
2021
|
+
5. Group concepts into categories (chapters/sections)
|
|
2022
|
+
6. ALL text in ENGLISH (even if input is Vietnamese)
|
|
2023
|
+
7. NEVER invent concepts not present in the syllabus
|
|
2024
|
+
8. KEYWORDS: Each concept MUST include 5-10 domain-specific keywords that:
|
|
2025
|
+
- Are concrete terms a student encounters when studying THIS concept
|
|
2026
|
+
- Include at least 2 technology-specific terms (APIs, functions, types, tools)
|
|
2027
|
+
- Are DISTINCT from keywords of other concepts (minimal overlap)
|
|
2028
|
+
- NEVER generic English words (e.g. "view", "data", "code" are TOO generic)
|
|
2029
|
+
- Example for "SwiftUI Layout": ["VStack", "HStack", "ZStack", ".padding()", ".frame()", "alignment", "Spacer", "LazyVGrid"]
|
|
2030
|
+
9. Return JSON matching the KnowledgeGraphSchema`;
|
|
1967
2031
|
const userPrompt = `Subject: ${subject}
|
|
1968
2032
|
${description ? `Description: ${description}` : ""}
|
|
1969
2033
|
${structuredTopicsGuide}${truncatedSyllabus ? `Syllabus:
|
|
@@ -1984,8 +2048,9 @@ Return JSON:
|
|
|
1984
2048
|
{
|
|
1985
2049
|
"id": "C1",
|
|
1986
2050
|
"name": "Concept name",
|
|
1987
|
-
"description": "
|
|
2051
|
+
"description": "2-3 sentence description: what this concept covers, why a learner encounters it, and what mastering it enables. Must be specific enough that a student reading only this description understands the concept's scope.",
|
|
1988
2052
|
"category": "category_id",
|
|
2053
|
+
"keywords": ["specific_term_1", "concrete_api_2", "method_call_3", "tool_or_type_4", "syntax_keyword_5", "concept_term_6"],
|
|
1989
2054
|
"ulo": "WHAT + WHY in technology-agnostic language: what the concept is and the problem it exists to solve. No library/API/tool names.",
|
|
1990
2055
|
"cio": "HOW the mechanism works, still technology-agnostic but more concrete than the ULO.",
|
|
1991
2056
|
"sio": ["Concrete task naming a REAL API/function/module/file from the target technology, completable end to end", "Another technology-bound task using a different concrete keyword"],
|
|
@@ -2026,18 +2091,23 @@ Return JSON:
|
|
|
2026
2091
|
log("STEP_2", "Validating and cleaning knowledge graph...");
|
|
2027
2092
|
const concepts = result.concepts || [];
|
|
2028
2093
|
const categories = result.categories || [];
|
|
2094
|
+
for (const c of concepts) {
|
|
2095
|
+
if ((c.description || "").length < 50) {
|
|
2096
|
+
warnings.push("Concept " + c.id + ": description too short (" + (c.description || "").length + " chars) \u2014 may produce shallow KX narratives");
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2029
2099
|
{
|
|
2030
2100
|
const STOP = /* @__PURE__ */ new Set(["and", "the", "for", "with", "from", "that", "this", "into", "when", "while", "using", "their", "your", "each", "how", "why", "what", "between", "through", "about"]);
|
|
2031
|
-
const tokensFrom = (text) => text
|
|
2101
|
+
const tokensFrom = (text) => stripDiacritics(text).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP.has(w));
|
|
2032
2102
|
for (const c of concepts) {
|
|
2033
2103
|
const existing = (c.keywords || []).map((k) => String(k).trim()).filter(Boolean);
|
|
2034
2104
|
if (existing.length === 0) {
|
|
2035
|
-
const derived = [.../* @__PURE__ */ new Set([...tokensFrom(c.name), ...tokensFrom(c.description || "")])].slice(0,
|
|
2105
|
+
const derived = [.../* @__PURE__ */ new Set([...tokensFrom(c.name), ...tokensFrom(c.description || "")])].slice(0, 8);
|
|
2036
2106
|
if (derived.length === 0) {
|
|
2037
2107
|
throw new Error("Concept " + c.id + " (" + c.name + ") has no keywords and none could be derived from name/description \u2014 fail-closed");
|
|
2038
2108
|
}
|
|
2039
2109
|
c.keywords = derived;
|
|
2040
|
-
warnings.push("Concept " + c.id + ": empty keyword set - derived " + derived.join(", ") + " from name/description");
|
|
2110
|
+
warnings.push("Concept " + c.id + ": empty keyword set - derived " + derived.join(", ") + " from name/description (WARNING: LLM returned no keywords)");
|
|
2041
2111
|
} else {
|
|
2042
2112
|
c.keywords = existing;
|
|
2043
2113
|
}
|
|
@@ -2162,6 +2232,49 @@ Return JSON:
|
|
|
2162
2232
|
}
|
|
2163
2233
|
log("STEP_5", "Depth layers authored: " + concepts.length + " concepts, audit " + lo.audits.filter((a) => a.passed).length + "/" + concepts.length + " passed");
|
|
2164
2234
|
}
|
|
2235
|
+
{
|
|
2236
|
+
for (const concept of concepts) {
|
|
2237
|
+
const sioText = (concept.sio || []).join(" ");
|
|
2238
|
+
const techTokens = [];
|
|
2239
|
+
let m;
|
|
2240
|
+
const techTokenRe = /`([^`]+)`/g;
|
|
2241
|
+
while ((m = techTokenRe.exec(sioText)) !== null) {
|
|
2242
|
+
if (m[1].length >= 3 && m[1].length <= 40) techTokens.push(m[1]);
|
|
2243
|
+
}
|
|
2244
|
+
const annotationRe = /(?:^|[\s(])(@[A-Za-z]\w*)/g;
|
|
2245
|
+
while ((m = annotationRe.exec(sioText)) !== null) techTokens.push(m[1]);
|
|
2246
|
+
const dotRe = /\b\w+\.\w+\(/g;
|
|
2247
|
+
while ((m = dotRe.exec(sioText)) !== null) techTokens.push(m[0].replace("(", ""));
|
|
2248
|
+
const modifierRe = /(?:^|[\s(])(\.[a-z]\w*)\b/g;
|
|
2249
|
+
while ((m = modifierRe.exec(sioText)) !== null) techTokens.push(m[1]);
|
|
2250
|
+
const camelRe = /\b[a-z]+[A-Z][A-Za-z]+\b/g;
|
|
2251
|
+
while ((m = camelRe.exec(sioText)) !== null) techTokens.push(m[0]);
|
|
2252
|
+
const existing = new Set((concept.keywords || []).map((k) => k.toLowerCase()));
|
|
2253
|
+
const newTokens = [...new Set(techTokens)].filter((t) => !existing.has(t.toLowerCase())).slice(0, 8);
|
|
2254
|
+
if (newTokens.length > 0) {
|
|
2255
|
+
concept.keywords = [...concept.keywords || [], ...newTokens];
|
|
2256
|
+
warnings.push("Concept " + concept.id + ": enriched keywords with " + newTokens.length + " SIO-derived tokens");
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
log("STEP_5b", "SIO-keyword enrichment complete");
|
|
2260
|
+
}
|
|
2261
|
+
{
|
|
2262
|
+
for (const concept of concepts) {
|
|
2263
|
+
const sioCount = (concept.sio || []).length;
|
|
2264
|
+
const ptCount = (concept.problem_types || []).length;
|
|
2265
|
+
const prereqCount = (concept.prerequisites || []).length;
|
|
2266
|
+
const hasHighBloom = (concept.problem_types || []).some(
|
|
2267
|
+
(pt) => ["Analyze", "Evaluate", "Create"].includes(pt.bloom_level || "")
|
|
2268
|
+
);
|
|
2269
|
+
const calibrated = calibrateConceptMinutes(sioCount, ptCount, prereqCount, hasHighBloom);
|
|
2270
|
+
const llmEstimate = concept.estimated_minutes || DEFAULT_CONCEPT_MINUTES;
|
|
2271
|
+
if (Math.abs(calibrated - llmEstimate) / llmEstimate > MIN_CALIBRATION_DIVERGENCE) {
|
|
2272
|
+
concept.estimated_minutes = calibrated;
|
|
2273
|
+
warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m");
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
log("STEP_5c", "Minute calibration complete");
|
|
2277
|
+
}
|
|
2165
2278
|
for (const step of learningPath) {
|
|
2166
2279
|
if (!conceptIds.has(step.concept_id)) {
|
|
2167
2280
|
warnings.push(`Learning path references non-existent concept ${step.concept_id}`);
|
|
@@ -2182,7 +2295,8 @@ Return JSON:
|
|
|
2182
2295
|
return {
|
|
2183
2296
|
knowledgeGraph: verifiedGraph,
|
|
2184
2297
|
warnings,
|
|
2185
|
-
verificationReport: report
|
|
2298
|
+
verificationReport: report,
|
|
2299
|
+
provenance: client.callLog
|
|
2186
2300
|
};
|
|
2187
2301
|
}
|
|
2188
2302
|
|
|
@@ -2209,9 +2323,10 @@ async function generateHybridGraph(options) {
|
|
|
2209
2323
|
description: "",
|
|
2210
2324
|
total_project_minutes: 0,
|
|
2211
2325
|
total_concept_minutes: 0,
|
|
2212
|
-
|
|
2326
|
+
linked_prereq_minutes: 0
|
|
2213
2327
|
},
|
|
2214
|
-
warnings
|
|
2328
|
+
warnings,
|
|
2329
|
+
provenance: []
|
|
2215
2330
|
};
|
|
2216
2331
|
}
|
|
2217
2332
|
const client = createLlmClient(llmConfig);
|
|
@@ -2292,9 +2407,14 @@ async function generateHybridGraph(options) {
|
|
|
2292
2407
|
description: "Progressive completion path: " + phases.length + " phases, " + features.length + " features, " + concepts.length + " concepts",
|
|
2293
2408
|
total_project_minutes: totalProjectMinutes,
|
|
2294
2409
|
total_concept_minutes: totalConceptMinutes,
|
|
2295
|
-
|
|
2410
|
+
// HG-1 (2026-10-05): renamed from estimated_total_minutes. It counts only
|
|
2411
|
+
// concepts that HAVE a feature link (max per concept, not summed), so it
|
|
2412
|
+
// is NOT the course total — see HybridGraphSchema.linked_prereq_minutes.
|
|
2413
|
+
// The phase planner below commits to totalProjectMinutes + totalConceptMinutes.
|
|
2414
|
+
linked_prereq_minutes: totalProjectMinutes + totalPrereqMinutes
|
|
2296
2415
|
},
|
|
2297
|
-
warnings
|
|
2416
|
+
warnings,
|
|
2417
|
+
provenance: client.callLog
|
|
2298
2418
|
};
|
|
2299
2419
|
}
|
|
2300
2420
|
function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
@@ -2370,12 +2490,20 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
|
2370
2490
|
} else {
|
|
2371
2491
|
newIntroPhase.set(cid, order);
|
|
2372
2492
|
}
|
|
2493
|
+
introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
|
|
2494
|
+
}
|
|
2495
|
+
for (const intro of introductions) {
|
|
2496
|
+
const cid = intro.concept_id;
|
|
2497
|
+
if (intro.aspect === "advanced") continue;
|
|
2498
|
+
const introOrder = newIntroPhase.get(cid);
|
|
2499
|
+
if (introOrder === void 0 || introOrder !== order) continue;
|
|
2373
2500
|
for (const pre of prereqOf.get(cid) || []) {
|
|
2374
|
-
|
|
2375
|
-
|
|
2501
|
+
const previewOrder = previewPhase.get(pre);
|
|
2502
|
+
const covered = newIntroPhase.has(pre) || previewOrder !== void 0 && previewOrder < order;
|
|
2503
|
+
if (!covered) {
|
|
2504
|
+
errors.push("Phase " + id + ": concept " + cid + " introduced before its prerequisite " + pre + " is introduced (or previewed in an earlier phase) - violates the prerequisite chain");
|
|
2376
2505
|
}
|
|
2377
2506
|
}
|
|
2378
|
-
introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
|
|
2379
2507
|
}
|
|
2380
2508
|
phases.push({
|
|
2381
2509
|
id,
|
|
@@ -2413,6 +2541,9 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
|
2413
2541
|
if (anchor !== void 0 && anchor <= pOrder) {
|
|
2414
2542
|
errors.push("Concept " + cid + " previewed in phase " + pOrder + " but fully introduced in phase " + anchor + " - preview must precede the anchor introduction");
|
|
2415
2543
|
}
|
|
2544
|
+
if (anchor === void 0) {
|
|
2545
|
+
errors.push("Concept " + cid + " is previewed in phase " + pOrder + " but never fully introduced (aspect new) in any phase - a preview must have an anchor teach");
|
|
2546
|
+
}
|
|
2416
2547
|
}
|
|
2417
2548
|
return { phases, errors };
|
|
2418
2549
|
}
|
|
@@ -2427,8 +2558,12 @@ async function decomposePhases(args) {
|
|
|
2427
2558
|
const linkList = links.map(
|
|
2428
2559
|
(l) => "- " + l.feature_id + " -> " + l.concept_id + " (" + l.depth + ")"
|
|
2429
2560
|
).join("\n") || "(none)";
|
|
2430
|
-
const systemPrompt = 'You decompose a software project build into PROGRESSIVE COMPLETION PHASES for learning purposes.\n\nA phase is NOT a feature and NOT a milestone list: it is a cross-feature slice of the\nproduct whose completion yields a DEMONSTRABLY more complete product. Build up gradually:\nthe product grows feature by feature across phases, and each phase adds its features to\nwhat previous phases already built.\n\nRULES:\n1. Every feature must be assigned to EXACTLY ONE phase (no feature reused, none skipped).\n2. Phases are ordered; later phases depend on earlier ones and yield a more complete product.\n3. product_completion describes concretely what the product can DO once the phase is done.\n4. introduces lists which concepts the phase must teach BEFORE its feature work starts:\n - aspect new: first time the concept appears in the course\n - aspect advanced: a concept introduced in an EARLIER phase, revisited here at a\n higher depth (an advanced aspect of the same concept, e.g. validation rules on top\n of basic data modeling). Use the note field to say which aspect.\n - aspect preview: a light awareness pass ("know it exists", ULO depth only, ~5 min)\n placed in an EARLIER phase when its feature work needs the vocabulary before the\n full teach. The anchor phase still owns the full new introduction.\n5. A concept MUST be introduced (new) no later than the first phase that consumes it.\n6. A concept must not be introduced before its own prerequisites are introduced.\n7. Typically 3-6 phases for a small project; keep phases balanced.\n8. Phase 1 usually covers project setup/foundation features plus the first minimal slice.\
|
|
2431
|
-
const
|
|
2561
|
+
const systemPrompt = 'You decompose a software project build into PROGRESSIVE COMPLETION PHASES for learning purposes.\n\nA phase is NOT a feature and NOT a milestone list: it is a cross-feature slice of the\nproduct whose completion yields a DEMONSTRABLY more complete product. Build up gradually:\nthe product grows feature by feature across phases, and each phase adds its features to\nwhat previous phases already built.\n\nRULES:\n1. Every feature must be assigned to EXACTLY ONE phase (no feature reused, none skipped).\n2. Phases are ordered; later phases depend on earlier ones and yield a more complete product.\n3. product_completion describes concretely what the product can DO once the phase is done.\n4. introduces lists which concepts the phase must teach BEFORE its feature work starts:\n - aspect new: first time the concept appears in the course\n - aspect advanced: a concept introduced in an EARLIER phase, revisited here at a\n higher depth (an advanced aspect of the same concept, e.g. validation rules on top\n of basic data modeling). Use the note field to say which aspect.\n - aspect preview: a light awareness pass ("know it exists", ULO depth only, ~5 min)\n placed in an EARLIER phase when its feature work needs the vocabulary before the\n full teach. The anchor phase still owns the full new introduction.\n5. A concept MUST be introduced (new) no later than the first phase that consumes it.\n6. A concept must not be introduced before its own prerequisites are introduced.\n7. Typically 3-6 phases for a small project; keep phases balanced.\n8. MINUTE BALANCE: Each phase should contain approximately 120-240 total minutes of\n concept + feature work (about 3-5 sessions of 90 minutes each). If a phase\n would contain < 60 minutes (too thin) or > 400 minutes (too heavy), rebalance.\n9. Phase 1 usually covers project setup/foundation features plus the first minimal slice.\n10. Return JSON only.';
|
|
2562
|
+
const totalFeatureMinutes = features.reduce((s, f) => s + (f.steps || []).reduce((ss, st) => ss + (st.effort?.estimated_minutes || 15), 0), 0);
|
|
2563
|
+
const totalConceptMinutes = concepts.reduce((s, c) => s + (c.estimated_minutes || 30), 0);
|
|
2564
|
+
const totalMinutes = totalFeatureMinutes + totalConceptMinutes;
|
|
2565
|
+
const targetPhaseCount = Math.max(3, Math.round(totalMinutes / 180));
|
|
2566
|
+
const baseUserPrompt = "FEATURES:\n" + featureList + "\n\nCONCEPTS:\n" + conceptList + "\n\nFEATURE-CONCEPT LINKS:\n" + linkList + "\n\nTOTAL MINUTES: ~" + totalMinutes + "m (" + totalFeatureMinutes + "m features + " + totalConceptMinutes + "m concepts)\nTARGET: ~" + targetPhaseCount + " phases of ~180m each\n\nReturn JSON with a phases array. Each phase: { id: PH1, name, product_completion, feature_ids: [F0, F1], introduces: [{ concept_id: C1, aspect: new or advanced, note }] }.\n";
|
|
2432
2567
|
const MAX_DECOMPOSE_ATTEMPTS = 3;
|
|
2433
2568
|
let auditErrors = [];
|
|
2434
2569
|
let phases = [];
|