@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
|
@@ -1,265 +1,15 @@
|
|
|
1
|
+
import { createLlmClient, llmChatJson, parseJsonFromLlm } from './chunk-7A2SG466.mjs';
|
|
2
|
+
import { transportResultFrom } from './chunk-IBVXXVGE.mjs';
|
|
1
3
|
import { parseSyllabus } from './chunk-T4Y2DE2W.mjs';
|
|
4
|
+
import { stripDiacritics } from './chunk-OYSZQVPY.mjs';
|
|
2
5
|
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
6
|
import { tmpdir } from 'os';
|
|
4
7
|
import { join } from 'path';
|
|
5
8
|
|
|
6
|
-
// src/utils/llmClient.ts
|
|
7
|
-
var LlmClientError = class extends Error {
|
|
8
|
-
constructor(message, status, cause) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.status = status;
|
|
11
|
-
this.cause = cause;
|
|
12
|
-
this.name = "LlmClientError";
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
function resolveProviderChain(config) {
|
|
16
|
-
const chain = [];
|
|
17
|
-
const push = (label, baseUrl, apiKey, model) => {
|
|
18
|
-
if (apiKey && baseUrl && model && !chain.some((c) => c.baseUrl === baseUrl && c.model === model)) {
|
|
19
|
-
chain.push({ label, baseUrl, apiKey, model });
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
if (config?.apiKey) {
|
|
23
|
-
push("config", config.baseUrl || "https://api.openai.com/v1", config.apiKey, config.model || "gpt-4o-mini");
|
|
24
|
-
}
|
|
25
|
-
push("legacy", process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || "", process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || "", process.env.LLM_MODEL || process.env.LLM_TIER_FAST || "");
|
|
26
|
-
push("dashscope", process.env.DASHSCOPE_BASE_URL || process.env.ALIBABA_BASE_URL || "", process.env.DASHSCOPE_API_KEY || process.env.ALIBABA_API_KEY || "", process.env.DASHSCOPE_MODEL || process.env.DEFAULT_AI_MODEL || "");
|
|
27
|
-
push("nvidia", "https://integrate.api.nvidia.com/v1", process.env.NVIDIA_API_KEY || "", process.env.NVIDIA_MODEL || "nvidia/nemotron-3-ultra-550b-a55b:free");
|
|
28
|
-
push("openrouter", "https://openrouter.ai/api/v1", process.env.OPENROUTER_API_KEY || "", process.env.OPENROUTER_MODEL || "nvidia/nemotron-3-ultra-550b-a55b:free");
|
|
29
|
-
if (process.env.OPENROUTER_MODEL) {
|
|
30
|
-
const or = chain.find((c) => c.label === "openrouter");
|
|
31
|
-
if (or) {
|
|
32
|
-
const rest = chain.filter((c) => c !== or);
|
|
33
|
-
return [or, ...rest];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return chain;
|
|
37
|
-
}
|
|
38
|
-
var MIN_REQUEST_INTERVAL_MS = 2500;
|
|
39
|
-
var lastRequestAt = 0;
|
|
40
|
-
async function pacedDelay() {
|
|
41
|
-
const wait = lastRequestAt + MIN_REQUEST_INTERVAL_MS - Date.now();
|
|
42
|
-
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
43
|
-
lastRequestAt = Date.now();
|
|
44
|
-
}
|
|
45
|
-
function createLlmClient(config) {
|
|
46
|
-
const chain = resolveProviderChain(config);
|
|
47
|
-
const primary = chain[0];
|
|
48
|
-
if (!primary) {
|
|
49
|
-
throw new LlmClientError(
|
|
50
|
-
"No LLM provider configured. Set NVIDIA_API_KEY (primary) and/or OPENROUTER_API_KEY (fallback), or pass llmConfig."
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
primary.apiKey;
|
|
54
|
-
primary.baseUrl;
|
|
55
|
-
const model = primary.model;
|
|
56
|
-
async function chat(messages, options) {
|
|
57
|
-
const temperature = options?.temperature ?? config?.temperature ?? 0.1;
|
|
58
|
-
const maxTokens = options?.maxTokens ?? config?.maxTokens ?? parseInt(process.env.LLM_MAX_TOKENS || "65536", 10);
|
|
59
|
-
const attempts = [];
|
|
60
|
-
let lastError = null;
|
|
61
|
-
const TRANSIENT = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
62
|
-
const REQUEST_TIMEOUT_MS = Number(process.env.LLM_REQUEST_TIMEOUT_MS || 3e5);
|
|
63
|
-
const MAX_CHAIN_ROUNDS = 3;
|
|
64
|
-
for (let round = 1; round <= MAX_CHAIN_ROUNDS; round++) {
|
|
65
|
-
if (round > 1) {
|
|
66
|
-
attempts.push("round " + (round - 1) + " failed \u2014 backing off 20s before rewalking the chain");
|
|
67
|
-
await new Promise((resolve) => setTimeout(resolve, 2e4));
|
|
68
|
-
}
|
|
69
|
-
for (const provider of chain) {
|
|
70
|
-
try {
|
|
71
|
-
const headers = {
|
|
72
|
-
"Content-Type": "application/json",
|
|
73
|
-
"Authorization": `Bearer ${provider.apiKey}`
|
|
74
|
-
};
|
|
75
|
-
const payload = {
|
|
76
|
-
model: provider.model,
|
|
77
|
-
messages,
|
|
78
|
-
temperature,
|
|
79
|
-
max_tokens: maxTokens
|
|
80
|
-
};
|
|
81
|
-
await pacedDelay();
|
|
82
|
-
const doFetch = () => fetch(`${provider.baseUrl}/chat/completions`, {
|
|
83
|
-
method: "POST",
|
|
84
|
-
headers: {
|
|
85
|
-
"Content-Type": "application/json",
|
|
86
|
-
"Authorization": `Bearer ${provider.apiKey}`
|
|
87
|
-
},
|
|
88
|
-
body: JSON.stringify({
|
|
89
|
-
model: provider.model,
|
|
90
|
-
messages,
|
|
91
|
-
temperature,
|
|
92
|
-
max_tokens: maxTokens
|
|
93
|
-
}),
|
|
94
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
95
|
-
});
|
|
96
|
-
let response;
|
|
97
|
-
try {
|
|
98
|
-
response = await doFetch();
|
|
99
|
-
} catch (netErr) {
|
|
100
|
-
attempts.push(`${provider.label}: network error (${netErr instanceof Error ? netErr.message : String(netErr)}) \u2014 retrying once`);
|
|
101
|
-
await new Promise((r) => setTimeout(r, 5e3));
|
|
102
|
-
lastRequestAt = Date.now();
|
|
103
|
-
response = await doFetch();
|
|
104
|
-
}
|
|
105
|
-
if (!response.ok) {
|
|
106
|
-
const body = await response.text().catch(() => "");
|
|
107
|
-
const err = new LlmClientError(
|
|
108
|
-
`[${provider.label}] LLM API error: ${response.status} ${response.statusText} \u2014 ${body.slice(0, 200)}`,
|
|
109
|
-
response.status
|
|
110
|
-
);
|
|
111
|
-
if (response.status === 429) {
|
|
112
|
-
const retryAfterRaw = response.headers.get("retry-after");
|
|
113
|
-
const retryAfterMs = Math.min(
|
|
114
|
-
6e4,
|
|
115
|
-
Math.max(15e3, (Number.isFinite(Number(retryAfterRaw)) ? Number(retryAfterRaw) : 20) * 1e3)
|
|
116
|
-
);
|
|
117
|
-
attempts.push(`${provider.label}: 429 rate-limited \u2014 backing off ${Math.round(retryAfterMs / 1e3)}s`);
|
|
118
|
-
await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
|
|
119
|
-
lastRequestAt = Date.now();
|
|
120
|
-
const retry = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
121
|
-
method: "POST",
|
|
122
|
-
headers,
|
|
123
|
-
body: JSON.stringify(payload),
|
|
124
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
125
|
-
});
|
|
126
|
-
if (!retry.ok) throw err;
|
|
127
|
-
const retryData = await retry.json();
|
|
128
|
-
const retryContent2 = retryData.choices?.[0]?.message?.content || "";
|
|
129
|
-
if (!retryContent2.trim()) throw err;
|
|
130
|
-
return {
|
|
131
|
-
content: retryContent2,
|
|
132
|
-
model: retryData.model || provider.model,
|
|
133
|
-
provider: provider.label,
|
|
134
|
-
attempts: attempts.slice(),
|
|
135
|
-
usage: retryData.usage,
|
|
136
|
-
finishReason: retryData.choices?.[0]?.finish_reason
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
if (TRANSIENT.has(response.status)) {
|
|
140
|
-
await new Promise((r) => setTimeout(r, 3e3));
|
|
141
|
-
const retry = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
142
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
143
|
-
method: "POST",
|
|
144
|
-
headers: {
|
|
145
|
-
"Content-Type": "application/json",
|
|
146
|
-
"Authorization": `Bearer ${provider.apiKey}`
|
|
147
|
-
},
|
|
148
|
-
body: JSON.stringify({
|
|
149
|
-
model: provider.model,
|
|
150
|
-
messages,
|
|
151
|
-
temperature,
|
|
152
|
-
max_tokens: maxTokens
|
|
153
|
-
})
|
|
154
|
-
});
|
|
155
|
-
if (!retry.ok) throw err;
|
|
156
|
-
const retryData = await retry.json();
|
|
157
|
-
const retryContent = retryData.choices?.[0]?.message?.content || "";
|
|
158
|
-
return {
|
|
159
|
-
content: retryContent,
|
|
160
|
-
model: retryData.model || provider.model,
|
|
161
|
-
provider: provider.label,
|
|
162
|
-
attempts: attempts.slice(),
|
|
163
|
-
usage: retryData.usage,
|
|
164
|
-
finishReason: retryData.choices?.[0]?.finish_reason
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
throw err;
|
|
168
|
-
}
|
|
169
|
-
const data = await response.json();
|
|
170
|
-
const wrappedError = data.error;
|
|
171
|
-
if (wrappedError) {
|
|
172
|
-
throw new LlmClientError("[" + provider.label + "] upstream error " + (wrappedError.code ?? "") + ": " + (wrappedError.message ?? "unknown"));
|
|
173
|
-
}
|
|
174
|
-
const content = data.choices?.[0]?.message?.content || "";
|
|
175
|
-
if (!content.trim()) {
|
|
176
|
-
throw new LlmClientError("[" + provider.label + "] empty completion returned");
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
content,
|
|
180
|
-
model: data.model || provider.model,
|
|
181
|
-
provider: provider.label,
|
|
182
|
-
attempts: attempts.slice(),
|
|
183
|
-
usage: data.usage,
|
|
184
|
-
finishReason: data.choices?.[0]?.finish_reason
|
|
185
|
-
};
|
|
186
|
-
} catch (err) {
|
|
187
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
188
|
-
attempts.push(`${provider.label}: ${msg}`);
|
|
189
|
-
lastError = err instanceof LlmClientError ? err : new LlmClientError(`[${provider.label}] ${msg}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (round < MAX_CHAIN_ROUNDS) continue;
|
|
193
|
-
throw lastError ?? new LlmClientError("All LLM providers failed (empty chain).");
|
|
194
|
-
}
|
|
195
|
-
throw lastError ?? new LlmClientError("All LLM providers failed after " + MAX_CHAIN_ROUNDS + " rounds.");
|
|
196
|
-
}
|
|
197
|
-
return { chat, model };
|
|
198
|
-
}
|
|
199
|
-
function parseJsonFromLlm(text) {
|
|
200
|
-
let cleaned = text.trim();
|
|
201
|
-
if (cleaned.startsWith("```json")) {
|
|
202
|
-
cleaned = cleaned.slice(7);
|
|
203
|
-
} else if (cleaned.startsWith("```")) {
|
|
204
|
-
cleaned = cleaned.slice(3);
|
|
205
|
-
}
|
|
206
|
-
if (cleaned.endsWith("```")) {
|
|
207
|
-
cleaned = cleaned.slice(0, -3);
|
|
208
|
-
}
|
|
209
|
-
cleaned = cleaned.trim();
|
|
210
|
-
try {
|
|
211
|
-
return JSON.parse(cleaned);
|
|
212
|
-
} catch {
|
|
213
|
-
const start = cleaned.indexOf("{");
|
|
214
|
-
if (start >= 0) {
|
|
215
|
-
let depth = 0;
|
|
216
|
-
let inString = false;
|
|
217
|
-
let escape = false;
|
|
218
|
-
for (let i = start; i < cleaned.length; i++) {
|
|
219
|
-
const ch = cleaned[i];
|
|
220
|
-
if (escape) {
|
|
221
|
-
escape = false;
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (ch === "\\") {
|
|
225
|
-
escape = true;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
if (ch === '"') {
|
|
229
|
-
inString = !inString;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (inString) continue;
|
|
233
|
-
if (ch === "{") depth++;
|
|
234
|
-
if (ch === "}") {
|
|
235
|
-
depth--;
|
|
236
|
-
if (depth === 0) {
|
|
237
|
-
try {
|
|
238
|
-
return JSON.parse(cleaned.slice(start, i + 1));
|
|
239
|
-
} catch {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
throw new LlmClientError(`Failed to parse JSON from LLM response: ${cleaned.slice(0, 200)}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
async function llmChatJson(client, systemPrompt, userPrompt, options) {
|
|
249
|
-
const result = await client.chat(
|
|
250
|
-
[
|
|
251
|
-
{ role: "system", content: systemPrompt },
|
|
252
|
-
{ role: "user", content: userPrompt }
|
|
253
|
-
],
|
|
254
|
-
options
|
|
255
|
-
);
|
|
256
|
-
return parseJsonFromLlm(result.content);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
9
|
// src/graph/scaffoldExtractor.ts
|
|
260
10
|
async function extractScaffold(options) {
|
|
261
|
-
const { goal, techStack, fileList, llmConfig } = options;
|
|
262
|
-
const client = createLlmClient(llmConfig);
|
|
11
|
+
const { goal, techStack, fileList, llmConfig, sharedClient } = options;
|
|
12
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
263
13
|
const fileListStr = fileList.length > 0 ? fileList.join("\n") : "(empty)";
|
|
264
14
|
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
15
|
const userPrompt = `App goal: ${goal}
|
|
@@ -344,8 +94,8 @@ ${content}`);
|
|
|
344
94
|
return blocks.join("\n\n");
|
|
345
95
|
}
|
|
346
96
|
async function extractProjectOverview(options) {
|
|
347
|
-
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig } = options;
|
|
348
|
-
const client = createLlmClient(llmConfig);
|
|
97
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig, sharedClient } = options;
|
|
98
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
349
99
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
350
100
|
const allFilesStr = buildSourceText(fileContentsMap);
|
|
351
101
|
let keywordsAnchor = "";
|
|
@@ -354,7 +104,7 @@ async function extractProjectOverview(options) {
|
|
|
354
104
|
(kw) => `- ${kw.keyword} (from ${kw.source})`
|
|
355
105
|
);
|
|
356
106
|
if (kwItems.length > 0) {
|
|
357
|
-
keywordsAnchor = "\
|
|
107
|
+
keywordsAnchor = "\nDomain Keywords & Patterns (from regex-based static scan \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
|
|
358
108
|
}
|
|
359
109
|
}
|
|
360
110
|
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.";
|
|
@@ -414,8 +164,8 @@ ${content}`);
|
|
|
414
164
|
return blocks.join("\n\n");
|
|
415
165
|
}
|
|
416
166
|
async function extractFeatureSteps(options) {
|
|
417
|
-
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig } = options;
|
|
418
|
-
const client = createLlmClient(llmConfig);
|
|
167
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig, sharedClient } = options;
|
|
168
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
419
169
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
420
170
|
const allFilesStr = buildSourceText2(fileContentsMap);
|
|
421
171
|
const fid = feature.id || "F_UNK";
|
|
@@ -482,8 +232,8 @@ Split this feature into 2-6 action steps. Return JSON:
|
|
|
482
232
|
concept_codes: []
|
|
483
233
|
}));
|
|
484
234
|
}
|
|
485
|
-
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig) {
|
|
486
|
-
const client = createLlmClient(llmConfig);
|
|
235
|
+
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig, sharedClient) {
|
|
236
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
487
237
|
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
488
238
|
const sourceText = buildSourceText2(fileContentsMap);
|
|
489
239
|
const featuresListing = featuresMeta.map(
|
|
@@ -750,7 +500,7 @@ function inferDepthFromContext(keyword, fileContentsMap) {
|
|
|
750
500
|
return { depth: "sio", rationale: `Default: treating '${keyword}' as SIO (implementation-level)` };
|
|
751
501
|
}
|
|
752
502
|
async function escalateAndMapConcepts(options) {
|
|
753
|
-
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig } = options;
|
|
503
|
+
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig, sharedClient } = options;
|
|
754
504
|
const features = projectGraph.features || [];
|
|
755
505
|
const allTerms = /* @__PURE__ */ new Set();
|
|
756
506
|
for (const feature of features) {
|
|
@@ -766,7 +516,7 @@ async function escalateAndMapConcepts(options) {
|
|
|
766
516
|
llmResults = conceptMapOverride;
|
|
767
517
|
} else {
|
|
768
518
|
try {
|
|
769
|
-
const client = createLlmClient(llmConfig);
|
|
519
|
+
const client = sharedClient ?? createLlmClient(llmConfig);
|
|
770
520
|
const conceptBank = (availableConcepts || []).sort().join(", ") || "(empty)";
|
|
771
521
|
const termEntries = termList.map((term) => {
|
|
772
522
|
const snippets = extractTermSnippets(term, fileContentsMap);
|
|
@@ -819,13 +569,16 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
|
|
|
819
569
|
const cName = info.concept_name || kw;
|
|
820
570
|
let depth = "sio";
|
|
821
571
|
let depthRationale = "Default: SIO";
|
|
572
|
+
let depthSource = "override";
|
|
822
573
|
if (info.depth && ["ulo", "cio", "sio"].includes(info.depth)) {
|
|
823
574
|
depth = info.depth;
|
|
824
575
|
depthRationale = `LLM classified as ${depth}`;
|
|
576
|
+
depthSource = "llm";
|
|
825
577
|
} else {
|
|
826
578
|
const inferred = inferDepthFromContext(kw, fileContentsMap);
|
|
827
579
|
depth = inferred.depth;
|
|
828
580
|
depthRationale = inferred.rationale;
|
|
581
|
+
depthSource = "heuristic";
|
|
829
582
|
}
|
|
830
583
|
const kwKey = `${cCode}::${kw}`;
|
|
831
584
|
if (seen.has(kwKey)) continue;
|
|
@@ -843,6 +596,7 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
|
|
|
843
596
|
keyword: kw,
|
|
844
597
|
depth,
|
|
845
598
|
depth_rationale: depthRationale,
|
|
599
|
+
depth_source: depthSource,
|
|
846
600
|
evidence_files: evidenceFiles
|
|
847
601
|
});
|
|
848
602
|
}
|
|
@@ -1150,7 +904,110 @@ function techSignalHits(text) {
|
|
|
1150
904
|
}
|
|
1151
905
|
return hits;
|
|
1152
906
|
}
|
|
1153
|
-
|
|
907
|
+
var COMPLETABLE_VERBS = [
|
|
908
|
+
"build",
|
|
909
|
+
"create",
|
|
910
|
+
"implement",
|
|
911
|
+
"write",
|
|
912
|
+
"configure",
|
|
913
|
+
"add",
|
|
914
|
+
"use",
|
|
915
|
+
"apply",
|
|
916
|
+
"display",
|
|
917
|
+
"store",
|
|
918
|
+
"load",
|
|
919
|
+
"save",
|
|
920
|
+
"handle",
|
|
921
|
+
"parse",
|
|
922
|
+
"validate",
|
|
923
|
+
"test",
|
|
924
|
+
"register",
|
|
925
|
+
"compute",
|
|
926
|
+
"generate",
|
|
927
|
+
"print",
|
|
928
|
+
"format",
|
|
929
|
+
"convert",
|
|
930
|
+
"extend",
|
|
931
|
+
"define",
|
|
932
|
+
"execute",
|
|
933
|
+
"run",
|
|
934
|
+
"read",
|
|
935
|
+
"fetch",
|
|
936
|
+
"wrap",
|
|
937
|
+
"log",
|
|
938
|
+
"call",
|
|
939
|
+
"raise",
|
|
940
|
+
"install",
|
|
941
|
+
"import",
|
|
942
|
+
"export",
|
|
943
|
+
"declare",
|
|
944
|
+
"embed",
|
|
945
|
+
"model",
|
|
946
|
+
"represent",
|
|
947
|
+
"override",
|
|
948
|
+
"customize",
|
|
949
|
+
"enforce",
|
|
950
|
+
"mock",
|
|
951
|
+
"assert",
|
|
952
|
+
"query",
|
|
953
|
+
"index",
|
|
954
|
+
"cache",
|
|
955
|
+
"stream",
|
|
956
|
+
"split",
|
|
957
|
+
"join",
|
|
958
|
+
"strip",
|
|
959
|
+
"encode",
|
|
960
|
+
"decode",
|
|
961
|
+
"traverse",
|
|
962
|
+
"measure",
|
|
963
|
+
"evaluate",
|
|
964
|
+
"benchmark",
|
|
965
|
+
"diagnose",
|
|
966
|
+
"demonstrate",
|
|
967
|
+
"verify",
|
|
968
|
+
"organize",
|
|
969
|
+
"structure",
|
|
970
|
+
"choose",
|
|
971
|
+
"select",
|
|
972
|
+
"compare",
|
|
973
|
+
"distinguish",
|
|
974
|
+
"attach",
|
|
975
|
+
"annotate",
|
|
976
|
+
"expose",
|
|
977
|
+
"accept",
|
|
978
|
+
"manage",
|
|
979
|
+
"group",
|
|
980
|
+
"sort",
|
|
981
|
+
"filter",
|
|
982
|
+
"map",
|
|
983
|
+
"reduce",
|
|
984
|
+
"catch",
|
|
985
|
+
"throw",
|
|
986
|
+
"migrate",
|
|
987
|
+
"inspect",
|
|
988
|
+
"trace",
|
|
989
|
+
"parameterize",
|
|
990
|
+
"construct",
|
|
991
|
+
"design",
|
|
992
|
+
"capture",
|
|
993
|
+
// Completable implementation verbs present in production SIO output but
|
|
994
|
+
// missing from the original registry (2026-10-07 smoke test, swift kg):
|
|
995
|
+
"update",
|
|
996
|
+
"inject",
|
|
997
|
+
"compose",
|
|
998
|
+
"pass",
|
|
999
|
+
"modify",
|
|
1000
|
+
"connect",
|
|
1001
|
+
"remove",
|
|
1002
|
+
"transform",
|
|
1003
|
+
"trigger",
|
|
1004
|
+
"refresh",
|
|
1005
|
+
"persist",
|
|
1006
|
+
"observe",
|
|
1007
|
+
"arrange",
|
|
1008
|
+
"derive"
|
|
1009
|
+
];
|
|
1010
|
+
function auditDepthLayers(concepts, transport) {
|
|
1154
1011
|
return concepts.map((c) => {
|
|
1155
1012
|
const ulo = (c.ulo ?? "").trim();
|
|
1156
1013
|
const cio = (c.cio ?? "").trim();
|
|
@@ -1171,7 +1028,7 @@ function auditDepthLayers(concepts) {
|
|
|
1171
1028
|
});
|
|
1172
1029
|
const conceptKeywords = (c.keywords ?? []).map((k) => k.toLowerCase()).filter(Boolean);
|
|
1173
1030
|
const sioOffConceptItems = conceptKeywords.length > 0 ? sioItems.filter((s) => !boundToConcept(s)) : [];
|
|
1174
|
-
const COMPLETABLE =
|
|
1031
|
+
const COMPLETABLE = new RegExp("\\b(?:" + COMPLETABLE_VERBS.join("|") + ")\\b", "i");
|
|
1175
1032
|
const sioNotCompletable = sioItems.filter((s) => !COMPLETABLE.test(s) && !/`[^`]+`/.test(s));
|
|
1176
1033
|
const density = (text) => {
|
|
1177
1034
|
if (!text) return 0;
|
|
@@ -1211,7 +1068,8 @@ function auditDepthLayers(concepts) {
|
|
|
1211
1068
|
clauseOk,
|
|
1212
1069
|
codesOk,
|
|
1213
1070
|
codeIssues,
|
|
1214
|
-
passed
|
|
1071
|
+
passed,
|
|
1072
|
+
transport
|
|
1215
1073
|
};
|
|
1216
1074
|
});
|
|
1217
1075
|
}
|
|
@@ -1246,14 +1104,14 @@ function excerpt(text, max) {
|
|
|
1246
1104
|
const t = (text || "").trim();
|
|
1247
1105
|
return t.length > max ? t.slice(0, max) + "..." : t;
|
|
1248
1106
|
}
|
|
1249
|
-
async function chatJson(client, system, user) {
|
|
1107
|
+
async function chatJson(client, system, user, transport) {
|
|
1250
1108
|
const r = await client.chat(
|
|
1251
1109
|
[{ role: "system", content: system }, { role: "user", content: user }],
|
|
1252
1110
|
{ temperature: 0.1, maxTokens: parseInt(process.env.KG_MAX_TOKENS || "65536", 10) }
|
|
1253
1111
|
);
|
|
1254
1112
|
return parseJsonFromLlm(r.content);
|
|
1255
1113
|
}
|
|
1256
|
-
var LO_BATCH_SIZE =
|
|
1114
|
+
var LO_BATCH_SIZE = 10;
|
|
1257
1115
|
function chunkConcepts(items, size) {
|
|
1258
1116
|
const out = [];
|
|
1259
1117
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
@@ -1292,7 +1150,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1292
1150
|
for (const batch of chunkConcepts(concepts, LO_BATCH_SIZE)) {
|
|
1293
1151
|
const batchList = batch.map((c) => "- " + c.id + " (" + c.name + ")" + (c.description ? ": " + excerpt(c.description, 160) : "")).join("\n");
|
|
1294
1152
|
const batchUser = "Concepts:\n" + batchList + '\n\nReturn JSON:\n{"ulos":[{"concept_id":"C1","ulo":"..."}]}';
|
|
1295
|
-
const uloRes = await chatJson(client, uloSystem, batchUser);
|
|
1153
|
+
const uloRes = await chatJson(client, uloSystem, batchUser, transportResultFrom("9router", client.model, { framingStripped: true }));
|
|
1296
1154
|
for (const u of uloRes.ulos ?? []) {
|
|
1297
1155
|
if (u.concept_id && u.ulo) uloByConcept[u.concept_id] = standardStatement(u.ulo);
|
|
1298
1156
|
}
|
|
@@ -1331,7 +1189,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1331
1189
|
new Set(batch.map((c) => c.id));
|
|
1332
1190
|
const batchUloContext = batch.map((c) => "- " + c.id + " (" + c.name + "): " + (uloByConcept[c.id] || "(missing)")).join("\n");
|
|
1333
1191
|
const batchCioUser = "Approved ULOs:\n" + batchUloContext + '\n\nReturn JSON:\n{"cios":[{"concept_id":"C1","cio":"..."}]}';
|
|
1334
|
-
const cioRes = await chatJson(client, cioSystem, batchCioUser);
|
|
1192
|
+
const cioRes = await chatJson(client, cioSystem, batchCioUser, transportResultFrom("9router", client.model, { framingStripped: true }));
|
|
1335
1193
|
for (const x of cioRes.cios ?? []) {
|
|
1336
1194
|
if (x.concept_id && x.cio) cioByConcept[x.concept_id] = standardStatement(x.cio);
|
|
1337
1195
|
}
|
|
@@ -1356,15 +1214,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1356
1214
|
" context where it is used. Application/Analysis/Creation skills are assessed",
|
|
1357
1215
|
" INDIRECTLY through completing these tasks \u2014 there is no fixed Bloom ceiling.",
|
|
1358
1216
|
" MACHINE CHECK: every item MUST contain at least one ACTION VERB from this whitelist",
|
|
1359
|
-
" (
|
|
1360
|
-
" save, handle, parse, validate, test, register, compute, generate, print, format,",
|
|
1361
|
-
" convert, extend, define, execute, run, read, fetch, wrap, log, call, raise, install,",
|
|
1362
|
-
" import, export, declare, embed, model, represent, override, customize, enforce, mock,",
|
|
1363
|
-
" assert, query, index, cache, stream, split, join, strip, encode, decode, traverse,",
|
|
1364
|
-
" measure, evaluate, benchmark, diagnose, demonstrate, verify, organize, structure,",
|
|
1365
|
-
" choose, select, compare, distinguish, attach, annotate, expose, accept, manage, group,",
|
|
1366
|
-
" sort, filter, map, reduce, catch, throw, migrate, inspect, trace, parameterize,",
|
|
1367
|
-
" construct, design, capture) \u2014 OR wrap the concrete construct in backticks.",
|
|
1217
|
+
" (" + COMPLETABLE_VERBS.join(", ") + ") \u2014 OR wrap the concrete construct in backticks.",
|
|
1368
1218
|
" Items with abstract verbs (understand, learn, know, grasp, appreciate, explore) and",
|
|
1369
1219
|
" no backtick construct FAIL the audit. Prefer starting the task phrase with the verb.",
|
|
1370
1220
|
"3. 2-3 items per concept, each referencing a DIFFERENT concrete construct.",
|
|
@@ -1392,7 +1242,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1392
1242
|
"Return JSON:",
|
|
1393
1243
|
'{"sios":[{"concept_id":"C1","sio":["...","..."]}]}'
|
|
1394
1244
|
].join("\n");
|
|
1395
|
-
const sioRes = await chatJson(client, sioSystem, batchSioUser);
|
|
1245
|
+
const sioRes = await chatJson(client, sioSystem, batchSioUser, transportResultFrom("9router", client.model, { framingStripped: true }));
|
|
1396
1246
|
for (const x of sioRes.sios ?? []) {
|
|
1397
1247
|
if (x.concept_id && Array.isArray(x.sio)) sioByConcept[x.concept_id] = x.sio.map((item) => standardStatement(String(item)));
|
|
1398
1248
|
}
|
|
@@ -1445,7 +1295,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1445
1295
|
"- CIO: HOW mechanism, technology-agnostic, more concrete than ULO, Marr 2-Language neutral.",
|
|
1446
1296
|
"- SIO: keyword-bound completable implementation tasks naming real constructs of " + techStack + ".",
|
|
1447
1297
|
"- 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).",
|
|
1448
|
-
"- COMPLETABILITY RULE: every SIO item must contain an actionable verb (build, create,
|
|
1298
|
+
"- 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.",
|
|
1449
1299
|
"",
|
|
1450
1300
|
JSON_ONLY
|
|
1451
1301
|
].join("\n");
|
|
@@ -1493,7 +1343,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
|
|
|
1493
1343
|
"Depth-layer contract audit failed after repair for: " + detail + " \u2014 regenerate or fix the prompt contract (fail-closed, no silent fallback)"
|
|
1494
1344
|
);
|
|
1495
1345
|
}
|
|
1496
|
-
return { uloByConcept, cioByConcept, sioByConcept, audits, warnings };
|
|
1346
|
+
return { uloByConcept, cioByConcept, sioByConcept, audits, warnings, lastTransport: audits[0]?.transport };
|
|
1497
1347
|
}
|
|
1498
1348
|
function cell(v) {
|
|
1499
1349
|
return (v || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -1545,7 +1395,7 @@ function loadKnowledgeTreeCatalog(tsvPath) {
|
|
|
1545
1395
|
}
|
|
1546
1396
|
function tokens(s) {
|
|
1547
1397
|
return new Set(
|
|
1548
|
-
s
|
|
1398
|
+
stripDiacritics(s).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4)
|
|
1549
1399
|
);
|
|
1550
1400
|
}
|
|
1551
1401
|
function filterBySelection(catalog, selectedCategories, selectedTopics, analysisConcepts, maxConcepts = 120) {
|
|
@@ -1636,7 +1486,7 @@ async function standardizeConcepts(opts) {
|
|
|
1636
1486
|
" Adopt the tree's terminology. Only propose new when the tree genuinely has NO concept teaching that knowledge (broader AND narrower both fail).",
|
|
1637
1487
|
' 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.',
|
|
1638
1488
|
" If between 1 and 3 tree concepts are CLOSE but not exact, list their codes in possible_tree_matches for human review.",
|
|
1639
|
-
`For a "standardized" decision ALSO return "adopted_keywords":
|
|
1489
|
+
`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.`,
|
|
1640
1490
|
"Rules:",
|
|
1641
1491
|
"- Judge by MEANING (definition + keywords), not by string similarity.",
|
|
1642
1492
|
'- 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.',
|
|
@@ -1676,10 +1526,25 @@ async function standardizeConcepts(opts) {
|
|
|
1676
1526
|
const tc = catalog.concepts.find((x) => x.code === d.tree_code);
|
|
1677
1527
|
if (!tc) throw new Error("Concept " + c.id + " standardized to unknown tree code " + d.tree_code + " \u2014 fail-closed");
|
|
1678
1528
|
const treeKw = new Set(tc.keywords.map((k) => k.toLowerCase()));
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1529
|
+
const rawAdopted = (d.adopted_keywords || []).map((k) => String(k)).filter(Boolean);
|
|
1530
|
+
let adopted;
|
|
1531
|
+
if (tc.keywords.length === 0) {
|
|
1532
|
+
adopted = rawAdopted.filter((k) => (c.keywords ?? []).some((ck) => ck.toLowerCase() === k.toLowerCase()));
|
|
1533
|
+
if (adopted.length === 0) adopted = (c.keywords ?? []).slice(0, 8);
|
|
1534
|
+
warnings.push("Concept " + c.id + ": tree concept " + tc.code + " has no keyword registry \u2014 adopted " + adopted.length + " keyword(s) from the analysis concept");
|
|
1535
|
+
} else {
|
|
1536
|
+
const norm = (k) => k.toLowerCase().replace(/ies$/, "y").replace(/s$/, "");
|
|
1537
|
+
const treeKwNorm = new Set([...treeKw].map(norm));
|
|
1538
|
+
const dropped = [];
|
|
1539
|
+
adopted = rawAdopted.filter((k) => {
|
|
1540
|
+
if (treeKw.has(k.toLowerCase()) || treeKwNorm.has(norm(k))) return true;
|
|
1541
|
+
dropped.push(k);
|
|
1542
|
+
return false;
|
|
1543
|
+
});
|
|
1544
|
+
if (dropped.length > 0) {
|
|
1545
|
+
warnings.push("Concept " + c.id + ": dropped " + dropped.length + " adopted keyword(s) not in tree concept " + tc.code + " registry: " + dropped.join(", "));
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1683
1548
|
if (tc.keywords.length > 0 && adopted.length === 0) {
|
|
1684
1549
|
throw new Error("Concept " + c.id + " standardized to " + tc.code + " without adopted_keywords \u2014 fail-closed");
|
|
1685
1550
|
}
|
|
@@ -1719,6 +1584,15 @@ async function standardizeConcepts(opts) {
|
|
|
1719
1584
|
}
|
|
1720
1585
|
|
|
1721
1586
|
// src/graph/knowledgeGraphPipeline.ts
|
|
1587
|
+
var MIN_CALIBRATION_MINUTES = 15;
|
|
1588
|
+
var MAX_CALIBRATION_MINUTES = 60;
|
|
1589
|
+
var DEFAULT_CONCEPT_MINUTES = 30;
|
|
1590
|
+
var MIN_CALIBRATION_DIVERGENCE = 0.5;
|
|
1591
|
+
function calibrateConceptMinutes(sioCount, problemTypeCount, prereqCount, hasHighBloom) {
|
|
1592
|
+
const ratio = (value, cap) => Math.min(1, Math.max(0, value) / cap);
|
|
1593
|
+
const score = 0.3 * ratio(sioCount, 4) + 0.25 * ratio(problemTypeCount, 4) + 0.2 * ratio(prereqCount, 4) + 0.25 * (hasHighBloom ? 1 : 0);
|
|
1594
|
+
return Math.round(MIN_CALIBRATION_MINUTES + score * (MAX_CALIBRATION_MINUTES - MIN_CALIBRATION_MINUTES));
|
|
1595
|
+
}
|
|
1722
1596
|
function topologicalSort(concepts) {
|
|
1723
1597
|
const inDegree = /* @__PURE__ */ new Map();
|
|
1724
1598
|
const adjacency = /* @__PURE__ */ new Map();
|
|
@@ -1765,7 +1639,7 @@ async function generateKnowledgeGraph(options) {
|
|
|
1765
1639
|
log("STEP_1", `Parsed syllabus: ${parsedSyllabus.units.length} units, ${parsedSyllabus.allTopics.length} topics`);
|
|
1766
1640
|
}
|
|
1767
1641
|
const client = createLlmClient(llmConfig);
|
|
1768
|
-
const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "
|
|
1642
|
+
const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "32000", 10);
|
|
1769
1643
|
let truncatedSyllabus = "";
|
|
1770
1644
|
if (syllabusText) {
|
|
1771
1645
|
if (syllabusText.length > maxSyllabusChars) {
|
|
@@ -1884,7 +1758,7 @@ Return JSON:
|
|
|
1884
1758
|
}
|
|
1885
1759
|
{
|
|
1886
1760
|
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"]);
|
|
1887
|
-
const tokensFrom = (text) => text
|
|
1761
|
+
const tokensFrom = (text) => stripDiacritics(text).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP.has(w));
|
|
1888
1762
|
for (const c of concepts) {
|
|
1889
1763
|
const existing = (c.keywords || []).map((k) => String(k).trim()).filter(Boolean);
|
|
1890
1764
|
if (existing.length === 0) {
|
|
@@ -1955,23 +1829,32 @@ Return JSON:
|
|
|
1955
1829
|
}
|
|
1956
1830
|
}
|
|
1957
1831
|
const idByTreeCode = new Map(concepts.filter((c) => c.tree_code).map((c) => [c.tree_code, c.id]));
|
|
1832
|
+
const unmappedTreePrereqs = /* @__PURE__ */ new Set();
|
|
1958
1833
|
for (const c of concepts) {
|
|
1959
1834
|
for (const tp of c.tree_prerequisites || []) {
|
|
1960
1835
|
const target = idByTreeCode.get(tp);
|
|
1961
1836
|
if (target && target !== c.id && !(c.prerequisites || []).includes(target)) {
|
|
1962
1837
|
c.prerequisites = [...c.prerequisites || [], target];
|
|
1963
1838
|
warnings.push("Prereq seeded from tree: " + c.id + " <- " + tp + " (" + target + ")");
|
|
1839
|
+
} else if (!target) {
|
|
1840
|
+
unmappedTreePrereqs.add(tp);
|
|
1964
1841
|
}
|
|
1965
1842
|
}
|
|
1966
1843
|
}
|
|
1844
|
+
if (unmappedTreePrereqs.size > 0) {
|
|
1845
|
+
const shown = [...unmappedTreePrereqs].slice(0, 10).join(", ");
|
|
1846
|
+
warnings.push("Tree prereqs outside THIS analysis (no edge seeded, " + unmappedTreePrereqs.size + " codes): " + shown + (unmappedTreePrereqs.size > 10 ? ", ..." : ""));
|
|
1847
|
+
}
|
|
1967
1848
|
log("STEP_2_5", "Standardized: " + std.decisions.filter((d) => d.decision === "standardized").length + ", proposed new: " + std.decisions.filter((d) => d.decision === "proposed_new").length);
|
|
1849
|
+
} else {
|
|
1850
|
+
warnings.push("No knowledgeTreePath provided \u2014 concepts are NOT standardized against the knowledge tree (all proposed_new)");
|
|
1968
1851
|
}
|
|
1969
1852
|
log("STEP_3", "Checking for prerequisite cycles...");
|
|
1970
1853
|
const cycles = detectPrerequisiteCycles(concepts);
|
|
1971
1854
|
if (cycles.length > 0) {
|
|
1972
1855
|
log("STEP_3", `Found ${cycles.length} cycle(s) in prerequisite chain \u2014 breaking cycles`);
|
|
1973
1856
|
for (const cycle of cycles) {
|
|
1974
|
-
warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014
|
|
1857
|
+
warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 removing the closing back-edge (last DFS edge; pedagogically arbitrary)`);
|
|
1975
1858
|
}
|
|
1976
1859
|
const { fixed, removed } = breakPrerequisiteCycles(concepts, cycles);
|
|
1977
1860
|
for (let i = 0; i < concepts.length; i++) {
|
|
@@ -2052,14 +1935,11 @@ Return JSON:
|
|
|
2052
1935
|
const hasHighBloom = (concept.problem_types || []).some(
|
|
2053
1936
|
(pt) => ["Analyze", "Evaluate", "Create"].includes(pt.bloom_level || "")
|
|
2054
1937
|
);
|
|
2055
|
-
const
|
|
2056
|
-
const
|
|
2057
|
-
|
|
2058
|
-
if (llmEstimate === 30 || Math.abs(calibrated - llmEstimate) / llmEstimate > 0.5) {
|
|
1938
|
+
const calibrated = calibrateConceptMinutes(sioCount, ptCount, prereqCount, hasHighBloom);
|
|
1939
|
+
const llmEstimate = concept.estimated_minutes || DEFAULT_CONCEPT_MINUTES;
|
|
1940
|
+
if (Math.abs(calibrated - llmEstimate) / llmEstimate > MIN_CALIBRATION_DIVERGENCE) {
|
|
2059
1941
|
concept.estimated_minutes = calibrated;
|
|
2060
|
-
|
|
2061
|
-
warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m (score=" + score.toFixed(2) + ")");
|
|
2062
|
-
}
|
|
1942
|
+
warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m");
|
|
2063
1943
|
}
|
|
2064
1944
|
}
|
|
2065
1945
|
log("STEP_5c", "Minute calibration complete");
|
|
@@ -2084,7 +1964,8 @@ Return JSON:
|
|
|
2084
1964
|
return {
|
|
2085
1965
|
knowledgeGraph: verifiedGraph,
|
|
2086
1966
|
warnings,
|
|
2087
|
-
verificationReport: report
|
|
1967
|
+
verificationReport: report,
|
|
1968
|
+
provenance: client.callLog
|
|
2088
1969
|
};
|
|
2089
1970
|
}
|
|
2090
1971
|
|
|
@@ -2111,9 +1992,10 @@ async function generateHybridGraph(options) {
|
|
|
2111
1992
|
description: "",
|
|
2112
1993
|
total_project_minutes: 0,
|
|
2113
1994
|
total_concept_minutes: 0,
|
|
2114
|
-
|
|
1995
|
+
linked_prereq_minutes: 0
|
|
2115
1996
|
},
|
|
2116
|
-
warnings
|
|
1997
|
+
warnings,
|
|
1998
|
+
provenance: []
|
|
2117
1999
|
};
|
|
2118
2000
|
}
|
|
2119
2001
|
const client = createLlmClient(llmConfig);
|
|
@@ -2194,9 +2076,14 @@ async function generateHybridGraph(options) {
|
|
|
2194
2076
|
description: "Progressive completion path: " + phases.length + " phases, " + features.length + " features, " + concepts.length + " concepts",
|
|
2195
2077
|
total_project_minutes: totalProjectMinutes,
|
|
2196
2078
|
total_concept_minutes: totalConceptMinutes,
|
|
2197
|
-
|
|
2079
|
+
// HG-1 (2026-10-05): renamed from estimated_total_minutes. It counts only
|
|
2080
|
+
// concepts that HAVE a feature link (max per concept, not summed), so it
|
|
2081
|
+
// is NOT the course total — see HybridGraphSchema.linked_prereq_minutes.
|
|
2082
|
+
// The phase planner below commits to totalProjectMinutes + totalConceptMinutes.
|
|
2083
|
+
linked_prereq_minutes: totalProjectMinutes + totalPrereqMinutes
|
|
2198
2084
|
},
|
|
2199
|
-
warnings
|
|
2085
|
+
warnings,
|
|
2086
|
+
provenance: client.callLog
|
|
2200
2087
|
};
|
|
2201
2088
|
}
|
|
2202
2089
|
function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
@@ -2272,12 +2159,20 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
|
2272
2159
|
} else {
|
|
2273
2160
|
newIntroPhase.set(cid, order);
|
|
2274
2161
|
}
|
|
2162
|
+
introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
|
|
2163
|
+
}
|
|
2164
|
+
for (const intro of introductions) {
|
|
2165
|
+
const cid = intro.concept_id;
|
|
2166
|
+
if (intro.aspect === "advanced") continue;
|
|
2167
|
+
const introOrder = newIntroPhase.get(cid);
|
|
2168
|
+
if (introOrder === void 0 || introOrder !== order) continue;
|
|
2275
2169
|
for (const pre of prereqOf.get(cid) || []) {
|
|
2276
|
-
|
|
2277
|
-
|
|
2170
|
+
const previewOrder = previewPhase.get(pre);
|
|
2171
|
+
const covered = newIntroPhase.has(pre) || previewOrder !== void 0 && previewOrder < order;
|
|
2172
|
+
if (!covered) {
|
|
2173
|
+
errors.push("Phase " + id + ": concept " + cid + " introduced before its prerequisite " + pre + " is introduced (or previewed in an earlier phase) - violates the prerequisite chain");
|
|
2278
2174
|
}
|
|
2279
2175
|
}
|
|
2280
|
-
introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
|
|
2281
2176
|
}
|
|
2282
2177
|
phases.push({
|
|
2283
2178
|
id,
|
|
@@ -2315,6 +2210,9 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
|
|
|
2315
2210
|
if (anchor !== void 0 && anchor <= pOrder) {
|
|
2316
2211
|
errors.push("Concept " + cid + " previewed in phase " + pOrder + " but fully introduced in phase " + anchor + " - preview must precede the anchor introduction");
|
|
2317
2212
|
}
|
|
2213
|
+
if (anchor === void 0) {
|
|
2214
|
+
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");
|
|
2215
|
+
}
|
|
2318
2216
|
}
|
|
2319
2217
|
return { phases, errors };
|
|
2320
2218
|
}
|
|
@@ -2371,6 +2269,6 @@ async function decomposePhases(args) {
|
|
|
2371
2269
|
return phases;
|
|
2372
2270
|
}
|
|
2373
2271
|
|
|
2374
|
-
export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditPhasePlan, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName,
|
|
2375
|
-
//# sourceMappingURL=chunk-
|
|
2376
|
-
//# sourceMappingURL=chunk-
|
|
2272
|
+
export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditPhasePlan, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName, detectPrerequisiteCycles, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, generateHybridGraph, generateKnowledgeGraph, sioCode, standardStatement, techTagFor, uloCode, verifyKnowledgeGraph, verifyProjectGraph };
|
|
2273
|
+
//# sourceMappingURL=chunk-4UCXYRM3.mjs.map
|
|
2274
|
+
//# sourceMappingURL=chunk-4UCXYRM3.mjs.map
|