@thanh01.pmt/domain-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1101 -0
- package/dist/assembly/index.cjs +213 -0
- package/dist/assembly/index.cjs.map +1 -0
- package/dist/assembly/index.d.cts +66 -0
- package/dist/assembly/index.d.ts +66 -0
- package/dist/assembly/index.mjs +4 -0
- package/dist/assembly/index.mjs.map +1 -0
- package/dist/chunk-3H2HX7PR.mjs +324 -0
- package/dist/chunk-3H2HX7PR.mjs.map +1 -0
- package/dist/chunk-3VVPSRAM.mjs +1297 -0
- package/dist/chunk-3VVPSRAM.mjs.map +1 -0
- package/dist/chunk-DRVOV5ZD.mjs +314 -0
- package/dist/chunk-DRVOV5ZD.mjs.map +1 -0
- package/dist/chunk-F4RZNBOE.mjs +3 -0
- package/dist/chunk-F4RZNBOE.mjs.map +1 -0
- package/dist/chunk-GEUDHZE7.mjs +446 -0
- package/dist/chunk-GEUDHZE7.mjs.map +1 -0
- package/dist/chunk-HH4UX65C.mjs +100 -0
- package/dist/chunk-HH4UX65C.mjs.map +1 -0
- package/dist/chunk-KMYENICQ.mjs +3 -0
- package/dist/chunk-KMYENICQ.mjs.map +1 -0
- package/dist/chunk-KNDBODUQ.mjs +153 -0
- package/dist/chunk-KNDBODUQ.mjs.map +1 -0
- package/dist/chunk-MM2RWYNW.mjs +3 -0
- package/dist/chunk-MM2RWYNW.mjs.map +1 -0
- package/dist/chunk-MOKNFLUD.mjs +851 -0
- package/dist/chunk-MOKNFLUD.mjs.map +1 -0
- package/dist/chunk-ONQHJ4OH.mjs +211 -0
- package/dist/chunk-ONQHJ4OH.mjs.map +1 -0
- package/dist/chunk-PM42MMDJ.mjs +516 -0
- package/dist/chunk-PM42MMDJ.mjs.map +1 -0
- package/dist/chunk-RKRNOVKO.mjs +166 -0
- package/dist/chunk-RKRNOVKO.mjs.map +1 -0
- package/dist/chunk-VFART56O.mjs +884 -0
- package/dist/chunk-VFART56O.mjs.map +1 -0
- package/dist/chunk-XMHKWHVK.mjs +218 -0
- package/dist/chunk-XMHKWHVK.mjs.map +1 -0
- package/dist/conceptEscalator-YfMKIKCZ.d.ts +40 -0
- package/dist/conceptEscalator-ltRLtPhf.d.cts +40 -0
- package/dist/concepts/index.cjs +155 -0
- package/dist/concepts/index.cjs.map +1 -0
- package/dist/concepts/index.d.cts +79 -0
- package/dist/concepts/index.d.ts +79 -0
- package/dist/concepts/index.mjs +4 -0
- package/dist/concepts/index.mjs.map +1 -0
- package/dist/curriculumFeedSchema-TEeQg2bY.d.cts +549 -0
- package/dist/curriculumFeedSchema-TEeQg2bY.d.ts +549 -0
- package/dist/detector/index.cjs +889 -0
- package/dist/detector/index.cjs.map +1 -0
- package/dist/detector/index.d.cts +112 -0
- package/dist/detector/index.d.ts +112 -0
- package/dist/detector/index.mjs +3 -0
- package/dist/detector/index.mjs.map +1 -0
- package/dist/domainProfileSchema-CwT3Ffsw.d.cts +105 -0
- package/dist/domainProfileSchema-CwT3Ffsw.d.ts +105 -0
- package/dist/extractors/index.cjs +387 -0
- package/dist/extractors/index.cjs.map +1 -0
- package/dist/extractors/index.d.cts +46 -0
- package/dist/extractors/index.d.ts +46 -0
- package/dist/extractors/index.mjs +4 -0
- package/dist/extractors/index.mjs.map +1 -0
- package/dist/feed/index.cjs +421 -0
- package/dist/feed/index.cjs.map +1 -0
- package/dist/feed/index.d.cts +23 -0
- package/dist/feed/index.d.ts +23 -0
- package/dist/feed/index.mjs +4 -0
- package/dist/feed/index.mjs.map +1 -0
- package/dist/graph/index.cjs +2167 -0
- package/dist/graph/index.cjs.map +1 -0
- package/dist/graph/index.d.cts +226 -0
- package/dist/graph/index.d.ts +226 -0
- package/dist/graph/index.mjs +4 -0
- package/dist/graph/index.mjs.map +1 -0
- package/dist/graphVerifier-DsTP9uAN.d.ts +37 -0
- package/dist/graphVerifier-QjgDAJce.d.cts +37 -0
- package/dist/hybridGraphSchema-BCgXicgA.d.cts +2840 -0
- package/dist/hybridGraphSchema-BCgXicgA.d.ts +2840 -0
- package/dist/index.cjs +5539 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.mjs +17 -0
- package/dist/index.mjs.map +1 -0
- package/dist/keywordExtractor-CKaXqSku.d.ts +34 -0
- package/dist/keywordExtractor-zPAz2isq.d.cts +34 -0
- package/dist/llmClient-ysPhLjcH.d.cts +16 -0
- package/dist/llmClient-ysPhLjcH.d.ts +16 -0
- package/dist/parsers/index.cjs +323 -0
- package/dist/parsers/index.cjs.map +1 -0
- package/dist/parsers/index.d.cts +98 -0
- package/dist/parsers/index.d.ts +98 -0
- package/dist/parsers/index.mjs +4 -0
- package/dist/parsers/index.mjs.map +1 -0
- package/dist/pipeline/index.cjs +2166 -0
- package/dist/pipeline/index.cjs.map +1 -0
- package/dist/pipeline/index.d.cts +45 -0
- package/dist/pipeline/index.d.ts +45 -0
- package/dist/pipeline/index.mjs +8 -0
- package/dist/pipeline/index.mjs.map +1 -0
- package/dist/projectGraphSchema-DnD7orZV.d.cts +2581 -0
- package/dist/projectGraphSchema-DnD7orZV.d.ts +2581 -0
- package/dist/schemas/index.cjs +675 -0
- package/dist/schemas/index.cjs.map +1 -0
- package/dist/schemas/index.d.cts +253 -0
- package/dist/schemas/index.d.ts +253 -0
- package/dist/schemas/index.mjs +4 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
// src/utils/llmClient.ts
|
|
2
|
+
var LlmClientError = class extends Error {
|
|
3
|
+
constructor(message, status, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
this.name = "LlmClientError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function resolveProviderChain(config) {
|
|
11
|
+
const chain = [];
|
|
12
|
+
const push = (label, baseUrl, apiKey, model) => {
|
|
13
|
+
if (apiKey && baseUrl && model && !chain.some((c) => c.baseUrl === baseUrl && c.model === model)) {
|
|
14
|
+
chain.push({ label, baseUrl, apiKey, model });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
if (config?.apiKey) {
|
|
18
|
+
push("config", config.baseUrl || "https://api.openai.com/v1", config.apiKey, config.model || "gpt-4o-mini");
|
|
19
|
+
}
|
|
20
|
+
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 || "");
|
|
21
|
+
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 || "");
|
|
22
|
+
push("nvidia", "https://integrate.api.nvidia.com/v1", process.env.NVIDIA_API_KEY || "", process.env.NVIDIA_MODEL || "nvidia/nemotron-3-ultra-550b-a55b");
|
|
23
|
+
push("openrouter", "https://openrouter.ai/api/v1", process.env.OPENROUTER_API_KEY || "", process.env.OPENROUTER_MODEL || "nvidia/nemotron-3-ultra-550b-a55b");
|
|
24
|
+
if (process.env.OPENROUTER_MODEL) {
|
|
25
|
+
const or = chain.find((c) => c.label === "openrouter");
|
|
26
|
+
if (or) {
|
|
27
|
+
const rest = chain.filter((c) => c !== or);
|
|
28
|
+
return [or, ...rest];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return chain;
|
|
32
|
+
}
|
|
33
|
+
var MIN_REQUEST_INTERVAL_MS = 2500;
|
|
34
|
+
var lastRequestAt = 0;
|
|
35
|
+
async function pacedDelay() {
|
|
36
|
+
const wait = lastRequestAt + MIN_REQUEST_INTERVAL_MS - Date.now();
|
|
37
|
+
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
38
|
+
lastRequestAt = Date.now();
|
|
39
|
+
}
|
|
40
|
+
function createLlmClient(config) {
|
|
41
|
+
const chain = resolveProviderChain(config);
|
|
42
|
+
const primary = chain[0];
|
|
43
|
+
if (!primary) {
|
|
44
|
+
throw new LlmClientError(
|
|
45
|
+
"No LLM provider configured. Set NVIDIA_API_KEY (primary) and/or OPENROUTER_API_KEY (fallback), or pass llmConfig."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
primary.apiKey;
|
|
49
|
+
primary.baseUrl;
|
|
50
|
+
const model = primary.model;
|
|
51
|
+
async function chat(messages, options) {
|
|
52
|
+
const temperature = options?.temperature ?? config?.temperature ?? 0.1;
|
|
53
|
+
const maxTokens = options?.maxTokens ?? config?.maxTokens ?? parseInt(process.env.LLM_MAX_TOKENS || "65536", 10);
|
|
54
|
+
const attempts = [];
|
|
55
|
+
let lastError = null;
|
|
56
|
+
const TRANSIENT = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
57
|
+
const REQUEST_TIMEOUT_MS = Number(process.env.LLM_REQUEST_TIMEOUT_MS || 3e5);
|
|
58
|
+
const MAX_CHAIN_ROUNDS = 3;
|
|
59
|
+
for (let round = 1; round <= MAX_CHAIN_ROUNDS; round++) {
|
|
60
|
+
if (round > 1) {
|
|
61
|
+
attempts.push("round " + (round - 1) + " failed \u2014 backing off 20s before rewalking the chain");
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 2e4));
|
|
63
|
+
}
|
|
64
|
+
for (const provider of chain) {
|
|
65
|
+
try {
|
|
66
|
+
const headers = {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
"Authorization": `Bearer ${provider.apiKey}`
|
|
69
|
+
};
|
|
70
|
+
const payload = {
|
|
71
|
+
model: provider.model,
|
|
72
|
+
messages,
|
|
73
|
+
temperature,
|
|
74
|
+
max_tokens: maxTokens
|
|
75
|
+
};
|
|
76
|
+
await pacedDelay();
|
|
77
|
+
const doFetch = () => fetch(`${provider.baseUrl}/chat/completions`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"Authorization": `Bearer ${provider.apiKey}`
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
model: provider.model,
|
|
85
|
+
messages,
|
|
86
|
+
temperature,
|
|
87
|
+
max_tokens: maxTokens
|
|
88
|
+
}),
|
|
89
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
90
|
+
});
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await doFetch();
|
|
94
|
+
} catch (netErr) {
|
|
95
|
+
attempts.push(`${provider.label}: network error (${netErr instanceof Error ? netErr.message : String(netErr)}) \u2014 retrying once`);
|
|
96
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
97
|
+
lastRequestAt = Date.now();
|
|
98
|
+
response = await doFetch();
|
|
99
|
+
}
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const body = await response.text().catch(() => "");
|
|
102
|
+
const err = new LlmClientError(
|
|
103
|
+
`[${provider.label}] LLM API error: ${response.status} ${response.statusText} \u2014 ${body.slice(0, 200)}`,
|
|
104
|
+
response.status
|
|
105
|
+
);
|
|
106
|
+
if (response.status === 429) {
|
|
107
|
+
const retryAfterRaw = response.headers.get("retry-after");
|
|
108
|
+
const retryAfterMs = Math.min(
|
|
109
|
+
6e4,
|
|
110
|
+
Math.max(15e3, (Number.isFinite(Number(retryAfterRaw)) ? Number(retryAfterRaw) : 20) * 1e3)
|
|
111
|
+
);
|
|
112
|
+
attempts.push(`${provider.label}: 429 rate-limited \u2014 backing off ${Math.round(retryAfterMs / 1e3)}s`);
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
|
|
114
|
+
lastRequestAt = Date.now();
|
|
115
|
+
const retry = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers,
|
|
118
|
+
body: JSON.stringify(payload),
|
|
119
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
120
|
+
});
|
|
121
|
+
if (!retry.ok) throw err;
|
|
122
|
+
const retryData = await retry.json();
|
|
123
|
+
const retryContent2 = retryData.choices?.[0]?.message?.content || "";
|
|
124
|
+
if (!retryContent2.trim()) throw err;
|
|
125
|
+
return {
|
|
126
|
+
content: retryContent2,
|
|
127
|
+
model: retryData.model || provider.model,
|
|
128
|
+
provider: provider.label,
|
|
129
|
+
attempts: attempts.slice(),
|
|
130
|
+
usage: retryData.usage,
|
|
131
|
+
finishReason: retryData.choices?.[0]?.finish_reason
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (TRANSIENT.has(response.status)) {
|
|
135
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
136
|
+
const retry = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
137
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
"Authorization": `Bearer ${provider.apiKey}`
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
model: provider.model,
|
|
145
|
+
messages,
|
|
146
|
+
temperature,
|
|
147
|
+
max_tokens: maxTokens
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
if (!retry.ok) throw err;
|
|
151
|
+
const retryData = await retry.json();
|
|
152
|
+
const retryContent = retryData.choices?.[0]?.message?.content || "";
|
|
153
|
+
return {
|
|
154
|
+
content: retryContent,
|
|
155
|
+
model: retryData.model || provider.model,
|
|
156
|
+
provider: provider.label,
|
|
157
|
+
attempts: attempts.slice(),
|
|
158
|
+
usage: retryData.usage,
|
|
159
|
+
finishReason: retryData.choices?.[0]?.finish_reason
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
const data = await response.json();
|
|
165
|
+
const wrappedError = data.error;
|
|
166
|
+
if (wrappedError) {
|
|
167
|
+
throw new LlmClientError("[" + provider.label + "] upstream error " + (wrappedError.code ?? "") + ": " + (wrappedError.message ?? "unknown"));
|
|
168
|
+
}
|
|
169
|
+
const content = data.choices?.[0]?.message?.content || "";
|
|
170
|
+
if (!content.trim()) {
|
|
171
|
+
throw new LlmClientError("[" + provider.label + "] empty completion returned");
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
content,
|
|
175
|
+
model: data.model || provider.model,
|
|
176
|
+
provider: provider.label,
|
|
177
|
+
attempts: attempts.slice(),
|
|
178
|
+
usage: data.usage,
|
|
179
|
+
finishReason: data.choices?.[0]?.finish_reason
|
|
180
|
+
};
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
183
|
+
attempts.push(`${provider.label}: ${msg}`);
|
|
184
|
+
lastError = err instanceof LlmClientError ? err : new LlmClientError(`[${provider.label}] ${msg}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (round < MAX_CHAIN_ROUNDS) continue;
|
|
188
|
+
throw lastError ?? new LlmClientError("All LLM providers failed (empty chain).");
|
|
189
|
+
}
|
|
190
|
+
throw lastError ?? new LlmClientError("All LLM providers failed after " + MAX_CHAIN_ROUNDS + " rounds.");
|
|
191
|
+
}
|
|
192
|
+
return { chat, model };
|
|
193
|
+
}
|
|
194
|
+
function parseJsonFromLlm(text) {
|
|
195
|
+
let cleaned = text.trim();
|
|
196
|
+
if (cleaned.startsWith("```json")) {
|
|
197
|
+
cleaned = cleaned.slice(7);
|
|
198
|
+
} else if (cleaned.startsWith("```")) {
|
|
199
|
+
cleaned = cleaned.slice(3);
|
|
200
|
+
}
|
|
201
|
+
if (cleaned.endsWith("```")) {
|
|
202
|
+
cleaned = cleaned.slice(0, -3);
|
|
203
|
+
}
|
|
204
|
+
cleaned = cleaned.trim();
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(cleaned);
|
|
207
|
+
} catch {
|
|
208
|
+
const start = cleaned.indexOf("{");
|
|
209
|
+
if (start >= 0) {
|
|
210
|
+
let depth = 0;
|
|
211
|
+
let inString = false;
|
|
212
|
+
let escape = false;
|
|
213
|
+
for (let i = start; i < cleaned.length; i++) {
|
|
214
|
+
const ch = cleaned[i];
|
|
215
|
+
if (escape) {
|
|
216
|
+
escape = false;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (ch === "\\") {
|
|
220
|
+
escape = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (ch === '"') {
|
|
224
|
+
inString = !inString;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (inString) continue;
|
|
228
|
+
if (ch === "{") depth++;
|
|
229
|
+
if (ch === "}") {
|
|
230
|
+
depth--;
|
|
231
|
+
if (depth === 0) {
|
|
232
|
+
try {
|
|
233
|
+
return JSON.parse(cleaned.slice(start, i + 1));
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw new LlmClientError(`Failed to parse JSON from LLM response: ${cleaned.slice(0, 200)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function llmChatJson(client, systemPrompt, userPrompt, options) {
|
|
244
|
+
const result = await client.chat(
|
|
245
|
+
[
|
|
246
|
+
{ role: "system", content: systemPrompt },
|
|
247
|
+
{ role: "user", content: userPrompt }
|
|
248
|
+
],
|
|
249
|
+
options
|
|
250
|
+
);
|
|
251
|
+
return parseJsonFromLlm(result.content);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/graph/scaffoldExtractor.ts
|
|
255
|
+
async function extractScaffold(options) {
|
|
256
|
+
const { goal, techStack, fileList, llmConfig } = options;
|
|
257
|
+
const client = createLlmClient(llmConfig);
|
|
258
|
+
const fileListStr = fileList.length > 0 ? fileList.join("\n") : "(empty)";
|
|
259
|
+
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.";
|
|
260
|
+
const userPrompt = `App goal: ${goal}
|
|
261
|
+
Tech stack: ${techStack}
|
|
262
|
+
|
|
263
|
+
PROJECT FILE LIST (base-project):
|
|
264
|
+
${fileListStr}
|
|
265
|
+
|
|
266
|
+
Return JSON:
|
|
267
|
+
{
|
|
268
|
+
"feature": {
|
|
269
|
+
"id": "F0",
|
|
270
|
+
"name": "FOUNDATION & SETUP",
|
|
271
|
+
"description": "Get familiar with the tools, create/clone the project, learn minimal knowledge and the repo map",
|
|
272
|
+
"platform": "app",
|
|
273
|
+
"steps": [
|
|
274
|
+
{
|
|
275
|
+
"id": "F0-S1",
|
|
276
|
+
"sequence": 1,
|
|
277
|
+
"name": "Action step name",
|
|
278
|
+
"description": "Detailed description of the concrete actions",
|
|
279
|
+
"files": [],
|
|
280
|
+
"api_usage": [],
|
|
281
|
+
"keywords": ["Xcode", "Terminal"],
|
|
282
|
+
"completion_level": "base",
|
|
283
|
+
"intent": "Learn how to setup the IDE and run a minimal Swift app",
|
|
284
|
+
"outcome": {"user_visible": "App builds and runs", "technical": "Toolchain verified"},
|
|
285
|
+
"acceptance": ["Xcode builds without errors", "Simulator launches successfully"],
|
|
286
|
+
"effort": {"estimated_minutes": 20, "complexity": "low"}
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`;
|
|
292
|
+
try {
|
|
293
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
294
|
+
temperature: 0.1,
|
|
295
|
+
maxTokens: parseInt(process.env.PG_C0_MAX_TOKENS || "16384", 10)
|
|
296
|
+
});
|
|
297
|
+
const feat = result.feature;
|
|
298
|
+
if (!feat || !Array.isArray(feat.steps) || feat.steps.length === 0) {
|
|
299
|
+
console.warn("[WARN] Scaffold LLM returned no steps \u2014 skipping F0");
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const steps = feat.steps.map((s, i) => ({
|
|
303
|
+
id: `F0-S${i + 1}`,
|
|
304
|
+
sequence: i + 1,
|
|
305
|
+
name: String(s.name || `Step ${i + 1}`),
|
|
306
|
+
description: String(s.description || ""),
|
|
307
|
+
files: Array.isArray(s.files) ? s.files : [],
|
|
308
|
+
api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
|
|
309
|
+
keywords: Array.isArray(s.keywords) ? s.keywords : [],
|
|
310
|
+
completion_level: "base",
|
|
311
|
+
platform: String(s.platform || "app"),
|
|
312
|
+
intent: String(s.intent || ""),
|
|
313
|
+
outcome: s.outcome && typeof s.outcome === "object" ? s.outcome : void 0,
|
|
314
|
+
acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
|
|
315
|
+
effort: s.effort && typeof s.effort === "object" ? s.effort : void 0,
|
|
316
|
+
requirement_ids: [],
|
|
317
|
+
concept_codes: []
|
|
318
|
+
}));
|
|
319
|
+
return {
|
|
320
|
+
id: "F0",
|
|
321
|
+
name: String(feat.name || "FOUNDATION & SETUP"),
|
|
322
|
+
description: String(feat.description || ""),
|
|
323
|
+
platform: String(feat.platform || "app"),
|
|
324
|
+
steps
|
|
325
|
+
};
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.error(`[ERROR] extractScaffold failed: ${err}`);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/graph/overviewExtractor.ts
|
|
333
|
+
function buildSourceText(fileContentsMap) {
|
|
334
|
+
const blocks = [];
|
|
335
|
+
for (const [relPath, content] of fileContentsMap) {
|
|
336
|
+
blocks.push(`### FILE: ${relPath}
|
|
337
|
+
${content}`);
|
|
338
|
+
}
|
|
339
|
+
return blocks.join("\n\n");
|
|
340
|
+
}
|
|
341
|
+
async function extractProjectOverview(options) {
|
|
342
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig } = options;
|
|
343
|
+
const client = createLlmClient(llmConfig);
|
|
344
|
+
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
345
|
+
const allFilesStr = buildSourceText(fileContentsMap);
|
|
346
|
+
let keywordsAnchor = "";
|
|
347
|
+
if (astKeywords && astKeywords.length > 0) {
|
|
348
|
+
const kwItems = astKeywords.slice(0, 30).map(
|
|
349
|
+
(kw) => `- ${kw.keyword} (from ${kw.source})`
|
|
350
|
+
);
|
|
351
|
+
if (kwItems.length > 0) {
|
|
352
|
+
keywordsAnchor = "\nAST Domain Keywords & Patterns (from static analysis \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
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.";
|
|
356
|
+
const userPrompt = `App goal: ${goal}
|
|
357
|
+
Tech stack: ${techStack}
|
|
358
|
+
${keywordsAnchor}
|
|
359
|
+
Filtered SDK API list (used_in_demo):
|
|
360
|
+
${JSON.stringify(usedSdkApis, null, 0)}
|
|
361
|
+
|
|
362
|
+
Source code of the files:
|
|
363
|
+
${allFilesStr}
|
|
364
|
+
|
|
365
|
+
Return JSON per the schema:
|
|
366
|
+
{
|
|
367
|
+
"schema_version": 2,
|
|
368
|
+
"project": {"name": "...", "project_type": "app", "platforms": ["ios"]},
|
|
369
|
+
"product": {
|
|
370
|
+
"purpose": "END-USER problem being solved",
|
|
371
|
+
"problem_statement": "Core user problem",
|
|
372
|
+
"primary_users": ["who the main users are"],
|
|
373
|
+
"development_stages": [
|
|
374
|
+
{
|
|
375
|
+
"stage": "Stage 1: Foundation & Setup",
|
|
376
|
+
"need": ["Core app shell"],
|
|
377
|
+
"learn": ["App lifecycle, basic UI"],
|
|
378
|
+
"product_state": "App launches with static mock data",
|
|
379
|
+
"temporary_approach": "Hardcoded array",
|
|
380
|
+
"validation": "Main screen loads"
|
|
381
|
+
}
|
|
382
|
+
],
|
|
383
|
+
"features": [
|
|
384
|
+
{"id": "F1", "name": "Feature name", "description": "Feature description", "platform": "ios"}
|
|
385
|
+
],
|
|
386
|
+
"user_journeys": [{"name": "...", "feature_ids": ["F1"]}]
|
|
387
|
+
},
|
|
388
|
+
"architecture": {"layers": [], "services": [], "state_management": "..."},
|
|
389
|
+
"decomposition": {"milestones": [{"id": "M1", "phase": "MVP", "name": "...", "goal": "...", "feature_ids": ["F1"]}]}
|
|
390
|
+
}`;
|
|
391
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
392
|
+
temperature: 0.1,
|
|
393
|
+
maxTokens: parseInt(process.env.PG_C1_MAX_TOKENS || "32768", 10)
|
|
394
|
+
});
|
|
395
|
+
const prod = result.product;
|
|
396
|
+
if (prod && !Array.isArray(prod.development_stages)) {
|
|
397
|
+
prod.development_stages = [];
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/graph/stepExtractor.ts
|
|
403
|
+
function buildSourceText2(fileContentsMap) {
|
|
404
|
+
const blocks = [];
|
|
405
|
+
for (const [relPath, content] of fileContentsMap) {
|
|
406
|
+
blocks.push(`### FILE: ${relPath}
|
|
407
|
+
${content}`);
|
|
408
|
+
}
|
|
409
|
+
return blocks.join("\n\n");
|
|
410
|
+
}
|
|
411
|
+
async function extractFeatureSteps(options) {
|
|
412
|
+
const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig } = options;
|
|
413
|
+
const client = createLlmClient(llmConfig);
|
|
414
|
+
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
415
|
+
const allFilesStr = buildSourceText2(fileContentsMap);
|
|
416
|
+
const fid = feature.id || "F_UNK";
|
|
417
|
+
const fname = feature.name || fid;
|
|
418
|
+
const fdesc = feature.description || "";
|
|
419
|
+
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.";
|
|
420
|
+
const userPrompt = `App goal: ${goal}
|
|
421
|
+
Tech stack: ${techStack}
|
|
422
|
+
|
|
423
|
+
FEATURE TO SPLIT: ${fid} \u2014 ${fname}
|
|
424
|
+
Feature description: ${fdesc}
|
|
425
|
+
|
|
426
|
+
SDK API list (used_in_demo):
|
|
427
|
+
${JSON.stringify(usedSdkApis)}
|
|
428
|
+
|
|
429
|
+
Source code:
|
|
430
|
+
${allFilesStr}
|
|
431
|
+
|
|
432
|
+
Split this feature into 2-6 action steps. Return JSON:
|
|
433
|
+
{
|
|
434
|
+
"steps": [
|
|
435
|
+
{
|
|
436
|
+
"id": "${fid}-S1",
|
|
437
|
+
"sequence": 1,
|
|
438
|
+
"name": "Action step name",
|
|
439
|
+
"description": "One concise sentence (max 140 chars)",
|
|
440
|
+
"files": ["path/to/file.swift"],
|
|
441
|
+
"api_usage": ["ChatClient"],
|
|
442
|
+
"keywords": ["@State", "NavigationStack"],
|
|
443
|
+
"completion_level": "mvp",
|
|
444
|
+
"intent": "Create reactive state container",
|
|
445
|
+
"outcome": {"user_visible": "Chat view updates", "technical": "ChatViewModel conforms to ObservableObject"},
|
|
446
|
+
"acceptance": ["ViewModel publishes messages", "UI re-renders on state mutation"],
|
|
447
|
+
"effort": {"estimated_minutes": 30, "complexity": "medium"}
|
|
448
|
+
}
|
|
449
|
+
]
|
|
450
|
+
}`;
|
|
451
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
452
|
+
temperature: 0.1,
|
|
453
|
+
maxTokens: parseInt(process.env.PG_C2_MAX_TOKENS || "32768", 10)
|
|
454
|
+
});
|
|
455
|
+
const rawSteps = result.steps || [];
|
|
456
|
+
const clampedSteps = rawSteps.length > 6 ? rawSteps.slice(0, 6) : rawSteps.length < 2 && rawSteps.length > 0 ? rawSteps : rawSteps;
|
|
457
|
+
if (rawSteps.length > 6) {
|
|
458
|
+
console.warn(`[WARN] LLM returned ${rawSteps.length} steps for ${fid}, clamped to 6`);
|
|
459
|
+
} else if (rawSteps.length === 0) {
|
|
460
|
+
console.warn(`[WARN] LLM returned 0 steps for ${fid}`);
|
|
461
|
+
}
|
|
462
|
+
return clampedSteps.map((s, i) => ({
|
|
463
|
+
id: `${fid}-S${i + 1}`,
|
|
464
|
+
sequence: i + 1,
|
|
465
|
+
name: String(s.name || `Step ${i + 1}`),
|
|
466
|
+
description: String(s.description || ""),
|
|
467
|
+
files: Array.isArray(s.files) ? s.files : [],
|
|
468
|
+
api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
|
|
469
|
+
keywords: Array.isArray(s.keywords) ? s.keywords : [],
|
|
470
|
+
completion_level: String(s.completion_level || "mvp"),
|
|
471
|
+
platform: String(s.platform || feature.platform || "app"),
|
|
472
|
+
intent: String(s.intent || ""),
|
|
473
|
+
outcome: s.outcome,
|
|
474
|
+
acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
|
|
475
|
+
effort: s.effort,
|
|
476
|
+
requirement_ids: [],
|
|
477
|
+
concept_codes: []
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig) {
|
|
481
|
+
const client = createLlmClient(llmConfig);
|
|
482
|
+
const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
|
|
483
|
+
const sourceText = buildSourceText2(fileContentsMap);
|
|
484
|
+
const featuresListing = featuresMeta.map(
|
|
485
|
+
(f) => `- ${f.id || "F?"}: ${f.name || ""} \u2014 ${f.description || ""}`
|
|
486
|
+
).join("\n");
|
|
487
|
+
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.";
|
|
488
|
+
const userPrompt = `App goal: ${goal}
|
|
489
|
+
Tech stack: ${techStack}
|
|
490
|
+
|
|
491
|
+
SDK API list (used_in_demo):
|
|
492
|
+
${JSON.stringify(usedSdkApis)}
|
|
493
|
+
|
|
494
|
+
Source code:
|
|
495
|
+
${sourceText}
|
|
496
|
+
|
|
497
|
+
FEATURES to split into steps:
|
|
498
|
+
${featuresListing}
|
|
499
|
+
|
|
500
|
+
For EACH feature above, produce 4-6 action steps. Each step must include:
|
|
501
|
+
id, sequence, name, description, files, api_usage, keywords, completion_level, platform,
|
|
502
|
+
intent, outcome ({user_visible, technical}), acceptance[] (2 criteria), effort ({estimated_minutes, complexity}).
|
|
503
|
+
|
|
504
|
+
Return JSON with a "steps" dict keyed by feature_id:
|
|
505
|
+
{
|
|
506
|
+
"steps": {
|
|
507
|
+
"<feature_id>": [
|
|
508
|
+
{
|
|
509
|
+
"id": "<feature_id>-S1",
|
|
510
|
+
"sequence": 1,
|
|
511
|
+
"name": "Step name",
|
|
512
|
+
"description": "Concise sentence (max 140 chars)",
|
|
513
|
+
"files": ["path/to/file"],
|
|
514
|
+
"api_usage": ["APIName"],
|
|
515
|
+
"keywords": ["keyword"],
|
|
516
|
+
"completion_level": "mvp",
|
|
517
|
+
"platform": "app",
|
|
518
|
+
"intent": "Step intent",
|
|
519
|
+
"outcome": {"user_visible": "User visible result", "technical": "Technical result"},
|
|
520
|
+
"acceptance": ["Criterion 1", "Criterion 2"],
|
|
521
|
+
"effort": {"estimated_minutes": 30, "complexity": "medium"}
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
}
|
|
525
|
+
}`;
|
|
526
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
527
|
+
temperature: 0.1,
|
|
528
|
+
maxTokens: parseInt(process.env.PG_C2_MAX_TOKENS || "32768", 10)
|
|
529
|
+
});
|
|
530
|
+
const stepsDict = result.steps;
|
|
531
|
+
const output = /* @__PURE__ */ new Map();
|
|
532
|
+
if (stepsDict && typeof stepsDict === "object") {
|
|
533
|
+
for (const [fid, rawSteps] of Object.entries(stepsDict)) {
|
|
534
|
+
if (!Array.isArray(rawSteps)) continue;
|
|
535
|
+
const featMeta = featuresMeta.find((f) => f.id === fid);
|
|
536
|
+
const steps = rawSteps.map((s, i) => ({
|
|
537
|
+
id: `${fid}-S${i + 1}`,
|
|
538
|
+
sequence: i + 1,
|
|
539
|
+
name: String(s.name || `Step ${i + 1}`),
|
|
540
|
+
description: String(s.description || ""),
|
|
541
|
+
files: Array.isArray(s.files) ? s.files : [],
|
|
542
|
+
api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
|
|
543
|
+
keywords: Array.isArray(s.keywords) ? s.keywords : [],
|
|
544
|
+
completion_level: String(s.completion_level || "mvp"),
|
|
545
|
+
platform: String(s.platform || featMeta?.platform || "app"),
|
|
546
|
+
intent: String(s.intent || ""),
|
|
547
|
+
outcome: s.outcome,
|
|
548
|
+
acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
|
|
549
|
+
effort: s.effort,
|
|
550
|
+
requirement_ids: [],
|
|
551
|
+
concept_codes: []
|
|
552
|
+
}));
|
|
553
|
+
output.set(fid, steps);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return output;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/graph/graphVerifier.ts
|
|
560
|
+
function verifyProjectGraph(projectGraph, options) {
|
|
561
|
+
const { sdkApiIndex, fileContentsMap } = options;
|
|
562
|
+
const sdkApiNames = new Set(sdkApiIndex.sdk_apis.map((api) => api.name));
|
|
563
|
+
const hallucinations = [];
|
|
564
|
+
const features = projectGraph.features || [];
|
|
565
|
+
for (const feature of features) {
|
|
566
|
+
const fid = feature.id || "F_UNK";
|
|
567
|
+
const isFoundation = fid === "F0";
|
|
568
|
+
for (const step of feature.steps || []) {
|
|
569
|
+
const sid = step.id || `${fid}-SUNK`;
|
|
570
|
+
const validFiles = [];
|
|
571
|
+
for (const f of step.files || []) {
|
|
572
|
+
if (fileContentsMap.has(f)) {
|
|
573
|
+
validFiles.push(f);
|
|
574
|
+
} else {
|
|
575
|
+
hallucinations.push({
|
|
576
|
+
type: "file",
|
|
577
|
+
item: f,
|
|
578
|
+
feature_id: fid,
|
|
579
|
+
step_id: sid,
|
|
580
|
+
reason: `File '${f}' not found in repository`
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
step.files = validFiles;
|
|
585
|
+
const validApis = [];
|
|
586
|
+
for (const api of step.api_usage || []) {
|
|
587
|
+
if (sdkApiNames.size === 0 || sdkApiNames.has(api)) {
|
|
588
|
+
validApis.push(api);
|
|
589
|
+
} else {
|
|
590
|
+
hallucinations.push({
|
|
591
|
+
type: "api",
|
|
592
|
+
item: api,
|
|
593
|
+
feature_id: fid,
|
|
594
|
+
step_id: sid,
|
|
595
|
+
reason: `API '${api}' not found in SDK API index`
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
step.api_usage = validApis;
|
|
600
|
+
if (isFoundation) continue;
|
|
601
|
+
const validKws = [];
|
|
602
|
+
for (const kw of step.keywords || []) {
|
|
603
|
+
const kwLow = kw.toLowerCase();
|
|
604
|
+
const isSingleWord = !kwLow.includes(" ");
|
|
605
|
+
let found = false;
|
|
606
|
+
for (const f of validFiles) {
|
|
607
|
+
const content = (fileContentsMap.get(f) || "").toLowerCase();
|
|
608
|
+
if (isSingleWord) {
|
|
609
|
+
found = new RegExp(`\\b${kwLow.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(content);
|
|
610
|
+
} else {
|
|
611
|
+
found = content.includes(kwLow);
|
|
612
|
+
}
|
|
613
|
+
if (found) break;
|
|
614
|
+
}
|
|
615
|
+
if (!found) {
|
|
616
|
+
for (const content of fileContentsMap.values()) {
|
|
617
|
+
const lc = content.toLowerCase();
|
|
618
|
+
if (isSingleWord) {
|
|
619
|
+
found = new RegExp(`\\b${kwLow.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(lc);
|
|
620
|
+
} else {
|
|
621
|
+
found = lc.includes(kwLow);
|
|
622
|
+
}
|
|
623
|
+
if (found) break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (found) {
|
|
627
|
+
validKws.push(kw);
|
|
628
|
+
} else {
|
|
629
|
+
hallucinations.push({
|
|
630
|
+
type: "keyword",
|
|
631
|
+
item: kw,
|
|
632
|
+
feature_id: fid,
|
|
633
|
+
step_id: sid,
|
|
634
|
+
reason: `Keyword '${kw}' not found in target files`
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
step.keywords = validKws;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return { graph: projectGraph, hallucinations };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/graph/conceptEscalator.ts
|
|
645
|
+
function toUpperSnake(term) {
|
|
646
|
+
let s = term.replace(/^[^\w]+/, "");
|
|
647
|
+
s = s.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
|
|
648
|
+
s = s.replace(/[^\w]+/g, "_");
|
|
649
|
+
const res = s.toUpperCase().replace(/^_+|_+$/g, "");
|
|
650
|
+
return res || "CONCEPT";
|
|
651
|
+
}
|
|
652
|
+
function extractTermSnippets(term, fileContentsMap, maxSnippets = 1) {
|
|
653
|
+
const snippets = [];
|
|
654
|
+
const termLow = term.toLowerCase();
|
|
655
|
+
for (const [relPath, content] of fileContentsMap) {
|
|
656
|
+
const lines = content.split("\n");
|
|
657
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
658
|
+
if (lines[idx].toLowerCase().includes(termLow)) {
|
|
659
|
+
const start = Math.max(0, idx - 1);
|
|
660
|
+
const end = Math.min(lines.length, idx + 2);
|
|
661
|
+
let snippet = lines.slice(start, end).map((l) => l.trim()).filter(Boolean).join(" ");
|
|
662
|
+
if (snippet.length > 120) snippet = snippet.slice(0, 120) + "...";
|
|
663
|
+
if (snippet) snippets.push(`[${relPath}]: ${snippet}`);
|
|
664
|
+
if (snippets.length >= maxSnippets) break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (snippets.length >= maxSnippets) break;
|
|
668
|
+
}
|
|
669
|
+
return snippets;
|
|
670
|
+
}
|
|
671
|
+
function inferDepthFromContext(keyword, fileContentsMap) {
|
|
672
|
+
const kw = keyword.toLowerCase();
|
|
673
|
+
const sioPatterns = [
|
|
674
|
+
/^@/,
|
|
675
|
+
// SwiftUI decorators: @State, @Binding
|
|
676
|
+
/^import\s/,
|
|
677
|
+
// Import statements
|
|
678
|
+
/\(\)$/,
|
|
679
|
+
// Function calls
|
|
680
|
+
/\.prototype/,
|
|
681
|
+
// Prototype methods
|
|
682
|
+
/^new\s/,
|
|
683
|
+
// Constructor calls
|
|
684
|
+
/^async\s/,
|
|
685
|
+
// Async keywords
|
|
686
|
+
/^await\s/
|
|
687
|
+
];
|
|
688
|
+
if (sioPatterns.some((p) => p.test(keyword))) {
|
|
689
|
+
return { depth: "sio", rationale: `Keyword '${keyword}' is a specific API/syntax pattern` };
|
|
690
|
+
}
|
|
691
|
+
const cioPatterns = [
|
|
692
|
+
/pattern/i,
|
|
693
|
+
/protocol/i,
|
|
694
|
+
/architecture/i,
|
|
695
|
+
/lifecycle/i,
|
|
696
|
+
/observer/i,
|
|
697
|
+
/singleton/i,
|
|
698
|
+
/factory/i,
|
|
699
|
+
/delegate/i,
|
|
700
|
+
/state\s*management/i,
|
|
701
|
+
/data\s*flow/i,
|
|
702
|
+
/binding/i,
|
|
703
|
+
/mvvm/i,
|
|
704
|
+
/mvc/i,
|
|
705
|
+
/flux/i,
|
|
706
|
+
/redux/i,
|
|
707
|
+
/authentication/i,
|
|
708
|
+
/authorization/i,
|
|
709
|
+
/persistence/i,
|
|
710
|
+
/caching/i,
|
|
711
|
+
/serialization/i
|
|
712
|
+
];
|
|
713
|
+
if (cioPatterns.some((p) => p.test(keyword))) {
|
|
714
|
+
return { depth: "cio", rationale: `Keyword '${keyword}' represents a mechanism/pattern` };
|
|
715
|
+
}
|
|
716
|
+
const uloPatterns = [
|
|
717
|
+
/design\s*thinking/i,
|
|
718
|
+
/user\s*needs/i,
|
|
719
|
+
/problem\s*statement/i,
|
|
720
|
+
/foundation/i,
|
|
721
|
+
/setup/i,
|
|
722
|
+
/overview/i,
|
|
723
|
+
/introduction/i,
|
|
724
|
+
/scaffold/i,
|
|
725
|
+
/hello\s*world/i,
|
|
726
|
+
/getting\s*started/i
|
|
727
|
+
];
|
|
728
|
+
if (uloPatterns.some((p) => p.test(keyword))) {
|
|
729
|
+
return { depth: "ulo", rationale: `Keyword '${keyword}' is a foundational/introductory concept` };
|
|
730
|
+
}
|
|
731
|
+
for (const content of fileContentsMap.values()) {
|
|
732
|
+
const lines = content.split("\n");
|
|
733
|
+
for (const line of lines) {
|
|
734
|
+
if (line.toLowerCase().includes(kw)) {
|
|
735
|
+
if (/^(import|from|require|use |#include)/.test(line.trim())) {
|
|
736
|
+
return { depth: "sio", rationale: `Keyword '${keyword}' found in import statement` };
|
|
737
|
+
}
|
|
738
|
+
if (/^\/\/|^\/\*|^#|^-->/.test(line.trim())) {
|
|
739
|
+
return { depth: "ulo", rationale: `Keyword '${keyword}' found in comment/doc` };
|
|
740
|
+
}
|
|
741
|
+
return { depth: "sio", rationale: `Keyword '${keyword}' used in implementation code` };
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return { depth: "sio", rationale: `Default: treating '${keyword}' as SIO (implementation-level)` };
|
|
746
|
+
}
|
|
747
|
+
async function escalateAndMapConcepts(options) {
|
|
748
|
+
const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig } = options;
|
|
749
|
+
const features = projectGraph.features || [];
|
|
750
|
+
const allTerms = /* @__PURE__ */ new Set();
|
|
751
|
+
for (const feature of features) {
|
|
752
|
+
for (const step of feature.steps || []) {
|
|
753
|
+
for (const api of step.api_usage || []) allTerms.add(api);
|
|
754
|
+
for (const kw of step.keywords || []) allTerms.add(kw);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (allTerms.size === 0) return /* @__PURE__ */ new Map();
|
|
758
|
+
const termList = Array.from(allTerms).sort();
|
|
759
|
+
let llmResults = {};
|
|
760
|
+
if (conceptMapOverride) {
|
|
761
|
+
llmResults = conceptMapOverride;
|
|
762
|
+
} else {
|
|
763
|
+
try {
|
|
764
|
+
const client = createLlmClient(llmConfig);
|
|
765
|
+
const conceptBank = (availableConcepts || []).sort().join(", ") || "(empty)";
|
|
766
|
+
const termEntries = termList.map((term) => {
|
|
767
|
+
const snippets = extractTermSnippets(term, fileContentsMap);
|
|
768
|
+
return snippets.length > 0 ? `- ${term} (Usage context: ${snippets[0]})` : `- ${term}`;
|
|
769
|
+
});
|
|
770
|
+
const systemPrompt = `You are a knowledge classification expert. For each concrete keyword/API, determine:
|
|
771
|
+
1. The neutral concept it belongs to (UPPER_SNAKE_CASE)
|
|
772
|
+
2. The depth level: ulo | cio | sio
|
|
773
|
+
|
|
774
|
+
DEPTH DEFINITIONS:
|
|
775
|
+
- ulo (WHAT + WHY): The keyword introduces a concept \u2014 "what is it, why does it exist"
|
|
776
|
+
Example: In a scaffold step, "@State is SwiftUI's way of managing local view state"
|
|
777
|
+
- cio (HOW): The keyword demonstrates a mechanism \u2014 "how it works"
|
|
778
|
+
Example: "@State triggers view re-render when value changes \u2014 mechanism is value-type observation"
|
|
779
|
+
- sio (IMPLEMENTATION): The keyword is a concrete API/syntax used in code
|
|
780
|
+
Example: "Using @State var isActive: Bool = false in ContentView"
|
|
781
|
+
|
|
782
|
+
RULES:
|
|
783
|
+
1. PREFER an EXISTING concept from the CONCEPT_BANK below if the keyword is a manifestation of that concept.
|
|
784
|
+
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.
|
|
785
|
+
3. CONCEPT_BANK: ${conceptBank.slice(0, 12e3)}
|
|
786
|
+
4. Depth is per-keyword-in-context, NOT per-concept. The same concept can have ULO at one step and SIO at another.
|
|
787
|
+
Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "...", "depth": "ulo|cio|sio"}}}`;
|
|
788
|
+
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"}}}';
|
|
789
|
+
const result = await llmChatJson(client, systemPrompt, userPrompt, {
|
|
790
|
+
temperature: 0.1,
|
|
791
|
+
maxTokens: 16384
|
|
792
|
+
});
|
|
793
|
+
llmResults = result.results || {};
|
|
794
|
+
} catch (err) {
|
|
795
|
+
console.warn(`[WARN] Concept escalation LLM failed (${err}), using fallback depth inference`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const featureConcepts = /* @__PURE__ */ new Map();
|
|
799
|
+
for (const feature of features) {
|
|
800
|
+
const fid = feature.id || "F_UNK";
|
|
801
|
+
const featureFiles = /* @__PURE__ */ new Set();
|
|
802
|
+
for (const step of feature.steps || []) {
|
|
803
|
+
for (const f of step.files || []) featureFiles.add(f);
|
|
804
|
+
}
|
|
805
|
+
const fTerms = [
|
|
806
|
+
...feature.api_usage || [],
|
|
807
|
+
...(feature.steps || []).flatMap((s) => [...s.api_usage || [], ...s.keywords || []])
|
|
808
|
+
];
|
|
809
|
+
const items = [];
|
|
810
|
+
const seen = /* @__PURE__ */ new Set();
|
|
811
|
+
for (const kw of fTerms) {
|
|
812
|
+
const info = llmResults[kw] || {};
|
|
813
|
+
const cCode = info.concept_code || toUpperSnake(kw);
|
|
814
|
+
const cName = info.concept_name || kw;
|
|
815
|
+
let depth = "sio";
|
|
816
|
+
let depthRationale = "Default: SIO";
|
|
817
|
+
if (info.depth && ["ulo", "cio", "sio"].includes(info.depth)) {
|
|
818
|
+
depth = info.depth;
|
|
819
|
+
depthRationale = `LLM classified as ${depth}`;
|
|
820
|
+
} else {
|
|
821
|
+
const inferred = inferDepthFromContext(kw, fileContentsMap);
|
|
822
|
+
depth = inferred.depth;
|
|
823
|
+
depthRationale = inferred.rationale;
|
|
824
|
+
}
|
|
825
|
+
const kwKey = `${cCode}::${kw}`;
|
|
826
|
+
if (seen.has(kwKey)) continue;
|
|
827
|
+
seen.add(kwKey);
|
|
828
|
+
const evidenceFiles = [];
|
|
829
|
+
for (const f of featureFiles) {
|
|
830
|
+
const content = fileContentsMap.get(f) || "";
|
|
831
|
+
if (content.toLowerCase().includes(kw.toLowerCase())) {
|
|
832
|
+
evidenceFiles.push(f);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
items.push({
|
|
836
|
+
concept_code: cCode,
|
|
837
|
+
concept_name: cName,
|
|
838
|
+
keyword: kw,
|
|
839
|
+
depth,
|
|
840
|
+
depth_rationale: depthRationale,
|
|
841
|
+
evidence_files: evidenceFiles
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
featureConcepts.set(fid, items);
|
|
845
|
+
}
|
|
846
|
+
return featureConcepts;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export { createLlmClient, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, llmChatJson, parseJsonFromLlm, verifyProjectGraph };
|
|
850
|
+
//# sourceMappingURL=chunk-MOKNFLUD.mjs.map
|
|
851
|
+
//# sourceMappingURL=chunk-MOKNFLUD.mjs.map
|