@thanh01.pmt/domain-kit 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/{chunk-GEUDHZE7.mjs → chunk-23TQPTHG.mjs} +173 -4
- package/dist/chunk-23TQPTHG.mjs.map +1 -0
- package/dist/chunk-7XGWW6CB.mjs +3 -0
- package/dist/{chunk-F4RZNBOE.mjs.map → chunk-7XGWW6CB.mjs.map} +1 -1
- package/dist/{chunk-VFART56O.mjs → chunk-BL5NBKPH.mjs} +12 -8
- package/dist/chunk-BL5NBKPH.mjs.map +1 -0
- package/dist/{chunk-3VVPSRAM.mjs → chunk-CRN6D4HG.mjs} +1056 -92
- package/dist/chunk-CRN6D4HG.mjs.map +1 -0
- package/dist/{chunk-3H2HX7PR.mjs → chunk-HUDFN4IX.mjs} +58 -8
- package/dist/chunk-HUDFN4IX.mjs.map +1 -0
- package/dist/chunk-IJZUV7N3.mjs +3 -0
- package/dist/chunk-IJZUV7N3.mjs.map +1 -0
- package/dist/chunk-R57WTNXW.mjs +3 -0
- package/dist/chunk-R57WTNXW.mjs.map +1 -0
- package/dist/chunk-T4Y2DE2W.mjs +187 -0
- package/dist/chunk-T4Y2DE2W.mjs.map +1 -0
- package/dist/{chunk-PM42MMDJ.mjs → chunk-WHBAGJWN.mjs} +48 -3
- package/dist/chunk-WHBAGJWN.mjs.map +1 -0
- package/dist/chunk-ZKLT27T4.mjs +3 -0
- package/dist/chunk-ZKLT27T4.mjs.map +1 -0
- package/dist/concepts/index.d.cts +2 -2
- package/dist/concepts/index.d.ts +2 -2
- package/dist/cpp-CZUPqJt9.d.cts +98 -0
- package/dist/cpp-CZUPqJt9.d.ts +98 -0
- package/dist/curriculumFeedEmitter-E5DAJYUZ.mjs +4 -0
- package/dist/curriculumFeedEmitter-E5DAJYUZ.mjs.map +1 -0
- package/dist/detector/index.cjs +10 -6
- package/dist/detector/index.cjs.map +1 -1
- package/dist/detector/index.d.cts +4 -19
- package/dist/detector/index.d.ts +4 -19
- package/dist/detector/index.mjs +2 -1
- package/dist/domainProfileDetector-DHb1T0zB.d.ts +22 -0
- package/dist/domainProfileDetector-bzugdQmS.d.cts +22 -0
- package/dist/extractors/index.d.cts +3 -3
- package/dist/extractors/index.d.ts +3 -3
- package/dist/feed/index.cjs +56 -6
- package/dist/feed/index.cjs.map +1 -1
- package/dist/feed/index.mjs +2 -1
- package/dist/graph/index.cjs +370 -66
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +7 -67
- package/dist/graph/index.d.ts +7 -67
- package/dist/graph/index.mjs +3 -2
- package/dist/hybridGraphPipeline-DVZYLCAQ.d.ts +159 -0
- package/dist/hybridGraphPipeline-DmWRKYXf.d.cts +159 -0
- package/dist/index.cjs +1303 -700
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.mjs +10 -7
- package/dist/{keywordExtractor-zPAz2isq.d.cts → keywordExtractor-BxlGRMHC.d.cts} +1 -1
- package/dist/{keywordExtractor-CKaXqSku.d.ts → keywordExtractor-DU5XRN8-.d.ts} +1 -1
- package/dist/parsers/index.cjs +185 -0
- package/dist/parsers/index.cjs.map +1 -1
- package/dist/parsers/index.d.cts +2 -98
- package/dist/parsers/index.d.ts +2 -98
- package/dist/parsers/index.mjs +2 -1
- package/dist/pipeline/index.cjs +3142 -8
- package/dist/pipeline/index.cjs.map +1 -1
- package/dist/pipeline/index.d.cts +87 -4
- package/dist/pipeline/index.d.ts +87 -4
- package/dist/pipeline/index.mjs +6 -2
- package/dist/schemas/index.cjs +51 -0
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +222 -3
- package/dist/schemas/index.d.ts +222 -3
- package/dist/schemas/index.mjs +1 -1
- package/dist/syllabusParser-MSX1fEzW.d.cts +40 -0
- package/dist/syllabusParser-MSX1fEzW.d.ts +40 -0
- package/package.json +9 -10
- package/dist/chunk-3H2HX7PR.mjs.map +0 -1
- package/dist/chunk-3VVPSRAM.mjs.map +0 -1
- package/dist/chunk-F4RZNBOE.mjs +0 -3
- package/dist/chunk-GEUDHZE7.mjs.map +0 -1
- package/dist/chunk-MOKNFLUD.mjs +0 -851
- package/dist/chunk-MOKNFLUD.mjs.map +0 -1
- package/dist/chunk-PM42MMDJ.mjs.map +0 -1
- package/dist/chunk-VFART56O.mjs.map +0 -1
- package/dist/graphVerifier-DsTP9uAN.d.ts +0 -37
- package/dist/graphVerifier-QjgDAJce.d.cts +0 -37
|
@@ -1,8 +1,856 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseSyllabus } from './chunk-T4Y2DE2W.mjs';
|
|
2
2
|
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
|
|
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
|
+
// src/graph/scaffoldExtractor.ts
|
|
260
|
+
async function extractScaffold(options) {
|
|
261
|
+
const { goal, techStack, fileList, llmConfig } = options;
|
|
262
|
+
const client = createLlmClient(llmConfig);
|
|
263
|
+
const fileListStr = fileList.length > 0 ? fileList.join("\n") : "(empty)";
|
|
264
|
+
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
|
+
const userPrompt = `App goal: ${goal}
|
|
266
|
+
Tech stack: ${techStack}
|
|
267
|
+
|
|
268
|
+
PROJECT FILE LIST (base-project):
|
|
269
|
+
${fileListStr}
|
|
270
|
+
|
|
271
|
+
Return JSON:
|
|
272
|
+
{
|
|
273
|
+
"feature": {
|
|
274
|
+
"id": "F0",
|
|
275
|
+
"name": "FOUNDATION & SETUP",
|
|
276
|
+
"description": "Get familiar with the tools, create/clone the project, learn minimal knowledge and the repo map",
|
|
277
|
+
"platform": "app",
|
|
278
|
+
"steps": [
|
|
279
|
+
{
|
|
280
|
+
"id": "F0-S1",
|
|
281
|
+
"sequence": 1,
|
|
282
|
+
"name": "Action step name",
|
|
283
|
+
"description": "Detailed description of the concrete actions",
|
|
284
|
+
"files": [],
|
|
285
|
+
"api_usage": [],
|
|
286
|
+
"keywords": ["Xcode", "Terminal"],
|
|
287
|
+
"completion_level": "base",
|
|
288
|
+
"intent": "Learn how to setup the IDE and run a minimal Swift app",
|
|
289
|
+
"outcome": {"user_visible": "App builds and runs", "technical": "Toolchain verified"},
|
|
290
|
+
"acceptance": ["Xcode builds without errors", "Simulator launches successfully"],
|
|
291
|
+
"effort": {"estimated_minutes": 20, "complexity": "low"}
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
`;
|
|
297
|
+
try {
|
|
298
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
299
|
+
temperature: 0.1,
|
|
300
|
+
maxTokens: parseInt(process.env.PG_C0_MAX_TOKENS || "16384", 10)
|
|
301
|
+
});
|
|
302
|
+
const feat = result.feature;
|
|
303
|
+
if (!feat || !Array.isArray(feat.steps) || feat.steps.length === 0) {
|
|
304
|
+
console.warn("[WARN] Scaffold LLM returned no steps \u2014 skipping F0");
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const steps = feat.steps.map((s, i) => ({
|
|
308
|
+
id: `F0-S${i + 1}`,
|
|
309
|
+
sequence: i + 1,
|
|
310
|
+
name: String(s.name || `Step ${i + 1}`),
|
|
311
|
+
description: String(s.description || ""),
|
|
312
|
+
files: Array.isArray(s.files) ? s.files : [],
|
|
313
|
+
api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
|
|
314
|
+
keywords: Array.isArray(s.keywords) ? s.keywords : [],
|
|
315
|
+
completion_level: "base",
|
|
316
|
+
platform: String(s.platform || "app"),
|
|
317
|
+
intent: String(s.intent || ""),
|
|
318
|
+
outcome: s.outcome && typeof s.outcome === "object" ? s.outcome : void 0,
|
|
319
|
+
acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
|
|
320
|
+
effort: s.effort && typeof s.effort === "object" ? s.effort : void 0,
|
|
321
|
+
requirement_ids: [],
|
|
322
|
+
concept_codes: []
|
|
323
|
+
}));
|
|
324
|
+
return {
|
|
325
|
+
id: "F0",
|
|
326
|
+
name: String(feat.name || "FOUNDATION & SETUP"),
|
|
327
|
+
description: String(feat.description || ""),
|
|
328
|
+
platform: String(feat.platform || "app"),
|
|
329
|
+
steps
|
|
330
|
+
};
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.error(`[ERROR] extractScaffold failed: ${err}`);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/graph/overviewExtractor.ts
|
|
338
|
+
function buildSourceText(fileContentsMap) {
|
|
339
|
+
const blocks = [];
|
|
340
|
+
for (const [relPath, content] of fileContentsMap) {
|
|
341
|
+
blocks.push(`### FILE: ${relPath}
|
|
342
|
+
${content}`);
|
|
343
|
+
}
|
|
344
|
+
return blocks.join("\n\n");
|
|
345
|
+
}
|
|
346
|
+
async function extractProjectOverview(options) {
|
|
347
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig } = options;
|
|
348
|
+
const client = createLlmClient(llmConfig);
|
|
349
|
+
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
350
|
+
const allFilesStr = buildSourceText(fileContentsMap);
|
|
351
|
+
let keywordsAnchor = "";
|
|
352
|
+
if (astKeywords && astKeywords.length > 0) {
|
|
353
|
+
const kwItems = astKeywords.slice(0, 30).map(
|
|
354
|
+
(kw) => `- ${kw.keyword} (from ${kw.source})`
|
|
355
|
+
);
|
|
356
|
+
if (kwItems.length > 0) {
|
|
357
|
+
keywordsAnchor = "\nAST Domain Keywords & Patterns (from static analysis \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
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.";
|
|
361
|
+
const userPrompt = `App goal: ${goal}
|
|
362
|
+
Tech stack: ${techStack}
|
|
363
|
+
${keywordsAnchor}
|
|
364
|
+
Filtered SDK API list (used_in_demo):
|
|
365
|
+
${JSON.stringify(usedSdkApis, null, 0)}
|
|
366
|
+
|
|
367
|
+
Source code of the files:
|
|
368
|
+
${allFilesStr}
|
|
369
|
+
|
|
370
|
+
Return JSON per the schema:
|
|
371
|
+
{
|
|
372
|
+
"schema_version": 2,
|
|
373
|
+
"project": {"name": "...", "project_type": "app", "platforms": ["ios"]},
|
|
374
|
+
"product": {
|
|
375
|
+
"purpose": "END-USER problem being solved",
|
|
376
|
+
"problem_statement": "Core user problem",
|
|
377
|
+
"primary_users": ["who the main users are"],
|
|
378
|
+
"development_stages": [
|
|
379
|
+
{
|
|
380
|
+
"stage": "Stage 1: Foundation & Setup",
|
|
381
|
+
"need": ["Core app shell"],
|
|
382
|
+
"learn": ["App lifecycle, basic UI"],
|
|
383
|
+
"product_state": "App launches with static mock data",
|
|
384
|
+
"temporary_approach": "Hardcoded array",
|
|
385
|
+
"validation": "Main screen loads"
|
|
386
|
+
}
|
|
387
|
+
],
|
|
388
|
+
"features": [
|
|
389
|
+
{"id": "F1", "name": "Feature name", "description": "Feature description", "platform": "ios"}
|
|
390
|
+
],
|
|
391
|
+
"user_journeys": [{"name": "...", "feature_ids": ["F1"]}]
|
|
392
|
+
},
|
|
393
|
+
"architecture": {"layers": [], "services": [], "state_management": "..."},
|
|
394
|
+
"decomposition": {"milestones": [{"id": "M1", "phase": "MVP", "name": "...", "goal": "...", "feature_ids": ["F1"]}]}
|
|
395
|
+
}`;
|
|
396
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
397
|
+
temperature: 0.1,
|
|
398
|
+
maxTokens: parseInt(process.env.PG_C1_MAX_TOKENS || "32768", 10)
|
|
399
|
+
});
|
|
400
|
+
const prod = result.product;
|
|
401
|
+
if (prod && !Array.isArray(prod.development_stages)) {
|
|
402
|
+
prod.development_stages = [];
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/graph/stepExtractor.ts
|
|
408
|
+
function buildSourceText2(fileContentsMap) {
|
|
409
|
+
const blocks = [];
|
|
410
|
+
for (const [relPath, content] of fileContentsMap) {
|
|
411
|
+
blocks.push(`### FILE: ${relPath}
|
|
412
|
+
${content}`);
|
|
413
|
+
}
|
|
414
|
+
return blocks.join("\n\n");
|
|
415
|
+
}
|
|
416
|
+
async function extractFeatureSteps(options) {
|
|
417
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig } = options;
|
|
418
|
+
const client = createLlmClient(llmConfig);
|
|
419
|
+
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
420
|
+
const allFilesStr = buildSourceText2(fileContentsMap);
|
|
421
|
+
const fid = feature.id || "F_UNK";
|
|
422
|
+
const fname = feature.name || fid;
|
|
423
|
+
const fdesc = feature.description || "";
|
|
424
|
+
const systemPrompt = "You are a software architect. Split ONE feature into ACTION steps (implementation steps) the learner must code sequentially. Each step must carry files[] (CORRECT paths in the source), api_usage[] (only from the provided SDK API index), keywords[] (real terms/language features in the files), and completion_level \u2208 {base, mvp, extend, polish}.\n - base = foundation/setup\n - mvp = make the app minimally WORKING for this feature\n - extend = make it real/richer\n - polish = robustness (error handling, tests, validation)\nInclude metadata: intent, outcome ({user_visible, technical}), acceptance[] (2 verifiable criteria), effort ({estimated_minutes, complexity}).\ndescription: ONE concise sentence, MAX 140 characters. NEVER invent files/APIs. Return ONLY the steps array. ALL text must be in ENGLISH.";
|
|
425
|
+
const userPrompt = `App goal: ${goal}
|
|
426
|
+
Tech stack: ${techStack}
|
|
427
|
+
|
|
428
|
+
FEATURE TO SPLIT: ${fid} \u2014 ${fname}
|
|
429
|
+
Feature description: ${fdesc}
|
|
430
|
+
|
|
431
|
+
SDK API list (used_in_demo):
|
|
432
|
+
${JSON.stringify(usedSdkApis)}
|
|
433
|
+
|
|
434
|
+
Source code:
|
|
435
|
+
${allFilesStr}
|
|
436
|
+
|
|
437
|
+
Split this feature into 2-6 action steps. Return JSON:
|
|
438
|
+
{
|
|
439
|
+
"steps": [
|
|
440
|
+
{
|
|
441
|
+
"id": "${fid}-S1",
|
|
442
|
+
"sequence": 1,
|
|
443
|
+
"name": "Action step name",
|
|
444
|
+
"description": "One concise sentence (max 140 chars)",
|
|
445
|
+
"files": ["path/to/file.swift"],
|
|
446
|
+
"api_usage": ["ChatClient"],
|
|
447
|
+
"keywords": ["@State", "NavigationStack"],
|
|
448
|
+
"completion_level": "mvp",
|
|
449
|
+
"intent": "Create reactive state container",
|
|
450
|
+
"outcome": {"user_visible": "Chat view updates", "technical": "ChatViewModel conforms to ObservableObject"},
|
|
451
|
+
"acceptance": ["ViewModel publishes messages", "UI re-renders on state mutation"],
|
|
452
|
+
"effort": {"estimated_minutes": 30, "complexity": "medium"}
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
}`;
|
|
456
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
457
|
+
temperature: 0.1,
|
|
458
|
+
maxTokens: parseInt(process.env.PG_C2_MAX_TOKENS || "32768", 10)
|
|
459
|
+
});
|
|
460
|
+
const rawSteps = result.steps || [];
|
|
461
|
+
const clampedSteps = rawSteps.length > 6 ? rawSteps.slice(0, 6) : rawSteps.length < 2 && rawSteps.length > 0 ? rawSteps : rawSteps;
|
|
462
|
+
if (rawSteps.length > 6) {
|
|
463
|
+
console.warn(`[WARN] LLM returned ${rawSteps.length} steps for ${fid}, clamped to 6`);
|
|
464
|
+
} else if (rawSteps.length === 0) {
|
|
465
|
+
console.warn(`[WARN] LLM returned 0 steps for ${fid}`);
|
|
466
|
+
}
|
|
467
|
+
return clampedSteps.map((s, i) => ({
|
|
468
|
+
id: `${fid}-S${i + 1}`,
|
|
469
|
+
sequence: i + 1,
|
|
470
|
+
name: String(s.name || `Step ${i + 1}`),
|
|
471
|
+
description: String(s.description || ""),
|
|
472
|
+
files: Array.isArray(s.files) ? s.files : [],
|
|
473
|
+
api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
|
|
474
|
+
keywords: Array.isArray(s.keywords) ? s.keywords : [],
|
|
475
|
+
completion_level: String(s.completion_level || "mvp"),
|
|
476
|
+
platform: String(s.platform || feature.platform || "app"),
|
|
477
|
+
intent: String(s.intent || ""),
|
|
478
|
+
outcome: s.outcome,
|
|
479
|
+
acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
|
|
480
|
+
effort: s.effort,
|
|
481
|
+
requirement_ids: [],
|
|
482
|
+
concept_codes: []
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig) {
|
|
486
|
+
const client = createLlmClient(llmConfig);
|
|
487
|
+
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
488
|
+
const sourceText = buildSourceText2(fileContentsMap);
|
|
489
|
+
const featuresListing = featuresMeta.map(
|
|
490
|
+
(f) => `- ${f.id || "F?"}: ${f.name || ""} \u2014 ${f.description || ""}`
|
|
491
|
+
).join("\n");
|
|
492
|
+
const systemPrompt = "You are a software architect. Split MULTIPLE features into ACTION steps. Each step must carry files[], api_usage[], keywords[], and completion_level \u2208 {base, mvp, extend, polish}. Include metadata: intent, outcome ({user_visible, technical}), acceptance[] (2 verifiable criteria), effort ({estimated_minutes, complexity}). description: ONE concise sentence, MAX 140 characters. NEVER invent files/APIs. Return ONLY the JSON. ALL text must be in ENGLISH.";
|
|
493
|
+
const userPrompt = `App goal: ${goal}
|
|
494
|
+
Tech stack: ${techStack}
|
|
495
|
+
|
|
496
|
+
SDK API list (used_in_demo):
|
|
497
|
+
${JSON.stringify(usedSdkApis)}
|
|
498
|
+
|
|
499
|
+
Source code:
|
|
500
|
+
${sourceText}
|
|
501
|
+
|
|
502
|
+
FEATURES to split into steps:
|
|
503
|
+
${featuresListing}
|
|
504
|
+
|
|
505
|
+
For EACH feature above, produce 4-6 action steps. Each step must include:
|
|
506
|
+
id, sequence, name, description, files, api_usage, keywords, completion_level, platform,
|
|
507
|
+
intent, outcome ({user_visible, technical}), acceptance[] (2 criteria), effort ({estimated_minutes, complexity}).
|
|
508
|
+
|
|
509
|
+
Return JSON with a "steps" dict keyed by feature_id:
|
|
510
|
+
{
|
|
511
|
+
"steps": {
|
|
512
|
+
"<feature_id>": [
|
|
513
|
+
{
|
|
514
|
+
"id": "<feature_id>-S1",
|
|
515
|
+
"sequence": 1,
|
|
516
|
+
"name": "Step name",
|
|
517
|
+
"description": "Concise sentence (max 140 chars)",
|
|
518
|
+
"files": ["path/to/file"],
|
|
519
|
+
"api_usage": ["APIName"],
|
|
520
|
+
"keywords": ["keyword"],
|
|
521
|
+
"completion_level": "mvp",
|
|
522
|
+
"platform": "app",
|
|
523
|
+
"intent": "Step intent",
|
|
524
|
+
"outcome": {"user_visible": "User visible result", "technical": "Technical result"},
|
|
525
|
+
"acceptance": ["Criterion 1", "Criterion 2"],
|
|
526
|
+
"effort": {"estimated_minutes": 30, "complexity": "medium"}
|
|
527
|
+
}
|
|
528
|
+
]
|
|
529
|
+
}
|
|
530
|
+
}`;
|
|
531
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
532
|
+
temperature: 0.1,
|
|
533
|
+
maxTokens: parseInt(process.env.PG_C2_MAX_TOKENS || "32768", 10)
|
|
534
|
+
});
|
|
535
|
+
const stepsDict = result.steps;
|
|
536
|
+
const output = /* @__PURE__ */ new Map();
|
|
537
|
+
if (stepsDict && typeof stepsDict === "object") {
|
|
538
|
+
for (const [fid, rawSteps] of Object.entries(stepsDict)) {
|
|
539
|
+
if (!Array.isArray(rawSteps)) continue;
|
|
540
|
+
const featMeta = featuresMeta.find((f) => f.id === fid);
|
|
541
|
+
const steps = rawSteps.map((s, i) => ({
|
|
542
|
+
id: `${fid}-S${i + 1}`,
|
|
543
|
+
sequence: i + 1,
|
|
544
|
+
name: String(s.name || `Step ${i + 1}`),
|
|
545
|
+
description: String(s.description || ""),
|
|
546
|
+
files: Array.isArray(s.files) ? s.files : [],
|
|
547
|
+
api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
|
|
548
|
+
keywords: Array.isArray(s.keywords) ? s.keywords : [],
|
|
549
|
+
completion_level: String(s.completion_level || "mvp"),
|
|
550
|
+
platform: String(s.platform || featMeta?.platform || "app"),
|
|
551
|
+
intent: String(s.intent || ""),
|
|
552
|
+
outcome: s.outcome,
|
|
553
|
+
acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
|
|
554
|
+
effort: s.effort,
|
|
555
|
+
requirement_ids: [],
|
|
556
|
+
concept_codes: []
|
|
557
|
+
}));
|
|
558
|
+
output.set(fid, steps);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return output;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/graph/graphVerifier.ts
|
|
565
|
+
function verifyProjectGraph(projectGraph, options) {
|
|
566
|
+
const { sdkApiIndex, fileContentsMap } = options;
|
|
567
|
+
const sdkApiNames = new Set(sdkApiIndex.sdk_apis.map((api) => api.name));
|
|
568
|
+
const hallucinations = [];
|
|
569
|
+
const features = projectGraph.features || [];
|
|
570
|
+
for (const feature of features) {
|
|
571
|
+
const fid = feature.id || "F_UNK";
|
|
572
|
+
const isFoundation = fid === "F0";
|
|
573
|
+
for (const step of feature.steps || []) {
|
|
574
|
+
const sid = step.id || `${fid}-SUNK`;
|
|
575
|
+
const validFiles = [];
|
|
576
|
+
for (const f of step.files || []) {
|
|
577
|
+
if (fileContentsMap.has(f)) {
|
|
578
|
+
validFiles.push(f);
|
|
579
|
+
} else {
|
|
580
|
+
hallucinations.push({
|
|
581
|
+
type: "file",
|
|
582
|
+
item: f,
|
|
583
|
+
feature_id: fid,
|
|
584
|
+
step_id: sid,
|
|
585
|
+
reason: `File '${f}' not found in repository`
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
step.files = validFiles;
|
|
590
|
+
const validApis = [];
|
|
591
|
+
for (const api of step.api_usage || []) {
|
|
592
|
+
if (sdkApiNames.size === 0 || sdkApiNames.has(api)) {
|
|
593
|
+
validApis.push(api);
|
|
594
|
+
} else {
|
|
595
|
+
hallucinations.push({
|
|
596
|
+
type: "api",
|
|
597
|
+
item: api,
|
|
598
|
+
feature_id: fid,
|
|
599
|
+
step_id: sid,
|
|
600
|
+
reason: `API '${api}' not found in SDK API index`
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
step.api_usage = validApis;
|
|
605
|
+
if (isFoundation) continue;
|
|
606
|
+
const validKws = [];
|
|
607
|
+
for (const kw of step.keywords || []) {
|
|
608
|
+
const kwLow = kw.toLowerCase();
|
|
609
|
+
const isSingleWord = !kwLow.includes(" ");
|
|
610
|
+
let found = false;
|
|
611
|
+
for (const f of validFiles) {
|
|
612
|
+
const content = (fileContentsMap.get(f) || "").toLowerCase();
|
|
613
|
+
if (isSingleWord) {
|
|
614
|
+
found = new RegExp(`\\b${kwLow.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(content);
|
|
615
|
+
} else {
|
|
616
|
+
found = content.includes(kwLow);
|
|
617
|
+
}
|
|
618
|
+
if (found) break;
|
|
619
|
+
}
|
|
620
|
+
if (!found) {
|
|
621
|
+
for (const content of fileContentsMap.values()) {
|
|
622
|
+
const lc = content.toLowerCase();
|
|
623
|
+
if (isSingleWord) {
|
|
624
|
+
found = new RegExp(`\\b${kwLow.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(lc);
|
|
625
|
+
} else {
|
|
626
|
+
found = lc.includes(kwLow);
|
|
627
|
+
}
|
|
628
|
+
if (found) break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (found) {
|
|
632
|
+
validKws.push(kw);
|
|
633
|
+
} else {
|
|
634
|
+
hallucinations.push({
|
|
635
|
+
type: "keyword",
|
|
636
|
+
item: kw,
|
|
637
|
+
feature_id: fid,
|
|
638
|
+
step_id: sid,
|
|
639
|
+
reason: `Keyword '${kw}' not found in target files`
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
step.keywords = validKws;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return { graph: projectGraph, hallucinations };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/graph/conceptEscalator.ts
|
|
650
|
+
function toUpperSnake(term) {
|
|
651
|
+
let s = term.replace(/^[^\w]+/, "");
|
|
652
|
+
s = s.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
|
|
653
|
+
s = s.replace(/[^\w]+/g, "_");
|
|
654
|
+
const res = s.toUpperCase().replace(/^_+|_+$/g, "");
|
|
655
|
+
return res || "CONCEPT";
|
|
656
|
+
}
|
|
657
|
+
function extractTermSnippets(term, fileContentsMap, maxSnippets = 1) {
|
|
658
|
+
const snippets = [];
|
|
659
|
+
const termLow = term.toLowerCase();
|
|
660
|
+
for (const [relPath, content] of fileContentsMap) {
|
|
661
|
+
const lines = content.split("\n");
|
|
662
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
663
|
+
if (lines[idx].toLowerCase().includes(termLow)) {
|
|
664
|
+
const start = Math.max(0, idx - 1);
|
|
665
|
+
const end = Math.min(lines.length, idx + 2);
|
|
666
|
+
let snippet = lines.slice(start, end).map((l) => l.trim()).filter(Boolean).join(" ");
|
|
667
|
+
if (snippet.length > 120) snippet = snippet.slice(0, 120) + "...";
|
|
668
|
+
if (snippet) snippets.push(`[${relPath}]: ${snippet}`);
|
|
669
|
+
if (snippets.length >= maxSnippets) break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (snippets.length >= maxSnippets) break;
|
|
673
|
+
}
|
|
674
|
+
return snippets;
|
|
675
|
+
}
|
|
676
|
+
function inferDepthFromContext(keyword, fileContentsMap) {
|
|
677
|
+
const kw = keyword.toLowerCase();
|
|
678
|
+
const sioPatterns = [
|
|
679
|
+
/^@/,
|
|
680
|
+
// SwiftUI decorators: @State, @Binding
|
|
681
|
+
/^import\s/,
|
|
682
|
+
// Import statements
|
|
683
|
+
/\(\)$/,
|
|
684
|
+
// Function calls
|
|
685
|
+
/\.prototype/,
|
|
686
|
+
// Prototype methods
|
|
687
|
+
/^new\s/,
|
|
688
|
+
// Constructor calls
|
|
689
|
+
/^async\s/,
|
|
690
|
+
// Async keywords
|
|
691
|
+
/^await\s/
|
|
692
|
+
];
|
|
693
|
+
if (sioPatterns.some((p) => p.test(keyword))) {
|
|
694
|
+
return { depth: "sio", rationale: `Keyword '${keyword}' is a specific API/syntax pattern` };
|
|
695
|
+
}
|
|
696
|
+
const cioPatterns = [
|
|
697
|
+
/pattern/i,
|
|
698
|
+
/protocol/i,
|
|
699
|
+
/architecture/i,
|
|
700
|
+
/lifecycle/i,
|
|
701
|
+
/observer/i,
|
|
702
|
+
/singleton/i,
|
|
703
|
+
/factory/i,
|
|
704
|
+
/delegate/i,
|
|
705
|
+
/state\s*management/i,
|
|
706
|
+
/data\s*flow/i,
|
|
707
|
+
/binding/i,
|
|
708
|
+
/mvvm/i,
|
|
709
|
+
/mvc/i,
|
|
710
|
+
/flux/i,
|
|
711
|
+
/redux/i,
|
|
712
|
+
/authentication/i,
|
|
713
|
+
/authorization/i,
|
|
714
|
+
/persistence/i,
|
|
715
|
+
/caching/i,
|
|
716
|
+
/serialization/i
|
|
717
|
+
];
|
|
718
|
+
if (cioPatterns.some((p) => p.test(keyword))) {
|
|
719
|
+
return { depth: "cio", rationale: `Keyword '${keyword}' represents a mechanism/pattern` };
|
|
720
|
+
}
|
|
721
|
+
const uloPatterns = [
|
|
722
|
+
/design\s*thinking/i,
|
|
723
|
+
/user\s*needs/i,
|
|
724
|
+
/problem\s*statement/i,
|
|
725
|
+
/foundation/i,
|
|
726
|
+
/setup/i,
|
|
727
|
+
/overview/i,
|
|
728
|
+
/introduction/i,
|
|
729
|
+
/scaffold/i,
|
|
730
|
+
/hello\s*world/i,
|
|
731
|
+
/getting\s*started/i
|
|
732
|
+
];
|
|
733
|
+
if (uloPatterns.some((p) => p.test(keyword))) {
|
|
734
|
+
return { depth: "ulo", rationale: `Keyword '${keyword}' is a foundational/introductory concept` };
|
|
735
|
+
}
|
|
736
|
+
for (const content of fileContentsMap.values()) {
|
|
737
|
+
const lines = content.split("\n");
|
|
738
|
+
for (const line of lines) {
|
|
739
|
+
if (line.toLowerCase().includes(kw)) {
|
|
740
|
+
if (/^(import|from|require|use |#include)/.test(line.trim())) {
|
|
741
|
+
return { depth: "sio", rationale: `Keyword '${keyword}' found in import statement` };
|
|
742
|
+
}
|
|
743
|
+
if (/^\/\/|^\/\*|^#|^-->/.test(line.trim())) {
|
|
744
|
+
return { depth: "ulo", rationale: `Keyword '${keyword}' found in comment/doc` };
|
|
745
|
+
}
|
|
746
|
+
return { depth: "sio", rationale: `Keyword '${keyword}' used in implementation code` };
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return { depth: "sio", rationale: `Default: treating '${keyword}' as SIO (implementation-level)` };
|
|
751
|
+
}
|
|
752
|
+
async function escalateAndMapConcepts(options) {
|
|
753
|
+
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig } = options;
|
|
754
|
+
const features = projectGraph.features || [];
|
|
755
|
+
const allTerms = /* @__PURE__ */ new Set();
|
|
756
|
+
for (const feature of features) {
|
|
757
|
+
for (const step of feature.steps || []) {
|
|
758
|
+
for (const api of step.api_usage || []) allTerms.add(api);
|
|
759
|
+
for (const kw of step.keywords || []) allTerms.add(kw);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (allTerms.size === 0) return /* @__PURE__ */ new Map();
|
|
763
|
+
const termList = Array.from(allTerms).sort();
|
|
764
|
+
let llmResults = {};
|
|
765
|
+
if (conceptMapOverride) {
|
|
766
|
+
llmResults = conceptMapOverride;
|
|
767
|
+
} else {
|
|
768
|
+
try {
|
|
769
|
+
const client = createLlmClient(llmConfig);
|
|
770
|
+
const conceptBank = (availableConcepts || []).sort().join(", ") || "(empty)";
|
|
771
|
+
const termEntries = termList.map((term) => {
|
|
772
|
+
const snippets = extractTermSnippets(term, fileContentsMap);
|
|
773
|
+
return snippets.length > 0 ? `- ${term} (Usage context: ${snippets[0]})` : `- ${term}`;
|
|
774
|
+
});
|
|
775
|
+
const systemPrompt = `You are a knowledge classification expert. For each concrete keyword/API, determine:
|
|
776
|
+
1. The neutral concept it belongs to (UPPER_SNAKE_CASE)
|
|
777
|
+
2. The depth level: ulo | cio | sio
|
|
778
|
+
|
|
779
|
+
DEPTH DEFINITIONS:
|
|
780
|
+
- ulo (WHAT + WHY): The keyword introduces a concept \u2014 "what is it, why does it exist"
|
|
781
|
+
Example: In a scaffold step, "@State is SwiftUI's way of managing local view state"
|
|
782
|
+
- cio (HOW): The keyword demonstrates a mechanism \u2014 "how it works"
|
|
783
|
+
Example: "@State triggers view re-render when value changes \u2014 mechanism is value-type observation"
|
|
784
|
+
- sio (IMPLEMENTATION): The keyword is a concrete API/syntax used in code
|
|
785
|
+
Example: "Using @State var isActive: Bool = false in ContentView"
|
|
786
|
+
|
|
787
|
+
RULES:
|
|
788
|
+
1. PREFER an EXISTING concept from the CONCEPT_BANK below if the keyword is a manifestation of that concept.
|
|
789
|
+
2. ONLY create a NEW concept (UPPER_SNAKE_CASE, NOT ending in _CONCEPT, NOT containing a specific technology name) when NO concept in the bank covers it.
|
|
790
|
+
3. CONCEPT_BANK: ${conceptBank.slice(0, 12e3)}
|
|
791
|
+
4. Depth is per-keyword-in-context, NOT per-concept. The same concept can have ULO at one step and SIO at another.
|
|
792
|
+
Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "...", "depth": "ulo|cio|sio"}}}`;
|
|
793
|
+
const userPrompt = "Classify the following keyword/API list into neutral concepts WITH depth levels.\nCode usage context is provided to disambiguate depth:\n" + termEntries.join("\n") + '\n\nReturn JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "...", "depth": "ulo|cio|sio"}}}';
|
|
794
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
795
|
+
temperature: 0.1,
|
|
796
|
+
maxTokens: 16384
|
|
797
|
+
});
|
|
798
|
+
llmResults = result.results || {};
|
|
799
|
+
} catch (err) {
|
|
800
|
+
console.warn(`[WARN] Concept escalation LLM failed (${err}), using fallback depth inference`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const featureConcepts = /* @__PURE__ */ new Map();
|
|
804
|
+
for (const feature of features) {
|
|
805
|
+
const fid = feature.id || "F_UNK";
|
|
806
|
+
const featureFiles = /* @__PURE__ */ new Set();
|
|
807
|
+
for (const step of feature.steps || []) {
|
|
808
|
+
for (const f of step.files || []) featureFiles.add(f);
|
|
809
|
+
}
|
|
810
|
+
const fTerms = [
|
|
811
|
+
...feature.api_usage || [],
|
|
812
|
+
...(feature.steps || []).flatMap((s) => [...s.api_usage || [], ...s.keywords || []])
|
|
813
|
+
];
|
|
814
|
+
const items = [];
|
|
815
|
+
const seen = /* @__PURE__ */ new Set();
|
|
816
|
+
for (const kw of fTerms) {
|
|
817
|
+
const info = llmResults[kw] || {};
|
|
818
|
+
const cCode = info.concept_code || toUpperSnake(kw);
|
|
819
|
+
const cName = info.concept_name || kw;
|
|
820
|
+
let depth = "sio";
|
|
821
|
+
let depthRationale = "Default: SIO";
|
|
822
|
+
if (info.depth && ["ulo", "cio", "sio"].includes(info.depth)) {
|
|
823
|
+
depth = info.depth;
|
|
824
|
+
depthRationale = `LLM classified as ${depth}`;
|
|
825
|
+
} else {
|
|
826
|
+
const inferred = inferDepthFromContext(kw, fileContentsMap);
|
|
827
|
+
depth = inferred.depth;
|
|
828
|
+
depthRationale = inferred.rationale;
|
|
829
|
+
}
|
|
830
|
+
const kwKey = `${cCode}::${kw}`;
|
|
831
|
+
if (seen.has(kwKey)) continue;
|
|
832
|
+
seen.add(kwKey);
|
|
833
|
+
const evidenceFiles = [];
|
|
834
|
+
for (const f of featureFiles) {
|
|
835
|
+
const content = fileContentsMap.get(f) || "";
|
|
836
|
+
if (content.toLowerCase().includes(kw.toLowerCase())) {
|
|
837
|
+
evidenceFiles.push(f);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
items.push({
|
|
841
|
+
concept_code: cCode,
|
|
842
|
+
concept_name: cName,
|
|
843
|
+
keyword: kw,
|
|
844
|
+
depth,
|
|
845
|
+
depth_rationale: depthRationale,
|
|
846
|
+
evidence_files: evidenceFiles
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
featureConcepts.set(fid, items);
|
|
850
|
+
}
|
|
851
|
+
return featureConcepts;
|
|
852
|
+
}
|
|
853
|
+
|
|
6
854
|
// src/graph/loCodeStandard.ts
|
|
7
855
|
var TECH_TAG_MAP = {
|
|
8
856
|
python: "PY",
|
|
@@ -119,30 +967,160 @@ var ULO_CODE_RE = /^ULO-[A-Z][A-Z0-9_]*-\d{2}$/;
|
|
|
119
967
|
var CIO_CODE_RE = /^CIO-[A-Z][A-Z0-9_]*-\d{2}-[A-Z][A-Z0-9_]*$/;
|
|
120
968
|
var SIO_CODE_RE = /^SIO-[A-Z0-9]+-[A-Z][A-Z0-9_]*-\d{2}$/;
|
|
121
969
|
|
|
122
|
-
// src/graph/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
970
|
+
// src/graph/knowledgeGraphVerifier.ts
|
|
971
|
+
function detectPrerequisiteCycles(concepts) {
|
|
972
|
+
const cycles = [];
|
|
973
|
+
const visited = /* @__PURE__ */ new Set();
|
|
974
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
975
|
+
const path = [];
|
|
976
|
+
const prereqMap = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const c of concepts) {
|
|
978
|
+
prereqMap.set(c.id, c.prerequisites || []);
|
|
979
|
+
}
|
|
980
|
+
function dfs(conceptId) {
|
|
981
|
+
if (inStack.has(conceptId)) {
|
|
982
|
+
const cycleStart = path.indexOf(conceptId);
|
|
983
|
+
if (cycleStart >= 0) {
|
|
984
|
+
cycles.push([...path.slice(cycleStart), conceptId]);
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (visited.has(conceptId)) return;
|
|
989
|
+
visited.add(conceptId);
|
|
990
|
+
inStack.add(conceptId);
|
|
991
|
+
path.push(conceptId);
|
|
992
|
+
for (const prereq of prereqMap.get(conceptId) || []) {
|
|
993
|
+
dfs(prereq);
|
|
994
|
+
}
|
|
995
|
+
path.pop();
|
|
996
|
+
inStack.delete(conceptId);
|
|
997
|
+
}
|
|
998
|
+
for (const c of concepts) {
|
|
999
|
+
if (!visited.has(c.id)) {
|
|
1000
|
+
dfs(c.id);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return cycles;
|
|
1004
|
+
}
|
|
1005
|
+
function breakPrerequisiteCycles(concepts, cycles) {
|
|
1006
|
+
const removed = [];
|
|
1007
|
+
const fixed = concepts.map((c) => ({ ...c, prerequisites: [...c.prerequisites || []] }));
|
|
1008
|
+
for (const cycle of cycles) {
|
|
1009
|
+
if (cycle.length >= 2) {
|
|
1010
|
+
const breaker = cycle[cycle.length - 2];
|
|
1011
|
+
const target = cycle[cycle.length - 1];
|
|
1012
|
+
for (const concept of fixed) {
|
|
1013
|
+
if (concept.id === breaker && target) {
|
|
1014
|
+
concept.prerequisites = concept.prerequisites.filter((p) => p !== target);
|
|
1015
|
+
removed.push({ from: breaker, to: target });
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return { fixed, removed };
|
|
1021
|
+
}
|
|
1022
|
+
function auditSyllabusCoverage(concepts, syllabus) {
|
|
1023
|
+
if (!syllabus || syllabus.allTopics.length === 0) {
|
|
1024
|
+
return { coverageRatio: 1, totalTopics: 0, coveredTopics: 0, uncovered: [] };
|
|
1025
|
+
}
|
|
1026
|
+
const topics = syllabus.allTopics;
|
|
1027
|
+
const coveredSet = /* @__PURE__ */ new Set();
|
|
1028
|
+
for (const topic of topics) {
|
|
1029
|
+
const topicKeywords = /* @__PURE__ */ new Set([
|
|
1030
|
+
...topic.title.toLowerCase().split(/\s+/),
|
|
1031
|
+
...(topic.keywords || []).map((k) => k.toLowerCase())
|
|
1032
|
+
]);
|
|
1033
|
+
for (const concept of concepts) {
|
|
1034
|
+
concept.name.toLowerCase();
|
|
1035
|
+
(concept.description || "").toLowerCase();
|
|
1036
|
+
(concept.keywords || []).map((k) => k.toLowerCase());
|
|
1037
|
+
if (concept.id.toLowerCase() === topic.id.toLowerCase()) {
|
|
1038
|
+
coveredSet.add(topic.id);
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
const refs = concept.references || [];
|
|
1042
|
+
const hasTopicRef = refs.some(
|
|
1043
|
+
(r) => r.id === topic.id || r.title && r.title.toLowerCase() === topic.title.toLowerCase()
|
|
1044
|
+
);
|
|
1045
|
+
if (hasTopicRef) {
|
|
1046
|
+
coveredSet.add(topic.id);
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
const conceptKeywords = (concept.keywords || []).map((k) => k.toLowerCase());
|
|
1050
|
+
const hasOverlap = conceptKeywords.some((k) => topicKeywords.has(k)) || concept.name.toLowerCase().includes(topic.title.toLowerCase()) || topic.title.toLowerCase().includes(concept.name.toLowerCase());
|
|
1051
|
+
if (hasOverlap) {
|
|
1052
|
+
coveredSet.add(topic.id);
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const totalTopics = topics.length;
|
|
1058
|
+
const coveredTopics = coveredSet.size;
|
|
1059
|
+
const coverageRatio = totalTopics > 0 ? coveredTopics / totalTopics : 1;
|
|
1060
|
+
const uncovered = topics.filter((t) => !coveredSet.has(t.id)).map((t) => `${t.id}: ${t.title}`);
|
|
1061
|
+
return { coverageRatio, totalTopics, coveredTopics, uncovered };
|
|
1062
|
+
}
|
|
1063
|
+
function verifyKnowledgeGraph(kg, syllabusOrOptions) {
|
|
1064
|
+
const syllabus = syllabusOrOptions && "parsedSyllabus" in syllabusOrOptions ? syllabusOrOptions.parsedSyllabus : syllabusOrOptions;
|
|
1065
|
+
const warnings = [];
|
|
1066
|
+
let concepts = [...kg.concepts || []];
|
|
1067
|
+
const conceptIds = new Set(concepts.map((c) => c.id));
|
|
1068
|
+
for (const c of concepts) {
|
|
1069
|
+
if ((c.prerequisites || []).includes(c.id)) {
|
|
1070
|
+
c.prerequisites = (c.prerequisites || []).filter((p) => p !== c.id);
|
|
1071
|
+
warnings.push(`Self-loop removed on concept ${c.id}`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const danglingPrerequisites = [];
|
|
1075
|
+
for (const c of concepts) {
|
|
1076
|
+
const validPrereqs = [];
|
|
1077
|
+
for (const p of c.prerequisites || []) {
|
|
1078
|
+
if (!conceptIds.has(p)) {
|
|
1079
|
+
danglingPrerequisites.push({ conceptId: c.id, missingPrereqId: p });
|
|
1080
|
+
warnings.push(`Dangling prerequisite removed: ${c.id} -> ${p}`);
|
|
1081
|
+
} else {
|
|
1082
|
+
validPrereqs.push(p);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
c.prerequisites = validPrereqs;
|
|
1086
|
+
}
|
|
1087
|
+
const initialCycles = detectPrerequisiteCycles(concepts);
|
|
1088
|
+
let removedEdges = [];
|
|
1089
|
+
if (initialCycles.length > 0) {
|
|
1090
|
+
const broken = breakPrerequisiteCycles(concepts, initialCycles);
|
|
1091
|
+
concepts = broken.fixed;
|
|
1092
|
+
removedEdges = broken.removed;
|
|
1093
|
+
for (const r of removedEdges) {
|
|
1094
|
+
warnings.push(`Cycle broken: removed edge ${r.from} -> ${r.to}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const coverage = auditSyllabusCoverage(concepts, syllabus);
|
|
1098
|
+
if (coverage.coverageRatio < 1) {
|
|
1099
|
+
warnings.push(`Syllabus coverage ${(coverage.coverageRatio * 100).toFixed(1)}% (${coverage.coveredTopics}/${coverage.totalTopics} topics). Uncovered: ${coverage.uncovered.join(", ")}`);
|
|
1100
|
+
}
|
|
1101
|
+
for (const c of concepts) {
|
|
1102
|
+
if (!c.ulo) warnings.push(`Concept ${c.id} missing ULO (WHAT + WHY)`);
|
|
1103
|
+
if (!c.cio) warnings.push(`Concept ${c.id} missing CIO (HOW mechanism)`);
|
|
1104
|
+
if (!c.sio || c.sio.length === 0) warnings.push(`Concept ${c.id} missing SIO implementation items`);
|
|
1105
|
+
}
|
|
1106
|
+
const valid = initialCycles.length === 0 && danglingPrerequisites.length === 0 && coverage.coverageRatio >= 0.8;
|
|
1107
|
+
const verifiedGraph = {
|
|
1108
|
+
...kg,
|
|
1109
|
+
concepts
|
|
1110
|
+
};
|
|
1111
|
+
const report = {
|
|
1112
|
+
valid,
|
|
1113
|
+
cycles: initialCycles,
|
|
1114
|
+
removedEdges,
|
|
1115
|
+
danglingPrerequisites,
|
|
1116
|
+
coverageRatio: coverage.coverageRatio,
|
|
1117
|
+
totalSyllabusTopics: coverage.totalTopics,
|
|
1118
|
+
coveredSyllabusTopics: coverage.coveredTopics,
|
|
1119
|
+
uncoveredTopics: coverage.uncovered,
|
|
1120
|
+
warnings
|
|
1121
|
+
};
|
|
1122
|
+
return { verifiedGraph, report };
|
|
1123
|
+
}
|
|
146
1124
|
|
|
147
1125
|
// src/graph/depthAudit.ts
|
|
148
1126
|
var TECH_SIGNAL_RES = [
|
|
@@ -238,6 +1216,31 @@ function auditDepthLayers(concepts) {
|
|
|
238
1216
|
});
|
|
239
1217
|
}
|
|
240
1218
|
|
|
1219
|
+
// src/graph/loExemplars.ts
|
|
1220
|
+
var ULO_EXEMPLARS = [
|
|
1221
|
+
// Calibrated against production learning_objectives (UNIVERSAL rows, neutral)
|
|
1222
|
+
"The learner will be able to recall the key concepts, terminology, and characteristics of layers of abstraction.",
|
|
1223
|
+
"The learner will be able to explain how layers of abstraction hide implementation details while exposing essential behavior.",
|
|
1224
|
+
"The learner will be able to explain how a program changes state through statements and control flow.",
|
|
1225
|
+
"The learner will be able to differentiate local from global scope and describe how return values affect data flow.",
|
|
1226
|
+
"The learner will be able to describe the mechanism of a loop, including its condition, step and stopping rule.",
|
|
1227
|
+
"The learner will be able to explain why a representation format must balance readability against processing efficiency."
|
|
1228
|
+
];
|
|
1229
|
+
var CIO_EXEMPLARS = [
|
|
1230
|
+
// Production CONCEPTUAL_IMPL rows: mechanism behind the clause, Marr 2-Language neutral
|
|
1231
|
+
"The learner will be able to develop a conceptual model that explains the internal mechanism by which the representation produces its effect.",
|
|
1232
|
+
"The learner will be able to compare alternative approaches for the same operation and describe the trade-offs between them.",
|
|
1233
|
+
"The learner will be able to decompose the choice of representation into decisive criteria such as cost, fidelity and tolerance to change.",
|
|
1234
|
+
"The learner will be able to describe the sequence of steps by which a process transforms its input into its output, including the stopping condition."
|
|
1235
|
+
];
|
|
1236
|
+
var SIO_EXEMPLARS = [
|
|
1237
|
+
// Production SPECIFIC_IMPL style: keyword-bound completable tasks, tech-tagged tier
|
|
1238
|
+
"The learner will be able to use argparse.ArgumentParser with add_subparsers to register add, done and list commands in a Python CLI.",
|
|
1239
|
+
"The learner will be able to define a @dataclass with type hints and a __post_init__ method that raises ValueError on invariant violation.",
|
|
1240
|
+
"The learner will be able to execute parameterized statements via sqlite3.Connection.execute inside a with conn: transaction to perform CRUD on a tasks table.",
|
|
1241
|
+
"The learner will be able to write a pytest test that runs the CLI entry point via subprocess.run and asserts the exit code and printed output."
|
|
1242
|
+
];
|
|
1243
|
+
|
|
241
1244
|
// src/graph/hierarchicalLoAuthoring.ts
|
|
242
1245
|
function excerpt(text, max) {
|
|
243
1246
|
const t = (text || "").trim();
|
|
@@ -716,58 +1719,6 @@ async function standardizeConcepts(opts) {
|
|
|
716
1719
|
}
|
|
717
1720
|
|
|
718
1721
|
// src/graph/knowledgeGraphPipeline.ts
|
|
719
|
-
function detectCycles(concepts) {
|
|
720
|
-
const cycles = [];
|
|
721
|
-
const visited = /* @__PURE__ */ new Set();
|
|
722
|
-
const inStack = /* @__PURE__ */ new Set();
|
|
723
|
-
const path = [];
|
|
724
|
-
function dfs(conceptId, prereqMap2) {
|
|
725
|
-
if (inStack.has(conceptId)) {
|
|
726
|
-
const cycleStart = path.indexOf(conceptId);
|
|
727
|
-
if (cycleStart >= 0) {
|
|
728
|
-
cycles.push([...path.slice(cycleStart), conceptId]);
|
|
729
|
-
}
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
if (visited.has(conceptId)) return;
|
|
733
|
-
visited.add(conceptId);
|
|
734
|
-
inStack.add(conceptId);
|
|
735
|
-
path.push(conceptId);
|
|
736
|
-
const prereqs = prereqMap2.get(conceptId) || [];
|
|
737
|
-
for (const prereq of prereqs) {
|
|
738
|
-
dfs(prereq, prereqMap2);
|
|
739
|
-
}
|
|
740
|
-
path.pop();
|
|
741
|
-
inStack.delete(conceptId);
|
|
742
|
-
}
|
|
743
|
-
const prereqMap = /* @__PURE__ */ new Map();
|
|
744
|
-
for (const concept of concepts) {
|
|
745
|
-
prereqMap.set(concept.id, concept.prerequisites || []);
|
|
746
|
-
}
|
|
747
|
-
for (const concept of concepts) {
|
|
748
|
-
if (!visited.has(concept.id)) {
|
|
749
|
-
dfs(concept.id, prereqMap);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return cycles;
|
|
753
|
-
}
|
|
754
|
-
function breakCycles(concepts, cycles) {
|
|
755
|
-
const removed = [];
|
|
756
|
-
const fixed = concepts.map((c) => ({ ...c, prerequisites: [...c.prerequisites || []] }));
|
|
757
|
-
for (const cycle of cycles) {
|
|
758
|
-
if (cycle.length >= 2) {
|
|
759
|
-
const breaker = cycle[cycle.length - 2];
|
|
760
|
-
const target = cycle[cycle.length - 1];
|
|
761
|
-
for (const concept of fixed) {
|
|
762
|
-
if (concept.id === breaker) {
|
|
763
|
-
concept.prerequisites = concept.prerequisites.filter((p) => p !== target);
|
|
764
|
-
removed.push({ from: breaker, to: target });
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return { fixed, removed };
|
|
770
|
-
}
|
|
771
1722
|
function topologicalSort(concepts) {
|
|
772
1723
|
const inDegree = /* @__PURE__ */ new Map();
|
|
773
1724
|
const adjacency = /* @__PURE__ */ new Map();
|
|
@@ -808,6 +1759,11 @@ async function generateKnowledgeGraph(options) {
|
|
|
808
1759
|
onProgress?.(step, msg);
|
|
809
1760
|
};
|
|
810
1761
|
log("STEP_1", "Decomposing subject into concepts...");
|
|
1762
|
+
let parsedSyllabus = options.parsedSyllabus;
|
|
1763
|
+
if (!parsedSyllabus && syllabusText) {
|
|
1764
|
+
parsedSyllabus = parseSyllabus(syllabusText);
|
|
1765
|
+
log("STEP_1", `Parsed syllabus: ${parsedSyllabus.units.length} units, ${parsedSyllabus.allTopics.length} topics`);
|
|
1766
|
+
}
|
|
811
1767
|
const client = createLlmClient(llmConfig);
|
|
812
1768
|
const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "16000", 10);
|
|
813
1769
|
let truncatedSyllabus = "";
|
|
@@ -819,10 +1775,13 @@ async function generateKnowledgeGraph(options) {
|
|
|
819
1775
|
truncatedSyllabus = syllabusText;
|
|
820
1776
|
}
|
|
821
1777
|
}
|
|
1778
|
+
const structuredTopicsGuide = parsedSyllabus && parsedSyllabus.allTopics.length > 0 ? `
|
|
1779
|
+
STRUCTURED SYLLABUS TOPICS (Every topic MUST map to at least one concept to ensure 100% syllabus coverage):
|
|
1780
|
+
` + parsedSyllabus.allTopics.map((t) => `- [${t.unitTitle}] ${t.id}: ${t.title} (Keywords: ${t.keywords.join(", ")})`).join("\n") + "\n" : "";
|
|
822
1781
|
const systemPrompt = "You are an expert curriculum designer. Decompose the given subject into a structured knowledge graph with concepts, prerequisites, and problem types.\n\nCRITICAL RULES:\n1. Each concept MUST have the three depth layers, authored against this CONTRACT:\n - ulo (WHAT + WHY): bound to the CONCEPT's intrinsic NATURE. It explains what the\n concept is and why it exists \u2014 the problem it solves in principle. It must be\n TECHNOLOGY-AGNOSTIC: never name a concrete library, API, function, file format,\n tool or language feature. Typical assessment level: Remember / Understand.\n - cio (HOW): the MECHANISM \u2014 how the concept works and solves that problem, step\n by step. Still bound to the concept's nature: more concrete than the ULO, but\n NOT tied to one specific technology (no concrete API names). CIO capability is\n assessed INDIRECTLY, through completing contextual work, not in isolation.\n - sio (IMPLEMENTATION): an array of 2-4 items. EVERY item MUST reference concrete\n KEYWORDS/technologies (a real API, function, module, file, or named pattern) and\n describe a completable, context-specific implementation task. Application,\n Analysis and Creation skills are assessed INDIRECTLY through completing these\n items. There is NO fixed Bloom ceiling for a layer \u2014 but concreteness MUST\n strictly increase: SIO more technology-bound than CIO, CIO more concrete than ULO.\n - prerequisites: concept IDs that must be learned first (MUST NOT create cycles)\n - problem_types: 2-4 problem types with bloom_level and difficulty. Remember and\n Understand problem types test the ULO/CIO directly; Apply-and-above types are\n proxies for SIO completion in context.\n - techniques: methods, formulas, tools\n2. Prerequisite chains MUST be acyclic (no concept can depend on itself or create a loop)\n3. Concepts should be ordered by prerequisite dependency (topological sort)\n4. Problem types should range from Remember to Apply (not all at same level)\n5. Group concepts into categories (chapters/sections)\n6. ALL text in ENGLISH (even if input is Vietnamese)\n7. NEVER invent concepts not present in the syllabus\n8. Return JSON matching the KnowledgeGraphSchema";
|
|
823
1782
|
const userPrompt = `Subject: ${subject}
|
|
824
1783
|
${description ? `Description: ${description}` : ""}
|
|
825
|
-
${truncatedSyllabus ? `Syllabus:
|
|
1784
|
+
${structuredTopicsGuide}${truncatedSyllabus ? `Syllabus:
|
|
826
1785
|
${truncatedSyllabus}` : ""}
|
|
827
1786
|
${gradeBand ? `Grade band: ${gradeBand[0]}-${gradeBand[1]}` : ""}
|
|
828
1787
|
${domain ? `Domain: ${domain}` : ""}
|
|
@@ -967,13 +1926,13 @@ Return JSON:
|
|
|
967
1926
|
log("STEP_2_5", "Standardized: " + std.decisions.filter((d) => d.decision === "standardized").length + ", proposed new: " + std.decisions.filter((d) => d.decision === "proposed_new").length);
|
|
968
1927
|
}
|
|
969
1928
|
log("STEP_3", "Checking for prerequisite cycles...");
|
|
970
|
-
const cycles =
|
|
1929
|
+
const cycles = detectPrerequisiteCycles(concepts);
|
|
971
1930
|
if (cycles.length > 0) {
|
|
972
1931
|
log("STEP_3", `Found ${cycles.length} cycle(s) in prerequisite chain \u2014 breaking cycles`);
|
|
973
1932
|
for (const cycle of cycles) {
|
|
974
1933
|
warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 breaking at weakest edge`);
|
|
975
1934
|
}
|
|
976
|
-
const { fixed, removed } =
|
|
1935
|
+
const { fixed, removed } = breakPrerequisiteCycles(concepts, cycles);
|
|
977
1936
|
for (let i = 0; i < concepts.length; i++) {
|
|
978
1937
|
const fixedConcept = fixed.find((f) => f.id === concepts[i].id);
|
|
979
1938
|
if (fixedConcept) {
|
|
@@ -1024,16 +1983,21 @@ Return JSON:
|
|
|
1024
1983
|
}
|
|
1025
1984
|
}
|
|
1026
1985
|
log("STEP_2", `Validated ${concepts.length} concepts, ${categories.length} categories, ${learningPath.length} learning path steps`);
|
|
1986
|
+
const rawGraph = {
|
|
1987
|
+
schema_version: 1,
|
|
1988
|
+
type: "knowledge_graph",
|
|
1989
|
+
subject: result.subject,
|
|
1990
|
+
concepts,
|
|
1991
|
+
categories,
|
|
1992
|
+
learning_path: learningPath
|
|
1993
|
+
};
|
|
1994
|
+
log("STEP_6", "Running Knowledge Graph verification & reverse coverage audit...");
|
|
1995
|
+
const { verifiedGraph, report } = verifyKnowledgeGraph(rawGraph, parsedSyllabus);
|
|
1996
|
+
warnings.push(...report.warnings);
|
|
1027
1997
|
return {
|
|
1028
|
-
knowledgeGraph:
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
subject: result.subject,
|
|
1032
|
-
concepts,
|
|
1033
|
-
categories,
|
|
1034
|
-
learning_path: learningPath
|
|
1035
|
-
},
|
|
1036
|
-
warnings
|
|
1998
|
+
knowledgeGraph: verifiedGraph,
|
|
1999
|
+
warnings,
|
|
2000
|
+
verificationReport: report
|
|
1037
2001
|
};
|
|
1038
2002
|
}
|
|
1039
2003
|
|
|
@@ -1292,6 +2256,6 @@ async function decomposePhases(args) {
|
|
|
1292
2256
|
return phases;
|
|
1293
2257
|
}
|
|
1294
2258
|
|
|
1295
|
-
export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, cioActionSlug, cioCode, conceptCodeFromName, generateHybridGraph, generateKnowledgeGraph, sioCode, standardStatement, techTagFor, uloCode };
|
|
1296
|
-
//# sourceMappingURL=chunk-
|
|
1297
|
-
//# sourceMappingURL=chunk-
|
|
2259
|
+
export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName, createLlmClient, detectPrerequisiteCycles, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, generateHybridGraph, generateKnowledgeGraph, llmChatJson, sioCode, standardStatement, techTagFor, uloCode, verifyKnowledgeGraph, verifyProjectGraph };
|
|
2260
|
+
//# sourceMappingURL=chunk-CRN6D4HG.mjs.map
|
|
2261
|
+
//# sourceMappingURL=chunk-CRN6D4HG.mjs.map
|