@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { u as ProjectGraph, c as Step } from '../projectGraphSchema-DnD7orZV.cjs';
|
|
2
|
-
import { C as ConceptMapping } from '../conceptEscalator-
|
|
2
|
+
import { C as ConceptMapping } from '../conceptEscalator-DPrs4PwC.cjs';
|
|
3
3
|
import 'zod';
|
|
4
|
-
import '../llmClient-
|
|
4
|
+
import '../llmClient-CX5uUiQ1.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Roadmap assembly — assembles final roadmap with keyword tracking and time awareness.
|
package/dist/assembly/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { u as ProjectGraph, c as Step } from '../projectGraphSchema-DnD7orZV.js';
|
|
2
|
-
import { C as ConceptMapping } from '../conceptEscalator-
|
|
2
|
+
import { C as ConceptMapping } from '../conceptEscalator-BCsw0ys_.js';
|
|
3
3
|
import 'zod';
|
|
4
|
-
import '../llmClient-
|
|
4
|
+
import '../llmClient-CX5uUiQ1.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Roadmap assembly — assembles final roadmap with keyword tracking and time awareness.
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { parseSyllabus } from './chunk-T4Y2DE2W.mjs';
|
|
2
|
+
import { stripDiacritics } from './chunk-OYSZQVPY.mjs';
|
|
2
3
|
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
4
|
import { tmpdir } from 'os';
|
|
4
5
|
import { join } from 'path';
|
|
5
6
|
|
|
6
7
|
// src/utils/llmClient.ts
|
|
8
|
+
function summarizeProvenance(callLog) {
|
|
9
|
+
const providers = [];
|
|
10
|
+
const models = [];
|
|
11
|
+
let failed = 0;
|
|
12
|
+
for (const r of callLog) {
|
|
13
|
+
if (!providers.includes(r.provider)) providers.push(r.provider);
|
|
14
|
+
if (!models.includes(r.model)) models.push(r.model);
|
|
15
|
+
if (!r.ok) failed++;
|
|
16
|
+
}
|
|
17
|
+
return { providers, models, calls: callLog.length, failed_calls: failed, degraded: failed > 0 };
|
|
18
|
+
}
|
|
7
19
|
var LlmClientError = class extends Error {
|
|
8
20
|
constructor(message, status, cause) {
|
|
9
21
|
super(message);
|
|
@@ -36,9 +48,10 @@ function resolveProviderChain(config) {
|
|
|
36
48
|
return chain;
|
|
37
49
|
}
|
|
38
50
|
var MIN_REQUEST_INTERVAL_MS = 2500;
|
|
51
|
+
var requestIntervalMs = Number(process.env.LLM_MIN_REQUEST_INTERVAL_MS || MIN_REQUEST_INTERVAL_MS);
|
|
39
52
|
var lastRequestAt = 0;
|
|
40
53
|
async function pacedDelay() {
|
|
41
|
-
const wait = lastRequestAt +
|
|
54
|
+
const wait = lastRequestAt + requestIntervalMs - Date.now();
|
|
42
55
|
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
43
56
|
lastRequestAt = Date.now();
|
|
44
57
|
}
|
|
@@ -53,6 +66,10 @@ function createLlmClient(config) {
|
|
|
53
66
|
primary.apiKey;
|
|
54
67
|
primary.baseUrl;
|
|
55
68
|
const model = primary.model;
|
|
69
|
+
if (config?.minRequestIntervalMs !== void 0) {
|
|
70
|
+
requestIntervalMs = Math.max(0, config.minRequestIntervalMs);
|
|
71
|
+
}
|
|
72
|
+
const callLog = [];
|
|
56
73
|
async function chat(messages, options) {
|
|
57
74
|
const temperature = options?.temperature ?? config?.temperature ?? 0.1;
|
|
58
75
|
const maxTokens = options?.maxTokens ?? config?.maxTokens ?? parseInt(process.env.LLM_MAX_TOKENS || "65536", 10);
|
|
@@ -67,6 +84,7 @@ function createLlmClient(config) {
|
|
|
67
84
|
await new Promise((resolve) => setTimeout(resolve, 2e4));
|
|
68
85
|
}
|
|
69
86
|
for (const provider of chain) {
|
|
87
|
+
const recIdx = callLog.push({ provider: provider.label, model: provider.model, ok: false }) - 1;
|
|
70
88
|
try {
|
|
71
89
|
const headers = {
|
|
72
90
|
"Content-Type": "application/json",
|
|
@@ -127,6 +145,7 @@ function createLlmClient(config) {
|
|
|
127
145
|
const retryData = await retry.json();
|
|
128
146
|
const retryContent2 = retryData.choices?.[0]?.message?.content || "";
|
|
129
147
|
if (!retryContent2.trim()) throw err;
|
|
148
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: true };
|
|
130
149
|
return {
|
|
131
150
|
content: retryContent2,
|
|
132
151
|
model: retryData.model || provider.model,
|
|
@@ -155,6 +174,7 @@ function createLlmClient(config) {
|
|
|
155
174
|
if (!retry.ok) throw err;
|
|
156
175
|
const retryData = await retry.json();
|
|
157
176
|
const retryContent = retryData.choices?.[0]?.message?.content || "";
|
|
177
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: true };
|
|
158
178
|
return {
|
|
159
179
|
content: retryContent,
|
|
160
180
|
model: retryData.model || provider.model,
|
|
@@ -185,6 +205,7 @@ function createLlmClient(config) {
|
|
|
185
205
|
};
|
|
186
206
|
} catch (err) {
|
|
187
207
|
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: false, error: msg.slice(0, 200) };
|
|
188
209
|
attempts.push(`${provider.label}: ${msg}`);
|
|
189
210
|
lastError = err instanceof LlmClientError ? err : new LlmClientError(`[${provider.label}] ${msg}`);
|
|
190
211
|
}
|
|
@@ -194,7 +215,7 @@ function createLlmClient(config) {
|
|
|
194
215
|
}
|
|
195
216
|
throw lastError ?? new LlmClientError("All LLM providers failed after " + MAX_CHAIN_ROUNDS + " rounds.");
|
|
196
217
|
}
|
|
197
|
-
return { chat, model };
|
|
218
|
+
return { chat, model, callLog };
|
|
198
219
|
}
|
|
199
220
|
function parseJsonFromLlm(text) {
|
|
200
221
|
let cleaned = text.trim();
|
|
@@ -258,8 +279,8 @@ async function llmChatJson(client, systemPrompt, userPrompt, options) {
|
|
|
258
279
|
|
|
259
280
|
// src/graph/scaffoldExtractor.ts
|
|
260
281
|
async function extractScaffold(options) {
|
|
261
|
-
const { goal, techStack, fileList, llmConfig } = options;
|
|
262
|
-
const client = createLlmClient(llmConfig);
|
|
282
|
+
const { goal, techStack, fileList, llmConfig, sharedClient } = options;
|
|
283
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
263
284
|
const fileListStr = fileList.length > 0 ? fileList.join("\n") : "(empty)";
|
|
264
285
|
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.";
|
|
265
286
|
const userPrompt = `App goal: ${goal}
|
|
@@ -344,8 +365,8 @@ ${content}`);
|
|
|
344
365
|
return blocks.join("\n\n");
|
|
345
366
|
}
|
|
346
367
|
async function extractProjectOverview(options) {
|
|
347
|
-
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig } = options;
|
|
348
|
-
const client = createLlmClient(llmConfig);
|
|
368
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig, sharedClient } = options;
|
|
369
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
349
370
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
350
371
|
const allFilesStr = buildSourceText(fileContentsMap);
|
|
351
372
|
let keywordsAnchor = "";
|
|
@@ -414,8 +435,8 @@ ${content}`);
|
|
|
414
435
|
return blocks.join("\n\n");
|
|
415
436
|
}
|
|
416
437
|
async function extractFeatureSteps(options) {
|
|
417
|
-
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig } = options;
|
|
418
|
-
const client = createLlmClient(llmConfig);
|
|
438
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig, sharedClient } = options;
|
|
439
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
419
440
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
420
441
|
const allFilesStr = buildSourceText2(fileContentsMap);
|
|
421
442
|
const fid = feature.id || "F_UNK";
|
|
@@ -482,8 +503,8 @@ Split this feature into 2-6 action steps. Return JSON:
|
|
|
482
503
|
concept_codes: []
|
|
483
504
|
}));
|
|
484
505
|
}
|
|
485
|
-
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig) {
|
|
486
|
-
const client = createLlmClient(llmConfig);
|
|
506
|
+
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig, sharedClient) {
|
|
507
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
487
508
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
488
509
|
const sourceText = buildSourceText2(fileContentsMap);
|
|
489
510
|
const featuresListing = featuresMeta.map(
|
|
@@ -750,7 +771,7 @@ function inferDepthFromContext(keyword, fileContentsMap) {
|
|
|
750
771
|
return { depth: "sio", rationale: `Default: treating '${keyword}' as SIO (implementation-level)` };
|
|
751
772
|
}
|
|
752
773
|
async function escalateAndMapConcepts(options) {
|
|
753
|
-
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig } = options;
|
|
774
|
+
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig, sharedClient } = options;
|
|
754
775
|
const features = projectGraph.features || [];
|
|
755
776
|
const allTerms = /* @__PURE__ */ new Set();
|
|
756
777
|
for (const feature of features) {
|
|
@@ -766,7 +787,7 @@ async function escalateAndMapConcepts(options) {
|
|
|
766
787
|
llmResults = conceptMapOverride;
|
|
767
788
|
} else {
|
|
768
789
|
try {
|
|
769
|
-
const client = createLlmClient(llmConfig);
|
|
790
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
770
791
|
const conceptBank = (availableConcepts || []).sort().join(", ") || "(empty)";
|
|
771
792
|
const termEntries = termList.map((term) => {
|
|
772
793
|
const snippets = extractTermSnippets(term, fileContentsMap);
|
|
@@ -819,13 +840,16 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
|
|
|
819
840
|
const cName = info.concept_name || kw;
|
|
820
841
|
let depth = "sio";
|
|
821
842
|
let depthRationale = "Default: SIO";
|
|
843
|
+
let depthSource = "override";
|
|
822
844
|
if (info.depth && ["ulo", "cio", "sio"].includes(info.depth)) {
|
|
823
845
|
depth = info.depth;
|
|
824
846
|
depthRationale = `LLM classified as ${depth}`;
|
|
847
|
+
depthSource = "llm";
|
|
825
848
|
} else {
|
|
826
849
|
const inferred = inferDepthFromContext(kw, fileContentsMap);
|
|
827
850
|
depth = inferred.depth;
|
|
828
851
|
depthRationale = inferred.rationale;
|
|
852
|
+
depthSource = "heuristic";
|
|
829
853
|
}
|
|
830
854
|
const kwKey = `${cCode}::${kw}`;
|
|
831
855
|
if (seen.has(kwKey)) continue;
|
|
@@ -843,6 +867,7 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
|
|
|
843
867
|
keyword: kw,
|
|
844
868
|
depth,
|
|
845
869
|
depth_rationale: depthRationale,
|
|
870
|
+
depth_source: depthSource,
|
|
846
871
|
evidence_files: evidenceFiles
|
|
847
872
|
});
|
|
848
873
|
}
|
|
@@ -1545,7 +1570,7 @@ function loadKnowledgeTreeCatalog(tsvPath) {
|
|
|
1545
1570
|
}
|
|
1546
1571
|
function tokens(s) {
|
|
1547
1572
|
return new Set(
|
|
1548
|
-
s
|
|
1573
|
+
stripDiacritics(s).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4)
|
|
1549
1574
|
);
|
|
1550
1575
|
}
|
|
1551
1576
|
function filterBySelection(catalog, selectedCategories, selectedTopics, analysisConcepts, maxConcepts = 120) {
|
|
@@ -1719,6 +1744,15 @@ async function standardizeConcepts(opts) {
|
|
|
1719
1744
|
}
|
|
1720
1745
|
|
|
1721
1746
|
// src/graph/knowledgeGraphPipeline.ts
|
|
1747
|
+
var MIN_CALIBRATION_MINUTES = 15;
|
|
1748
|
+
var MAX_CALIBRATION_MINUTES = 60;
|
|
1749
|
+
var DEFAULT_CONCEPT_MINUTES = 30;
|
|
1750
|
+
var MIN_CALIBRATION_DIVERGENCE = 0.5;
|
|
1751
|
+
function calibrateConceptMinutes(sioCount, problemTypeCount, prereqCount, hasHighBloom) {
|
|
1752
|
+
const ratio = (value, cap) => Math.min(1, Math.max(0, value) / cap);
|
|
1753
|
+
const score = 0.3 * ratio(sioCount, 4) + 0.25 * ratio(problemTypeCount, 4) + 0.2 * ratio(prereqCount, 4) + 0.25 * (hasHighBloom ? 1 : 0);
|
|
1754
|
+
return Math.round(MIN_CALIBRATION_MINUTES + score * (MAX_CALIBRATION_MINUTES - MIN_CALIBRATION_MINUTES));
|
|
1755
|
+
}
|
|
1722
1756
|
function topologicalSort(concepts) {
|
|
1723
1757
|
const inDegree = /* @__PURE__ */ new Map();
|
|
1724
1758
|
const adjacency = /* @__PURE__ */ new Map();
|
|
@@ -1778,7 +1812,42 @@ async function generateKnowledgeGraph(options) {
|
|
|
1778
1812
|
const structuredTopicsGuide = parsedSyllabus && parsedSyllabus.allTopics.length > 0 ? `
|
|
1779
1813
|
STRUCTURED SYLLABUS TOPICS (Every topic MUST map to at least one concept to ensure 100% syllabus coverage):
|
|
1780
1814
|
` + parsedSyllabus.allTopics.map((t) => `- [${t.unitTitle}] ${t.id}: ${t.title} (Keywords: ${t.keywords.join(", ")})`).join("\n") + "\n" : "";
|
|
1781
|
-
const systemPrompt =
|
|
1815
|
+
const systemPrompt = `You are an expert curriculum designer. Decompose the given subject into a structured knowledge graph with concepts, prerequisites, and problem types.
|
|
1816
|
+
|
|
1817
|
+
CRITICAL RULES:
|
|
1818
|
+
1. Each concept MUST have the three depth layers, authored against this CONTRACT:
|
|
1819
|
+
- ulo (WHAT + WHY): bound to the CONCEPT's intrinsic NATURE. It explains what the
|
|
1820
|
+
concept is and why it exists \u2014 the problem it solves in principle. It must be
|
|
1821
|
+
TECHNOLOGY-AGNOSTIC: never name a concrete library, API, function, file format,
|
|
1822
|
+
tool or language feature. Typical assessment level: Remember / Understand.
|
|
1823
|
+
- cio (HOW): the MECHANISM \u2014 how the concept works and solves that problem, step
|
|
1824
|
+
by step. Still bound to the concept's nature: more concrete than the ULO, but
|
|
1825
|
+
NOT tied to one specific technology (no concrete API names). CIO capability is
|
|
1826
|
+
assessed INDIRECTLY, through completing contextual work, not in isolation.
|
|
1827
|
+
- sio (IMPLEMENTATION): an array of 2-4 items. EVERY item MUST reference concrete
|
|
1828
|
+
KEYWORDS/technologies (a real API, function, module, file, or named pattern) and
|
|
1829
|
+
describe a completable, context-specific implementation task. Application,
|
|
1830
|
+
Analysis and Creation skills are assessed INDIRECTLY through completing these
|
|
1831
|
+
items. There is NO fixed Bloom ceiling for a layer \u2014 but concreteness MUST
|
|
1832
|
+
strictly increase: SIO more technology-bound than CIO, CIO more concrete than ULO.
|
|
1833
|
+
- prerequisites: concept IDs that must be learned first (MUST NOT create cycles)
|
|
1834
|
+
- problem_types: 2-4 problem types with bloom_level and difficulty. Remember and
|
|
1835
|
+
Understand problem types test the ULO/CIO directly; Apply-and-above types are
|
|
1836
|
+
proxies for SIO completion in context.
|
|
1837
|
+
- techniques: methods, formulas, tools
|
|
1838
|
+
2. Prerequisite chains MUST be acyclic (no concept can depend on itself or create a loop)
|
|
1839
|
+
3. Concepts should be ordered by prerequisite dependency (topological sort)
|
|
1840
|
+
4. Problem types should range from Remember to Apply (not all at same level)
|
|
1841
|
+
5. Group concepts into categories (chapters/sections)
|
|
1842
|
+
6. ALL text in ENGLISH (even if input is Vietnamese)
|
|
1843
|
+
7. NEVER invent concepts not present in the syllabus
|
|
1844
|
+
8. KEYWORDS: Each concept MUST include 5-10 domain-specific keywords that:
|
|
1845
|
+
- Are concrete terms a student encounters when studying THIS concept
|
|
1846
|
+
- Include at least 2 technology-specific terms (APIs, functions, types, tools)
|
|
1847
|
+
- Are DISTINCT from keywords of other concepts (minimal overlap)
|
|
1848
|
+
- NEVER generic English words (e.g. "view", "data", "code" are TOO generic)
|
|
1849
|
+
- Example for "SwiftUI Layout": ["VStack", "HStack", "ZStack", ".padding()", ".frame()", "alignment", "Spacer", "LazyVGrid"]
|
|
1850
|
+
9. Return JSON matching the KnowledgeGraphSchema`;
|
|
1782
1851
|
const userPrompt = `Subject: ${subject}
|
|
1783
1852
|
${description ? `Description: ${description}` : ""}
|
|
1784
1853
|
${structuredTopicsGuide}${truncatedSyllabus ? `Syllabus:
|
|
@@ -1799,8 +1868,9 @@ Return JSON:
|
|
|
1799
1868
|
{
|
|
1800
1869
|
"id": "C1",
|
|
1801
1870
|
"name": "Concept name",
|
|
1802
|
-
"description": "
|
|
1871
|
+
"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.",
|
|
1803
1872
|
"category": "category_id",
|
|
1873
|
+
"keywords": ["specific_term_1", "concrete_api_2", "method_call_3", "tool_or_type_4", "syntax_keyword_5", "concept_term_6"],
|
|
1804
1874
|
"ulo": "WHAT + WHY in technology-agnostic language: what the concept is and the problem it exists to solve. No library/API/tool names.",
|
|
1805
1875
|
"cio": "HOW the mechanism works, still technology-agnostic but more concrete than the ULO.",
|
|
1806
1876
|
"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"],
|
|
@@ -1841,18 +1911,23 @@ Return JSON:
|
|
|
1841
1911
|
log("STEP_2", "Validating and cleaning knowledge graph...");
|
|
1842
1912
|
const concepts = result.concepts || [];
|
|
1843
1913
|
const categories = result.categories || [];
|
|
1914
|
+
for (const c of concepts) {
|
|
1915
|
+
if ((c.description || "").length < 50) {
|
|
1916
|
+
warnings.push("Concept " + c.id + ": description too short (" + (c.description || "").length + " chars) \u2014 may produce shallow KX narratives");
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1844
1919
|
{
|
|
1845
1920
|
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"]);
|
|
1846
|
-
const tokensFrom = (text) => text
|
|
1921
|
+
const tokensFrom = (text) => stripDiacritics(text).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP.has(w));
|
|
1847
1922
|
for (const c of concepts) {
|
|
1848
1923
|
const existing = (c.keywords || []).map((k) => String(k).trim()).filter(Boolean);
|
|
1849
1924
|
if (existing.length === 0) {
|
|
1850
|
-
const derived = [.../* @__PURE__ */ new Set([...tokensFrom(c.name), ...tokensFrom(c.description || "")])].slice(0,
|
|
1925
|
+
const derived = [.../* @__PURE__ */ new Set([...tokensFrom(c.name), ...tokensFrom(c.description || "")])].slice(0, 8);
|
|
1851
1926
|
if (derived.length === 0) {
|
|
1852
1927
|
throw new Error("Concept " + c.id + " (" + c.name + ") has no keywords and none could be derived from name/description \u2014 fail-closed");
|
|
1853
1928
|
}
|
|
1854
1929
|
c.keywords = derived;
|
|
1855
|
-
warnings.push("Concept " + c.id + ": empty keyword set - derived " + derived.join(", ") + " from name/description");
|
|
1930
|
+
warnings.push("Concept " + c.id + ": empty keyword set - derived " + derived.join(", ") + " from name/description (WARNING: LLM returned no keywords)");
|
|
1856
1931
|
} else {
|
|
1857
1932
|
c.keywords = existing;
|
|
1858
1933
|
}
|
|
@@ -1977,6 +2052,49 @@ Return JSON:
|
|
|
1977
2052
|
}
|
|
1978
2053
|
log("STEP_5", "Depth layers authored: " + concepts.length + " concepts, audit " + lo.audits.filter((a) => a.passed).length + "/" + concepts.length + " passed");
|
|
1979
2054
|
}
|
|
2055
|
+
{
|
|
2056
|
+
for (const concept of concepts) {
|
|
2057
|
+
const sioText = (concept.sio || []).join(" ");
|
|
2058
|
+
const techTokens = [];
|
|
2059
|
+
let m;
|
|
2060
|
+
const techTokenRe = /`([^`]+)`/g;
|
|
2061
|
+
while ((m = techTokenRe.exec(sioText)) !== null) {
|
|
2062
|
+
if (m[1].length >= 3 && m[1].length <= 40) techTokens.push(m[1]);
|
|
2063
|
+
}
|
|
2064
|
+
const annotationRe = /(?:^|[\s(])(@[A-Za-z]\w*)/g;
|
|
2065
|
+
while ((m = annotationRe.exec(sioText)) !== null) techTokens.push(m[1]);
|
|
2066
|
+
const dotRe = /\b\w+\.\w+\(/g;
|
|
2067
|
+
while ((m = dotRe.exec(sioText)) !== null) techTokens.push(m[0].replace("(", ""));
|
|
2068
|
+
const modifierRe = /(?:^|[\s(])(\.[a-z]\w*)\b/g;
|
|
2069
|
+
while ((m = modifierRe.exec(sioText)) !== null) techTokens.push(m[1]);
|
|
2070
|
+
const camelRe = /\b[a-z]+[A-Z][A-Za-z]+\b/g;
|
|
2071
|
+
while ((m = camelRe.exec(sioText)) !== null) techTokens.push(m[0]);
|
|
2072
|
+
const existing = new Set((concept.keywords || []).map((k) => k.toLowerCase()));
|
|
2073
|
+
const newTokens = [...new Set(techTokens)].filter((t) => !existing.has(t.toLowerCase())).slice(0, 8);
|
|
2074
|
+
if (newTokens.length > 0) {
|
|
2075
|
+
concept.keywords = [...concept.keywords || [], ...newTokens];
|
|
2076
|
+
warnings.push("Concept " + concept.id + ": enriched keywords with " + newTokens.length + " SIO-derived tokens");
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
log("STEP_5b", "SIO-keyword enrichment complete");
|
|
2080
|
+
}
|
|
2081
|
+
{
|
|
2082
|
+
for (const concept of concepts) {
|
|
2083
|
+
const sioCount = (concept.sio || []).length;
|
|
2084
|
+
const ptCount = (concept.problem_types || []).length;
|
|
2085
|
+
const prereqCount = (concept.prerequisites || []).length;
|
|
2086
|
+
const hasHighBloom = (concept.problem_types || []).some(
|
|
2087
|
+
(pt) => ["Analyze", "Evaluate", "Create"].includes(pt.bloom_level || "")
|
|
2088
|
+
);
|
|
2089
|
+
const calibrated = calibrateConceptMinutes(sioCount, ptCount, prereqCount, hasHighBloom);
|
|
2090
|
+
const llmEstimate = concept.estimated_minutes || DEFAULT_CONCEPT_MINUTES;
|
|
2091
|
+
if (Math.abs(calibrated - llmEstimate) / llmEstimate > MIN_CALIBRATION_DIVERGENCE) {
|
|
2092
|
+
concept.estimated_minutes = calibrated;
|
|
2093
|
+
warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m");
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
log("STEP_5c", "Minute calibration complete");
|
|
2097
|
+
}
|
|
1980
2098
|
for (const step of learningPath) {
|
|
1981
2099
|
if (!conceptIds.has(step.concept_id)) {
|
|
1982
2100
|
warnings.push(`Learning path references non-existent concept ${step.concept_id}`);
|
|
@@ -1997,7 +2115,8 @@ Return JSON:
|
|
|
1997
2115
|
return {
|
|
1998
2116
|
knowledgeGraph: verifiedGraph,
|
|
1999
2117
|
warnings,
|
|
2000
|
-
verificationReport: report
|
|
2118
|
+
verificationReport: report,
|
|
2119
|
+
provenance: client.callLog
|
|
2001
2120
|
};
|
|
2002
2121
|
}
|
|
2003
2122
|
|
|
@@ -2024,9 +2143,10 @@ async function generateHybridGraph(options) {
|
|
|
2024
2143
|
description: "",
|
|
2025
2144
|
total_project_minutes: 0,
|
|
2026
2145
|
total_concept_minutes: 0,
|
|
2027
|
-
|
|
2146
|
+
linked_prereq_minutes: 0
|
|
2028
2147
|
},
|
|
2029
|
-
warnings
|
|
2148
|
+
warnings,
|
|
2149
|
+
provenance: []
|
|
2030
2150
|
};
|
|
2031
2151
|
}
|
|
2032
2152
|
const client = createLlmClient(llmConfig);
|
|
@@ -2107,9 +2227,14 @@ async function generateHybridGraph(options) {
|
|
|
2107
2227
|
description: "Progressive completion path: " + phases.length + " phases, " + features.length + " features, " + concepts.length + " concepts",
|
|
2108
2228
|
total_project_minutes: totalProjectMinutes,
|
|
2109
2229
|
total_concept_minutes: totalConceptMinutes,
|
|
2110
|
-
|
|
2230
|
+
// HG-1 (2026-10-05): renamed from estimated_total_minutes. It counts only
|
|
2231
|
+
// concepts that HAVE a feature link (max per concept, not summed), so it
|
|
2232
|
+
// is NOT the course total — see HybridGraphSchema.linked_prereq_minutes.
|
|
2233
|
+
// The phase planner below commits to totalProjectMinutes + totalConceptMinutes.
|
|
2234
|
+
linked_prereq_minutes: totalProjectMinutes + totalPrereqMinutes
|
|
2111
2235
|
},
|
|
2112
|
-
warnings
|
|
2236
|
+
warnings,
|
|
2237
|
+
provenance: client.callLog
|
|
2113
2238
|
};
|
|
2114
2239
|
}
|
|
2115
2240
|
function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
@@ -2185,12 +2310,20 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
|
2185
2310
|
} else {
|
|
2186
2311
|
newIntroPhase.set(cid, order);
|
|
2187
2312
|
}
|
|
2313
|
+
introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
|
|
2314
|
+
}
|
|
2315
|
+
for (const intro of introductions) {
|
|
2316
|
+
const cid = intro.concept_id;
|
|
2317
|
+
if (intro.aspect === "advanced") continue;
|
|
2318
|
+
const introOrder = newIntroPhase.get(cid);
|
|
2319
|
+
if (introOrder === void 0 || introOrder !== order) continue;
|
|
2188
2320
|
for (const pre of prereqOf.get(cid) || []) {
|
|
2189
|
-
|
|
2190
|
-
|
|
2321
|
+
const previewOrder = previewPhase.get(pre);
|
|
2322
|
+
const covered = newIntroPhase.has(pre) || previewOrder !== void 0 && previewOrder < order;
|
|
2323
|
+
if (!covered) {
|
|
2324
|
+
errors.push("Phase " + id + ": concept " + cid + " introduced before its prerequisite " + pre + " is introduced (or previewed in an earlier phase) - violates the prerequisite chain");
|
|
2191
2325
|
}
|
|
2192
2326
|
}
|
|
2193
|
-
introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
|
|
2194
2327
|
}
|
|
2195
2328
|
phases.push({
|
|
2196
2329
|
id,
|
|
@@ -2228,6 +2361,9 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
|
2228
2361
|
if (anchor !== void 0 && anchor <= pOrder) {
|
|
2229
2362
|
errors.push("Concept " + cid + " previewed in phase " + pOrder + " but fully introduced in phase " + anchor + " - preview must precede the anchor introduction");
|
|
2230
2363
|
}
|
|
2364
|
+
if (anchor === void 0) {
|
|
2365
|
+
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");
|
|
2366
|
+
}
|
|
2231
2367
|
}
|
|
2232
2368
|
return { phases, errors };
|
|
2233
2369
|
}
|
|
@@ -2242,8 +2378,12 @@ async function decomposePhases(args) {
|
|
|
2242
2378
|
const linkList = links.map(
|
|
2243
2379
|
(l) => "- " + l.feature_id + " -> " + l.concept_id + " (" + l.depth + ")"
|
|
2244
2380
|
).join("\n") || "(none)";
|
|
2245
|
-
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.\
|
|
2246
|
-
const
|
|
2381
|
+
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.';
|
|
2382
|
+
const totalFeatureMinutes = features.reduce((s, f) => s + (f.steps || []).reduce((ss, st) => ss + (st.effort?.estimated_minutes || 15), 0), 0);
|
|
2383
|
+
const totalConceptMinutes = concepts.reduce((s, c) => s + (c.estimated_minutes || 30), 0);
|
|
2384
|
+
const totalMinutes = totalFeatureMinutes + totalConceptMinutes;
|
|
2385
|
+
const targetPhaseCount = Math.max(3, Math.round(totalMinutes / 180));
|
|
2386
|
+
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";
|
|
2247
2387
|
const MAX_DECOMPOSE_ATTEMPTS = 3;
|
|
2248
2388
|
let auditErrors = [];
|
|
2249
2389
|
let phases = [];
|
|
@@ -2280,6 +2420,6 @@ async function decomposePhases(args) {
|
|
|
2280
2420
|
return phases;
|
|
2281
2421
|
}
|
|
2282
2422
|
|
|
2283
|
-
export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditPhasePlan, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName, createLlmClient, detectPrerequisiteCycles, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, generateHybridGraph, generateKnowledgeGraph, llmChatJson, sioCode, standardStatement, techTagFor, uloCode, verifyKnowledgeGraph, verifyProjectGraph };
|
|
2284
|
-
//# sourceMappingURL=chunk-
|
|
2285
|
-
//# sourceMappingURL=chunk-
|
|
2423
|
+
export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditPhasePlan, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName, createLlmClient, detectPrerequisiteCycles, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, generateHybridGraph, generateKnowledgeGraph, llmChatJson, sioCode, standardStatement, summarizeProvenance, techTagFor, uloCode, verifyKnowledgeGraph, verifyProjectGraph };
|
|
2424
|
+
//# sourceMappingURL=chunk-2OGNQUXC.mjs.map
|
|
2425
|
+
//# sourceMappingURL=chunk-2OGNQUXC.mjs.map
|