@thanh01.pmt/domain-kit 0.5.0 → 0.6.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/README.md +682 -168
- package/dist/assembly/index.d.cts +3 -2
- package/dist/assembly/index.d.ts +3 -2
- package/dist/{chunk-TYXSJUQU.mjs → chunk-4UCXYRM3.mjs} +208 -310
- package/dist/chunk-4UCXYRM3.mjs.map +1 -0
- package/dist/chunk-7A2SG466.mjs +325 -0
- package/dist/chunk-7A2SG466.mjs.map +1 -0
- package/dist/{chunk-E4GCCWJO.mjs → chunk-CGNXGVHV.mjs} +37 -4
- package/dist/chunk-CGNXGVHV.mjs.map +1 -0
- package/dist/{chunk-DRVOV5ZD.mjs → chunk-DSEJLTUO.mjs} +2 -2
- package/dist/chunk-DSEJLTUO.mjs.map +1 -0
- package/dist/{chunk-VCPIWJ7R.mjs → chunk-H4W3YJLK.mjs} +29 -4
- package/dist/chunk-H4W3YJLK.mjs.map +1 -0
- package/dist/chunk-IBVXXVGE.mjs +14 -0
- package/dist/chunk-IBVXXVGE.mjs.map +1 -0
- package/dist/{chunk-G7HLFMD2.mjs → chunk-KHCZYLKM.mjs} +68 -33
- package/dist/chunk-KHCZYLKM.mjs.map +1 -0
- package/dist/chunk-OYSZQVPY.mjs +10 -0
- package/dist/chunk-OYSZQVPY.mjs.map +1 -0
- package/dist/{chunk-BL5NBKPH.mjs → chunk-WD3P5V3B.mjs} +19 -8
- package/dist/chunk-WD3P5V3B.mjs.map +1 -0
- package/dist/{chunk-TWKIVUTQ.mjs → chunk-WKTH3JPA.mjs} +197 -36
- package/dist/chunk-WKTH3JPA.mjs.map +1 -0
- package/dist/{conceptEscalator-ltRLtPhf.d.cts → conceptEscalator-BmMJD8C5.d.cts} +10 -1
- package/dist/{conceptEscalator-YfMKIKCZ.d.ts → conceptEscalator-DMsehscs.d.ts} +10 -1
- package/dist/concepts/index.d.cts +4 -3
- package/dist/concepts/index.d.ts +4 -3
- package/dist/{cpp-CZUPqJt9.d.cts → cpp-vlDl6pCL.d.cts} +4 -4
- package/dist/{cpp-CZUPqJt9.d.ts → cpp-vlDl6pCL.d.ts} +4 -4
- package/dist/{curriculumFeedSchema-DUFwFyr2.d.ts → curriculumFeedSchema-DK-TUAJZ.d.cts} +164 -9
- package/dist/{curriculumFeedSchema-DUFwFyr2.d.cts → curriculumFeedSchema-DK-TUAJZ.d.ts} +164 -9
- package/dist/detector/index.cjs +22 -6
- package/dist/detector/index.cjs.map +1 -1
- package/dist/detector/index.d.cts +2 -2
- package/dist/detector/index.d.ts +2 -2
- package/dist/detector/index.mjs +2 -1
- package/dist/{domainProfileDetector-DHb1T0zB.d.ts → domainProfileDetector-6pjgL-6_.d.ts} +1 -1
- package/dist/{domainProfileDetector-bzugdQmS.d.cts → domainProfileDetector-BsADsuRV.d.cts} +1 -1
- package/dist/{domainProfileSchema-CwT3Ffsw.d.ts → domainProfileSchema-779FGk14.d.cts} +38 -1
- package/dist/{domainProfileSchema-CwT3Ffsw.d.cts → domainProfileSchema-779FGk14.d.ts} +38 -1
- package/dist/extractors/index.d.cts +3 -3
- package/dist/extractors/index.d.ts +3 -3
- package/dist/feed/index.cjs +229 -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 +308 -86
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +20 -8
- package/dist/graph/index.d.ts +20 -8
- package/dist/graph/index.mjs +4 -1
- package/dist/{hybridGraphPipeline-dT-bio-3.d.cts → hybridGraphPipeline-B4mamLim.d.cts} +6 -2
- package/dist/{hybridGraphPipeline-DXQDbBEj.d.ts → hybridGraphPipeline-D2_1GUva.d.ts} +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 +3948 -3464
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.mjs +14 -11
- package/dist/{keywordExtractor-BxlGRMHC.d.cts → keywordExtractor-1YAFntsn.d.cts} +1 -1
- package/dist/{keywordExtractor-DU5XRN8-.d.ts → keywordExtractor-DWw8z_AF.d.ts} +1 -1
- package/dist/parsers/index.cjs.map +1 -1
- package/dist/parsers/index.d.cts +1 -1
- package/dist/parsers/index.d.ts +1 -1
- package/dist/parsers/index.mjs +1 -1
- package/dist/pipeline/index.cjs +2436 -1994
- package/dist/pipeline/index.cjs.map +1 -1
- package/dist/pipeline/index.d.cts +19 -10
- package/dist/pipeline/index.d.ts +19 -10
- package/dist/pipeline/index.mjs +11 -8
- package/dist/schemas/index.cjs +63 -2
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +3 -3
- package/dist/schemas/index.d.ts +3 -3
- package/dist/schemas/index.mjs +2 -2
- package/dist/utils/llmClient.cjs +344 -0
- package/dist/utils/llmClient.cjs.map +1 -0
- package/dist/utils/llmClient.d.cts +118 -0
- package/dist/utils/llmClient.d.ts +118 -0
- package/dist/utils/llmClient.mjs +4 -0
- package/dist/utils/llmClient.mjs.map +1 -0
- package/dist/utils/llmTransportResult.cjs +16 -0
- package/dist/utils/llmTransportResult.cjs.map +1 -0
- package/dist/utils/llmTransportResult.d.cts +36 -0
- package/dist/utils/llmTransportResult.d.ts +36 -0
- package/dist/utils/llmTransportResult.mjs +3 -0
- package/dist/utils/llmTransportResult.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-BL5NBKPH.mjs.map +0 -1
- package/dist/chunk-DRVOV5ZD.mjs.map +0 -1
- package/dist/chunk-E4GCCWJO.mjs.map +0 -1
- package/dist/chunk-G7HLFMD2.mjs.map +0 -1
- package/dist/chunk-TWKIVUTQ.mjs.map +0 -1
- package/dist/chunk-TYXSJUQU.mjs.map +0 -1
- package/dist/chunk-VCPIWJ7R.mjs.map +0 -1
- package/dist/curriculumFeedEmitter-T7NUBLAM.mjs +0 -4
- package/dist/curriculumFeedEmitter-T7NUBLAM.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
|
@@ -4,7 +4,56 @@ var fs = require('fs');
|
|
|
4
4
|
var os = require('os');
|
|
5
5
|
var path = require('path');
|
|
6
6
|
|
|
7
|
+
// src/utils/llmTransportResult.ts
|
|
8
|
+
function transportResultFrom(provider, model, options) {
|
|
9
|
+
return {
|
|
10
|
+
provider,
|
|
11
|
+
model,
|
|
12
|
+
framingStripped: !!options?.framingStripped,
|
|
13
|
+
upstreamError: options?.upstreamError,
|
|
14
|
+
usage: options?.usage
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
// src/utils/llmClient.ts
|
|
19
|
+
async function responseJsonCompat(response) {
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
} catch {
|
|
24
|
+
const start = text.indexOf("{");
|
|
25
|
+
if (start < 0) {
|
|
26
|
+
throw new LlmClientError(`LLM API returned a non-JSON body: ${text.slice(0, 200)}`);
|
|
27
|
+
}
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let inString = false;
|
|
30
|
+
let escape = false;
|
|
31
|
+
for (let i = start; i < text.length; i++) {
|
|
32
|
+
const ch = text[i];
|
|
33
|
+
if (escape) {
|
|
34
|
+
escape = false;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (ch === "\\") {
|
|
38
|
+
escape = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === '"') {
|
|
42
|
+
inString = !inString;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (inString) continue;
|
|
46
|
+
if (ch === "{") depth++;
|
|
47
|
+
if (ch === "}") {
|
|
48
|
+
depth--;
|
|
49
|
+
if (depth === 0) {
|
|
50
|
+
return JSON.parse(text.slice(start, i + 1));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new LlmClientError(`LLM API returned an unterminated JSON body: ${text.slice(0, 200)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
8
57
|
var LlmClientError = class extends Error {
|
|
9
58
|
constructor(message, status, cause) {
|
|
10
59
|
super(message);
|
|
@@ -13,47 +62,46 @@ var LlmClientError = class extends Error {
|
|
|
13
62
|
this.name = "LlmClientError";
|
|
14
63
|
}
|
|
15
64
|
};
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
if (or) {
|
|
33
|
-
const rest = chain.filter((c) => c !== or);
|
|
34
|
-
return [or, ...rest];
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return chain;
|
|
65
|
+
function build9routerProvider(model) {
|
|
66
|
+
const apiKey = process.env.NINEROUTER_API_KEY;
|
|
67
|
+
if (!apiKey) return null;
|
|
68
|
+
const baseUrl = (process.env.NINEROUTER_BASE_URL || "https://ai-router.orchable.app/v1").replace(/^["']|["']$/g, "").trim();
|
|
69
|
+
const label = "9router";
|
|
70
|
+
const modelName = (model || process.env.NINEROUTER_MODEL || "laguna-s-2.1").trim();
|
|
71
|
+
return { label, baseUrl, apiKey, model: modelName };
|
|
72
|
+
}
|
|
73
|
+
function resolve9routerChain(configModel) {
|
|
74
|
+
const primary = build9routerProvider(configModel);
|
|
75
|
+
if (!primary) return [];
|
|
76
|
+
const alt = process.env.NINEROUTER_MODEL_ALT && process.env.NINEROUTER_MODEL_ALT.trim();
|
|
77
|
+
if (!alt) return [primary];
|
|
78
|
+
const secondary = build9routerProvider(alt);
|
|
79
|
+
if (!secondary) return [primary];
|
|
80
|
+
return primary.model === secondary.model ? [primary] : [primary, secondary];
|
|
38
81
|
}
|
|
39
82
|
var MIN_REQUEST_INTERVAL_MS = 2500;
|
|
83
|
+
var requestIntervalMs = Number(process.env.LLM_MIN_REQUEST_INTERVAL_MS || MIN_REQUEST_INTERVAL_MS);
|
|
40
84
|
var lastRequestAt = 0;
|
|
41
85
|
async function pacedDelay() {
|
|
42
|
-
const wait = lastRequestAt +
|
|
86
|
+
const wait = lastRequestAt + requestIntervalMs - Date.now();
|
|
43
87
|
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
44
88
|
lastRequestAt = Date.now();
|
|
45
89
|
}
|
|
46
90
|
function createLlmClient(config) {
|
|
47
|
-
const chain =
|
|
91
|
+
const chain = resolve9routerChain(config?.model);
|
|
48
92
|
const primary = chain[0];
|
|
49
93
|
if (!primary) {
|
|
50
94
|
throw new LlmClientError(
|
|
51
|
-
"No
|
|
95
|
+
"No 9router provider configured. Set NINEROUTER_API_KEY and NINEROUTER_MODEL (or the NINEROUTER_* env defaults expected by the kit)."
|
|
52
96
|
);
|
|
53
97
|
}
|
|
54
98
|
primary.apiKey;
|
|
55
99
|
primary.baseUrl;
|
|
56
100
|
const model = primary.model;
|
|
101
|
+
if (config?.minRequestIntervalMs !== void 0) {
|
|
102
|
+
requestIntervalMs = Math.max(0, config.minRequestIntervalMs);
|
|
103
|
+
}
|
|
104
|
+
const callLog = [];
|
|
57
105
|
async function chat(messages, options) {
|
|
58
106
|
const temperature = options?.temperature ?? config?.temperature ?? 0.1;
|
|
59
107
|
const maxTokens = options?.maxTokens ?? config?.maxTokens ?? parseInt(process.env.LLM_MAX_TOKENS || "65536", 10);
|
|
@@ -68,6 +116,7 @@ function createLlmClient(config) {
|
|
|
68
116
|
await new Promise((resolve) => setTimeout(resolve, 2e4));
|
|
69
117
|
}
|
|
70
118
|
for (const provider of chain) {
|
|
119
|
+
const recIdx = callLog.push({ provider: provider.label, model: provider.model, ok: false }) - 1;
|
|
71
120
|
try {
|
|
72
121
|
const headers = {
|
|
73
122
|
"Content-Type": "application/json",
|
|
@@ -125,16 +174,22 @@ function createLlmClient(config) {
|
|
|
125
174
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
126
175
|
});
|
|
127
176
|
if (!retry.ok) throw err;
|
|
128
|
-
const retryData = await retry
|
|
177
|
+
const retryData = await responseJsonCompat(retry);
|
|
129
178
|
const retryContent2 = retryData.choices?.[0]?.message?.content || "";
|
|
130
179
|
if (!retryContent2.trim()) throw err;
|
|
180
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: true };
|
|
181
|
+
const transport2 = transportResultFrom(provider.label, retryData.model || provider.model, {
|
|
182
|
+
usage: retryData.usage,
|
|
183
|
+
framingStripped: true
|
|
184
|
+
});
|
|
131
185
|
return {
|
|
132
186
|
content: retryContent2,
|
|
133
187
|
model: retryData.model || provider.model,
|
|
134
188
|
provider: provider.label,
|
|
135
189
|
attempts: attempts.slice(),
|
|
136
190
|
usage: retryData.usage,
|
|
137
|
-
finishReason: retryData.choices?.[0]?.finish_reason
|
|
191
|
+
finishReason: retryData.choices?.[0]?.finish_reason,
|
|
192
|
+
_transport: transport2
|
|
138
193
|
};
|
|
139
194
|
}
|
|
140
195
|
if (TRANSIENT.has(response.status)) {
|
|
@@ -154,20 +209,26 @@ function createLlmClient(config) {
|
|
|
154
209
|
})
|
|
155
210
|
});
|
|
156
211
|
if (!retry.ok) throw err;
|
|
157
|
-
const retryData = await retry
|
|
212
|
+
const retryData = await responseJsonCompat(retry);
|
|
158
213
|
const retryContent = retryData.choices?.[0]?.message?.content || "";
|
|
214
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: true };
|
|
215
|
+
const transport2 = transportResultFrom(provider.label, retryData.model || provider.model, {
|
|
216
|
+
usage: retryData.usage,
|
|
217
|
+
framingStripped: true
|
|
218
|
+
});
|
|
159
219
|
return {
|
|
160
220
|
content: retryContent,
|
|
161
221
|
model: retryData.model || provider.model,
|
|
162
222
|
provider: provider.label,
|
|
163
223
|
attempts: attempts.slice(),
|
|
164
224
|
usage: retryData.usage,
|
|
165
|
-
finishReason: retryData.choices?.[0]?.finish_reason
|
|
225
|
+
finishReason: retryData.choices?.[0]?.finish_reason,
|
|
226
|
+
_transport: transport2
|
|
166
227
|
};
|
|
167
228
|
}
|
|
168
229
|
throw err;
|
|
169
230
|
}
|
|
170
|
-
const data = await response
|
|
231
|
+
const data = await responseJsonCompat(response);
|
|
171
232
|
const wrappedError = data.error;
|
|
172
233
|
if (wrappedError) {
|
|
173
234
|
throw new LlmClientError("[" + provider.label + "] upstream error " + (wrappedError.code ?? "") + ": " + (wrappedError.message ?? "unknown"));
|
|
@@ -176,16 +237,22 @@ function createLlmClient(config) {
|
|
|
176
237
|
if (!content.trim()) {
|
|
177
238
|
throw new LlmClientError("[" + provider.label + "] empty completion returned");
|
|
178
239
|
}
|
|
240
|
+
const transport = transportResultFrom(provider.label, data.model || provider.model, {
|
|
241
|
+
usage: data.usage,
|
|
242
|
+
framingStripped: true
|
|
243
|
+
});
|
|
179
244
|
return {
|
|
180
245
|
content,
|
|
181
246
|
model: data.model || provider.model,
|
|
182
247
|
provider: provider.label,
|
|
183
248
|
attempts: attempts.slice(),
|
|
184
249
|
usage: data.usage,
|
|
185
|
-
finishReason: data.choices?.[0]?.finish_reason
|
|
250
|
+
finishReason: data.choices?.[0]?.finish_reason,
|
|
251
|
+
_transport: transport
|
|
186
252
|
};
|
|
187
253
|
} catch (err) {
|
|
188
254
|
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
callLog[recIdx] = { ...callLog[recIdx], ok: false, error: msg.slice(0, 200) };
|
|
189
256
|
attempts.push(`${provider.label}: ${msg}`);
|
|
190
257
|
lastError = err instanceof LlmClientError ? err : new LlmClientError(`[${provider.label}] ${msg}`);
|
|
191
258
|
}
|
|
@@ -195,7 +262,7 @@ function createLlmClient(config) {
|
|
|
195
262
|
}
|
|
196
263
|
throw lastError ?? new LlmClientError("All LLM providers failed after " + MAX_CHAIN_ROUNDS + " rounds.");
|
|
197
264
|
}
|
|
198
|
-
return { chat, model };
|
|
265
|
+
return { chat, model, callLog };
|
|
199
266
|
}
|
|
200
267
|
function parseJsonFromLlm(text) {
|
|
201
268
|
let cleaned = text.trim();
|
|
@@ -259,8 +326,8 @@ async function llmChatJson(client, systemPrompt, userPrompt, options) {
|
|
|
259
326
|
|
|
260
327
|
// src/graph/scaffoldExtractor.ts
|
|
261
328
|
async function extractScaffold(options) {
|
|
262
|
-
const { goal, techStack, fileList, llmConfig } = options;
|
|
263
|
-
const client = createLlmClient(llmConfig);
|
|
329
|
+
const { goal, techStack, fileList, llmConfig, sharedClient } = options;
|
|
330
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
264
331
|
const fileListStr = fileList.length > 0 ? fileList.join("\n") : "(empty)";
|
|
265
332
|
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
333
|
const userPrompt = `App goal: ${goal}
|
|
@@ -345,8 +412,8 @@ ${content}`);
|
|
|
345
412
|
return blocks.join("\n\n");
|
|
346
413
|
}
|
|
347
414
|
async function extractProjectOverview(options) {
|
|
348
|
-
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig } = options;
|
|
349
|
-
const client = createLlmClient(llmConfig);
|
|
415
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig, sharedClient } = options;
|
|
416
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
350
417
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
351
418
|
const allFilesStr = buildSourceText(fileContentsMap);
|
|
352
419
|
let keywordsAnchor = "";
|
|
@@ -355,7 +422,7 @@ async function extractProjectOverview(options) {
|
|
|
355
422
|
(kw) => `- ${kw.keyword} (from ${kw.source})`
|
|
356
423
|
);
|
|
357
424
|
if (kwItems.length > 0) {
|
|
358
|
-
keywordsAnchor = "\
|
|
425
|
+
keywordsAnchor = "\nDomain Keywords & Patterns (from regex-based static scan \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
|
|
359
426
|
}
|
|
360
427
|
}
|
|
361
428
|
const systemPrompt = "You are a software architect. The app the user will build IS EXACTLY the provided files (not an SDK/lib). Analyze them and return a PROJECT OVERVIEW \u2014 meta only, DO NOT list steps (steps are handled in a later step). NEVER invent files/APIs. NEVER reference internal SDK/lib unless it appears in the given files.\nEND-USER VIEW (M1/Design Thinking): product.purpose must describe the USER PROBLEM (what the user does with the app, which need it solves). NEVER describe repo/artifacts.\nDEVELOPMENT STAGES: Include 4-6 development stages in product.development_stages. Each stage must have: stage (name), need (list of feature needs), learn (concepts learned), product_state (what works), temporary_approach, and validation.\nMISSING GAPS: Identify 0-5 missing_gaps (features/functionality that should exist but don't).\nTECH DEBT: Identify 0-5 tech_debt issues (code quality problems).\nALL text must be in ENGLISH.";
|
|
@@ -415,8 +482,8 @@ ${content}`);
|
|
|
415
482
|
return blocks.join("\n\n");
|
|
416
483
|
}
|
|
417
484
|
async function extractFeatureSteps(options) {
|
|
418
|
-
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig } = options;
|
|
419
|
-
const client = createLlmClient(llmConfig);
|
|
485
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig, sharedClient } = options;
|
|
486
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
420
487
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
421
488
|
const allFilesStr = buildSourceText2(fileContentsMap);
|
|
422
489
|
const fid = feature.id || "F_UNK";
|
|
@@ -483,8 +550,8 @@ Split this feature into 2-6 action steps. Return JSON:
|
|
|
483
550
|
concept_codes: []
|
|
484
551
|
}));
|
|
485
552
|
}
|
|
486
|
-
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig) {
|
|
487
|
-
const client = createLlmClient(llmConfig);
|
|
553
|
+
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig, sharedClient) {
|
|
554
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
488
555
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
489
556
|
const sourceText = buildSourceText2(fileContentsMap);
|
|
490
557
|
const featuresListing = featuresMeta.map(
|
|
@@ -751,7 +818,7 @@ function inferDepthFromContext(keyword, fileContentsMap) {
|
|
|
751
818
|
return { depth: "sio", rationale: `Default: treating '${keyword}' as SIO (implementation-level)` };
|
|
752
819
|
}
|
|
753
820
|
async function escalateAndMapConcepts(options) {
|
|
754
|
-
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig } = options;
|
|
821
|
+
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig, sharedClient } = options;
|
|
755
822
|
const features = projectGraph.features || [];
|
|
756
823
|
const allTerms = /* @__PURE__ */ new Set();
|
|
757
824
|
for (const feature of features) {
|
|
@@ -767,7 +834,7 @@ async function escalateAndMapConcepts(options) {
|
|
|
767
834
|
llmResults = conceptMapOverride;
|
|
768
835
|
} else {
|
|
769
836
|
try {
|
|
770
|
-
const client = createLlmClient(llmConfig);
|
|
837
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
771
838
|
const conceptBank = (availableConcepts || []).sort().join(", ") || "(empty)";
|
|
772
839
|
const termEntries = termList.map((term) => {
|
|
773
840
|
const snippets = extractTermSnippets(term, fileContentsMap);
|
|
@@ -820,13 +887,16 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
|
|
|
820
887
|
const cName = info.concept_name || kw;
|
|
821
888
|
let depth = "sio";
|
|
822
889
|
let depthRationale = "Default: SIO";
|
|
890
|
+
let depthSource = "override";
|
|
823
891
|
if (info.depth && ["ulo", "cio", "sio"].includes(info.depth)) {
|
|
824
892
|
depth = info.depth;
|
|
825
893
|
depthRationale = `LLM classified as ${depth}`;
|
|
894
|
+
depthSource = "llm";
|
|
826
895
|
} else {
|
|
827
896
|
const inferred = inferDepthFromContext(kw, fileContentsMap);
|
|
828
897
|
depth = inferred.depth;
|
|
829
898
|
depthRationale = inferred.rationale;
|
|
899
|
+
depthSource = "heuristic";
|
|
830
900
|
}
|
|
831
901
|
const kwKey = `${cCode}::${kw}`;
|
|
832
902
|
if (seen.has(kwKey)) continue;
|
|
@@ -844,6 +914,7 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
|
|
|
844
914
|
keyword: kw,
|
|
845
915
|
depth,
|
|
846
916
|
depth_rationale: depthRationale,
|
|
917
|
+
depth_source: depthSource,
|
|
847
918
|
evidence_files: evidenceFiles
|
|
848
919
|
});
|
|
849
920
|
}
|
|
@@ -968,6 +1039,13 @@ var ULO_CODE_RE = /^ULO-[A-Z][A-Z0-9_]*-\d{2}$/;
|
|
|
968
1039
|
var CIO_CODE_RE = /^CIO-[A-Z][A-Z0-9_]*-\d{2}-[A-Z][A-Z0-9_]*$/;
|
|
969
1040
|
var SIO_CODE_RE = /^SIO-[A-Z0-9]+-[A-Z][A-Z0-9_]*-\d{2}$/;
|
|
970
1041
|
|
|
1042
|
+
// src/utils/diacritics.ts
|
|
1043
|
+
var COMBINING_MARKS = /[\u0300-\u036f]/g;
|
|
1044
|
+
var D_WITH_STROKE = /đ/gi;
|
|
1045
|
+
function stripDiacritics(s) {
|
|
1046
|
+
return s.normalize("NFD").replace(COMBINING_MARKS, "").replace(D_WITH_STROKE, "d").toLowerCase();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
971
1049
|
// src/parsers/syllabusParser.ts
|
|
972
1050
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
973
1051
|
"and",
|
|
@@ -1360,7 +1438,110 @@ function techSignalHits(text) {
|
|
|
1360
1438
|
}
|
|
1361
1439
|
return hits;
|
|
1362
1440
|
}
|
|
1363
|
-
|
|
1441
|
+
var COMPLETABLE_VERBS = [
|
|
1442
|
+
"build",
|
|
1443
|
+
"create",
|
|
1444
|
+
"implement",
|
|
1445
|
+
"write",
|
|
1446
|
+
"configure",
|
|
1447
|
+
"add",
|
|
1448
|
+
"use",
|
|
1449
|
+
"apply",
|
|
1450
|
+
"display",
|
|
1451
|
+
"store",
|
|
1452
|
+
"load",
|
|
1453
|
+
"save",
|
|
1454
|
+
"handle",
|
|
1455
|
+
"parse",
|
|
1456
|
+
"validate",
|
|
1457
|
+
"test",
|
|
1458
|
+
"register",
|
|
1459
|
+
"compute",
|
|
1460
|
+
"generate",
|
|
1461
|
+
"print",
|
|
1462
|
+
"format",
|
|
1463
|
+
"convert",
|
|
1464
|
+
"extend",
|
|
1465
|
+
"define",
|
|
1466
|
+
"execute",
|
|
1467
|
+
"run",
|
|
1468
|
+
"read",
|
|
1469
|
+
"fetch",
|
|
1470
|
+
"wrap",
|
|
1471
|
+
"log",
|
|
1472
|
+
"call",
|
|
1473
|
+
"raise",
|
|
1474
|
+
"install",
|
|
1475
|
+
"import",
|
|
1476
|
+
"export",
|
|
1477
|
+
"declare",
|
|
1478
|
+
"embed",
|
|
1479
|
+
"model",
|
|
1480
|
+
"represent",
|
|
1481
|
+
"override",
|
|
1482
|
+
"customize",
|
|
1483
|
+
"enforce",
|
|
1484
|
+
"mock",
|
|
1485
|
+
"assert",
|
|
1486
|
+
"query",
|
|
1487
|
+
"index",
|
|
1488
|
+
"cache",
|
|
1489
|
+
"stream",
|
|
1490
|
+
"split",
|
|
1491
|
+
"join",
|
|
1492
|
+
"strip",
|
|
1493
|
+
"encode",
|
|
1494
|
+
"decode",
|
|
1495
|
+
"traverse",
|
|
1496
|
+
"measure",
|
|
1497
|
+
"evaluate",
|
|
1498
|
+
"benchmark",
|
|
1499
|
+
"diagnose",
|
|
1500
|
+
"demonstrate",
|
|
1501
|
+
"verify",
|
|
1502
|
+
"organize",
|
|
1503
|
+
"structure",
|
|
1504
|
+
"choose",
|
|
1505
|
+
"select",
|
|
1506
|
+
"compare",
|
|
1507
|
+
"distinguish",
|
|
1508
|
+
"attach",
|
|
1509
|
+
"annotate",
|
|
1510
|
+
"expose",
|
|
1511
|
+
"accept",
|
|
1512
|
+
"manage",
|
|
1513
|
+
"group",
|
|
1514
|
+
"sort",
|
|
1515
|
+
"filter",
|
|
1516
|
+
"map",
|
|
1517
|
+
"reduce",
|
|
1518
|
+
"catch",
|
|
1519
|
+
"throw",
|
|
1520
|
+
"migrate",
|
|
1521
|
+
"inspect",
|
|
1522
|
+
"trace",
|
|
1523
|
+
"parameterize",
|
|
1524
|
+
"construct",
|
|
1525
|
+
"design",
|
|
1526
|
+
"capture",
|
|
1527
|
+
// Completable implementation verbs present in production SIO output but
|
|
1528
|
+
// missing from the original registry (2026-10-07 smoke test, swift kg):
|
|
1529
|
+
"update",
|
|
1530
|
+
"inject",
|
|
1531
|
+
"compose",
|
|
1532
|
+
"pass",
|
|
1533
|
+
"modify",
|
|
1534
|
+
"connect",
|
|
1535
|
+
"remove",
|
|
1536
|
+
"transform",
|
|
1537
|
+
"trigger",
|
|
1538
|
+
"refresh",
|
|
1539
|
+
"persist",
|
|
1540
|
+
"observe",
|
|
1541
|
+
"arrange",
|
|
1542
|
+
"derive"
|
|
1543
|
+
];
|
|
1544
|
+
function auditDepthLayers(concepts, transport) {
|
|
1364
1545
|
return concepts.map((c) => {
|
|
1365
1546
|
const ulo = (c.ulo ?? "").trim();
|
|
1366
1547
|
const cio = (c.cio ?? "").trim();
|
|
@@ -1381,7 +1562,7 @@ function auditDepthLayers(concepts) {
|
|
|
1381
1562
|
});
|
|
1382
1563
|
const conceptKeywords = (c.keywords ?? []).map((k) => k.toLowerCase()).filter(Boolean);
|
|
1383
1564
|
const sioOffConceptItems = conceptKeywords.length > 0 ? sioItems.filter((s) => !boundToConcept(s)) : [];
|
|
1384
|
-
const COMPLETABLE =
|
|
1565
|
+
const COMPLETABLE = new RegExp("\\b(?:" + COMPLETABLE_VERBS.join("|") + ")\\b", "i");
|
|
1385
1566
|
const sioNotCompletable = sioItems.filter((s) => !COMPLETABLE.test(s) && !/`[^`]+`/.test(s));
|
|
1386
1567
|
const density = (text) => {
|
|
1387
1568
|
if (!text) return 0;
|
|
@@ -1421,7 +1602,8 @@ function auditDepthLayers(concepts) {
|
|
|
1421
1602
|
clauseOk,
|
|
1422
1603
|
codesOk,
|
|
1423
1604
|
codeIssues,
|
|
1424
|
-
passed
|
|
1605
|
+
passed,
|
|
1606
|
+
transport
|
|
1425
1607
|
};
|
|
1426
1608
|
});
|
|
1427
1609
|
}
|
|
@@ -1431,14 +1613,14 @@ function excerpt(text, max) {
|
|
|
1431
1613
|
const t = (text || "").trim();
|
|
1432
1614
|
return t.length > max ? t.slice(0, max) + "..." : t;
|
|
1433
1615
|
}
|
|
1434
|
-
async function chatJson(client, system, user) {
|
|
1616
|
+
async function chatJson(client, system, user, transport) {
|
|
1435
1617
|
const r = await client.chat(
|
|
1436
1618
|
[{ role: "system", content: system }, { role: "user", content: user }],
|
|
1437
1619
|
{ temperature: 0.1, maxTokens: parseInt(process.env.KG_MAX_TOKENS || "65536", 10) }
|
|
1438
1620
|
);
|
|
1439
1621
|
return parseJsonFromLlm(r.content);
|
|
1440
1622
|
}
|
|
1441
|
-
var LO_BATCH_SIZE =
|
|
1623
|
+
var LO_BATCH_SIZE = 10;
|
|
1442
1624
|
function chunkConcepts(items, size) {
|
|
1443
1625
|
const out = [];
|
|
1444
1626
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
@@ -1477,7 +1659,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1477
1659
|
for (const batch of chunkConcepts(concepts, LO_BATCH_SIZE)) {
|
|
1478
1660
|
const batchList = batch.map((c) => "- " + c.id + " (" + c.name + ")" + (c.description ? ": " + excerpt(c.description, 160) : "")).join("\n");
|
|
1479
1661
|
const batchUser = "Concepts:\n" + batchList + '\n\nReturn JSON:\n{"ulos":[{"concept_id":"C1","ulo":"..."}]}';
|
|
1480
|
-
const uloRes = await chatJson(client, uloSystem, batchUser);
|
|
1662
|
+
const uloRes = await chatJson(client, uloSystem, batchUser, transportResultFrom("9router", client.model, { framingStripped: true }));
|
|
1481
1663
|
for (const u of uloRes.ulos ?? []) {
|
|
1482
1664
|
if (u.concept_id && u.ulo) uloByConcept[u.concept_id] = standardStatement(u.ulo);
|
|
1483
1665
|
}
|
|
@@ -1516,7 +1698,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1516
1698
|
new Set(batch.map((c) => c.id));
|
|
1517
1699
|
const batchUloContext = batch.map((c) => "- " + c.id + " (" + c.name + "): " + (uloByConcept[c.id] || "(missing)")).join("\n");
|
|
1518
1700
|
const batchCioUser = "Approved ULOs:\n" + batchUloContext + '\n\nReturn JSON:\n{"cios":[{"concept_id":"C1","cio":"..."}]}';
|
|
1519
|
-
const cioRes = await chatJson(client, cioSystem, batchCioUser);
|
|
1701
|
+
const cioRes = await chatJson(client, cioSystem, batchCioUser, transportResultFrom("9router", client.model, { framingStripped: true }));
|
|
1520
1702
|
for (const x of cioRes.cios ?? []) {
|
|
1521
1703
|
if (x.concept_id && x.cio) cioByConcept[x.concept_id] = standardStatement(x.cio);
|
|
1522
1704
|
}
|
|
@@ -1541,15 +1723,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1541
1723
|
" context where it is used. Application/Analysis/Creation skills are assessed",
|
|
1542
1724
|
" INDIRECTLY through completing these tasks \u2014 there is no fixed Bloom ceiling.",
|
|
1543
1725
|
" MACHINE CHECK: every item MUST contain at least one ACTION VERB from this whitelist",
|
|
1544
|
-
" (
|
|
1545
|
-
" save, handle, parse, validate, test, register, compute, generate, print, format,",
|
|
1546
|
-
" convert, extend, define, execute, run, read, fetch, wrap, log, call, raise, install,",
|
|
1547
|
-
" import, export, declare, embed, model, represent, override, customize, enforce, mock,",
|
|
1548
|
-
" assert, query, index, cache, stream, split, join, strip, encode, decode, traverse,",
|
|
1549
|
-
" measure, evaluate, benchmark, diagnose, demonstrate, verify, organize, structure,",
|
|
1550
|
-
" choose, select, compare, distinguish, attach, annotate, expose, accept, manage, group,",
|
|
1551
|
-
" sort, filter, map, reduce, catch, throw, migrate, inspect, trace, parameterize,",
|
|
1552
|
-
" construct, design, capture) \u2014 OR wrap the concrete construct in backticks.",
|
|
1726
|
+
" (" + COMPLETABLE_VERBS.join(", ") + ") \u2014 OR wrap the concrete construct in backticks.",
|
|
1553
1727
|
" Items with abstract verbs (understand, learn, know, grasp, appreciate, explore) and",
|
|
1554
1728
|
" no backtick construct FAIL the audit. Prefer starting the task phrase with the verb.",
|
|
1555
1729
|
"3. 2-3 items per concept, each referencing a DIFFERENT concrete construct.",
|
|
@@ -1577,7 +1751,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1577
1751
|
"Return JSON:",
|
|
1578
1752
|
'{"sios":[{"concept_id":"C1","sio":["...","..."]}]}'
|
|
1579
1753
|
].join("\n");
|
|
1580
|
-
const sioRes = await chatJson(client, sioSystem, batchSioUser);
|
|
1754
|
+
const sioRes = await chatJson(client, sioSystem, batchSioUser, transportResultFrom("9router", client.model, { framingStripped: true }));
|
|
1581
1755
|
for (const x of sioRes.sios ?? []) {
|
|
1582
1756
|
if (x.concept_id && Array.isArray(x.sio)) sioByConcept[x.concept_id] = x.sio.map((item) => standardStatement(String(item)));
|
|
1583
1757
|
}
|
|
@@ -1630,7 +1804,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1630
1804
|
"- CIO: HOW mechanism, technology-agnostic, more concrete than ULO, Marr 2-Language neutral.",
|
|
1631
1805
|
"- SIO: keyword-bound completable implementation tasks naming real constructs of " + techStack + ".",
|
|
1632
1806
|
"- BINDING RULE: every SIO item must contain at least one of the concept's listed keywords VERBATIM (copy the exact keyword text into the sentence).",
|
|
1633
|
-
"- COMPLETABILITY RULE: every SIO item must contain an actionable verb (build, create,
|
|
1807
|
+
"- COMPLETABILITY RULE: every SIO item must contain an actionable verb (" + ["build", "create", "update", "inject", "compose", "pass", "modify", "use"].join(", ") + " ...full registry in the SIO authoring step) or wrap a concrete construct in backticks \u2014 abstract verbs (understand, learn, know, grasp, explore, appreciate) always fail.",
|
|
1634
1808
|
"",
|
|
1635
1809
|
JSON_ONLY
|
|
1636
1810
|
].join("\n");
|
|
@@ -1678,7 +1852,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1678
1852
|
"Depth-layer contract audit failed after repair for: " + detail + " \u2014 regenerate or fix the prompt contract (fail-closed, no silent fallback)"
|
|
1679
1853
|
);
|
|
1680
1854
|
}
|
|
1681
|
-
return { uloByConcept, cioByConcept, sioByConcept, audits, warnings };
|
|
1855
|
+
return { uloByConcept, cioByConcept, sioByConcept, audits, warnings, lastTransport: audits[0]?.transport };
|
|
1682
1856
|
}
|
|
1683
1857
|
function cell(v) {
|
|
1684
1858
|
return (v || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -1730,7 +1904,7 @@ function loadKnowledgeTreeCatalog(tsvPath) {
|
|
|
1730
1904
|
}
|
|
1731
1905
|
function tokens(s) {
|
|
1732
1906
|
return new Set(
|
|
1733
|
-
s
|
|
1907
|
+
stripDiacritics(s).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4)
|
|
1734
1908
|
);
|
|
1735
1909
|
}
|
|
1736
1910
|
function filterBySelection(catalog, selectedCategories, selectedTopics, analysisConcepts, maxConcepts = 120) {
|
|
@@ -1821,7 +1995,7 @@ async function standardizeConcepts(opts) {
|
|
|
1821
1995
|
" Adopt the tree's terminology. Only propose new when the tree genuinely has NO concept teaching that knowledge (broader AND narrower both fail).",
|
|
1822
1996
|
' 2. "proposed_new" - no tree concept covers it at the same granularity. Propose an UPPER_SNAKE_CASE code (descriptive, stable, max 5 words) and a one-sentence definition.',
|
|
1823
1997
|
" If between 1 and 3 tree concepts are CLOSE but not exact, list their codes in possible_tree_matches for human review.",
|
|
1824
|
-
`For a "standardized" decision ALSO return "adopted_keywords":
|
|
1998
|
+
`For a "standardized" decision ALSO return "adopted_keywords": copy VERBATIM from that tree concept's keyword list in the shortlist (exact tokens, do not re-spell, singularize or translate). Pick the subset that makes sense for the course TECH STACK; if none fit, return an EMPTY array \u2014 never invent a keyword. Maximum 8, ordered most-relevant first.`,
|
|
1825
1999
|
"Rules:",
|
|
1826
2000
|
"- Judge by MEANING (definition + keywords), not by string similarity.",
|
|
1827
2001
|
'- COVERAGE RULE: a "standardized" match must cover ALL primary dimensions of the input concept \u2014 every distinct aspect named in its title or description. A tree concept covering only ONE of several aspects (e.g. "interpreting error messages" for a concept about error handling AND user experience) is a PARTIAL match: decide "proposed_new" and list that tree code in possible_tree_matches.',
|
|
@@ -1861,10 +2035,25 @@ async function standardizeConcepts(opts) {
|
|
|
1861
2035
|
const tc = catalog.concepts.find((x) => x.code === d.tree_code);
|
|
1862
2036
|
if (!tc) throw new Error("Concept " + c.id + " standardized to unknown tree code " + d.tree_code + " \u2014 fail-closed");
|
|
1863
2037
|
const treeKw = new Set(tc.keywords.map((k) => k.toLowerCase()));
|
|
1864
|
-
const
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2038
|
+
const rawAdopted = (d.adopted_keywords || []).map((k) => String(k)).filter(Boolean);
|
|
2039
|
+
let adopted;
|
|
2040
|
+
if (tc.keywords.length === 0) {
|
|
2041
|
+
adopted = rawAdopted.filter((k) => (c.keywords ?? []).some((ck) => ck.toLowerCase() === k.toLowerCase()));
|
|
2042
|
+
if (adopted.length === 0) adopted = (c.keywords ?? []).slice(0, 8);
|
|
2043
|
+
warnings.push("Concept " + c.id + ": tree concept " + tc.code + " has no keyword registry \u2014 adopted " + adopted.length + " keyword(s) from the analysis concept");
|
|
2044
|
+
} else {
|
|
2045
|
+
const norm = (k) => k.toLowerCase().replace(/ies$/, "y").replace(/s$/, "");
|
|
2046
|
+
const treeKwNorm = new Set([...treeKw].map(norm));
|
|
2047
|
+
const dropped = [];
|
|
2048
|
+
adopted = rawAdopted.filter((k) => {
|
|
2049
|
+
if (treeKw.has(k.toLowerCase()) || treeKwNorm.has(norm(k))) return true;
|
|
2050
|
+
dropped.push(k);
|
|
2051
|
+
return false;
|
|
2052
|
+
});
|
|
2053
|
+
if (dropped.length > 0) {
|
|
2054
|
+
warnings.push("Concept " + c.id + ": dropped " + dropped.length + " adopted keyword(s) not in tree concept " + tc.code + " registry: " + dropped.join(", "));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
1868
2057
|
if (tc.keywords.length > 0 && adopted.length === 0) {
|
|
1869
2058
|
throw new Error("Concept " + c.id + " standardized to " + tc.code + " without adopted_keywords \u2014 fail-closed");
|
|
1870
2059
|
}
|
|
@@ -1904,6 +2093,15 @@ async function standardizeConcepts(opts) {
|
|
|
1904
2093
|
}
|
|
1905
2094
|
|
|
1906
2095
|
// src/graph/knowledgeGraphPipeline.ts
|
|
2096
|
+
var MIN_CALIBRATION_MINUTES = 15;
|
|
2097
|
+
var MAX_CALIBRATION_MINUTES = 60;
|
|
2098
|
+
var DEFAULT_CONCEPT_MINUTES = 30;
|
|
2099
|
+
var MIN_CALIBRATION_DIVERGENCE = 0.5;
|
|
2100
|
+
function calibrateConceptMinutes(sioCount, problemTypeCount, prereqCount, hasHighBloom) {
|
|
2101
|
+
const ratio = (value, cap) => Math.min(1, Math.max(0, value) / cap);
|
|
2102
|
+
const score = 0.3 * ratio(sioCount, 4) + 0.25 * ratio(problemTypeCount, 4) + 0.2 * ratio(prereqCount, 4) + 0.25 * (hasHighBloom ? 1 : 0);
|
|
2103
|
+
return Math.round(MIN_CALIBRATION_MINUTES + score * (MAX_CALIBRATION_MINUTES - MIN_CALIBRATION_MINUTES));
|
|
2104
|
+
}
|
|
1907
2105
|
function topologicalSort(concepts) {
|
|
1908
2106
|
const inDegree = /* @__PURE__ */ new Map();
|
|
1909
2107
|
const adjacency = /* @__PURE__ */ new Map();
|
|
@@ -1950,7 +2148,7 @@ async function generateKnowledgeGraph(options) {
|
|
|
1950
2148
|
log("STEP_1", `Parsed syllabus: ${parsedSyllabus.units.length} units, ${parsedSyllabus.allTopics.length} topics`);
|
|
1951
2149
|
}
|
|
1952
2150
|
const client = createLlmClient(llmConfig);
|
|
1953
|
-
const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "
|
|
2151
|
+
const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "32000", 10);
|
|
1954
2152
|
let truncatedSyllabus = "";
|
|
1955
2153
|
if (syllabusText) {
|
|
1956
2154
|
if (syllabusText.length > maxSyllabusChars) {
|
|
@@ -2069,7 +2267,7 @@ Return JSON:
|
|
|
2069
2267
|
}
|
|
2070
2268
|
{
|
|
2071
2269
|
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"]);
|
|
2072
|
-
const tokensFrom = (text) => text
|
|
2270
|
+
const tokensFrom = (text) => stripDiacritics(text).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP.has(w));
|
|
2073
2271
|
for (const c of concepts) {
|
|
2074
2272
|
const existing = (c.keywords || []).map((k) => String(k).trim()).filter(Boolean);
|
|
2075
2273
|
if (existing.length === 0) {
|
|
@@ -2140,23 +2338,32 @@ Return JSON:
|
|
|
2140
2338
|
}
|
|
2141
2339
|
}
|
|
2142
2340
|
const idByTreeCode = new Map(concepts.filter((c) => c.tree_code).map((c) => [c.tree_code, c.id]));
|
|
2341
|
+
const unmappedTreePrereqs = /* @__PURE__ */ new Set();
|
|
2143
2342
|
for (const c of concepts) {
|
|
2144
2343
|
for (const tp of c.tree_prerequisites || []) {
|
|
2145
2344
|
const target = idByTreeCode.get(tp);
|
|
2146
2345
|
if (target && target !== c.id && !(c.prerequisites || []).includes(target)) {
|
|
2147
2346
|
c.prerequisites = [...c.prerequisites || [], target];
|
|
2148
2347
|
warnings.push("Prereq seeded from tree: " + c.id + " <- " + tp + " (" + target + ")");
|
|
2348
|
+
} else if (!target) {
|
|
2349
|
+
unmappedTreePrereqs.add(tp);
|
|
2149
2350
|
}
|
|
2150
2351
|
}
|
|
2151
2352
|
}
|
|
2353
|
+
if (unmappedTreePrereqs.size > 0) {
|
|
2354
|
+
const shown = [...unmappedTreePrereqs].slice(0, 10).join(", ");
|
|
2355
|
+
warnings.push("Tree prereqs outside THIS analysis (no edge seeded, " + unmappedTreePrereqs.size + " codes): " + shown + (unmappedTreePrereqs.size > 10 ? ", ..." : ""));
|
|
2356
|
+
}
|
|
2152
2357
|
log("STEP_2_5", "Standardized: " + std.decisions.filter((d) => d.decision === "standardized").length + ", proposed new: " + std.decisions.filter((d) => d.decision === "proposed_new").length);
|
|
2358
|
+
} else {
|
|
2359
|
+
warnings.push("No knowledgeTreePath provided \u2014 concepts are NOT standardized against the knowledge tree (all proposed_new)");
|
|
2153
2360
|
}
|
|
2154
2361
|
log("STEP_3", "Checking for prerequisite cycles...");
|
|
2155
2362
|
const cycles = detectPrerequisiteCycles(concepts);
|
|
2156
2363
|
if (cycles.length > 0) {
|
|
2157
2364
|
log("STEP_3", `Found ${cycles.length} cycle(s) in prerequisite chain \u2014 breaking cycles`);
|
|
2158
2365
|
for (const cycle of cycles) {
|
|
2159
|
-
warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014
|
|
2366
|
+
warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 removing the closing back-edge (last DFS edge; pedagogically arbitrary)`);
|
|
2160
2367
|
}
|
|
2161
2368
|
const { fixed, removed } = breakPrerequisiteCycles(concepts, cycles);
|
|
2162
2369
|
for (let i = 0; i < concepts.length; i++) {
|
|
@@ -2237,14 +2444,11 @@ Return JSON:
|
|
|
2237
2444
|
const hasHighBloom = (concept.problem_types || []).some(
|
|
2238
2445
|
(pt) => ["Analyze", "Evaluate", "Create"].includes(pt.bloom_level || "")
|
|
2239
2446
|
);
|
|
2240
|
-
const
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
if (llmEstimate === 30 || Math.abs(calibrated - llmEstimate) / llmEstimate > 0.5) {
|
|
2447
|
+
const calibrated = calibrateConceptMinutes(sioCount, ptCount, prereqCount, hasHighBloom);
|
|
2448
|
+
const llmEstimate = concept.estimated_minutes || DEFAULT_CONCEPT_MINUTES;
|
|
2449
|
+
if (Math.abs(calibrated - llmEstimate) / llmEstimate > MIN_CALIBRATION_DIVERGENCE) {
|
|
2244
2450
|
concept.estimated_minutes = calibrated;
|
|
2245
|
-
|
|
2246
|
-
warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m (score=" + score.toFixed(2) + ")");
|
|
2247
|
-
}
|
|
2451
|
+
warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m");
|
|
2248
2452
|
}
|
|
2249
2453
|
}
|
|
2250
2454
|
log("STEP_5c", "Minute calibration complete");
|
|
@@ -2269,7 +2473,8 @@ Return JSON:
|
|
|
2269
2473
|
return {
|
|
2270
2474
|
knowledgeGraph: verifiedGraph,
|
|
2271
2475
|
warnings,
|
|
2272
|
-
verificationReport: report
|
|
2476
|
+
verificationReport: report,
|
|
2477
|
+
provenance: client.callLog
|
|
2273
2478
|
};
|
|
2274
2479
|
}
|
|
2275
2480
|
|
|
@@ -2296,9 +2501,10 @@ async function generateHybridGraph(options) {
|
|
|
2296
2501
|
description: "",
|
|
2297
2502
|
total_project_minutes: 0,
|
|
2298
2503
|
total_concept_minutes: 0,
|
|
2299
|
-
|
|
2504
|
+
linked_prereq_minutes: 0
|
|
2300
2505
|
},
|
|
2301
|
-
warnings
|
|
2506
|
+
warnings,
|
|
2507
|
+
provenance: []
|
|
2302
2508
|
};
|
|
2303
2509
|
}
|
|
2304
2510
|
const client = createLlmClient(llmConfig);
|
|
@@ -2379,9 +2585,14 @@ async function generateHybridGraph(options) {
|
|
|
2379
2585
|
description: "Progressive completion path: " + phases.length + " phases, " + features.length + " features, " + concepts.length + " concepts",
|
|
2380
2586
|
total_project_minutes: totalProjectMinutes,
|
|
2381
2587
|
total_concept_minutes: totalConceptMinutes,
|
|
2382
|
-
|
|
2588
|
+
// HG-1 (2026-10-05): renamed from estimated_total_minutes. It counts only
|
|
2589
|
+
// concepts that HAVE a feature link (max per concept, not summed), so it
|
|
2590
|
+
// is NOT the course total — see HybridGraphSchema.linked_prereq_minutes.
|
|
2591
|
+
// The phase planner below commits to totalProjectMinutes + totalConceptMinutes.
|
|
2592
|
+
linked_prereq_minutes: totalProjectMinutes + totalPrereqMinutes
|
|
2383
2593
|
},
|
|
2384
|
-
warnings
|
|
2594
|
+
warnings,
|
|
2595
|
+
provenance: client.callLog
|
|
2385
2596
|
};
|
|
2386
2597
|
}
|
|
2387
2598
|
function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
@@ -2457,12 +2668,20 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
|
2457
2668
|
} else {
|
|
2458
2669
|
newIntroPhase.set(cid, order);
|
|
2459
2670
|
}
|
|
2671
|
+
introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
|
|
2672
|
+
}
|
|
2673
|
+
for (const intro of introductions) {
|
|
2674
|
+
const cid = intro.concept_id;
|
|
2675
|
+
if (intro.aspect === "advanced") continue;
|
|
2676
|
+
const introOrder = newIntroPhase.get(cid);
|
|
2677
|
+
if (introOrder === void 0 || introOrder !== order) continue;
|
|
2460
2678
|
for (const pre of prereqOf.get(cid) || []) {
|
|
2461
|
-
|
|
2462
|
-
|
|
2679
|
+
const previewOrder = previewPhase.get(pre);
|
|
2680
|
+
const covered = newIntroPhase.has(pre) || previewOrder !== void 0 && previewOrder < order;
|
|
2681
|
+
if (!covered) {
|
|
2682
|
+
errors.push("Phase " + id + ": concept " + cid + " introduced before its prerequisite " + pre + " is introduced (or previewed in an earlier phase) - violates the prerequisite chain");
|
|
2463
2683
|
}
|
|
2464
2684
|
}
|
|
2465
|
-
introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
|
|
2466
2685
|
}
|
|
2467
2686
|
phases.push({
|
|
2468
2687
|
id,
|
|
@@ -2500,6 +2719,9 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
|
2500
2719
|
if (anchor !== void 0 && anchor <= pOrder) {
|
|
2501
2720
|
errors.push("Concept " + cid + " previewed in phase " + pOrder + " but fully introduced in phase " + anchor + " - preview must precede the anchor introduction");
|
|
2502
2721
|
}
|
|
2722
|
+
if (anchor === void 0) {
|
|
2723
|
+
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");
|
|
2724
|
+
}
|
|
2503
2725
|
}
|
|
2504
2726
|
return { phases, errors };
|
|
2505
2727
|
}
|