@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.
Files changed (108) hide show
  1. package/README.md +1101 -0
  2. package/dist/assembly/index.cjs +213 -0
  3. package/dist/assembly/index.cjs.map +1 -0
  4. package/dist/assembly/index.d.cts +66 -0
  5. package/dist/assembly/index.d.ts +66 -0
  6. package/dist/assembly/index.mjs +4 -0
  7. package/dist/assembly/index.mjs.map +1 -0
  8. package/dist/chunk-3H2HX7PR.mjs +324 -0
  9. package/dist/chunk-3H2HX7PR.mjs.map +1 -0
  10. package/dist/chunk-3VVPSRAM.mjs +1297 -0
  11. package/dist/chunk-3VVPSRAM.mjs.map +1 -0
  12. package/dist/chunk-DRVOV5ZD.mjs +314 -0
  13. package/dist/chunk-DRVOV5ZD.mjs.map +1 -0
  14. package/dist/chunk-F4RZNBOE.mjs +3 -0
  15. package/dist/chunk-F4RZNBOE.mjs.map +1 -0
  16. package/dist/chunk-GEUDHZE7.mjs +446 -0
  17. package/dist/chunk-GEUDHZE7.mjs.map +1 -0
  18. package/dist/chunk-HH4UX65C.mjs +100 -0
  19. package/dist/chunk-HH4UX65C.mjs.map +1 -0
  20. package/dist/chunk-KMYENICQ.mjs +3 -0
  21. package/dist/chunk-KMYENICQ.mjs.map +1 -0
  22. package/dist/chunk-KNDBODUQ.mjs +153 -0
  23. package/dist/chunk-KNDBODUQ.mjs.map +1 -0
  24. package/dist/chunk-MM2RWYNW.mjs +3 -0
  25. package/dist/chunk-MM2RWYNW.mjs.map +1 -0
  26. package/dist/chunk-MOKNFLUD.mjs +851 -0
  27. package/dist/chunk-MOKNFLUD.mjs.map +1 -0
  28. package/dist/chunk-ONQHJ4OH.mjs +211 -0
  29. package/dist/chunk-ONQHJ4OH.mjs.map +1 -0
  30. package/dist/chunk-PM42MMDJ.mjs +516 -0
  31. package/dist/chunk-PM42MMDJ.mjs.map +1 -0
  32. package/dist/chunk-RKRNOVKO.mjs +166 -0
  33. package/dist/chunk-RKRNOVKO.mjs.map +1 -0
  34. package/dist/chunk-VFART56O.mjs +884 -0
  35. package/dist/chunk-VFART56O.mjs.map +1 -0
  36. package/dist/chunk-XMHKWHVK.mjs +218 -0
  37. package/dist/chunk-XMHKWHVK.mjs.map +1 -0
  38. package/dist/conceptEscalator-YfMKIKCZ.d.ts +40 -0
  39. package/dist/conceptEscalator-ltRLtPhf.d.cts +40 -0
  40. package/dist/concepts/index.cjs +155 -0
  41. package/dist/concepts/index.cjs.map +1 -0
  42. package/dist/concepts/index.d.cts +79 -0
  43. package/dist/concepts/index.d.ts +79 -0
  44. package/dist/concepts/index.mjs +4 -0
  45. package/dist/concepts/index.mjs.map +1 -0
  46. package/dist/curriculumFeedSchema-TEeQg2bY.d.cts +549 -0
  47. package/dist/curriculumFeedSchema-TEeQg2bY.d.ts +549 -0
  48. package/dist/detector/index.cjs +889 -0
  49. package/dist/detector/index.cjs.map +1 -0
  50. package/dist/detector/index.d.cts +112 -0
  51. package/dist/detector/index.d.ts +112 -0
  52. package/dist/detector/index.mjs +3 -0
  53. package/dist/detector/index.mjs.map +1 -0
  54. package/dist/domainProfileSchema-CwT3Ffsw.d.cts +105 -0
  55. package/dist/domainProfileSchema-CwT3Ffsw.d.ts +105 -0
  56. package/dist/extractors/index.cjs +387 -0
  57. package/dist/extractors/index.cjs.map +1 -0
  58. package/dist/extractors/index.d.cts +46 -0
  59. package/dist/extractors/index.d.ts +46 -0
  60. package/dist/extractors/index.mjs +4 -0
  61. package/dist/extractors/index.mjs.map +1 -0
  62. package/dist/feed/index.cjs +421 -0
  63. package/dist/feed/index.cjs.map +1 -0
  64. package/dist/feed/index.d.cts +23 -0
  65. package/dist/feed/index.d.ts +23 -0
  66. package/dist/feed/index.mjs +4 -0
  67. package/dist/feed/index.mjs.map +1 -0
  68. package/dist/graph/index.cjs +2167 -0
  69. package/dist/graph/index.cjs.map +1 -0
  70. package/dist/graph/index.d.cts +226 -0
  71. package/dist/graph/index.d.ts +226 -0
  72. package/dist/graph/index.mjs +4 -0
  73. package/dist/graph/index.mjs.map +1 -0
  74. package/dist/graphVerifier-DsTP9uAN.d.ts +37 -0
  75. package/dist/graphVerifier-QjgDAJce.d.cts +37 -0
  76. package/dist/hybridGraphSchema-BCgXicgA.d.cts +2840 -0
  77. package/dist/hybridGraphSchema-BCgXicgA.d.ts +2840 -0
  78. package/dist/index.cjs +5539 -0
  79. package/dist/index.cjs.map +1 -0
  80. package/dist/index.d.cts +18 -0
  81. package/dist/index.d.ts +18 -0
  82. package/dist/index.mjs +17 -0
  83. package/dist/index.mjs.map +1 -0
  84. package/dist/keywordExtractor-CKaXqSku.d.ts +34 -0
  85. package/dist/keywordExtractor-zPAz2isq.d.cts +34 -0
  86. package/dist/llmClient-ysPhLjcH.d.cts +16 -0
  87. package/dist/llmClient-ysPhLjcH.d.ts +16 -0
  88. package/dist/parsers/index.cjs +323 -0
  89. package/dist/parsers/index.cjs.map +1 -0
  90. package/dist/parsers/index.d.cts +98 -0
  91. package/dist/parsers/index.d.ts +98 -0
  92. package/dist/parsers/index.mjs +4 -0
  93. package/dist/parsers/index.mjs.map +1 -0
  94. package/dist/pipeline/index.cjs +2166 -0
  95. package/dist/pipeline/index.cjs.map +1 -0
  96. package/dist/pipeline/index.d.cts +45 -0
  97. package/dist/pipeline/index.d.ts +45 -0
  98. package/dist/pipeline/index.mjs +8 -0
  99. package/dist/pipeline/index.mjs.map +1 -0
  100. package/dist/projectGraphSchema-DnD7orZV.d.cts +2581 -0
  101. package/dist/projectGraphSchema-DnD7orZV.d.ts +2581 -0
  102. package/dist/schemas/index.cjs +675 -0
  103. package/dist/schemas/index.cjs.map +1 -0
  104. package/dist/schemas/index.d.cts +253 -0
  105. package/dist/schemas/index.d.ts +253 -0
  106. package/dist/schemas/index.mjs +4 -0
  107. package/dist/schemas/index.mjs.map +1 -0
  108. package/package.json +71 -0
@@ -0,0 +1,2166 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var child_process = require('child_process');
6
+
7
+ // src/parsers/swift.ts
8
+ var IMPORT_RE = /^import\s+(\w+)/gm;
9
+ var TYPE_RE = /\b(class|struct|enum|protocol)\s+(\w+)(?:\s*:\s*([^{]+))?/g;
10
+ var FUNC_RE = /\bfunc\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^{]+))?/g;
11
+ var WRAPPER_RE = /@(State|Binding|ObservedObject|StateObject|Published|Environment|EnvironmentObject|AppStorage|SceneStorage|FocusState|Observable)\b/g;
12
+ var FRAMEWORK_USAGE_RE = /\b(URLSession|URLRequest|JSONSerialization|JSONDecoder|JSONEncoder|Task|DispatchQueue|Timer|NotificationCenter|UserDefaults|FileManager|Bundle)\b/g;
13
+ function parseSwiftFile(filepath, content) {
14
+ const result = {
15
+ imports: [],
16
+ types: [],
17
+ functions: [],
18
+ property_wrappers: [],
19
+ error_handling: [],
20
+ frameworks_used: []
21
+ };
22
+ let match;
23
+ while ((match = IMPORT_RE.exec(content)) !== null) {
24
+ result.imports.push(match[1]);
25
+ }
26
+ const fwSeen = /* @__PURE__ */ new Set();
27
+ while ((match = FRAMEWORK_USAGE_RE.exec(content)) !== null) {
28
+ if (!fwSeen.has(match[1])) {
29
+ fwSeen.add(match[1]);
30
+ result.frameworks_used.push(match[1]);
31
+ }
32
+ }
33
+ while ((match = TYPE_RE.exec(content)) !== null) {
34
+ const conforms = (match[3] || "").split(",").map((p) => p.trim()).filter(Boolean);
35
+ result.types.push({
36
+ kind: match[1],
37
+ name: match[2],
38
+ conforms_to: conforms,
39
+ file: filepath
40
+ });
41
+ }
42
+ while ((match = FUNC_RE.exec(content)) !== null) {
43
+ const name = match[1];
44
+ if (name.startsWith("_")) continue;
45
+ result.functions.push({
46
+ name,
47
+ params: (match[2] || "").trim(),
48
+ returns: (match[3] || "").trim(),
49
+ file: filepath
50
+ });
51
+ }
52
+ const wrapperSet = /* @__PURE__ */ new Set();
53
+ while ((match = WRAPPER_RE.exec(content)) !== null) {
54
+ wrapperSet.add(match[1]);
55
+ }
56
+ result.property_wrappers = Array.from(wrapperSet);
57
+ if (content.includes("do {") || content.includes("do{")) {
58
+ result.error_handling.push("do-catch");
59
+ }
60
+ if (content.includes("throws")) {
61
+ result.error_handling.push("throws");
62
+ }
63
+ if (content.includes("Result<")) {
64
+ result.error_handling.push("Result type");
65
+ }
66
+ if (content.includes("try?")) {
67
+ result.error_handling.push("try?");
68
+ }
69
+ if (content.includes("try!")) {
70
+ result.error_handling.push("try!");
71
+ }
72
+ return result;
73
+ }
74
+ function mergeSwiftResults(results) {
75
+ const merged = {
76
+ imports: [],
77
+ types: [],
78
+ functions: [],
79
+ property_wrappers: [],
80
+ error_handling: [],
81
+ frameworks_used: []
82
+ };
83
+ const importCounts = /* @__PURE__ */ new Map();
84
+ const wrapperCounts = /* @__PURE__ */ new Map();
85
+ const errorCounts = /* @__PURE__ */ new Map();
86
+ const frameworkSet = /* @__PURE__ */ new Set();
87
+ for (const r of results) {
88
+ for (const imp of r.imports) {
89
+ importCounts.set(imp, (importCounts.get(imp) || 0) + 1);
90
+ }
91
+ merged.types.push(...r.types);
92
+ merged.functions.push(...r.functions);
93
+ for (const w of r.property_wrappers) {
94
+ wrapperCounts.set(w, (wrapperCounts.get(w) || 0) + 1);
95
+ }
96
+ for (const e of r.error_handling) {
97
+ errorCounts.set(e, (errorCounts.get(e) || 0) + 1);
98
+ }
99
+ for (const fw of r.frameworks_used) {
100
+ frameworkSet.add(fw);
101
+ }
102
+ }
103
+ merged.imports = Array.from(importCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([mod, count]) => mod);
104
+ merged.property_wrappers = Array.from(wrapperCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([w]) => w);
105
+ merged.error_handling = Array.from(errorCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([p]) => p);
106
+ merged.frameworks_used = Array.from(frameworkSet).sort();
107
+ merged.types = merged.types.slice(0, 100);
108
+ merged.functions = merged.functions.slice(0, 100);
109
+ return merged;
110
+ }
111
+
112
+ // src/parsers/typescript.ts
113
+ var IMPORT_RE2 = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
114
+ var TYPE_RE2 = /\b(interface|type|class|enum)\s+(\w+)/g;
115
+ var FUNC_RE2 = /\bfunction\s+(\w+)\s*\(([^)]*)\)/g;
116
+ var ARROW_RE = /\bconst\s+(\w+)\s*=\s*\([^)]*\)\s*(?::\s*\w+)?\s*=>/g;
117
+ function parseTsFile(filepath, content) {
118
+ const result = {
119
+ imports: [],
120
+ types: [],
121
+ functions: [],
122
+ error_handling: []
123
+ };
124
+ let match;
125
+ while ((match = IMPORT_RE2.exec(content)) !== null) {
126
+ result.imports.push(match[1]);
127
+ }
128
+ while ((match = TYPE_RE2.exec(content)) !== null) {
129
+ result.types.push({
130
+ kind: match[1],
131
+ name: match[2],
132
+ file: filepath
133
+ });
134
+ }
135
+ while ((match = FUNC_RE2.exec(content)) !== null) {
136
+ result.functions.push({
137
+ name: match[1],
138
+ params: (match[2] || "").trim(),
139
+ file: filepath
140
+ });
141
+ }
142
+ while ((match = ARROW_RE.exec(content)) !== null) {
143
+ result.functions.push({
144
+ name: match[1],
145
+ params: "",
146
+ file: filepath
147
+ });
148
+ }
149
+ if (content.includes("try {") || content.includes("try{")) {
150
+ result.error_handling.push("try-catch");
151
+ }
152
+ if (content.includes("Promise")) {
153
+ result.error_handling.push("Promise");
154
+ }
155
+ if (content.includes(".catch(")) {
156
+ result.error_handling.push(".catch()");
157
+ }
158
+ return result;
159
+ }
160
+ function mergeTsResults(results) {
161
+ const merged = {
162
+ imports: [],
163
+ types: [],
164
+ functions: [],
165
+ error_handling: []
166
+ };
167
+ const importCounts = /* @__PURE__ */ new Map();
168
+ const errorCounts = /* @__PURE__ */ new Map();
169
+ for (const r of results) {
170
+ for (const imp of r.imports) {
171
+ importCounts.set(imp, (importCounts.get(imp) || 0) + 1);
172
+ }
173
+ merged.types.push(...r.types);
174
+ merged.functions.push(...r.functions);
175
+ for (const e of r.error_handling) {
176
+ errorCounts.set(e, (errorCounts.get(e) || 0) + 1);
177
+ }
178
+ }
179
+ merged.imports = Array.from(importCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([mod]) => mod);
180
+ merged.error_handling = Array.from(errorCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([p]) => p);
181
+ merged.types = merged.types.slice(0, 100);
182
+ merged.functions = merged.functions.slice(0, 100);
183
+ return merged;
184
+ }
185
+
186
+ // src/parsers/python.ts
187
+ var IMPORT_RE3 = /^(?:import|from)\s+(\w+)/gm;
188
+ var CLASS_RE = /\bclass\s+(\w+)(?:\s*\(([^)]+)\))?/g;
189
+ var FUNC_RE3 = /\bdef\s+(\w+)\s*\(([^)]*)\)/g;
190
+ var DOCSTRING_RE = /"""([^"]{20,300})"""/g;
191
+ function parsePythonFile(filepath, content) {
192
+ const result = {
193
+ imports: [],
194
+ types: [],
195
+ functions: [],
196
+ docstrings: [],
197
+ error_handling: []
198
+ };
199
+ let match;
200
+ while ((match = IMPORT_RE3.exec(content)) !== null) {
201
+ result.imports.push(match[1]);
202
+ }
203
+ while ((match = DOCSTRING_RE.exec(content)) !== null) {
204
+ const firstSentence = match[1].trim().split("\n")[0].slice(0, 120);
205
+ if (firstSentence.length > 15) {
206
+ result.docstrings.push(firstSentence);
207
+ }
208
+ }
209
+ while ((match = CLASS_RE.exec(content)) !== null) {
210
+ const bases = (match[2] || "").split(",").map((b) => b.trim()).filter(Boolean);
211
+ result.types.push({
212
+ kind: "class",
213
+ name: match[1],
214
+ bases,
215
+ file: filepath
216
+ });
217
+ }
218
+ while ((match = FUNC_RE3.exec(content)) !== null) {
219
+ const name = match[1];
220
+ if (name.startsWith("__") && name.endsWith("__")) continue;
221
+ result.functions.push({
222
+ name,
223
+ params: (match[2] || "").trim(),
224
+ file: filepath
225
+ });
226
+ }
227
+ if (content.includes("try:")) {
228
+ result.error_handling.push("try-except");
229
+ }
230
+ if (content.includes("raise")) {
231
+ result.error_handling.push("raise");
232
+ }
233
+ return result;
234
+ }
235
+ function mergePythonResults(results) {
236
+ const merged = {
237
+ imports: [],
238
+ types: [],
239
+ functions: [],
240
+ docstrings: [],
241
+ error_handling: []
242
+ };
243
+ const importCounts = /* @__PURE__ */ new Map();
244
+ const errorCounts = /* @__PURE__ */ new Map();
245
+ for (const r of results) {
246
+ for (const imp of r.imports) {
247
+ importCounts.set(imp, (importCounts.get(imp) || 0) + 1);
248
+ }
249
+ merged.types.push(...r.types);
250
+ merged.functions.push(...r.functions);
251
+ merged.docstrings.push(...r.docstrings);
252
+ for (const e of r.error_handling) {
253
+ errorCounts.set(e, (errorCounts.get(e) || 0) + 1);
254
+ }
255
+ }
256
+ merged.imports = Array.from(importCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([mod]) => mod);
257
+ merged.error_handling = Array.from(errorCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([p]) => p);
258
+ merged.types = merged.types.slice(0, 100);
259
+ merged.functions = merged.functions.slice(0, 100);
260
+ merged.docstrings = merged.docstrings.slice(0, 20);
261
+ return merged;
262
+ }
263
+
264
+ // src/parsers/cpp.ts
265
+ var INCLUDE_RE = /#include\s*[<"]([^>"]+)[>"]/g;
266
+ var TYPE_RE3 = /\b(?:class|struct|enum)\s+(\w+)/g;
267
+ var FUNC_RE4 = /\b(?:void|int|float|double|char|bool|String|uint8_t|uint16_t|uint32_t)\s+(\w+)\s*\(/g;
268
+ function parseCppFile(filepath, content) {
269
+ const result = {
270
+ imports: [],
271
+ types: [],
272
+ functions: [],
273
+ error_handling: []
274
+ };
275
+ let match;
276
+ while ((match = INCLUDE_RE.exec(content)) !== null) {
277
+ result.imports.push(match[1]);
278
+ }
279
+ while ((match = TYPE_RE3.exec(content)) !== null) {
280
+ result.types.push({
281
+ kind: "cpp",
282
+ name: match[1],
283
+ file: filepath
284
+ });
285
+ }
286
+ while ((match = FUNC_RE4.exec(content)) !== null) {
287
+ result.functions.push({
288
+ name: match[1],
289
+ file: filepath
290
+ });
291
+ }
292
+ if (/\btry\b|\bthrow\b|\bcatch\b/.test(content)) {
293
+ result.error_handling.push("try/throw/catch");
294
+ }
295
+ return result;
296
+ }
297
+ function mergeCppResults(results) {
298
+ const merged = {
299
+ imports: [],
300
+ types: [],
301
+ functions: [],
302
+ error_handling: []
303
+ };
304
+ const importCounts = /* @__PURE__ */ new Map();
305
+ for (const r of results) {
306
+ for (const imp of r.imports) {
307
+ importCounts.set(imp, (importCounts.get(imp) || 0) + 1);
308
+ }
309
+ merged.types.push(...r.types);
310
+ merged.functions.push(...r.functions);
311
+ merged.error_handling.push(...r.error_handling);
312
+ }
313
+ merged.imports = Array.from(importCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([mod]) => mod);
314
+ merged.error_handling = [...new Set(merged.error_handling)];
315
+ return merged;
316
+ }
317
+
318
+ // src/extractors/keywordExtractor.ts
319
+ var STDLIB_MODULES = /* @__PURE__ */ new Set([
320
+ // Python
321
+ "os",
322
+ "sys",
323
+ "typing",
324
+ "json",
325
+ "re",
326
+ "math",
327
+ "random",
328
+ "time",
329
+ "datetime",
330
+ "pathlib",
331
+ "dataclasses",
332
+ "enum",
333
+ "collections",
334
+ "itertools",
335
+ "functools",
336
+ "logging",
337
+ "argparse",
338
+ "subprocess",
339
+ "threading",
340
+ "multiprocessing",
341
+ "asyncio",
342
+ "socket",
343
+ "http",
344
+ "urllib",
345
+ "ssl",
346
+ "hashlib",
347
+ "base64",
348
+ "csv",
349
+ "sqlite3",
350
+ "pickle",
351
+ "tempfile",
352
+ "shutil",
353
+ "glob",
354
+ "io",
355
+ "string",
356
+ "struct",
357
+ "uuid",
358
+ "abc",
359
+ "copy",
360
+ "decimal",
361
+ "fractions",
362
+ "statistics",
363
+ "queue",
364
+ "signal",
365
+ "traceback",
366
+ "warnings",
367
+ "weakref",
368
+ "contextlib",
369
+ "unittest",
370
+ "pytest",
371
+ "tkinter",
372
+ "tk",
373
+ "ttk",
374
+ "types",
375
+ "inspect",
376
+ "platform",
377
+ // Common third-party generic plumbing
378
+ "requests",
379
+ "urllib3",
380
+ "pydantic",
381
+ "dotenv",
382
+ "yaml",
383
+ "tomllib"
384
+ ]);
385
+ function platformFromPath(filePath) {
386
+ if (!filePath) return "app";
387
+ const fp = filePath.toLowerCase();
388
+ if (fp.endsWith(".ino") || fp.endsWith(".cpp") || fp.endsWith(".h") || fp.endsWith(".hpp") || fp.endsWith(".c")) {
389
+ if (fp.endsWith(".ino")) return "esp32";
390
+ if (fp.includes("firmware") || fp.includes("esp")) return "esp32";
391
+ return "app";
392
+ }
393
+ return "app";
394
+ }
395
+ function extractKeywords(sourceContext, basicAnalysis, repoDir, options) {
396
+ const keywords = [];
397
+ const effectiveStdlib = new Set(STDLIB_MODULES);
398
+ if ("imports" in sourceContext) {
399
+ for (const imp of sourceContext.imports) {
400
+ let module;
401
+ let count = 1;
402
+ let platform = "app";
403
+ if (typeof imp === "string") {
404
+ module = imp;
405
+ } else if (imp && typeof imp === "object" && "module" in imp) {
406
+ const impObj = imp;
407
+ module = impObj.module;
408
+ count = impObj.count || 1;
409
+ platform = platformFromPath(impObj.file || "");
410
+ } else {
411
+ continue;
412
+ }
413
+ if (effectiveStdlib.has(module.toLowerCase())) continue;
414
+ keywords.push({
415
+ keyword: module,
416
+ source: "import",
417
+ platform,
418
+ weight: 1 + (count - 1) * 0.1,
419
+ context: `Imported ${count} times`
420
+ });
421
+ }
422
+ }
423
+ if ("types" in sourceContext) {
424
+ for (const typeDecl of sourceContext.types) {
425
+ const name = typeDecl.name;
426
+ const kind = typeDecl.kind;
427
+ const platform = platformFromPath(typeDecl.file || "");
428
+ if (["View", "Model", "ViewModel", "Controller", "Service", "Manager"].includes(name)) continue;
429
+ keywords.push({
430
+ keyword: name,
431
+ source: "type_declaration",
432
+ platform,
433
+ weight: 1.5,
434
+ context: `${kind} declaration`
435
+ });
436
+ }
437
+ }
438
+ if ("functions" in sourceContext) {
439
+ for (const func of sourceContext.functions) {
440
+ const name = func.name;
441
+ const platform = platformFromPath(func.file || "");
442
+ if (["init", "setup", "configure", "initialize"].includes(name)) continue;
443
+ keywords.push({
444
+ keyword: name,
445
+ source: "function_signature",
446
+ platform,
447
+ weight: 1.2,
448
+ context: "Function signature"
449
+ });
450
+ }
451
+ }
452
+ if ("docstrings" in sourceContext && Array.isArray(sourceContext.docstrings)) {
453
+ for (const doc of sourceContext.docstrings) {
454
+ const firstSentence = doc.trim().split("\n")[0].slice(0, 120);
455
+ if (firstSentence.length > 15) {
456
+ keywords.push({
457
+ keyword: firstSentence,
458
+ source: "docstring",
459
+ platform: "app",
460
+ weight: 1.8,
461
+ context: "Docstring (domain intent)"
462
+ });
463
+ }
464
+ }
465
+ }
466
+ if ("frameworks_used" in sourceContext && Array.isArray(sourceContext.frameworks_used)) {
467
+ for (const fw of sourceContext.frameworks_used) {
468
+ const fwName = typeof fw === "string" ? fw : fw.framework || "";
469
+ if (fwName) {
470
+ keywords.push({
471
+ keyword: fwName,
472
+ source: "framework_usage",
473
+ platform: "app",
474
+ weight: 1.4,
475
+ context: "Framework usage (real practice)"
476
+ });
477
+ }
478
+ }
479
+ }
480
+ if ("property_wrappers" in sourceContext && Array.isArray(sourceContext.property_wrappers)) {
481
+ for (const wrapper of sourceContext.property_wrappers) {
482
+ const wrapperName = typeof wrapper === "string" ? wrapper : wrapper.wrapper || "";
483
+ if (wrapperName) {
484
+ keywords.push({
485
+ keyword: `@${wrapperName}`,
486
+ source: "property_wrapper",
487
+ platform: "app",
488
+ weight: 2,
489
+ context: "Property wrapper"
490
+ });
491
+ }
492
+ }
493
+ }
494
+ const errorPatterns = "error_handling" in sourceContext ? sourceContext.error_handling : "error_handling_patterns" in sourceContext ? sourceContext.error_handling_patterns || [] : [];
495
+ for (const error of errorPatterns) {
496
+ let pattern;
497
+ let platform = "app";
498
+ if (typeof error === "string") {
499
+ pattern = error;
500
+ } else if (error && typeof error === "object" && "pattern" in error) {
501
+ const errObj = error;
502
+ pattern = errObj.pattern;
503
+ platform = platformFromPath(errObj.file || "");
504
+ } else {
505
+ continue;
506
+ }
507
+ keywords.push({
508
+ keyword: pattern,
509
+ source: "error_handling",
510
+ platform,
511
+ weight: 1.3,
512
+ context: "Error handling pattern"
513
+ });
514
+ }
515
+ const seen = /* @__PURE__ */ new Set();
516
+ const uniqueKeywords = [];
517
+ for (const kw of keywords) {
518
+ const key = kw.keyword.toLowerCase();
519
+ if (!seen.has(key)) {
520
+ seen.add(key);
521
+ uniqueKeywords.push(kw);
522
+ }
523
+ }
524
+ return uniqueKeywords;
525
+ }
526
+
527
+ // src/utils/llmClient.ts
528
+ var LlmClientError = class extends Error {
529
+ constructor(message, status, cause) {
530
+ super(message);
531
+ this.status = status;
532
+ this.cause = cause;
533
+ this.name = "LlmClientError";
534
+ }
535
+ };
536
+ function resolveProviderChain(config) {
537
+ const chain = [];
538
+ const push = (label, baseUrl, apiKey, model) => {
539
+ if (apiKey && baseUrl && model && !chain.some((c) => c.baseUrl === baseUrl && c.model === model)) {
540
+ chain.push({ label, baseUrl, apiKey, model });
541
+ }
542
+ };
543
+ if (config?.apiKey) {
544
+ push("config", config.baseUrl || "https://api.openai.com/v1", config.apiKey, config.model || "gpt-4o-mini");
545
+ }
546
+ 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 || "");
547
+ 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 || "");
548
+ push("nvidia", "https://integrate.api.nvidia.com/v1", process.env.NVIDIA_API_KEY || "", process.env.NVIDIA_MODEL || "nvidia/nemotron-3-ultra-550b-a55b");
549
+ push("openrouter", "https://openrouter.ai/api/v1", process.env.OPENROUTER_API_KEY || "", process.env.OPENROUTER_MODEL || "nvidia/nemotron-3-ultra-550b-a55b");
550
+ if (process.env.OPENROUTER_MODEL) {
551
+ const or = chain.find((c) => c.label === "openrouter");
552
+ if (or) {
553
+ const rest = chain.filter((c) => c !== or);
554
+ return [or, ...rest];
555
+ }
556
+ }
557
+ return chain;
558
+ }
559
+ var MIN_REQUEST_INTERVAL_MS = 2500;
560
+ var lastRequestAt = 0;
561
+ async function pacedDelay() {
562
+ const wait = lastRequestAt + MIN_REQUEST_INTERVAL_MS - Date.now();
563
+ if (wait > 0) await new Promise((resolve2) => setTimeout(resolve2, wait));
564
+ lastRequestAt = Date.now();
565
+ }
566
+ function createLlmClient(config) {
567
+ const chain = resolveProviderChain(config);
568
+ const primary = chain[0];
569
+ if (!primary) {
570
+ throw new LlmClientError(
571
+ "No LLM provider configured. Set NVIDIA_API_KEY (primary) and/or OPENROUTER_API_KEY (fallback), or pass llmConfig."
572
+ );
573
+ }
574
+ primary.apiKey;
575
+ primary.baseUrl;
576
+ const model = primary.model;
577
+ async function chat(messages, options) {
578
+ const temperature = options?.temperature ?? config?.temperature ?? 0.1;
579
+ const maxTokens = options?.maxTokens ?? config?.maxTokens ?? parseInt(process.env.LLM_MAX_TOKENS || "65536", 10);
580
+ const attempts = [];
581
+ let lastError = null;
582
+ const TRANSIENT = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
583
+ const REQUEST_TIMEOUT_MS = Number(process.env.LLM_REQUEST_TIMEOUT_MS || 3e5);
584
+ const MAX_CHAIN_ROUNDS = 3;
585
+ for (let round = 1; round <= MAX_CHAIN_ROUNDS; round++) {
586
+ if (round > 1) {
587
+ attempts.push("round " + (round - 1) + " failed \u2014 backing off 20s before rewalking the chain");
588
+ await new Promise((resolve2) => setTimeout(resolve2, 2e4));
589
+ }
590
+ for (const provider of chain) {
591
+ try {
592
+ const headers = {
593
+ "Content-Type": "application/json",
594
+ "Authorization": `Bearer ${provider.apiKey}`
595
+ };
596
+ const payload = {
597
+ model: provider.model,
598
+ messages,
599
+ temperature,
600
+ max_tokens: maxTokens
601
+ };
602
+ await pacedDelay();
603
+ const doFetch = () => fetch(`${provider.baseUrl}/chat/completions`, {
604
+ method: "POST",
605
+ headers: {
606
+ "Content-Type": "application/json",
607
+ "Authorization": `Bearer ${provider.apiKey}`
608
+ },
609
+ body: JSON.stringify({
610
+ model: provider.model,
611
+ messages,
612
+ temperature,
613
+ max_tokens: maxTokens
614
+ }),
615
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
616
+ });
617
+ let response;
618
+ try {
619
+ response = await doFetch();
620
+ } catch (netErr) {
621
+ attempts.push(`${provider.label}: network error (${netErr instanceof Error ? netErr.message : String(netErr)}) \u2014 retrying once`);
622
+ await new Promise((r) => setTimeout(r, 5e3));
623
+ lastRequestAt = Date.now();
624
+ response = await doFetch();
625
+ }
626
+ if (!response.ok) {
627
+ const body = await response.text().catch(() => "");
628
+ const err = new LlmClientError(
629
+ `[${provider.label}] LLM API error: ${response.status} ${response.statusText} \u2014 ${body.slice(0, 200)}`,
630
+ response.status
631
+ );
632
+ if (response.status === 429) {
633
+ const retryAfterRaw = response.headers.get("retry-after");
634
+ const retryAfterMs = Math.min(
635
+ 6e4,
636
+ Math.max(15e3, (Number.isFinite(Number(retryAfterRaw)) ? Number(retryAfterRaw) : 20) * 1e3)
637
+ );
638
+ attempts.push(`${provider.label}: 429 rate-limited \u2014 backing off ${Math.round(retryAfterMs / 1e3)}s`);
639
+ await new Promise((resolve2) => setTimeout(resolve2, retryAfterMs));
640
+ lastRequestAt = Date.now();
641
+ const retry = await fetch(`${provider.baseUrl}/chat/completions`, {
642
+ method: "POST",
643
+ headers,
644
+ body: JSON.stringify(payload),
645
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
646
+ });
647
+ if (!retry.ok) throw err;
648
+ const retryData = await retry.json();
649
+ const retryContent2 = retryData.choices?.[0]?.message?.content || "";
650
+ if (!retryContent2.trim()) throw err;
651
+ return {
652
+ content: retryContent2,
653
+ model: retryData.model || provider.model,
654
+ provider: provider.label,
655
+ attempts: attempts.slice(),
656
+ usage: retryData.usage,
657
+ finishReason: retryData.choices?.[0]?.finish_reason
658
+ };
659
+ }
660
+ if (TRANSIENT.has(response.status)) {
661
+ await new Promise((r) => setTimeout(r, 3e3));
662
+ const retry = await fetch(`${provider.baseUrl}/chat/completions`, {
663
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
664
+ method: "POST",
665
+ headers: {
666
+ "Content-Type": "application/json",
667
+ "Authorization": `Bearer ${provider.apiKey}`
668
+ },
669
+ body: JSON.stringify({
670
+ model: provider.model,
671
+ messages,
672
+ temperature,
673
+ max_tokens: maxTokens
674
+ })
675
+ });
676
+ if (!retry.ok) throw err;
677
+ const retryData = await retry.json();
678
+ const retryContent = retryData.choices?.[0]?.message?.content || "";
679
+ return {
680
+ content: retryContent,
681
+ model: retryData.model || provider.model,
682
+ provider: provider.label,
683
+ attempts: attempts.slice(),
684
+ usage: retryData.usage,
685
+ finishReason: retryData.choices?.[0]?.finish_reason
686
+ };
687
+ }
688
+ throw err;
689
+ }
690
+ const data = await response.json();
691
+ const wrappedError = data.error;
692
+ if (wrappedError) {
693
+ throw new LlmClientError("[" + provider.label + "] upstream error " + (wrappedError.code ?? "") + ": " + (wrappedError.message ?? "unknown"));
694
+ }
695
+ const content = data.choices?.[0]?.message?.content || "";
696
+ if (!content.trim()) {
697
+ throw new LlmClientError("[" + provider.label + "] empty completion returned");
698
+ }
699
+ return {
700
+ content,
701
+ model: data.model || provider.model,
702
+ provider: provider.label,
703
+ attempts: attempts.slice(),
704
+ usage: data.usage,
705
+ finishReason: data.choices?.[0]?.finish_reason
706
+ };
707
+ } catch (err) {
708
+ const msg = err instanceof Error ? err.message : String(err);
709
+ attempts.push(`${provider.label}: ${msg}`);
710
+ lastError = err instanceof LlmClientError ? err : new LlmClientError(`[${provider.label}] ${msg}`);
711
+ }
712
+ }
713
+ if (round < MAX_CHAIN_ROUNDS) continue;
714
+ throw lastError ?? new LlmClientError("All LLM providers failed (empty chain).");
715
+ }
716
+ throw lastError ?? new LlmClientError("All LLM providers failed after " + MAX_CHAIN_ROUNDS + " rounds.");
717
+ }
718
+ return { chat, model };
719
+ }
720
+ function parseJsonFromLlm(text) {
721
+ let cleaned = text.trim();
722
+ if (cleaned.startsWith("```json")) {
723
+ cleaned = cleaned.slice(7);
724
+ } else if (cleaned.startsWith("```")) {
725
+ cleaned = cleaned.slice(3);
726
+ }
727
+ if (cleaned.endsWith("```")) {
728
+ cleaned = cleaned.slice(0, -3);
729
+ }
730
+ cleaned = cleaned.trim();
731
+ try {
732
+ return JSON.parse(cleaned);
733
+ } catch {
734
+ const start = cleaned.indexOf("{");
735
+ if (start >= 0) {
736
+ let depth = 0;
737
+ let inString = false;
738
+ let escape = false;
739
+ for (let i = start; i < cleaned.length; i++) {
740
+ const ch = cleaned[i];
741
+ if (escape) {
742
+ escape = false;
743
+ continue;
744
+ }
745
+ if (ch === "\\") {
746
+ escape = true;
747
+ continue;
748
+ }
749
+ if (ch === '"') {
750
+ inString = !inString;
751
+ continue;
752
+ }
753
+ if (inString) continue;
754
+ if (ch === "{") depth++;
755
+ if (ch === "}") {
756
+ depth--;
757
+ if (depth === 0) {
758
+ try {
759
+ return JSON.parse(cleaned.slice(start, i + 1));
760
+ } catch {
761
+ }
762
+ }
763
+ }
764
+ }
765
+ }
766
+ throw new LlmClientError(`Failed to parse JSON from LLM response: ${cleaned.slice(0, 200)}`);
767
+ }
768
+ }
769
+ async function llmChatJson(client, systemPrompt, userPrompt, options) {
770
+ const result = await client.chat(
771
+ [
772
+ { role: "system", content: systemPrompt },
773
+ { role: "user", content: userPrompt }
774
+ ],
775
+ options
776
+ );
777
+ return parseJsonFromLlm(result.content);
778
+ }
779
+
780
+ // src/graph/scaffoldExtractor.ts
781
+ async function extractScaffold(options) {
782
+ const { goal, techStack, fileList, llmConfig } = options;
783
+ const client = createLlmClient(llmConfig);
784
+ const fileListStr = fileList.length > 0 ? fileList.join("\n") : "(empty)";
785
+ 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.";
786
+ const userPrompt = `App goal: ${goal}
787
+ Tech stack: ${techStack}
788
+
789
+ PROJECT FILE LIST (base-project):
790
+ ${fileListStr}
791
+
792
+ Return JSON:
793
+ {
794
+ "feature": {
795
+ "id": "F0",
796
+ "name": "FOUNDATION & SETUP",
797
+ "description": "Get familiar with the tools, create/clone the project, learn minimal knowledge and the repo map",
798
+ "platform": "app",
799
+ "steps": [
800
+ {
801
+ "id": "F0-S1",
802
+ "sequence": 1,
803
+ "name": "Action step name",
804
+ "description": "Detailed description of the concrete actions",
805
+ "files": [],
806
+ "api_usage": [],
807
+ "keywords": ["Xcode", "Terminal"],
808
+ "completion_level": "base",
809
+ "intent": "Learn how to setup the IDE and run a minimal Swift app",
810
+ "outcome": {"user_visible": "App builds and runs", "technical": "Toolchain verified"},
811
+ "acceptance": ["Xcode builds without errors", "Simulator launches successfully"],
812
+ "effort": {"estimated_minutes": 20, "complexity": "low"}
813
+ }
814
+ ]
815
+ }
816
+ }
817
+ `;
818
+ try {
819
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
820
+ temperature: 0.1,
821
+ maxTokens: parseInt(process.env.PG_C0_MAX_TOKENS || "16384", 10)
822
+ });
823
+ const feat = result.feature;
824
+ if (!feat || !Array.isArray(feat.steps) || feat.steps.length === 0) {
825
+ console.warn("[WARN] Scaffold LLM returned no steps \u2014 skipping F0");
826
+ return null;
827
+ }
828
+ const steps = feat.steps.map((s, i) => ({
829
+ id: `F0-S${i + 1}`,
830
+ sequence: i + 1,
831
+ name: String(s.name || `Step ${i + 1}`),
832
+ description: String(s.description || ""),
833
+ files: Array.isArray(s.files) ? s.files : [],
834
+ api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
835
+ keywords: Array.isArray(s.keywords) ? s.keywords : [],
836
+ completion_level: "base",
837
+ platform: String(s.platform || "app"),
838
+ intent: String(s.intent || ""),
839
+ outcome: s.outcome && typeof s.outcome === "object" ? s.outcome : void 0,
840
+ acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
841
+ effort: s.effort && typeof s.effort === "object" ? s.effort : void 0,
842
+ requirement_ids: [],
843
+ concept_codes: []
844
+ }));
845
+ return {
846
+ id: "F0",
847
+ name: String(feat.name || "FOUNDATION & SETUP"),
848
+ description: String(feat.description || ""),
849
+ platform: String(feat.platform || "app"),
850
+ steps
851
+ };
852
+ } catch (err) {
853
+ console.error(`[ERROR] extractScaffold failed: ${err}`);
854
+ return null;
855
+ }
856
+ }
857
+
858
+ // src/graph/overviewExtractor.ts
859
+ function buildSourceText(fileContentsMap) {
860
+ const blocks = [];
861
+ for (const [relPath, content] of fileContentsMap) {
862
+ blocks.push(`### FILE: ${relPath}
863
+ ${content}`);
864
+ }
865
+ return blocks.join("\n\n");
866
+ }
867
+ async function extractProjectOverview(options) {
868
+ const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig } = options;
869
+ const client = createLlmClient(llmConfig);
870
+ const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
871
+ const allFilesStr = buildSourceText(fileContentsMap);
872
+ let keywordsAnchor = "";
873
+ if (astKeywords && astKeywords.length > 0) {
874
+ const kwItems = astKeywords.slice(0, 30).map(
875
+ (kw) => `- ${kw.keyword} (from ${kw.source})`
876
+ );
877
+ if (kwItems.length > 0) {
878
+ keywordsAnchor = "\nAST Domain Keywords & Patterns (from static analysis \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
879
+ }
880
+ }
881
+ 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.";
882
+ const userPrompt = `App goal: ${goal}
883
+ Tech stack: ${techStack}
884
+ ${keywordsAnchor}
885
+ Filtered SDK API list (used_in_demo):
886
+ ${JSON.stringify(usedSdkApis, null, 0)}
887
+
888
+ Source code of the files:
889
+ ${allFilesStr}
890
+
891
+ Return JSON per the schema:
892
+ {
893
+ "schema_version": 2,
894
+ "project": {"name": "...", "project_type": "app", "platforms": ["ios"]},
895
+ "product": {
896
+ "purpose": "END-USER problem being solved",
897
+ "problem_statement": "Core user problem",
898
+ "primary_users": ["who the main users are"],
899
+ "development_stages": [
900
+ {
901
+ "stage": "Stage 1: Foundation & Setup",
902
+ "need": ["Core app shell"],
903
+ "learn": ["App lifecycle, basic UI"],
904
+ "product_state": "App launches with static mock data",
905
+ "temporary_approach": "Hardcoded array",
906
+ "validation": "Main screen loads"
907
+ }
908
+ ],
909
+ "features": [
910
+ {"id": "F1", "name": "Feature name", "description": "Feature description", "platform": "ios"}
911
+ ],
912
+ "user_journeys": [{"name": "...", "feature_ids": ["F1"]}]
913
+ },
914
+ "architecture": {"layers": [], "services": [], "state_management": "..."},
915
+ "decomposition": {"milestones": [{"id": "M1", "phase": "MVP", "name": "...", "goal": "...", "feature_ids": ["F1"]}]}
916
+ }`;
917
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
918
+ temperature: 0.1,
919
+ maxTokens: parseInt(process.env.PG_C1_MAX_TOKENS || "32768", 10)
920
+ });
921
+ const prod = result.product;
922
+ if (prod && !Array.isArray(prod.development_stages)) {
923
+ prod.development_stages = [];
924
+ }
925
+ return result;
926
+ }
927
+
928
+ // src/graph/stepExtractor.ts
929
+ function buildSourceText2(fileContentsMap) {
930
+ const blocks = [];
931
+ for (const [relPath, content] of fileContentsMap) {
932
+ blocks.push(`### FILE: ${relPath}
933
+ ${content}`);
934
+ }
935
+ return blocks.join("\n\n");
936
+ }
937
+ async function extractFeatureSteps(options) {
938
+ const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig } = options;
939
+ const client = createLlmClient(llmConfig);
940
+ const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
941
+ const allFilesStr = buildSourceText2(fileContentsMap);
942
+ const fid = feature.id || "F_UNK";
943
+ const fname = feature.name || fid;
944
+ const fdesc = feature.description || "";
945
+ 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.";
946
+ const userPrompt = `App goal: ${goal}
947
+ Tech stack: ${techStack}
948
+
949
+ FEATURE TO SPLIT: ${fid} \u2014 ${fname}
950
+ Feature description: ${fdesc}
951
+
952
+ SDK API list (used_in_demo):
953
+ ${JSON.stringify(usedSdkApis)}
954
+
955
+ Source code:
956
+ ${allFilesStr}
957
+
958
+ Split this feature into 2-6 action steps. Return JSON:
959
+ {
960
+ "steps": [
961
+ {
962
+ "id": "${fid}-S1",
963
+ "sequence": 1,
964
+ "name": "Action step name",
965
+ "description": "One concise sentence (max 140 chars)",
966
+ "files": ["path/to/file.swift"],
967
+ "api_usage": ["ChatClient"],
968
+ "keywords": ["@State", "NavigationStack"],
969
+ "completion_level": "mvp",
970
+ "intent": "Create reactive state container",
971
+ "outcome": {"user_visible": "Chat view updates", "technical": "ChatViewModel conforms to ObservableObject"},
972
+ "acceptance": ["ViewModel publishes messages", "UI re-renders on state mutation"],
973
+ "effort": {"estimated_minutes": 30, "complexity": "medium"}
974
+ }
975
+ ]
976
+ }`;
977
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
978
+ temperature: 0.1,
979
+ maxTokens: parseInt(process.env.PG_C2_MAX_TOKENS || "32768", 10)
980
+ });
981
+ const rawSteps = result.steps || [];
982
+ const clampedSteps = rawSteps.length > 6 ? rawSteps.slice(0, 6) : rawSteps.length < 2 && rawSteps.length > 0 ? rawSteps : rawSteps;
983
+ if (rawSteps.length > 6) {
984
+ console.warn(`[WARN] LLM returned ${rawSteps.length} steps for ${fid}, clamped to 6`);
985
+ } else if (rawSteps.length === 0) {
986
+ console.warn(`[WARN] LLM returned 0 steps for ${fid}`);
987
+ }
988
+ return clampedSteps.map((s, i) => ({
989
+ id: `${fid}-S${i + 1}`,
990
+ sequence: i + 1,
991
+ name: String(s.name || `Step ${i + 1}`),
992
+ description: String(s.description || ""),
993
+ files: Array.isArray(s.files) ? s.files : [],
994
+ api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
995
+ keywords: Array.isArray(s.keywords) ? s.keywords : [],
996
+ completion_level: String(s.completion_level || "mvp"),
997
+ platform: String(s.platform || feature.platform || "app"),
998
+ intent: String(s.intent || ""),
999
+ outcome: s.outcome,
1000
+ acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
1001
+ effort: s.effort,
1002
+ requirement_ids: [],
1003
+ concept_codes: []
1004
+ }));
1005
+ }
1006
+ async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig) {
1007
+ const client = createLlmClient(llmConfig);
1008
+ const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
1009
+ const sourceText = buildSourceText2(fileContentsMap);
1010
+ const featuresListing = featuresMeta.map(
1011
+ (f) => `- ${f.id || "F?"}: ${f.name || ""} \u2014 ${f.description || ""}`
1012
+ ).join("\n");
1013
+ 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.";
1014
+ const userPrompt = `App goal: ${goal}
1015
+ Tech stack: ${techStack}
1016
+
1017
+ SDK API list (used_in_demo):
1018
+ ${JSON.stringify(usedSdkApis)}
1019
+
1020
+ Source code:
1021
+ ${sourceText}
1022
+
1023
+ FEATURES to split into steps:
1024
+ ${featuresListing}
1025
+
1026
+ For EACH feature above, produce 4-6 action steps. Each step must include:
1027
+ id, sequence, name, description, files, api_usage, keywords, completion_level, platform,
1028
+ intent, outcome ({user_visible, technical}), acceptance[] (2 criteria), effort ({estimated_minutes, complexity}).
1029
+
1030
+ Return JSON with a "steps" dict keyed by feature_id:
1031
+ {
1032
+ "steps": {
1033
+ "<feature_id>": [
1034
+ {
1035
+ "id": "<feature_id>-S1",
1036
+ "sequence": 1,
1037
+ "name": "Step name",
1038
+ "description": "Concise sentence (max 140 chars)",
1039
+ "files": ["path/to/file"],
1040
+ "api_usage": ["APIName"],
1041
+ "keywords": ["keyword"],
1042
+ "completion_level": "mvp",
1043
+ "platform": "app",
1044
+ "intent": "Step intent",
1045
+ "outcome": {"user_visible": "User visible result", "technical": "Technical result"},
1046
+ "acceptance": ["Criterion 1", "Criterion 2"],
1047
+ "effort": {"estimated_minutes": 30, "complexity": "medium"}
1048
+ }
1049
+ ]
1050
+ }
1051
+ }`;
1052
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
1053
+ temperature: 0.1,
1054
+ maxTokens: parseInt(process.env.PG_C2_MAX_TOKENS || "32768", 10)
1055
+ });
1056
+ const stepsDict = result.steps;
1057
+ const output = /* @__PURE__ */ new Map();
1058
+ if (stepsDict && typeof stepsDict === "object") {
1059
+ for (const [fid, rawSteps] of Object.entries(stepsDict)) {
1060
+ if (!Array.isArray(rawSteps)) continue;
1061
+ const featMeta = featuresMeta.find((f) => f.id === fid);
1062
+ const steps = rawSteps.map((s, i) => ({
1063
+ id: `${fid}-S${i + 1}`,
1064
+ sequence: i + 1,
1065
+ name: String(s.name || `Step ${i + 1}`),
1066
+ description: String(s.description || ""),
1067
+ files: Array.isArray(s.files) ? s.files : [],
1068
+ api_usage: Array.isArray(s.api_usage) ? s.api_usage : [],
1069
+ keywords: Array.isArray(s.keywords) ? s.keywords : [],
1070
+ completion_level: String(s.completion_level || "mvp"),
1071
+ platform: String(s.platform || featMeta?.platform || "app"),
1072
+ intent: String(s.intent || ""),
1073
+ outcome: s.outcome,
1074
+ acceptance: Array.isArray(s.acceptance) ? s.acceptance : [],
1075
+ effort: s.effort,
1076
+ requirement_ids: [],
1077
+ concept_codes: []
1078
+ }));
1079
+ output.set(fid, steps);
1080
+ }
1081
+ }
1082
+ return output;
1083
+ }
1084
+
1085
+ // src/graph/graphVerifier.ts
1086
+ function verifyProjectGraph(projectGraph, options) {
1087
+ const { sdkApiIndex, fileContentsMap } = options;
1088
+ const sdkApiNames = new Set(sdkApiIndex.sdk_apis.map((api) => api.name));
1089
+ const hallucinations = [];
1090
+ const features = projectGraph.features || [];
1091
+ for (const feature of features) {
1092
+ const fid = feature.id || "F_UNK";
1093
+ const isFoundation = fid === "F0";
1094
+ for (const step of feature.steps || []) {
1095
+ const sid = step.id || `${fid}-SUNK`;
1096
+ const validFiles = [];
1097
+ for (const f of step.files || []) {
1098
+ if (fileContentsMap.has(f)) {
1099
+ validFiles.push(f);
1100
+ } else {
1101
+ hallucinations.push({
1102
+ type: "file",
1103
+ item: f,
1104
+ feature_id: fid,
1105
+ step_id: sid,
1106
+ reason: `File '${f}' not found in repository`
1107
+ });
1108
+ }
1109
+ }
1110
+ step.files = validFiles;
1111
+ const validApis = [];
1112
+ for (const api of step.api_usage || []) {
1113
+ if (sdkApiNames.size === 0 || sdkApiNames.has(api)) {
1114
+ validApis.push(api);
1115
+ } else {
1116
+ hallucinations.push({
1117
+ type: "api",
1118
+ item: api,
1119
+ feature_id: fid,
1120
+ step_id: sid,
1121
+ reason: `API '${api}' not found in SDK API index`
1122
+ });
1123
+ }
1124
+ }
1125
+ step.api_usage = validApis;
1126
+ if (isFoundation) continue;
1127
+ const validKws = [];
1128
+ for (const kw of step.keywords || []) {
1129
+ const kwLow = kw.toLowerCase();
1130
+ const isSingleWord = !kwLow.includes(" ");
1131
+ let found = false;
1132
+ for (const f of validFiles) {
1133
+ const content = (fileContentsMap.get(f) || "").toLowerCase();
1134
+ if (isSingleWord) {
1135
+ found = new RegExp(`\\b${kwLow.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(content);
1136
+ } else {
1137
+ found = content.includes(kwLow);
1138
+ }
1139
+ if (found) break;
1140
+ }
1141
+ if (!found) {
1142
+ for (const content of fileContentsMap.values()) {
1143
+ const lc = content.toLowerCase();
1144
+ if (isSingleWord) {
1145
+ found = new RegExp(`\\b${kwLow.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(lc);
1146
+ } else {
1147
+ found = lc.includes(kwLow);
1148
+ }
1149
+ if (found) break;
1150
+ }
1151
+ }
1152
+ if (found) {
1153
+ validKws.push(kw);
1154
+ } else {
1155
+ hallucinations.push({
1156
+ type: "keyword",
1157
+ item: kw,
1158
+ feature_id: fid,
1159
+ step_id: sid,
1160
+ reason: `Keyword '${kw}' not found in target files`
1161
+ });
1162
+ }
1163
+ }
1164
+ step.keywords = validKws;
1165
+ }
1166
+ }
1167
+ return { graph: projectGraph, hallucinations };
1168
+ }
1169
+
1170
+ // src/graph/conceptEscalator.ts
1171
+ function toUpperSnake(term) {
1172
+ let s = term.replace(/^[^\w]+/, "");
1173
+ s = s.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
1174
+ s = s.replace(/[^\w]+/g, "_");
1175
+ const res = s.toUpperCase().replace(/^_+|_+$/g, "");
1176
+ return res || "CONCEPT";
1177
+ }
1178
+ function extractTermSnippets(term, fileContentsMap, maxSnippets = 1) {
1179
+ const snippets = [];
1180
+ const termLow = term.toLowerCase();
1181
+ for (const [relPath, content] of fileContentsMap) {
1182
+ const lines = content.split("\n");
1183
+ for (let idx = 0; idx < lines.length; idx++) {
1184
+ if (lines[idx].toLowerCase().includes(termLow)) {
1185
+ const start = Math.max(0, idx - 1);
1186
+ const end = Math.min(lines.length, idx + 2);
1187
+ let snippet = lines.slice(start, end).map((l) => l.trim()).filter(Boolean).join(" ");
1188
+ if (snippet.length > 120) snippet = snippet.slice(0, 120) + "...";
1189
+ if (snippet) snippets.push(`[${relPath}]: ${snippet}`);
1190
+ if (snippets.length >= maxSnippets) break;
1191
+ }
1192
+ }
1193
+ if (snippets.length >= maxSnippets) break;
1194
+ }
1195
+ return snippets;
1196
+ }
1197
+ function inferDepthFromContext(keyword, fileContentsMap) {
1198
+ const kw = keyword.toLowerCase();
1199
+ const sioPatterns = [
1200
+ /^@/,
1201
+ // SwiftUI decorators: @State, @Binding
1202
+ /^import\s/,
1203
+ // Import statements
1204
+ /\(\)$/,
1205
+ // Function calls
1206
+ /\.prototype/,
1207
+ // Prototype methods
1208
+ /^new\s/,
1209
+ // Constructor calls
1210
+ /^async\s/,
1211
+ // Async keywords
1212
+ /^await\s/
1213
+ ];
1214
+ if (sioPatterns.some((p) => p.test(keyword))) {
1215
+ return { depth: "sio", rationale: `Keyword '${keyword}' is a specific API/syntax pattern` };
1216
+ }
1217
+ const cioPatterns = [
1218
+ /pattern/i,
1219
+ /protocol/i,
1220
+ /architecture/i,
1221
+ /lifecycle/i,
1222
+ /observer/i,
1223
+ /singleton/i,
1224
+ /factory/i,
1225
+ /delegate/i,
1226
+ /state\s*management/i,
1227
+ /data\s*flow/i,
1228
+ /binding/i,
1229
+ /mvvm/i,
1230
+ /mvc/i,
1231
+ /flux/i,
1232
+ /redux/i,
1233
+ /authentication/i,
1234
+ /authorization/i,
1235
+ /persistence/i,
1236
+ /caching/i,
1237
+ /serialization/i
1238
+ ];
1239
+ if (cioPatterns.some((p) => p.test(keyword))) {
1240
+ return { depth: "cio", rationale: `Keyword '${keyword}' represents a mechanism/pattern` };
1241
+ }
1242
+ const uloPatterns = [
1243
+ /design\s*thinking/i,
1244
+ /user\s*needs/i,
1245
+ /problem\s*statement/i,
1246
+ /foundation/i,
1247
+ /setup/i,
1248
+ /overview/i,
1249
+ /introduction/i,
1250
+ /scaffold/i,
1251
+ /hello\s*world/i,
1252
+ /getting\s*started/i
1253
+ ];
1254
+ if (uloPatterns.some((p) => p.test(keyword))) {
1255
+ return { depth: "ulo", rationale: `Keyword '${keyword}' is a foundational/introductory concept` };
1256
+ }
1257
+ for (const content of fileContentsMap.values()) {
1258
+ const lines = content.split("\n");
1259
+ for (const line of lines) {
1260
+ if (line.toLowerCase().includes(kw)) {
1261
+ if (/^(import|from|require|use |#include)/.test(line.trim())) {
1262
+ return { depth: "sio", rationale: `Keyword '${keyword}' found in import statement` };
1263
+ }
1264
+ if (/^\/\/|^\/\*|^#|^-->/.test(line.trim())) {
1265
+ return { depth: "ulo", rationale: `Keyword '${keyword}' found in comment/doc` };
1266
+ }
1267
+ return { depth: "sio", rationale: `Keyword '${keyword}' used in implementation code` };
1268
+ }
1269
+ }
1270
+ }
1271
+ return { depth: "sio", rationale: `Default: treating '${keyword}' as SIO (implementation-level)` };
1272
+ }
1273
+ async function escalateAndMapConcepts(options) {
1274
+ const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig } = options;
1275
+ const features = projectGraph.features || [];
1276
+ const allTerms = /* @__PURE__ */ new Set();
1277
+ for (const feature of features) {
1278
+ for (const step of feature.steps || []) {
1279
+ for (const api of step.api_usage || []) allTerms.add(api);
1280
+ for (const kw of step.keywords || []) allTerms.add(kw);
1281
+ }
1282
+ }
1283
+ if (allTerms.size === 0) return /* @__PURE__ */ new Map();
1284
+ const termList = Array.from(allTerms).sort();
1285
+ let llmResults = {};
1286
+ if (conceptMapOverride) {
1287
+ llmResults = conceptMapOverride;
1288
+ } else {
1289
+ try {
1290
+ const client = createLlmClient(llmConfig);
1291
+ const conceptBank = (availableConcepts || []).sort().join(", ") || "(empty)";
1292
+ const termEntries = termList.map((term) => {
1293
+ const snippets = extractTermSnippets(term, fileContentsMap);
1294
+ return snippets.length > 0 ? `- ${term} (Usage context: ${snippets[0]})` : `- ${term}`;
1295
+ });
1296
+ const systemPrompt = `You are a knowledge classification expert. For each concrete keyword/API, determine:
1297
+ 1. The neutral concept it belongs to (UPPER_SNAKE_CASE)
1298
+ 2. The depth level: ulo | cio | sio
1299
+
1300
+ DEPTH DEFINITIONS:
1301
+ - ulo (WHAT + WHY): The keyword introduces a concept \u2014 "what is it, why does it exist"
1302
+ Example: In a scaffold step, "@State is SwiftUI's way of managing local view state"
1303
+ - cio (HOW): The keyword demonstrates a mechanism \u2014 "how it works"
1304
+ Example: "@State triggers view re-render when value changes \u2014 mechanism is value-type observation"
1305
+ - sio (IMPLEMENTATION): The keyword is a concrete API/syntax used in code
1306
+ Example: "Using @State var isActive: Bool = false in ContentView"
1307
+
1308
+ RULES:
1309
+ 1. PREFER an EXISTING concept from the CONCEPT_BANK below if the keyword is a manifestation of that concept.
1310
+ 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.
1311
+ 3. CONCEPT_BANK: ${conceptBank.slice(0, 12e3)}
1312
+ 4. Depth is per-keyword-in-context, NOT per-concept. The same concept can have ULO at one step and SIO at another.
1313
+ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "...", "depth": "ulo|cio|sio"}}}`;
1314
+ 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"}}}';
1315
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
1316
+ temperature: 0.1,
1317
+ maxTokens: 16384
1318
+ });
1319
+ llmResults = result.results || {};
1320
+ } catch (err) {
1321
+ console.warn(`[WARN] Concept escalation LLM failed (${err}), using fallback depth inference`);
1322
+ }
1323
+ }
1324
+ const featureConcepts = /* @__PURE__ */ new Map();
1325
+ for (const feature of features) {
1326
+ const fid = feature.id || "F_UNK";
1327
+ const featureFiles = /* @__PURE__ */ new Set();
1328
+ for (const step of feature.steps || []) {
1329
+ for (const f of step.files || []) featureFiles.add(f);
1330
+ }
1331
+ const fTerms = [
1332
+ ...feature.api_usage || [],
1333
+ ...(feature.steps || []).flatMap((s) => [...s.api_usage || [], ...s.keywords || []])
1334
+ ];
1335
+ const items = [];
1336
+ const seen = /* @__PURE__ */ new Set();
1337
+ for (const kw of fTerms) {
1338
+ const info = llmResults[kw] || {};
1339
+ const cCode = info.concept_code || toUpperSnake(kw);
1340
+ const cName = info.concept_name || kw;
1341
+ let depth = "sio";
1342
+ let depthRationale = "Default: SIO";
1343
+ if (info.depth && ["ulo", "cio", "sio"].includes(info.depth)) {
1344
+ depth = info.depth;
1345
+ depthRationale = `LLM classified as ${depth}`;
1346
+ } else {
1347
+ const inferred = inferDepthFromContext(kw, fileContentsMap);
1348
+ depth = inferred.depth;
1349
+ depthRationale = inferred.rationale;
1350
+ }
1351
+ const kwKey = `${cCode}::${kw}`;
1352
+ if (seen.has(kwKey)) continue;
1353
+ seen.add(kwKey);
1354
+ const evidenceFiles = [];
1355
+ for (const f of featureFiles) {
1356
+ const content = fileContentsMap.get(f) || "";
1357
+ if (content.toLowerCase().includes(kw.toLowerCase())) {
1358
+ evidenceFiles.push(f);
1359
+ }
1360
+ }
1361
+ items.push({
1362
+ concept_code: cCode,
1363
+ concept_name: cName,
1364
+ keyword: kw,
1365
+ depth,
1366
+ depth_rationale: depthRationale,
1367
+ evidence_files: evidenceFiles
1368
+ });
1369
+ }
1370
+ featureConcepts.set(fid, items);
1371
+ }
1372
+ return featureConcepts;
1373
+ }
1374
+
1375
+ // src/concepts/conceptResolver.ts
1376
+ var SOURCE_THRESHOLDS = {
1377
+ docstring: 0.45,
1378
+ readme_description: 0.45,
1379
+ readme: 0.5,
1380
+ type_declaration: 0.55,
1381
+ function_signature: 0.55,
1382
+ import: 0.6,
1383
+ property_wrapper: 0.55,
1384
+ error_handling: 0.6,
1385
+ config: 0.6,
1386
+ framework_usage: 0.45
1387
+ };
1388
+ var DEFAULT_THRESHOLD = 0.55;
1389
+ function uloCioAwareScore(query, depth, conceptData) {
1390
+ const queryTerms = new Set(query.toLowerCase().split(/\s+/));
1391
+ let targetText = "";
1392
+ if (depth === "sio") {
1393
+ targetText = conceptData.keywords || "";
1394
+ } else if (depth === "cio") {
1395
+ targetText = [conceptData.description || "", conceptData.cio || ""].join(" ");
1396
+ } else {
1397
+ targetText = [conceptData.ulo || "", conceptData.description || ""].join(" ");
1398
+ }
1399
+ const targetTerms = new Set(targetText.toLowerCase().split(/\s+/));
1400
+ if (!queryTerms.size || !targetTerms.size) return 0;
1401
+ let overlap = 0;
1402
+ for (const t of queryTerms) {
1403
+ if (targetTerms.has(t)) overlap++;
1404
+ }
1405
+ return Math.min(overlap / queryTerms.size, 1);
1406
+ }
1407
+ function inferFieldsFromGoal(goal) {
1408
+ const goalLower = goal.toLowerCase();
1409
+ const fieldKeywords = {
1410
+ ASE: ["algorithm", "software", "programming", "code", "develop", "build", "app"],
1411
+ DAI: ["data", "database", "storage", "query", "sql", "analytics"],
1412
+ CSN: ["network", "protocol", "server", "client", "api", "http"],
1413
+ HCI: ["user", "interface", "ui", "ux", "interaction", "design"],
1414
+ MOB: ["mobile", "ios", "android", "swift", "kotlin", "flutter"],
1415
+ WEB: ["web", "html", "css", "javascript", "react", "vue"]
1416
+ };
1417
+ const matched = [];
1418
+ for (const [code, keywords] of Object.entries(fieldKeywords)) {
1419
+ if (keywords.some((kw) => goalLower.includes(kw))) matched.push(code);
1420
+ }
1421
+ return matched.length > 0 ? matched : ["ASE"];
1422
+ }
1423
+ function inferDepthFromSource(source) {
1424
+ const uloSources = ["docstring", "readme", "readme_description", "framework_usage"];
1425
+ const cioSources = ["type_declaration", "function_signature", "config"];
1426
+ if (uloSources.includes(source)) return "ulo";
1427
+ if (cioSources.includes(source)) return "cio";
1428
+ return "sio";
1429
+ }
1430
+ async function resolveConcepts(options) {
1431
+ const { keywords, goal, embeddings, depthOverrides, topK = 3, threshold = DEFAULT_THRESHOLD} = options;
1432
+ inferFieldsFromGoal(goal);
1433
+ const resolved = [];
1434
+ const proposed = [];
1435
+ const hasEmbeddings = Object.keys(embeddings || {}).length > 0 && Object.values(embeddings || {}).some((d) => d.embedding && d.embedding.length > 0);
1436
+ let search_mode = hasEmbeddings ? "embedding" : "keyword_overlap";
1437
+ const concepts = embeddings || {};
1438
+ for (const kwData of keywords) {
1439
+ const keyword = kwData.keyword;
1440
+ const source = kwData.source;
1441
+ const depth = depthOverrides?.get(keyword) || inferDepthFromSource(source);
1442
+ const scored = [];
1443
+ for (const [code, data] of Object.entries(concepts)) {
1444
+ const conceptKeywords = data.keywords || "";
1445
+ const conceptDesc = data.description || "";
1446
+ const conceptUlo = data.ulo;
1447
+ const conceptCio = data.cio;
1448
+ const awareScore = uloCioAwareScore(keyword, depth, {
1449
+ keywords: conceptKeywords,
1450
+ description: conceptDesc,
1451
+ ulo: conceptUlo,
1452
+ cio: conceptCio
1453
+ });
1454
+ const queryTerms = new Set(keyword.toLowerCase().split(/\s+/));
1455
+ const allTerms = new Set([
1456
+ ...conceptKeywords.toLowerCase().split(/\s+/),
1457
+ ...conceptDesc.toLowerCase().split(/\s+/),
1458
+ ...(conceptUlo || "").toLowerCase().split(/\s+/),
1459
+ ...(conceptCio || "").toLowerCase().split(/\s+/)
1460
+ ].filter(Boolean));
1461
+ let overlapCount = 0;
1462
+ for (const t of queryTerms) {
1463
+ if (allTerms.has(t)) overlapCount++;
1464
+ }
1465
+ const fallbackScore = queryTerms.size > 0 ? Math.min(overlapCount / queryTerms.size, 1) : 0;
1466
+ const usedAware = awareScore >= fallbackScore;
1467
+ const finalScore = usedAware ? awareScore : fallbackScore;
1468
+ if (finalScore > 0) {
1469
+ scored.push({
1470
+ code,
1471
+ name: data.name || code,
1472
+ description: conceptDesc,
1473
+ keywords: conceptKeywords,
1474
+ ulo: conceptUlo,
1475
+ cio: conceptCio,
1476
+ score: finalScore,
1477
+ matched_level: usedAware ? depth : "sio",
1478
+ method: hasEmbeddings && data.embedding && data.embedding.length > 0 ? "embedding" : "keyword_overlap"
1479
+ });
1480
+ }
1481
+ }
1482
+ scored.sort((a, b) => b.score - a.score);
1483
+ const topMatches = scored.slice(0, topK);
1484
+ if (topMatches.length === 0) {
1485
+ proposed.push({
1486
+ keyword,
1487
+ source,
1488
+ depth,
1489
+ reason: "No matches in Master Tree"
1490
+ });
1491
+ continue;
1492
+ }
1493
+ const effThreshold = SOURCE_THRESHOLDS[source] || threshold;
1494
+ const topMatch = topMatches[0];
1495
+ if (topMatch.score >= effThreshold) {
1496
+ resolved.push({
1497
+ keyword,
1498
+ source,
1499
+ depth,
1500
+ concept_codes: topMatches.filter((m) => m.score >= effThreshold).map((m) => m.code),
1501
+ matches: topMatches
1502
+ });
1503
+ } else {
1504
+ proposed.push({
1505
+ keyword,
1506
+ source,
1507
+ depth,
1508
+ best_match: topMatch,
1509
+ reason: `Low confidence (${topMatch.score.toFixed(2)} < ${effThreshold})`
1510
+ });
1511
+ }
1512
+ }
1513
+ return {
1514
+ resolved,
1515
+ proposed,
1516
+ summary: {
1517
+ total_keywords: keywords.length,
1518
+ resolved_count: resolved.length,
1519
+ proposed_count: proposed.length,
1520
+ search_mode
1521
+ }
1522
+ };
1523
+ }
1524
+
1525
+ // src/assembly/roadmapAssembler.ts
1526
+ function calculateOverhead(rawMinutes, overheadFactor) {
1527
+ return Math.ceil(rawMinutes * overheadFactor);
1528
+ }
1529
+ function splitFeatureIntoMilestones(feature, featureConcepts, maxMinutes, overheadFactor) {
1530
+ const steps = feature.steps || [];
1531
+ if (steps.length === 0) return [];
1532
+ const featureId = feature.id || "F_UNK";
1533
+ const stepMinutes = steps.map((s) => s.effort?.estimated_minutes || 15);
1534
+ const totalRawMinutes = stepMinutes.reduce((a, b) => a + b, 0);
1535
+ if (calculateOverhead(totalRawMinutes, overheadFactor) <= maxMinutes) {
1536
+ return [buildMilestone(feature, featureConcepts, steps, stepMinutes, featureId, 1)];
1537
+ }
1538
+ const milestones = [];
1539
+ let currentSteps = [];
1540
+ let currentMinutes = [];
1541
+ let milestoneIndex = 1;
1542
+ for (let i = 0; i < steps.length; i++) {
1543
+ const step = steps[i];
1544
+ const mins = stepMinutes[i];
1545
+ const currentTotal = currentMinutes.reduce((a, b) => a + b, 0);
1546
+ if (calculateOverhead(currentTotal + mins, overheadFactor) > maxMinutes && currentSteps.length > 0) {
1547
+ milestones.push(buildMilestone(
1548
+ feature,
1549
+ featureConcepts,
1550
+ currentSteps,
1551
+ currentMinutes,
1552
+ featureId,
1553
+ milestoneIndex
1554
+ ));
1555
+ milestoneIndex++;
1556
+ currentSteps = [];
1557
+ currentMinutes = [];
1558
+ }
1559
+ currentSteps.push(step);
1560
+ currentMinutes.push(mins);
1561
+ }
1562
+ if (currentSteps.length > 0) {
1563
+ milestones.push(buildMilestone(
1564
+ feature,
1565
+ featureConcepts,
1566
+ currentSteps,
1567
+ currentMinutes,
1568
+ featureId,
1569
+ milestoneIndex
1570
+ ));
1571
+ }
1572
+ return milestones;
1573
+ }
1574
+ function buildMilestone(feature, featureConcepts, steps, stepMinutes, featureId, milestoneIndex) {
1575
+ const rawMinutes = stepMinutes.reduce((a, b) => a + b, 0);
1576
+ const levels = steps.map((s) => s.completion_level || "mvp");
1577
+ const completionLevel = levels.includes("extend") ? "extend" : levels.includes("mvp") ? "mvp" : "base";
1578
+ const milestoneConcepts = /* @__PURE__ */ new Set();
1579
+ const milestoneKeywords = /* @__PURE__ */ new Set();
1580
+ for (const step of steps) {
1581
+ const stepConceptCodes = step.concept_codes || [];
1582
+ for (const code of stepConceptCodes) {
1583
+ milestoneConcepts.add(code);
1584
+ }
1585
+ const stepKws = [...step.keywords || [], ...step.api_usage || []];
1586
+ for (const cm of featureConcepts) {
1587
+ if (stepKws.includes(cm.keyword)) {
1588
+ milestoneConcepts.add(cm.concept_code);
1589
+ milestoneKeywords.add(cm.keyword);
1590
+ }
1591
+ }
1592
+ }
1593
+ const depthCounts = { ulo: 0, cio: 0, sio: 0 };
1594
+ for (const cm of featureConcepts) {
1595
+ if (milestoneKeywords.has(cm.keyword)) {
1596
+ depthCounts[cm.depth]++;
1597
+ }
1598
+ }
1599
+ const dominantDepth = depthCounts.sio >= depthCounts.cio && depthCounts.sio >= depthCounts.ulo ? "sio" : depthCounts.cio >= depthCounts.ulo ? "cio" : "ulo";
1600
+ return {
1601
+ id: milestoneIndex === 1 ? featureId : `${featureId}-M${milestoneIndex}`,
1602
+ name: milestoneIndex === 1 ? feature.name || featureId : `${feature.name || featureId} (Part ${milestoneIndex})`,
1603
+ description: feature.description || "",
1604
+ feature_id: featureId,
1605
+ feature_name: feature.name || "",
1606
+ concept_code: [...milestoneConcepts].join(", "),
1607
+ depth: dominantDepth,
1608
+ all_keywords: [],
1609
+ new_keywords: [],
1610
+ prerequisite_keywords: [],
1611
+ steps,
1612
+ estimated_minutes: rawMinutes,
1613
+ adjusted_minutes: rawMinutes,
1614
+ // placeholder — assembleRoadmap() applies actual overheadFactor
1615
+ completion_level: completionLevel
1616
+ };
1617
+ }
1618
+ function buildKeywordMap(featureConcepts) {
1619
+ const map = /* @__PURE__ */ new Map();
1620
+ for (const mappings of featureConcepts.values()) {
1621
+ for (const m of mappings) {
1622
+ const existing = map.get(m.concept_code) || [];
1623
+ if (!existing.includes(m.keyword)) {
1624
+ existing.push(m.keyword);
1625
+ map.set(m.concept_code, existing);
1626
+ }
1627
+ }
1628
+ }
1629
+ return map;
1630
+ }
1631
+ function attachKeywordsToMilestones(milestones, keywordMap) {
1632
+ const cumulativeKeywords = /* @__PURE__ */ new Set();
1633
+ for (const milestone of milestones) {
1634
+ const allKw = /* @__PURE__ */ new Set();
1635
+ const conceptCodes = milestone.concept_code.split(",").map((c) => c.trim()).filter(Boolean);
1636
+ for (const code of conceptCodes) {
1637
+ const kws = keywordMap.get(code) || [];
1638
+ for (const kw of kws) allKw.add(kw);
1639
+ }
1640
+ milestone.all_keywords = Array.from(allKw).sort();
1641
+ const newKw = [];
1642
+ const prereqKw = [];
1643
+ for (const kw of milestone.all_keywords) {
1644
+ if (cumulativeKeywords.has(kw)) {
1645
+ prereqKw.push(kw);
1646
+ } else {
1647
+ newKw.push(kw);
1648
+ cumulativeKeywords.add(kw);
1649
+ }
1650
+ }
1651
+ milestone.new_keywords = newKw;
1652
+ milestone.prerequisite_keywords = prereqKw;
1653
+ }
1654
+ return milestones;
1655
+ }
1656
+ function groupIntoPhases(milestones, sessionMinutes) {
1657
+ const phases = [];
1658
+ let currentMilestones = [];
1659
+ let currentMinutes = 0;
1660
+ let phaseNum = 1;
1661
+ for (const milestone of milestones) {
1662
+ const milestoneMinutes = milestone.adjusted_minutes;
1663
+ if (currentMinutes + milestoneMinutes > sessionMinutes && currentMilestones.length > 0) {
1664
+ phases.push(buildPhase(currentMilestones, phaseNum));
1665
+ phaseNum++;
1666
+ currentMilestones = [];
1667
+ currentMinutes = 0;
1668
+ }
1669
+ currentMilestones.push(milestone);
1670
+ currentMinutes += milestoneMinutes;
1671
+ }
1672
+ if (currentMilestones.length > 0) {
1673
+ phases.push(buildPhase(currentMilestones, phaseNum));
1674
+ }
1675
+ return phases;
1676
+ }
1677
+ function buildPhase(milestones, phaseNum) {
1678
+ const totalMinutes = milestones.reduce((sum, m) => sum + m.adjusted_minutes, 0);
1679
+ const depths = milestones.map((m) => m.depth);
1680
+ const uniqueDepths = [...new Set(depths)];
1681
+ const progression = uniqueDepths.map((d) => d.toUpperCase()).join(" \u2192 ");
1682
+ return {
1683
+ phase_id: phaseNum,
1684
+ phase_name: `Phase ${phaseNum}`,
1685
+ milestones,
1686
+ total_minutes: totalMinutes,
1687
+ concept_progression: progression
1688
+ };
1689
+ }
1690
+ function assembleRoadmap(options) {
1691
+ const {
1692
+ projectGraph,
1693
+ featureConcepts = /* @__PURE__ */ new Map(),
1694
+ sessionMinutes = 90,
1695
+ overheadFactor = 1.15,
1696
+ maxMilestoneMinutes = 120
1697
+ } = options;
1698
+ const features = projectGraph.features || [];
1699
+ const keywordMap = buildKeywordMap(featureConcepts);
1700
+ const allMilestones = [];
1701
+ for (const feature of features) {
1702
+ const fid = feature.id || "F_UNK";
1703
+ const fConcepts = featureConcepts.get(fid) || [];
1704
+ const milestones = splitFeatureIntoMilestones(
1705
+ feature,
1706
+ fConcepts,
1707
+ maxMilestoneMinutes,
1708
+ overheadFactor
1709
+ );
1710
+ allMilestones.push(...milestones);
1711
+ }
1712
+ for (const milestone of allMilestones) {
1713
+ milestone.adjusted_minutes = calculateOverhead(milestone.estimated_minutes, overheadFactor);
1714
+ }
1715
+ attachKeywordsToMilestones(allMilestones, keywordMap);
1716
+ const phases = groupIntoPhases(allMilestones, sessionMinutes);
1717
+ const totalEstimatedMinutes = allMilestones.reduce((sum, m) => sum + m.estimated_minutes, 0);
1718
+ const totalAdjustedMinutes = allMilestones.reduce((sum, m) => sum + m.adjusted_minutes, 0);
1719
+ return {
1720
+ project: projectGraph.project,
1721
+ product: projectGraph.product,
1722
+ phases,
1723
+ feature_concepts: featureConcepts,
1724
+ time_budget: {
1725
+ session_minutes: sessionMinutes,
1726
+ overhead_factor: overheadFactor,
1727
+ total_estimated_minutes: totalEstimatedMinutes,
1728
+ total_adjusted_minutes: totalAdjustedMinutes
1729
+ }
1730
+ };
1731
+ }
1732
+
1733
+ // src/extractors/fileSelector.ts
1734
+ async function selectFilesWithLlm(filePaths, goal, techStack, llmConfig) {
1735
+ if (filePaths.length <= 15) {
1736
+ return { selected: filePaths, reasoning: "Too few files to filter" };
1737
+ }
1738
+ const client = createLlmClient(llmConfig);
1739
+ const treeLines = buildFileTree(filePaths);
1740
+ const systemPrompt = 'You are a software architect analyzing a project repository.\nGiven a file tree and a project goal, select which files are MOST IMPORTANT to read for understanding the project architecture and implementation.\n\nRULES:\n1. Prioritize source code files (Swift, TS, Python, C++) over config/docs\n2. Include entry points (App.swift, main.py, index.ts)\n3. Include core domain files (models, services, views, controllers)\n4. EXCLUDE test files, mock files, generated files\n5. EXCLUDE config files (package.json, Podfile, etc.) unless they reveal architecture\n6. Include files that directly implement the features mentioned in the goal\n7. Return 20-40 files maximum (fewer is better if the project is small)\n\nReturn JSON: {"selected": ["path/to/file1", "path/to/file2"], "reasoning": "brief explanation"}';
1741
+ const userPrompt = `Project goal: ${goal}
1742
+ Tech stack: ${techStack}
1743
+
1744
+ FILE TREE (${filePaths.length} files):
1745
+ ${treeLines}
1746
+
1747
+ Select the most important files to analyze. Return JSON.`;
1748
+ try {
1749
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
1750
+ temperature: 0.1,
1751
+ maxTokens: 4096
1752
+ });
1753
+ const selected = Array.isArray(result.selected) ? result.selected : [];
1754
+ const reasoning = String(result.reasoning || "");
1755
+ const pathSet = new Set(filePaths);
1756
+ const validSelected = selected.filter((p) => pathSet.has(p));
1757
+ if (validSelected.length === 0) {
1758
+ return { selected: filePaths, reasoning: "LLM selection empty, using all files" };
1759
+ }
1760
+ return { selected: validSelected, reasoning };
1761
+ } catch (err) {
1762
+ console.warn(`[WARN] LLM file selection failed (${err}), using all files`);
1763
+ return { selected: filePaths, reasoning: `LLM failed: ${err}` };
1764
+ }
1765
+ }
1766
+ function buildFileTree(filePaths) {
1767
+ const byDir = /* @__PURE__ */ new Map();
1768
+ for (const fp of filePaths) {
1769
+ const parts = fp.split("/");
1770
+ const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : "(root)";
1771
+ const file = parts[parts.length - 1];
1772
+ if (!byDir.has(dir)) byDir.set(dir, []);
1773
+ byDir.get(dir).push(file);
1774
+ }
1775
+ const lines = [];
1776
+ for (const [dir, files] of byDir) {
1777
+ lines.push(`${dir}/`);
1778
+ for (const f of files.sort()) {
1779
+ lines.push(` ${f}`);
1780
+ }
1781
+ }
1782
+ return lines.join("\n");
1783
+ }
1784
+ function collectSourceFiles(dir, options) {
1785
+ const allowedExts = [".swift", ".ts", ".tsx", ".js", ".jsx", ".py", ".ino", ".cpp", ".c", ".h", ".hpp"];
1786
+ const maxFiles = 70;
1787
+ const gitFiles = collectViaGit(dir, allowedExts, maxFiles);
1788
+ if (gitFiles.length > 0) return gitFiles;
1789
+ return collectViaFilesystem(dir, allowedExts, maxFiles);
1790
+ }
1791
+ function collectViaGit(dir, allowedExts, maxFiles) {
1792
+ try {
1793
+ const raw = child_process.execSync("git ls-files --cached --others --exclude-standard", {
1794
+ cwd: dir,
1795
+ encoding: "utf-8",
1796
+ timeout: 5e3,
1797
+ maxBuffer: 1024 * 1024
1798
+ });
1799
+ const extSet = new Set(allowedExts.map((e) => e.toLowerCase()));
1800
+ return raw.split("\n").filter((line) => line.length > 0).filter((line) => extSet.has(path.extname(line).toLowerCase())).slice(0, maxFiles).map((rel) => path.join(dir, rel));
1801
+ } catch {
1802
+ return [];
1803
+ }
1804
+ }
1805
+ function collectViaFilesystem(dir, allowedExts, maxFiles, skipDirs) {
1806
+ const skipSet = new Set([
1807
+ "node_modules",
1808
+ ".build",
1809
+ "Pods",
1810
+ ".git",
1811
+ ".venv",
1812
+ "venv",
1813
+ "__pycache__",
1814
+ "build",
1815
+ "dist",
1816
+ ".idea",
1817
+ ".vscode",
1818
+ "test",
1819
+ "tests",
1820
+ "demo",
1821
+ "sample",
1822
+ "example",
1823
+ "playground"
1824
+ ]);
1825
+ const extSet = new Set(allowedExts.map((e) => e.toLowerCase()));
1826
+ const result = [];
1827
+ function walk(currentDir) {
1828
+ if (result.length >= maxFiles) return;
1829
+ let entries;
1830
+ try {
1831
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
1832
+ } catch {
1833
+ return;
1834
+ }
1835
+ for (const entry of entries) {
1836
+ if (result.length >= maxFiles) return;
1837
+ const fullPath = path.join(currentDir, entry.name);
1838
+ if (entry.isDirectory()) {
1839
+ if (!skipSet.has(entry.name)) {
1840
+ walk(fullPath);
1841
+ }
1842
+ } else if (entry.isFile()) {
1843
+ const ext = path.extname(entry.name).toLowerCase();
1844
+ if (extSet.has(ext)) {
1845
+ result.push(fullPath);
1846
+ }
1847
+ }
1848
+ }
1849
+ }
1850
+ walk(dir);
1851
+ return result;
1852
+ }
1853
+ function collectSpecificFileContents(dir, relativePaths, maxChars = 5e5) {
1854
+ const contents = /* @__PURE__ */ new Map();
1855
+ let totalChars = 0;
1856
+ for (const relPath of relativePaths) {
1857
+ const fullPath = path.join(dir, relPath);
1858
+ try {
1859
+ const content = fs.readFileSync(fullPath, "utf-8");
1860
+ if (totalChars + content.length > maxChars) {
1861
+ const remaining = maxChars - totalChars;
1862
+ if (remaining > 100) {
1863
+ contents.set(relPath, content.slice(0, remaining));
1864
+ totalChars += remaining;
1865
+ }
1866
+ break;
1867
+ }
1868
+ contents.set(relPath, content);
1869
+ totalChars += content.length;
1870
+ } catch {
1871
+ }
1872
+ }
1873
+ return contents;
1874
+ }
1875
+ function detectPrimaryLanguage(filePaths) {
1876
+ const counts = /* @__PURE__ */ new Map();
1877
+ for (const fp of filePaths) {
1878
+ const ext = path.extname(fp).toLowerCase();
1879
+ let lang = "unknown";
1880
+ if (ext === ".swift") lang = "Swift";
1881
+ else if (ext === ".ts" || ext === ".tsx") lang = "TypeScript";
1882
+ else if (ext === ".js" || ext === ".jsx") lang = "JavaScript";
1883
+ else if (ext === ".py") lang = "Python";
1884
+ else if (ext === ".ino" || ext === ".cpp" || ext === ".c" || ext === ".h") lang = "C++";
1885
+ else if (ext === ".kt" || ext === ".kts") lang = "Kotlin";
1886
+ counts.set(lang, (counts.get(lang) || 0) + 1);
1887
+ }
1888
+ let maxCount = 0;
1889
+ let primary = "unknown";
1890
+ for (const [lang, count] of counts) {
1891
+ if (count > maxCount) {
1892
+ maxCount = count;
1893
+ primary = lang;
1894
+ }
1895
+ }
1896
+ return primary;
1897
+ }
1898
+ async function runProjectGraphPipeline(options) {
1899
+ const { repoDir, goal, techStack, llmConfig, embeddings, onProgress } = options;
1900
+ const log = (step, msg) => {
1901
+ console.log(`[${step}] ${msg}`);
1902
+ onProgress?.(step, msg);
1903
+ };
1904
+ log("STEP_1", "Scanning file tree (names only)...");
1905
+ const allFilePaths = collectSourceFiles(repoDir);
1906
+ const allRelPaths = allFilePaths.map((fp) => path.relative(repoDir, fp));
1907
+ log("STEP_1", `Found ${allRelPaths.length} source files in repository`);
1908
+ log("STEP_1", "Asking LLM to select relevant files from file tree...");
1909
+ const { selected: selectedPaths, reasoning } = await selectFilesWithLlm(
1910
+ allRelPaths,
1911
+ goal,
1912
+ techStack,
1913
+ llmConfig
1914
+ );
1915
+ log("STEP_1", `LLM selected ${selectedPaths.length}/${allRelPaths.length} files: ${reasoning.slice(0, 100)}`);
1916
+ log("STEP_1", "Reading content for selected files...");
1917
+ const fileContentsMap = collectSpecificFileContents(repoDir, selectedPaths);
1918
+ const filePaths = selectedPaths;
1919
+ const primaryLang = detectPrimaryLanguage(filePaths);
1920
+ log("STEP_1", `Primary language: ${primaryLang}, ${filePaths.length} files loaded (${fileContentsMap.size} with content)`);
1921
+ log("STEP_1", "Parsing source files with AST-level analysis...");
1922
+ const allParseResults = [];
1923
+ for (const [relPath, content] of fileContentsMap) {
1924
+ const fullPath = relPath;
1925
+ if (fullPath.endsWith(".swift")) {
1926
+ allParseResults.push(parseSwiftFile(fullPath, content));
1927
+ } else if (fullPath.endsWith(".ts") || fullPath.endsWith(".tsx") || fullPath.endsWith(".js") || fullPath.endsWith(".jsx")) {
1928
+ allParseResults.push(parseTsFile(fullPath, content));
1929
+ } else if (fullPath.endsWith(".py")) {
1930
+ allParseResults.push(parsePythonFile(fullPath, content));
1931
+ } else if (fullPath.endsWith(".ino") || fullPath.endsWith(".cpp") || fullPath.endsWith(".c") || fullPath.endsWith(".h")) {
1932
+ allParseResults.push(parseCppFile(fullPath, content));
1933
+ }
1934
+ }
1935
+ let mergedContext;
1936
+ if (primaryLang === "Swift") {
1937
+ mergedContext = mergeSwiftResults(allParseResults.filter((r) => "property_wrappers" in r));
1938
+ } else if (primaryLang === "TypeScript" || primaryLang === "JavaScript") {
1939
+ mergedContext = mergeTsResults(allParseResults.filter((r) => !("docstrings" in r) && !("property_wrappers" in r) && !("frameworks_used" in r)));
1940
+ } else if (primaryLang === "Python") {
1941
+ mergedContext = mergePythonResults(allParseResults.filter((r) => "docstrings" in r));
1942
+ } else {
1943
+ mergedContext = mergeCppResults(allParseResults.filter((r) => "error_handling" in r && !("docstrings" in r) && !("property_wrappers" in r)));
1944
+ }
1945
+ log("STEP_2", "Extracting keywords from AST analysis...");
1946
+ const astKeywords = extractKeywords(mergedContext);
1947
+ log("STEP_2", `Extracted ${astKeywords.length} keywords`);
1948
+ log("STEP_3", "Building SDK API index...");
1949
+ const sdkApiIndex = {
1950
+ sdk_apis: astKeywords.map((kw) => ({
1951
+ name: kw.keyword,
1952
+ kind: kw.source,
1953
+ used_in_demo: true
1954
+ }))
1955
+ };
1956
+ log("STEP_4", "Generating project graph via LLM (C0: Scaffold, C1: Overview, C2: Steps)...");
1957
+ log("STEP_4_C0", "Extracting FOUNDATION & SETUP feature (F0)...");
1958
+ const scaffoldFeature = await extractScaffold({
1959
+ goal,
1960
+ techStack,
1961
+ fileList: filePaths,
1962
+ llmConfig
1963
+ });
1964
+ log("STEP_4_C1", "Extracting project overview (features meta)...");
1965
+ const overview = await extractProjectOverview({
1966
+ goal,
1967
+ techStack,
1968
+ sdkApiIndex,
1969
+ fileContentsMap,
1970
+ astKeywords,
1971
+ llmConfig
1972
+ });
1973
+ const product = overview.product;
1974
+ const pblStep = {
1975
+ id: "F0-S1",
1976
+ sequence: 1,
1977
+ name: "Frame the problem & understand user needs (Design Thinking)",
1978
+ description: "BEFORE CODING \u2014 frame the problem and understand what the user needs",
1979
+ files: [],
1980
+ api_usage: [],
1981
+ keywords: ["Design Thinking", "User Needs", "Problem Statement", "User Journey", "PBL"],
1982
+ completion_level: "base",
1983
+ platform: "app",
1984
+ intent: "Understand user needs and frame problem before writing code",
1985
+ outcome: { user_visible: "Defined problem statement", technical: "Problem frame context" },
1986
+ acceptance: ["Problem statement is clear", "Primary users identified"],
1987
+ requirement_ids: [],
1988
+ concept_codes: [],
1989
+ effort: { estimated_minutes: 15, complexity: "low" }
1990
+ };
1991
+ let featuresFull = [];
1992
+ if (scaffoldFeature) {
1993
+ const scaffoldSteps = [pblStep, ...scaffoldFeature.steps || []].map((s, i) => ({
1994
+ ...s,
1995
+ id: `F0-S${i + 1}`,
1996
+ sequence: i + 1
1997
+ }));
1998
+ featuresFull.push({
1999
+ ...scaffoldFeature,
2000
+ id: "F0",
2001
+ name: "FOUNDATION & SETUP",
2002
+ steps: scaffoldSteps
2003
+ });
2004
+ } else {
2005
+ featuresFull.push({
2006
+ id: "F0",
2007
+ name: "FOUNDATION & SETUP",
2008
+ description: "Frame the problem, get familiar with the tools",
2009
+ steps: [pblStep]
2010
+ });
2011
+ }
2012
+ const overviewFeatures = product?.features || [];
2013
+ featuresFull = [...featuresFull, ...overviewFeatures];
2014
+ log("STEP_4_C2", `Extracting steps for ${overviewFeatures.length} features...`);
2015
+ const nonF0Features = overviewFeatures.filter((f) => f.id !== "F0");
2016
+ if (nonF0Features.length > 0) {
2017
+ try {
2018
+ const batchedSteps = await extractFeatureStepsBatched(
2019
+ goal,
2020
+ techStack,
2021
+ sdkApiIndex,
2022
+ fileContentsMap,
2023
+ nonF0Features,
2024
+ llmConfig
2025
+ );
2026
+ for (const feature of nonF0Features) {
2027
+ const steps = batchedSteps.get(feature.id || "");
2028
+ if (steps) feature.steps = steps;
2029
+ }
2030
+ } catch (err) {
2031
+ log("STEP_4_C2", `Batch extraction failed (${err}), falling back to per-feature...`);
2032
+ for (const feature of nonF0Features) {
2033
+ try {
2034
+ const steps = await extractFeatureSteps({
2035
+ goal,
2036
+ techStack,
2037
+ sdkApiIndex,
2038
+ fileContentsMap,
2039
+ feature,
2040
+ llmConfig
2041
+ });
2042
+ feature.steps = steps;
2043
+ } catch (e) {
2044
+ log("STEP_4_C2", `Failed for ${feature.id}: ${e}`);
2045
+ }
2046
+ }
2047
+ }
2048
+ }
2049
+ const projectGraph = {
2050
+ schema_version: 3,
2051
+ project: {
2052
+ id: overview.project?.name?.toString().toLowerCase().replace(/\s+/g, "-") || "project",
2053
+ name: overview.project?.name?.toString() || "Project",
2054
+ purpose: goal,
2055
+ version: "0.1.0",
2056
+ project_type: "app",
2057
+ platform: overview.project?.platforms || ["ios"],
2058
+ tech_stack: techStack.split(",").reduce((acc, t) => {
2059
+ const key = t.trim().toLowerCase();
2060
+ if (key) acc[key] = [t.trim()];
2061
+ return acc;
2062
+ }, {}),
2063
+ architecture: overview.architecture?.layers || []
2064
+ },
2065
+ product: {
2066
+ goals: product?.goals || [],
2067
+ users: product?.users || [],
2068
+ journeys: product?.user_journeys || [],
2069
+ requirements: [],
2070
+ development_stages: product?.development_stages || [],
2071
+ purpose: String(product?.purpose || ""),
2072
+ problem_statement: String(product?.problem_statement || ""),
2073
+ primary_users: product?.primary_users || []
2074
+ },
2075
+ features: featuresFull,
2076
+ capabilities: [],
2077
+ implementation: {
2078
+ tasks: []
2079
+ },
2080
+ missing_gaps: [],
2081
+ tech_debt: []
2082
+ };
2083
+ log("STEP_5", "Verifying project graph against source code...");
2084
+ const { graph: verifiedGraph, hallucinations } = verifyProjectGraph(projectGraph, {
2085
+ sdkApiIndex,
2086
+ fileContentsMap
2087
+ });
2088
+ log("STEP_5", `Verification complete: ${hallucinations.length} hallucinations found`);
2089
+ log("STEP_6", "Escalating keywords to neutral concepts...");
2090
+ const featureConcepts = await escalateAndMapConcepts({
2091
+ projectGraph: verifiedGraph,
2092
+ fileContentsMap,
2093
+ llmConfig
2094
+ });
2095
+ log("STEP_6", `Concept mapping complete: ${featureConcepts.size} features mapped`);
2096
+ for (const feature of verifiedGraph.features || []) {
2097
+ const fid = feature.id || "";
2098
+ const fConcepts = featureConcepts.get(fid) || [];
2099
+ for (const step of feature.steps || []) {
2100
+ const stepConcepts = fConcepts.filter((cm) => {
2101
+ const stepKws = [...step.keywords || [], ...step.api_usage || []];
2102
+ return stepKws.some((k) => k === cm.keyword);
2103
+ }).map((cm) => cm.concept_code);
2104
+ const existing = step.concept_codes || [];
2105
+ step.concept_codes = [.../* @__PURE__ */ new Set([...existing, ...stepConcepts])];
2106
+ }
2107
+ }
2108
+ log("STEP_7", "Resolving keywords to Master Tree concepts...");
2109
+ const llmKeywords = [];
2110
+ const seenKw = /* @__PURE__ */ new Set();
2111
+ for (const feature of verifiedGraph.features || []) {
2112
+ for (const step of feature.steps || []) {
2113
+ for (const kw of [...step.keywords || [], ...step.api_usage || []]) {
2114
+ if (!seenKw.has(kw.toLowerCase())) {
2115
+ seenKw.add(kw.toLowerCase());
2116
+ llmKeywords.push({ keyword: kw, source: "llm_step", platform: "app", weight: 1, context: "LLM-generated step keyword" });
2117
+ }
2118
+ }
2119
+ }
2120
+ }
2121
+ const astKwMap = new Map(astKeywords.map((k) => [k.keyword.toLowerCase(), k]));
2122
+ const mergedKeywords = [];
2123
+ const mergedSeen = /* @__PURE__ */ new Set();
2124
+ for (const kw of [...astKeywords, ...llmKeywords]) {
2125
+ const key = kw.keyword.toLowerCase();
2126
+ if (!mergedSeen.has(key)) {
2127
+ mergedSeen.add(key);
2128
+ mergedKeywords.push(astKwMap.get(key) || kw);
2129
+ }
2130
+ }
2131
+ const depthOverrides = /* @__PURE__ */ new Map();
2132
+ for (const mappings of featureConcepts.values()) {
2133
+ for (const cm of mappings) {
2134
+ if (!depthOverrides.has(cm.keyword)) {
2135
+ depthOverrides.set(cm.keyword, cm.depth);
2136
+ }
2137
+ }
2138
+ }
2139
+ const resolvedConcepts = await resolveConcepts({
2140
+ keywords: mergedKeywords,
2141
+ goal,
2142
+ embeddings,
2143
+ depthOverrides});
2144
+ log("STEP_7", `Resolved: ${resolvedConcepts.summary.resolved_count}, Proposed: ${resolvedConcepts.summary.proposed_count} (${resolvedConcepts.summary.search_mode})`);
2145
+ log("STEP_8", "Assembling roadmap with keyword tracking...");
2146
+ const roadmap = assembleRoadmap({
2147
+ projectGraph: verifiedGraph,
2148
+ featureConcepts,
2149
+ sessionMinutes: 90,
2150
+ overheadFactor: 1.15,
2151
+ maxMilestoneMinutes: 120
2152
+ });
2153
+ log("STEP_8", `Roadmap assembled: ${roadmap.phases.length} phases, ${roadmap.phases.reduce((s, p) => s + p.milestones.length, 0)} milestones`);
2154
+ return {
2155
+ projectGraph: verifiedGraph,
2156
+ resolvedConcepts,
2157
+ roadmap,
2158
+ hallucinations,
2159
+ keywords: astKeywords,
2160
+ featureConcepts
2161
+ };
2162
+ }
2163
+
2164
+ exports.runProjectGraphPipeline = runProjectGraphPipeline;
2165
+ //# sourceMappingURL=index.cjs.map
2166
+ //# sourceMappingURL=index.cjs.map