@thanh01.pmt/domain-kit 0.1.0 → 0.2.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-7XGWW6CB.mjs +3 -0
  3. package/dist/{chunk-F4RZNBOE.mjs.map → chunk-7XGWW6CB.mjs.map} +1 -1
  4. package/dist/{chunk-VFART56O.mjs → chunk-BL5NBKPH.mjs} +12 -8
  5. package/dist/chunk-BL5NBKPH.mjs.map +1 -0
  6. package/dist/chunk-IJZUV7N3.mjs +3 -0
  7. package/dist/chunk-IJZUV7N3.mjs.map +1 -0
  8. package/dist/{chunk-3VVPSRAM.mjs → chunk-JE57VFWX.mjs} +1056 -92
  9. package/dist/chunk-JE57VFWX.mjs.map +1 -0
  10. package/dist/{chunk-GEUDHZE7.mjs → chunk-KPZKPNOJ.mjs} +173 -4
  11. package/dist/chunk-KPZKPNOJ.mjs.map +1 -0
  12. package/dist/chunk-R57WTNXW.mjs +3 -0
  13. package/dist/chunk-R57WTNXW.mjs.map +1 -0
  14. package/dist/{chunk-3H2HX7PR.mjs → chunk-RAR5MUUK.mjs} +57 -7
  15. package/dist/chunk-RAR5MUUK.mjs.map +1 -0
  16. package/dist/chunk-T4Y2DE2W.mjs +187 -0
  17. package/dist/chunk-T4Y2DE2W.mjs.map +1 -0
  18. package/dist/{chunk-PM42MMDJ.mjs → chunk-WHBAGJWN.mjs} +48 -3
  19. package/dist/chunk-WHBAGJWN.mjs.map +1 -0
  20. package/dist/chunk-ZKLT27T4.mjs +3 -0
  21. package/dist/chunk-ZKLT27T4.mjs.map +1 -0
  22. package/dist/concepts/index.d.cts +2 -2
  23. package/dist/concepts/index.d.ts +2 -2
  24. package/dist/cpp-CZUPqJt9.d.cts +98 -0
  25. package/dist/cpp-CZUPqJt9.d.ts +98 -0
  26. package/dist/curriculumFeedEmitter-UG664SWY.mjs +4 -0
  27. package/dist/curriculumFeedEmitter-UG664SWY.mjs.map +1 -0
  28. package/dist/detector/index.cjs +10 -6
  29. package/dist/detector/index.cjs.map +1 -1
  30. package/dist/detector/index.d.cts +4 -19
  31. package/dist/detector/index.d.ts +4 -19
  32. package/dist/detector/index.mjs +2 -1
  33. package/dist/domainProfileDetector-DHb1T0zB.d.ts +22 -0
  34. package/dist/domainProfileDetector-bzugdQmS.d.cts +22 -0
  35. package/dist/extractors/index.d.cts +3 -3
  36. package/dist/extractors/index.d.ts +3 -3
  37. package/dist/feed/index.cjs +55 -5
  38. package/dist/feed/index.cjs.map +1 -1
  39. package/dist/feed/index.mjs +2 -1
  40. package/dist/graph/index.cjs +368 -64
  41. package/dist/graph/index.cjs.map +1 -1
  42. package/dist/graph/index.d.cts +7 -67
  43. package/dist/graph/index.d.ts +7 -67
  44. package/dist/graph/index.mjs +3 -2
  45. package/dist/hybridGraphPipeline-DVZYLCAQ.d.ts +159 -0
  46. package/dist/hybridGraphPipeline-DmWRKYXf.d.cts +159 -0
  47. package/dist/index.cjs +1300 -697
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +9 -7
  50. package/dist/index.d.ts +9 -7
  51. package/dist/index.mjs +10 -7
  52. package/dist/{keywordExtractor-zPAz2isq.d.cts → keywordExtractor-BxlGRMHC.d.cts} +1 -1
  53. package/dist/{keywordExtractor-CKaXqSku.d.ts → keywordExtractor-DU5XRN8-.d.ts} +1 -1
  54. package/dist/parsers/index.cjs +185 -0
  55. package/dist/parsers/index.cjs.map +1 -1
  56. package/dist/parsers/index.d.cts +2 -98
  57. package/dist/parsers/index.d.ts +2 -98
  58. package/dist/parsers/index.mjs +2 -1
  59. package/dist/pipeline/index.cjs +3140 -6
  60. package/dist/pipeline/index.cjs.map +1 -1
  61. package/dist/pipeline/index.d.cts +87 -4
  62. package/dist/pipeline/index.d.ts +87 -4
  63. package/dist/pipeline/index.mjs +6 -2
  64. package/dist/schemas/index.cjs +51 -0
  65. package/dist/schemas/index.cjs.map +1 -1
  66. package/dist/schemas/index.d.cts +222 -3
  67. package/dist/schemas/index.d.ts +222 -3
  68. package/dist/schemas/index.mjs +1 -1
  69. package/dist/syllabusParser-MSX1fEzW.d.cts +40 -0
  70. package/dist/syllabusParser-MSX1fEzW.d.ts +40 -0
  71. package/package.json +9 -10
  72. package/dist/chunk-3H2HX7PR.mjs.map +0 -1
  73. package/dist/chunk-3VVPSRAM.mjs.map +0 -1
  74. package/dist/chunk-F4RZNBOE.mjs +0 -3
  75. package/dist/chunk-GEUDHZE7.mjs.map +0 -1
  76. package/dist/chunk-MOKNFLUD.mjs +0 -851
  77. package/dist/chunk-MOKNFLUD.mjs.map +0 -1
  78. package/dist/chunk-PM42MMDJ.mjs.map +0 -1
  79. package/dist/chunk-VFART56O.mjs.map +0 -1
  80. package/dist/graphVerifier-DsTP9uAN.d.ts +0 -37
  81. package/dist/graphVerifier-QjgDAJce.d.cts +0 -37
@@ -1,8 +1,497 @@
1
1
  'use strict';
2
2
 
3
+ var zod = require('zod');
3
4
  var fs = require('fs');
4
5
  var path = require('path');
5
6
  var child_process = require('child_process');
7
+ var os = require('os');
8
+
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __esm = (fn, res) => function __init() {
12
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ function validateCurriculumFeed(feed) {
19
+ const parsed = CurriculumFeedSchema.parse(feed);
20
+ const ids = new Set(parsed.learning_nodes.map((n) => n.id));
21
+ if (ids.size !== parsed.learning_nodes.length) {
22
+ throw new Error("curriculum feed contains duplicate node ids");
23
+ }
24
+ for (const e of parsed.dependency_edges) {
25
+ if (!ids.has(e.from) || !ids.has(e.to)) {
26
+ throw new Error("curriculum feed edge " + e.from + " -> " + e.to + " references unknown node");
27
+ }
28
+ if (e.from === e.to) throw new Error("curriculum feed self-loop edge on " + e.from);
29
+ }
30
+ for (const g of parsed.suggested_groupings) {
31
+ for (const id of g.node_ids) {
32
+ if (!ids.has(id)) throw new Error("grouping " + g.id + " references unknown node " + id);
33
+ }
34
+ }
35
+ for (const ph of parsed.phases) {
36
+ for (const id of ph.node_ids) {
37
+ if (!ids.has(id)) throw new Error("phase " + ph.id + " references unknown node " + id);
38
+ }
39
+ }
40
+ const phaseIds = parsed.phases.map((p) => p.id);
41
+ if (new Set(phaseIds).size !== phaseIds.length) throw new Error("curriculum feed contains duplicate phase ids");
42
+ return parsed;
43
+ }
44
+ var FeedKeywordsSchema, FeedScaffoldCandidateSchema, FeedNodeSchema, FeedEdgeSchema, FeedGroupingSchema, FeedPhaseSchema, CurriculumFeedSchema;
45
+ var init_curriculumFeedSchema = __esm({
46
+ "src/schemas/curriculumFeedSchema.ts"() {
47
+ FeedKeywordsSchema = zod.z.object({
48
+ all: zod.z.array(zod.z.string()).default([]),
49
+ new: zod.z.array(zod.z.string()).default([]),
50
+ prerequisite: zod.z.array(zod.z.string()).default([])
51
+ });
52
+ FeedScaffoldCandidateSchema = zod.z.object({
53
+ item: zod.z.string(),
54
+ reason: zod.z.string().default(""),
55
+ minutes_saved: zod.z.number().int().nonnegative().default(0),
56
+ suggest: zod.z.enum(["provide_full", "provide_partial", "provide_reference"]).default("provide_full")
57
+ });
58
+ FeedNodeSchema = zod.z.object({
59
+ id: zod.z.string().min(1),
60
+ kind: zod.z.enum(["skill", "concept", "product_step"]),
61
+ name: zod.z.string().min(1),
62
+ concept_codes: zod.z.array(zod.z.string()).default([]),
63
+ keywords: FeedKeywordsSchema,
64
+ estimated_minutes: zod.z.number().int().nonnegative(),
65
+ bloom_hint: zod.z.enum(["Remember", "Understand", "Apply", "Analyze", "Evaluate", "Create"]).nullable().default(null),
66
+ depth_hint: zod.z.enum(["ulo", "cio", "sio"]).nullable().default(null),
67
+ completion_level: zod.z.enum(["mvp", "standard", "stretch"]).default("standard"),
68
+ scaffolding: zod.z.object({
69
+ scaffold_candidates: zod.z.array(FeedScaffoldCandidateSchema).default([]),
70
+ core_value_minutes: zod.z.number().int().nonnegative().default(0)
71
+ }).default({ scaffold_candidates: [], core_value_minutes: 0 }),
72
+ references: zod.z.array(zod.z.object({ file: zod.z.string().min(1), evidence: zod.z.string().default("") })).default([]),
73
+ feature_id: zod.z.string().nullable().default(null),
74
+ concept_ref: zod.z.string().nullable().default(null),
75
+ // Progressive-completion bookkeeping (feed v2, additive):
76
+ // phase_id = development phase that introduces/uses this node;
77
+ // introduce_aspect = how the phase teaches a concept node (first intro vs advanced revisit).
78
+ phase_id: zod.z.string().default(""),
79
+ introduce_aspect: zod.z.enum(["new", "advanced"]).nullable().default(null)
80
+ });
81
+ FeedEdgeSchema = zod.z.object({
82
+ from: zod.z.string().min(1),
83
+ to: zod.z.string().min(1),
84
+ kind: zod.z.enum(["knowledge", "task"]),
85
+ reason: zod.z.string().default("")
86
+ });
87
+ FeedGroupingSchema = zod.z.object({
88
+ id: zod.z.string().min(1),
89
+ name: zod.z.string().min(1),
90
+ node_ids: zod.z.array(zod.z.string()).min(1),
91
+ rationale: zod.z.string().default("")
92
+ });
93
+ FeedPhaseSchema = zod.z.object({
94
+ id: zod.z.string().min(1),
95
+ order: zod.z.number().int().positive(),
96
+ name: zod.z.string().min(1),
97
+ product_completion: zod.z.string().min(1),
98
+ node_ids: zod.z.array(zod.z.string()).min(1)
99
+ });
100
+ CurriculumFeedSchema = zod.z.object({
101
+ schema_version: zod.z.literal(2),
102
+ source: zod.z.object({
103
+ graph_type: zod.z.enum(["project_graph", "knowledge_graph", "hybrid_graph"]),
104
+ warnings: zod.z.array(zod.z.string()).default([]),
105
+ hallucination_count: zod.z.number().int().nonnegative().default(0)
106
+ }),
107
+ learning_nodes: zod.z.array(FeedNodeSchema).min(1, "curriculum feed must contain at least one learning node"),
108
+ dependency_edges: zod.z.array(FeedEdgeSchema).default([]),
109
+ suggested_groupings: zod.z.array(FeedGroupingSchema).default([]),
110
+ // Progressive completion phases (cross-feature). When non-empty, these — not
111
+ // suggested_groupings — define the teaching sequence and unit structure.
112
+ phases: zod.z.array(FeedPhaseSchema).default([])
113
+ });
114
+ }
115
+ });
116
+
117
+ // src/feed/curriculumFeedEmitter.ts
118
+ var curriculumFeedEmitter_exports = {};
119
+ __export(curriculumFeedEmitter_exports, {
120
+ emitCurriculumFeed: () => emitCurriculumFeed
121
+ });
122
+ function stepMinutes(step) {
123
+ const effort = step.effort ?? {};
124
+ return Math.max(0, Math.round(num(effort.estimated_minutes, num(step.estimated_minutes, 45))));
125
+ }
126
+ function newKeywordsInOrder(nodes) {
127
+ const seen = /* @__PURE__ */ new Set();
128
+ for (const n of nodes) {
129
+ const all = n.keywords.all;
130
+ n.keywords.new = [];
131
+ n.keywords.prerequisite = [];
132
+ for (const k of all) {
133
+ if (seen.has(k)) n.keywords.prerequisite.push(k);
134
+ else n.keywords.new.push(k);
135
+ seen.add(k);
136
+ }
137
+ }
138
+ }
139
+ function feedNodeFromStep(step, featureId) {
140
+ const id = str(step.id);
141
+ return {
142
+ id,
143
+ kind: "product_step",
144
+ name: str(step.name, id),
145
+ concept_codes: strs(step.concept_codes),
146
+ keywords: { all: strs(step.keywords), new: [], prerequisite: [] },
147
+ estimated_minutes: stepMinutes(step),
148
+ bloom_hint: "Apply",
149
+ depth_hint: null,
150
+ completion_level: "standard",
151
+ scaffolding: { scaffold_candidates: [], core_value_minutes: stepMinutes(step) },
152
+ references: arr(step.files).map((f) => ({ file: str(f), evidence: "step file" })).filter((f) => f.file),
153
+ feature_id: featureId,
154
+ concept_ref: null,
155
+ phase_id: "",
156
+ introduce_aspect: null
157
+ };
158
+ }
159
+ function deriveConceptDepthHint(concept) {
160
+ const explicit = str(concept.depth_hint);
161
+ if (explicit === "ulo" || explicit === "cio" || explicit === "sio") return explicit;
162
+ const sio = arr(concept.sio);
163
+ const prereqs = strs(concept.prerequisites);
164
+ const pts = arr(concept.problem_types);
165
+ const hasHighBloom = pts.some((p) => {
166
+ const b = str(p.bloom_level);
167
+ return b === "Apply" || b === "Analyze" || b === "Evaluate" || b === "Create";
168
+ });
169
+ if (sio.length >= 2 || hasHighBloom) return "sio";
170
+ if (str(concept.cio) || prereqs.length >= 1) return "cio";
171
+ return "ulo";
172
+ }
173
+ function feedNodeFromConcept(concept) {
174
+ const id = str(concept.id);
175
+ const pts = arr(concept.problem_types).map((p) => str(p.bloom_level, "Understand")).filter((b) => b in BLOOM_RANK).sort((a, b) => BLOOM_RANK[a] - BLOOM_RANK[b]);
176
+ const topBloom = pts[pts.length - 1] ?? "Understand";
177
+ const rawRefs = arr(concept.references);
178
+ const references = rawRefs.length > 0 ? rawRefs.map((r) => ({ file: str(r.file, "syllabus.md"), evidence: str(r.evidence, str(concept.name)) })).filter((r) => r.file) : [{ file: "syllabus.md", evidence: str(concept.category ? `Topic under category ${str(concept.category)}` : str(concept.name), id) }];
179
+ const problemScaffolds = arr(concept.problem_types).map((pt) => ({
180
+ item: str(pt.name, "Problem type"),
181
+ reason: str(pt.description, "Formative practice check"),
182
+ minutes_saved: num(pt.estimated_minutes, 10),
183
+ suggest: "provide_partial"
184
+ }));
185
+ return {
186
+ id,
187
+ kind: "concept",
188
+ name: str(concept.name, id),
189
+ concept_codes: [str(concept.code) || id].filter(Boolean),
190
+ keywords: { all: strs(concept.keywords), new: [], prerequisite: [] },
191
+ estimated_minutes: Math.max(0, Math.round(num(concept.estimated_minutes, 30))),
192
+ bloom_hint: topBloom,
193
+ depth_hint: deriveConceptDepthHint(concept),
194
+ completion_level: "standard",
195
+ scaffolding: {
196
+ scaffold_candidates: problemScaffolds,
197
+ core_value_minutes: Math.max(0, Math.round(num(concept.estimated_minutes, 30)))
198
+ },
199
+ references,
200
+ feature_id: null,
201
+ concept_ref: id,
202
+ phase_id: "",
203
+ introduce_aspect: null
204
+ };
205
+ }
206
+ function addFeatureGroup(f, fid, nodes, edges, groupings) {
207
+ const steps = arr(f.steps);
208
+ const stepIds = [];
209
+ let prev = "";
210
+ for (let i = 0; i < steps.length; i++) {
211
+ const stepNode = feedNodeFromStep(steps[i], fid);
212
+ if (!stepNode.id) continue;
213
+ stepIds.push(stepNode.id);
214
+ nodes.push(stepNode);
215
+ if (prev) edges.push({ from: prev, to: stepNode.id, kind: "task", reason: "feature build order" });
216
+ prev = stepNode.id;
217
+ }
218
+ for (const dep of strs(f.depends_on)) {
219
+ const depFirst = nodes.find((n) => n.feature_id === dep);
220
+ if (depFirst && stepIds[0]) edges.push({ from: depFirst.id, to: stepIds[0], kind: "task", reason: "feature depends_on " + dep });
221
+ }
222
+ if (stepIds.length > 0) groupings.push({ id: "G-" + fid, name: str(f.name, fid), node_ids: stepIds, rationale: str(f.description, "") });
223
+ return stepIds;
224
+ }
225
+ function emitFromHybrid(hybrid, opts) {
226
+ const nodes = [];
227
+ const edges = [];
228
+ const groupings = [];
229
+ const pg = hybrid.project_graph ?? {};
230
+ const kg = hybrid.knowledge_graph ?? {};
231
+ const rawPhases = arr(hybrid.phases).filter((p) => str(p.id));
232
+ const features = arr(pg.features);
233
+ const conceptRecs = arr(kg.concepts);
234
+ const conceptById = new Map(conceptRecs.map((c) => [str(c.id), c]));
235
+ if (rawPhases.length === 0) {
236
+ for (const c of conceptRecs) {
237
+ const node = feedNodeFromConcept(c);
238
+ nodes.push(node);
239
+ for (const p of strs(c.prerequisites)) edges.push({ from: p, to: node.id, kind: "knowledge", reason: "concept prerequisite" });
240
+ }
241
+ features.forEach((f, fi) => addFeatureGroup(f, str(f.id, "F" + pad22(fi + 1)), nodes, edges, groupings));
242
+ linkHybridKnowledgeEdges(hybrid, nodes, edges);
243
+ newKeywordsInOrder(nodes);
244
+ return validateCurriculumFeed({
245
+ schema_version: 2,
246
+ source: { graph_type: "hybrid_graph", warnings: opts.warnings ?? [], hallucination_count: opts.hallucinationCount ?? 0 },
247
+ learning_nodes: nodes,
248
+ dependency_edges: edges,
249
+ suggested_groupings: groupings,
250
+ phases: []
251
+ });
252
+ }
253
+ const featureOrder = /* @__PURE__ */ new Map();
254
+ features.forEach((f, fi) => featureOrder.set(str(f.id, "F" + pad22(fi + 1)), fi));
255
+ const stepIdsByFeature = /* @__PURE__ */ new Map();
256
+ for (const f of features) {
257
+ const fid = str(f.id);
258
+ stepIdsByFeature.set(fid, arr(f.steps).map((s) => str(s.id)).filter(Boolean));
259
+ }
260
+ const emitCtx = [];
261
+ const emittedConcepts = /* @__PURE__ */ new Set();
262
+ const linkList = arr(hybrid.links);
263
+ for (const rp of rawPhases) {
264
+ const pid = str(rp.id);
265
+ const order = num(rp.order, emitCtx.length + 1);
266
+ const conceptOrder = [];
267
+ const intros = arr(rp.introduces);
268
+ const news = intros.filter((i) => str(i.aspect, "new") === "new");
269
+ const advanced = intros.filter((i) => str(i.aspect) === "advanced");
270
+ for (const intro of [...news, ...advanced]) {
271
+ const cid = str(intro.concept_id);
272
+ const c = conceptById.get(cid);
273
+ if (!c) {
274
+ throw new Error("Phase " + pid + " introduces unknown concept " + cid + " - fail-closed");
275
+ }
276
+ if (str(intro.aspect, "new") === "advanced" && !emittedConcepts.has(cid)) {
277
+ throw new Error("Phase " + pid + " advanced-revisits concept " + cid + " that was never emitted - fail-closed");
278
+ }
279
+ if (!emittedConcepts.has(cid)) {
280
+ const node = feedNodeFromConcept(c);
281
+ node.phase_id = pid;
282
+ node.introduce_aspect = "new";
283
+ nodes.push(node);
284
+ for (const p of strs(c.prerequisites)) {
285
+ if (!emittedConcepts.has(p)) {
286
+ throw new Error("Concept " + cid + " (phase " + pid + ") has prerequisite " + p + " that is not introduced in an earlier phase - fail-closed");
287
+ }
288
+ edges.push({ from: p, to: cid, kind: "knowledge", reason: "concept prerequisite" });
289
+ }
290
+ emittedConcepts.add(cid);
291
+ conceptOrder.push(cid);
292
+ } else {
293
+ const revisitId = cid + "__ADV" + pad22(order);
294
+ const node = feedNodeFromConcept(c);
295
+ node.id = revisitId;
296
+ node.name = str(c.name, cid) + " (advanced)";
297
+ node.phase_id = pid;
298
+ node.introduce_aspect = "advanced";
299
+ node.estimated_minutes = Math.max(15, Math.round(node.estimated_minutes * 0.5));
300
+ node.concept_codes = [str(c.code) || cid];
301
+ edges.push({ from: cid, to: revisitId, kind: "knowledge", reason: "advanced revisit of " + cid + " at phase " + pid });
302
+ nodes.push(node);
303
+ conceptOrder.push(revisitId);
304
+ }
305
+ }
306
+ const featureIds = strs(rp.feature_ids).filter((fid) => featureOrder.has(fid)).sort((a, b) => featureOrder.get(a) - featureOrder.get(b));
307
+ emitCtx.push({ id: pid, order, name: str(rp.name, pid), productCompletion: str(rp.product_completion), conceptOrder, featureIds });
308
+ }
309
+ for (const l of linkList) {
310
+ const cid = str(l.concept_id);
311
+ if (cid && conceptById.has(cid) && !emittedConcepts.has(cid)) {
312
+ throw new Error("Concept " + cid + " is consumed by links but never introduced in any phase - fail-closed");
313
+ }
314
+ }
315
+ const seenFeatures = new Set(emitCtx.flatMap((e) => e.featureIds));
316
+ const missingFeatures = features.map((f) => str(f.id)).filter((fid) => fid && !seenFeatures.has(fid));
317
+ if (missingFeatures.length > 0) {
318
+ throw new Error("Features not covered by any phase: " + missingFeatures.join(", ") + " - fail-closed");
319
+ }
320
+ const phaseNodeIds = [];
321
+ for (const ctx of emitCtx) {
322
+ const ids = [...ctx.conceptOrder];
323
+ for (const fid of ctx.featureIds) {
324
+ const f = features.find((x) => str(x.id) === fid);
325
+ const stepIds = addFeatureGroup(f, fid, nodes, edges, groupings);
326
+ for (const sid of stepIds) {
327
+ const node = nodes.find((n) => n.id === sid);
328
+ node.phase_id = ctx.id;
329
+ }
330
+ ids.push(...stepIds);
331
+ }
332
+ phaseNodeIds.push(ids);
333
+ }
334
+ const edgeSeen = new Set(edges.map((e) => e.from + ">" + e.to));
335
+ const pushEdge = (from, to, reason) => {
336
+ const key = from + ">" + to;
337
+ if (edgeSeen.has(key)) return;
338
+ edgeSeen.add(key);
339
+ edges.push({ from, to, kind: "knowledge", reason });
340
+ };
341
+ for (const l of linkList) {
342
+ const cid = str(l.concept_id);
343
+ const fid = str(l.feature_id);
344
+ if (!conceptById.has(cid)) continue;
345
+ const firstStep = (stepIdsByFeature.get(fid) || [])[0];
346
+ if (!firstStep) continue;
347
+ const depth = str(l.depth, "cio");
348
+ pushEdge(cid, firstStep, str(l.relationship, "hybrid link") + " [" + depth + "]");
349
+ for (const n of nodes) {
350
+ if (n.kind === "concept" && n.concept_ref === cid && n.introduce_aspect === "advanced" && n.phase_id === str(l.phase_id)) {
351
+ pushEdge(n.id, firstStep, "advanced application [" + depth + "]");
352
+ }
353
+ }
354
+ }
355
+ newKeywordsInOrder(nodes);
356
+ const feedOrder = new Map(nodes.map((n, i) => [n.id, i]));
357
+ const introducer = /* @__PURE__ */ new Map();
358
+ for (const n of nodes) for (const k of n.keywords?.new ?? []) {
359
+ const key = k.toLowerCase();
360
+ if (!introducer.has(key)) introducer.set(key, n.id);
361
+ }
362
+ for (const n of nodes) {
363
+ for (const k of n.keywords?.prerequisite ?? []) {
364
+ const src = introducer.get(k.toLowerCase());
365
+ if (!src || src === n.id) continue;
366
+ if (feedOrder.get(src) < feedOrder.get(n.id)) pushEdge(src, n.id, "keyword prerequisite [" + k + "]");
367
+ }
368
+ }
369
+ const phases = emitCtx.map((ctx, i) => ({
370
+ id: ctx.id,
371
+ order: ctx.order,
372
+ name: ctx.name,
373
+ product_completion: ctx.productCompletion || "Phase " + (i + 1) + " deliverable",
374
+ node_ids: phaseNodeIds[i]
375
+ }));
376
+ return validateCurriculumFeed({
377
+ schema_version: 2,
378
+ source: { graph_type: "hybrid_graph", warnings: opts.warnings ?? [], hallucination_count: opts.hallucinationCount ?? 0 },
379
+ learning_nodes: nodes,
380
+ dependency_edges: edges,
381
+ suggested_groupings: groupings,
382
+ phases
383
+ });
384
+ }
385
+ function linkHybridKnowledgeEdges(hybrid, nodes, edges, setStepHints) {
386
+ const conceptIds = new Set(nodes.filter((n) => n.kind === "concept").map((n) => n.concept_ref || n.id));
387
+ for (const l of arr(hybrid.links)) {
388
+ const conceptId = str(l.concept_id);
389
+ const featureId = str(l.feature_id);
390
+ if (!conceptIds.has(conceptId)) continue;
391
+ const depth = str(l.depth, "cio");
392
+ const firstStep = nodes.find((n) => n.feature_id === featureId);
393
+ if (firstStep) {
394
+ edges.push({ from: conceptId, to: firstStep.id, kind: "knowledge", reason: str(l.relationship, "hybrid link") + " [" + depth + "]" });
395
+ if (!firstStep.depth_hint) {
396
+ firstStep.depth_hint = ["ulo", "cio", "sio"].includes(depth) ? depth : "cio";
397
+ }
398
+ }
399
+ }
400
+ }
401
+ function emitFromKnowledge(kg, opts) {
402
+ const nodes = [];
403
+ const edges = [];
404
+ const groupings = [];
405
+ const conceptIds = new Set(arr(kg.concepts).map((c) => str(c.id)));
406
+ for (const c of arr(kg.concepts)) {
407
+ const node = feedNodeFromConcept(c);
408
+ nodes.push(node);
409
+ for (const p of strs(c.prerequisites)) {
410
+ if (p !== node.id && conceptIds.has(p)) {
411
+ edges.push({
412
+ from: p,
413
+ to: node.id,
414
+ kind: "knowledge",
415
+ reason: `concept prerequisite: ${p} must be understood before ${node.id}`
416
+ });
417
+ }
418
+ }
419
+ }
420
+ if (arr(kg.learning_path).length > 0) {
421
+ const orderMap = /* @__PURE__ */ new Map();
422
+ arr(kg.learning_path).forEach((s, idx) => {
423
+ const cid = str(s.concept_id);
424
+ if (cid) orderMap.set(cid, num(s.order, idx + 1));
425
+ });
426
+ nodes.sort((a, b) => (orderMap.get(a.id) ?? 999) - (orderMap.get(b.id) ?? 999));
427
+ }
428
+ let ci = 0;
429
+ for (const cat of arr(kg.categories)) {
430
+ ci += 1;
431
+ const ids = strs(cat.concept_ids).filter((id) => nodes.some((n) => n.id === id));
432
+ if (ids.length > 0) {
433
+ groupings.push({
434
+ id: str(cat.id, "cat" + ci),
435
+ name: str(cat.name, "Category " + ci),
436
+ node_ids: ids,
437
+ rationale: str(cat.description, "")
438
+ });
439
+ }
440
+ }
441
+ newKeywordsInOrder(nodes);
442
+ return validateCurriculumFeed({
443
+ schema_version: 2,
444
+ source: { graph_type: "knowledge_graph", warnings: opts.warnings ?? [], hallucination_count: opts.hallucinationCount ?? 0 },
445
+ learning_nodes: nodes,
446
+ dependency_edges: edges,
447
+ suggested_groupings: groupings,
448
+ phases: []
449
+ });
450
+ }
451
+ function emitFromProject(pg, opts) {
452
+ const nodes = [];
453
+ const edges = [];
454
+ const groupings = [];
455
+ arr(pg.features).forEach((f, fi) => addFeatureGroup(f, str(f.id, "F" + pad22(fi + 1)), nodes, edges, groupings));
456
+ if (nodes.length === 0 && arr(pg.features).length > 0) {
457
+ const hasTasks = arr(pg.implementation ?? {}).length > 0 || arr(pg.implementation?.tasks).length > 0;
458
+ throw new Error(
459
+ "Project graph has " + arr(pg.features).length + " features but NO features[].steps - the curriculum feed cannot emit planning nodes." + (hasTasks ? " This looks like a legacy knowledge-tree graph (implementation.tasks). Regenerate it with the domain-kit v3 pipeline (runProjectGraphPipeline / hybridGraphPipeline), then re-run." : " Every feature must carry at least one step with id/name/keywords/effort for curriculum planning.")
460
+ );
461
+ }
462
+ newKeywordsInOrder(nodes);
463
+ return validateCurriculumFeed({
464
+ schema_version: 2,
465
+ source: { graph_type: "project_graph", warnings: opts.warnings ?? [], hallucination_count: opts.hallucinationCount ?? 0 },
466
+ learning_nodes: nodes,
467
+ dependency_edges: edges,
468
+ suggested_groupings: groupings,
469
+ phases: []
470
+ });
471
+ }
472
+ function emitCurriculumFeed(graph, opts = {}) {
473
+ const g = graph;
474
+ if (arr(g.learning_nodes).length > 0) {
475
+ return validateCurriculumFeed(g);
476
+ }
477
+ if (g.project_graph || g.knowledge_graph || arr(g.links).length > 0 || arr(g.phases).length > 0) {
478
+ return emitFromHybrid(g, opts);
479
+ }
480
+ if (arr(g.concepts).length > 0 || str(g.type) === "knowledge_graph") return emitFromKnowledge(g, opts);
481
+ return emitFromProject(g, opts);
482
+ }
483
+ var BLOOM_RANK, pad22, arr, str, num, strs;
484
+ var init_curriculumFeedEmitter = __esm({
485
+ "src/feed/curriculumFeedEmitter.ts"() {
486
+ init_curriculumFeedSchema();
487
+ BLOOM_RANK = { Remember: 0, Understand: 1, Apply: 2, Analyze: 3, Evaluate: 4, Create: 5 };
488
+ pad22 = (n) => String(n).padStart(2, "0");
489
+ arr = (v) => Array.isArray(v) ? v : [];
490
+ str = (v, d = "") => typeof v === "string" ? v : d;
491
+ num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
492
+ strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
493
+ }
494
+ });
6
495
 
7
496
  // src/parsers/swift.ts
8
497
  var IMPORT_RE = /^import\s+(\w+)/gm;
@@ -1530,10 +2019,10 @@ function splitFeatureIntoMilestones(feature, featureConcepts, maxMinutes, overhe
1530
2019
  const steps = feature.steps || [];
1531
2020
  if (steps.length === 0) return [];
1532
2021
  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);
2022
+ const stepMinutes2 = steps.map((s) => s.effort?.estimated_minutes || 15);
2023
+ const totalRawMinutes = stepMinutes2.reduce((a, b) => a + b, 0);
1535
2024
  if (calculateOverhead(totalRawMinutes, overheadFactor) <= maxMinutes) {
1536
- return [buildMilestone(feature, featureConcepts, steps, stepMinutes, featureId, 1)];
2025
+ return [buildMilestone(feature, featureConcepts, steps, stepMinutes2, featureId, 1)];
1537
2026
  }
1538
2027
  const milestones = [];
1539
2028
  let currentSteps = [];
@@ -1541,7 +2030,7 @@ function splitFeatureIntoMilestones(feature, featureConcepts, maxMinutes, overhe
1541
2030
  let milestoneIndex = 1;
1542
2031
  for (let i = 0; i < steps.length; i++) {
1543
2032
  const step = steps[i];
1544
- const mins = stepMinutes[i];
2033
+ const mins = stepMinutes2[i];
1545
2034
  const currentTotal = currentMinutes.reduce((a, b) => a + b, 0);
1546
2035
  if (calculateOverhead(currentTotal + mins, overheadFactor) > maxMinutes && currentSteps.length > 0) {
1547
2036
  milestones.push(buildMilestone(
@@ -1571,8 +2060,8 @@ function splitFeatureIntoMilestones(feature, featureConcepts, maxMinutes, overhe
1571
2060
  }
1572
2061
  return milestones;
1573
2062
  }
1574
- function buildMilestone(feature, featureConcepts, steps, stepMinutes, featureId, milestoneIndex) {
1575
- const rawMinutes = stepMinutes.reduce((a, b) => a + b, 0);
2063
+ function buildMilestone(feature, featureConcepts, steps, stepMinutes2, featureId, milestoneIndex) {
2064
+ const rawMinutes = stepMinutes2.reduce((a, b) => a + b, 0);
1576
2065
  const levels = steps.map((s) => s.completion_level || "mvp");
1577
2066
  const completionLevel = levels.includes("extend") ? "extend" : levels.includes("mvp") ? "mvp" : "base";
1578
2067
  const milestoneConcepts = /* @__PURE__ */ new Set();
@@ -2161,6 +2650,2651 @@ async function runProjectGraphPipeline(options) {
2161
2650
  };
2162
2651
  }
2163
2652
 
2653
+ // src/graph/loCodeStandard.ts
2654
+ var TECH_TAG_MAP = {
2655
+ python: "PY",
2656
+ swift: "SW",
2657
+ typescript: "TS",
2658
+ javascript: "JV",
2659
+ java: "JAVA",
2660
+ kotlin: "KT",
2661
+ go: "GO",
2662
+ rust: "RUST",
2663
+ csharp: "CS",
2664
+ cpp: "CPP",
2665
+ dart: "DT",
2666
+ php: "PHP",
2667
+ ruby: "RB",
2668
+ sql: "SQL",
2669
+ html: "HTML",
2670
+ css: "CSS"
2671
+ };
2672
+ function techTagFor(techStack) {
2673
+ const primary = (techStack).trim().toLowerCase();
2674
+ for (const [key, tag] of Object.entries(TECH_TAG_MAP)) {
2675
+ if (primary.includes(key)) return tag;
2676
+ }
2677
+ const first = primary.split(/[\s,+/]+/)[0] ?? "";
2678
+ if (!/^[a-z]/.test(first)) return "GEN";
2679
+ return first.length <= 4 ? first.toUpperCase() : first.slice(0, 3).toUpperCase();
2680
+ }
2681
+ var SMALL_WORDS = /* @__PURE__ */ new Set(["and", "or", "the", "of", "for", "in", "on", "a", "an", "vs"]);
2682
+ function conceptCodeFromName(name) {
2683
+ const words = name.replace(/[^A-Za-z0-9]+/g, " ").trim().split(/\s+/).filter((w) => w.length > 0 && !SMALL_WORDS.has(w.toLowerCase()));
2684
+ const base = words.join("_").toUpperCase().replace(/^_+|_+$/g, "").replace(/_{2,}/g, "_");
2685
+ const trimmed = (base || "CONCEPT").slice(0, 60).replace(/_+$/g, "");
2686
+ return trimmed;
2687
+ }
2688
+ function assignConceptCodes(concepts) {
2689
+ const used = /* @__PURE__ */ new Map();
2690
+ const out = {};
2691
+ for (const c of concepts) {
2692
+ const base = conceptCodeFromName(c.name);
2693
+ const n = (used.get(base) ?? 0) + 1;
2694
+ used.set(base, n);
2695
+ out[c.id] = n === 1 ? base : base + "_" + n;
2696
+ }
2697
+ return out;
2698
+ }
2699
+ var pad2 = (n) => String(n).padStart(2, "0");
2700
+ function uloCode(conceptCode, seq = 1) {
2701
+ return "ULO-" + conceptCode + "-" + pad2(seq);
2702
+ }
2703
+ var CIO_STOPWORDS = /* @__PURE__ */ new Set([
2704
+ // clause boilerplate
2705
+ "learner",
2706
+ "will",
2707
+ "be",
2708
+ "able",
2709
+ // generic fillers
2710
+ "a",
2711
+ "an",
2712
+ "the",
2713
+ "of",
2714
+ "to",
2715
+ "and",
2716
+ "or",
2717
+ "in",
2718
+ "on",
2719
+ "for",
2720
+ "by",
2721
+ "with",
2722
+ "is",
2723
+ "are",
2724
+ "that",
2725
+ "it",
2726
+ "its",
2727
+ "their",
2728
+ "when",
2729
+ "how",
2730
+ "as",
2731
+ "into",
2732
+ "from"
2733
+ ]);
2734
+ function cioActionSlug(cioText) {
2735
+ const body = cioText.startsWith(LEARNER_CLAUSE) ? cioText.slice(LEARNER_CLAUSE.length) : cioText;
2736
+ const words = body.replace(/[^A-Za-z0-9]+/g, " ").trim().split(/\s+/).map((w) => w.toLowerCase()).filter((w) => w.length > 2 && !CIO_STOPWORDS.has(w));
2737
+ const seen = /* @__PURE__ */ new Set();
2738
+ const picked = [];
2739
+ for (const w of words) {
2740
+ if (!seen.has(w)) {
2741
+ seen.add(w);
2742
+ picked.push(w);
2743
+ }
2744
+ if (picked.length === 3) break;
2745
+ }
2746
+ return (picked.length > 0 ? picked : ["apply"]).join("_").toUpperCase();
2747
+ }
2748
+ function cioCode(conceptCode, uloSeq, actionSlug) {
2749
+ return "CIO-" + conceptCode + "-" + pad2(uloSeq) + "-" + actionSlug;
2750
+ }
2751
+ function sioCode(techTag, conceptCode, seq) {
2752
+ return "SIO-" + techTag + "-" + conceptCode + "-" + pad2(seq);
2753
+ }
2754
+ var LEARNER_CLAUSE = "The learner will be able to ";
2755
+ function standardStatement(text) {
2756
+ let body = (text || "").trim().replace(/^the learner (will be able to|can)\s*/i, "").replace(/\s+/g, " ");
2757
+ if (body.length === 0) return "";
2758
+ const firstWord = body.split(" ")[0] ?? "";
2759
+ const looksAcronym = /^[A-Z0-9_.`]+$/.test(firstWord) && firstWord.length > 1;
2760
+ if (!looksAcronym) body = body.charAt(0).toLowerCase() + body.slice(1);
2761
+ body = body.replace(/[.\s]+$/, "");
2762
+ return LEARNER_CLAUSE + body + ".";
2763
+ }
2764
+ var CONCEPT_CODE_RE = /^[A-Z][A-Z0-9_]*$/;
2765
+ var ULO_CODE_RE = /^ULO-[A-Z][A-Z0-9_]*-\d{2}$/;
2766
+ var CIO_CODE_RE = /^CIO-[A-Z][A-Z0-9_]*-\d{2}-[A-Z][A-Z0-9_]*$/;
2767
+ var SIO_CODE_RE = /^SIO-[A-Z0-9]+-[A-Z][A-Z0-9_]*-\d{2}$/;
2768
+
2769
+ // src/parsers/syllabusParser.ts
2770
+ var STOP_WORDS = /* @__PURE__ */ new Set([
2771
+ "and",
2772
+ "the",
2773
+ "for",
2774
+ "with",
2775
+ "from",
2776
+ "that",
2777
+ "this",
2778
+ "into",
2779
+ "when",
2780
+ "while",
2781
+ "using",
2782
+ "their",
2783
+ "your",
2784
+ "each",
2785
+ "how",
2786
+ "why",
2787
+ "what",
2788
+ "between",
2789
+ "through",
2790
+ "about",
2791
+ "trong",
2792
+ "nhung",
2793
+ "cac",
2794
+ "cua",
2795
+ "cho",
2796
+ "voi",
2797
+ "duoc",
2798
+ "khong",
2799
+ "theo",
2800
+ "tren",
2801
+ "duoi",
2802
+ "va",
2803
+ "hoac",
2804
+ "thi",
2805
+ "la",
2806
+ "nhu"
2807
+ ]);
2808
+ function extractTokens(text) {
2809
+ const words = text.toLowerCase().replace(/[^a-z0-9à-ỹ\s-]/gi, " ").split(/\s+/).filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
2810
+ return [...new Set(words)];
2811
+ }
2812
+ function extractGradeBand(text) {
2813
+ const gbMatch = text.match(/(?:grade\s*band|độ\s*tuổi|lớp)\s*[:=]?\s*(?:lớp\s*)?(\d{1,2})\s*(?:-|đến|to)\s*(?:lớp\s*)?(\d{1,2})/i);
2814
+ if (gbMatch && gbMatch[1] && gbMatch[2]) {
2815
+ const min = parseInt(gbMatch[1], 10);
2816
+ const max = parseInt(gbMatch[2], 10);
2817
+ return [Math.min(min, max), Math.max(min, max)];
2818
+ }
2819
+ return void 0;
2820
+ }
2821
+ function extractDomain(text) {
2822
+ const lower = text.toLowerCase();
2823
+ if (lower.includes("computer science") || lower.includes("l\u1EADp tr\xECnh") || lower.includes("algorithm") || lower.includes("software")) return "software";
2824
+ if (lower.includes("to\xE1n") || lower.includes("geometry") || lower.includes("algebra") || lower.includes("h\xECnh h\u1ECDc") || lower.includes("mathematics")) return "math";
2825
+ if (lower.includes("v\u1EADt l\xFD") || lower.includes("h\xF3a h\u1ECDc") || lower.includes("sinh h\u1ECDc") || lower.includes("physics") || lower.includes("chemistry") || lower.includes("biology") || lower.includes("science")) return "science";
2826
+ if (lower.includes("ti\u1EBFng") || lower.includes("ng\u1EEF v\u0103n") || lower.includes("grammar") || lower.includes("language") || lower.includes("writing") || lower.includes("literature")) return "language";
2827
+ return void 0;
2828
+ }
2829
+ function parseSyllabus(rawText) {
2830
+ const lines = rawText.split(/\r?\n/);
2831
+ let subjectTitle = "";
2832
+ let subjectDesc = "";
2833
+ const units = [];
2834
+ const allTopics = [];
2835
+ const globalKeywordsSet = /* @__PURE__ */ new Set();
2836
+ let currentUnit = null;
2837
+ let currentTopic = null;
2838
+ let unitIndex = 0;
2839
+ let topicIndex = 0;
2840
+ for (let i = 0; i < lines.length; i++) {
2841
+ const line = lines[i].trim();
2842
+ const lineNum = i + 1;
2843
+ if (!line) continue;
2844
+ if (line.startsWith("# ") && !subjectTitle) {
2845
+ subjectTitle = line.replace(/^#\s+/, "").trim();
2846
+ continue;
2847
+ }
2848
+ if (!currentUnit) {
2849
+ if (line.toLowerCase().startsWith("m\xF4n h\u1ECDc:") || line.toLowerCase().startsWith("subject:")) {
2850
+ const val = line.split(":")[1]?.trim() || "";
2851
+ if (val && !subjectTitle) subjectTitle = val;
2852
+ }
2853
+ if (line.toLowerCase().startsWith("m\xF4 t\u1EA3:") || line.toLowerCase().startsWith("description:")) {
2854
+ subjectDesc = line.split(":")[1]?.trim() || "";
2855
+ }
2856
+ }
2857
+ const isUnitHeading = /^##\s+(?:chương|module|unit|phần|chủ đề)\s*(\d+|[ivx]+)?[:.-]?\s*(.*)/i.test(line) || /^##\s+([^#].*)/.test(line);
2858
+ if (isUnitHeading) {
2859
+ if (currentTopic) {
2860
+ currentTopic.rawLineEnd = lineNum - 1;
2861
+ currentTopic = null;
2862
+ }
2863
+ unitIndex += 1;
2864
+ const titleMatch = line.replace(/^##\s+/, "").trim();
2865
+ currentUnit = {
2866
+ id: `U${String(unitIndex).padStart(2, "0")}`,
2867
+ title: titleMatch,
2868
+ order: unitIndex,
2869
+ topics: []
2870
+ };
2871
+ units.push(currentUnit);
2872
+ continue;
2873
+ }
2874
+ const isTopicItem = /^(?:-\s*\*\*(?:bài|topic|lesson|chủ đề)\s*(\d+(?:\.\d+)?|\w+)?[:.-]?\s*(.*?)\*\*)/i.exec(line) || /^###\s+(?:bài|topic|lesson)?\s*(\d+(?:\.\d+)?|\w+)?[:.-]?\s*(.*)/i.exec(line) || /^-\s+\*\*(.*?)\*\*/.exec(line) || /^(\d+)\.\s+(?:(?:\*\*)?(?:bài|topic|lesson)?\s*(\d+(?:\.\d+)?|\w+)?[:.-]?\s*(.*?)(?:\*\*)?)$/i.exec(line);
2875
+ if (isTopicItem) {
2876
+ if (currentTopic) {
2877
+ currentTopic.rawLineEnd = lineNum - 1;
2878
+ }
2879
+ if (!currentUnit) {
2880
+ unitIndex += 1;
2881
+ currentUnit = {
2882
+ id: `U${String(unitIndex).padStart(2, "0")}`,
2883
+ title: subjectTitle || "General",
2884
+ order: unitIndex,
2885
+ topics: []
2886
+ };
2887
+ units.push(currentUnit);
2888
+ }
2889
+ topicIndex += 1;
2890
+ const rawName = (isTopicItem[3] || isTopicItem[2] || isTopicItem[1] || `Topic ${topicIndex}`).trim();
2891
+ currentTopic = {
2892
+ id: `T${String(topicIndex).padStart(2, "0")}`,
2893
+ title: rawName,
2894
+ unitId: currentUnit.id,
2895
+ unitTitle: currentUnit.title,
2896
+ description: "",
2897
+ learningObjectives: [],
2898
+ keywords: [],
2899
+ rawLineStart: lineNum,
2900
+ rawLineEnd: lineNum
2901
+ };
2902
+ currentUnit.topics.push(currentTopic);
2903
+ allTopics.push(currentTopic);
2904
+ continue;
2905
+ }
2906
+ if (currentTopic) {
2907
+ currentTopic.rawLineEnd = lineNum;
2908
+ const stripped = line.replace(/^[-*•\s]+/, "").trim();
2909
+ if (stripped.toLowerCase().startsWith("m\u1EE5c ti\xEAu:") || stripped.toLowerCase().startsWith("objectives:")) {
2910
+ const content = stripped.replace(/^(?:mục tiêu|objectives):\s*/i, "").trim();
2911
+ if (content) currentTopic.learningObjectives.push(content);
2912
+ continue;
2913
+ }
2914
+ if (stripped.toLowerCase().startsWith("t\u1EEB kh\xF3a:") || stripped.toLowerCase().startsWith("keywords:")) {
2915
+ const rawKw = stripped.replace(/^(?:từ khóa|keywords):\s*/i, "").trim();
2916
+ const tokens2 = rawKw.split(/[,;]+/).map((k) => k.trim()).filter(Boolean);
2917
+ for (const tok of tokens2) {
2918
+ currentTopic.keywords.push(tok);
2919
+ globalKeywordsSet.add(tok);
2920
+ }
2921
+ continue;
2922
+ }
2923
+ if (currentTopic.description) {
2924
+ currentTopic.description += " " + stripped;
2925
+ } else {
2926
+ currentTopic.description = stripped;
2927
+ }
2928
+ for (const tok of extractTokens(stripped)) {
2929
+ if (tok.length >= 4) globalKeywordsSet.add(tok);
2930
+ }
2931
+ }
2932
+ }
2933
+ if (currentTopic) {
2934
+ currentTopic.rawLineEnd = lines.length;
2935
+ }
2936
+ if (!subjectTitle && units.length > 0) {
2937
+ subjectTitle = units[0]?.title || "Untitled Subject";
2938
+ }
2939
+ return {
2940
+ subject: {
2941
+ title: subjectTitle || "Untitled Subject",
2942
+ domain: extractDomain(rawText),
2943
+ gradeBand: extractGradeBand(rawText),
2944
+ description: subjectDesc
2945
+ },
2946
+ units,
2947
+ allTopics,
2948
+ extractedKeywords: [...globalKeywordsSet],
2949
+ rawText
2950
+ };
2951
+ }
2952
+
2953
+ // src/graph/knowledgeGraphVerifier.ts
2954
+ function detectPrerequisiteCycles(concepts) {
2955
+ const cycles = [];
2956
+ const visited = /* @__PURE__ */ new Set();
2957
+ const inStack = /* @__PURE__ */ new Set();
2958
+ const path = [];
2959
+ const prereqMap = /* @__PURE__ */ new Map();
2960
+ for (const c of concepts) {
2961
+ prereqMap.set(c.id, c.prerequisites || []);
2962
+ }
2963
+ function dfs(conceptId) {
2964
+ if (inStack.has(conceptId)) {
2965
+ const cycleStart = path.indexOf(conceptId);
2966
+ if (cycleStart >= 0) {
2967
+ cycles.push([...path.slice(cycleStart), conceptId]);
2968
+ }
2969
+ return;
2970
+ }
2971
+ if (visited.has(conceptId)) return;
2972
+ visited.add(conceptId);
2973
+ inStack.add(conceptId);
2974
+ path.push(conceptId);
2975
+ for (const prereq of prereqMap.get(conceptId) || []) {
2976
+ dfs(prereq);
2977
+ }
2978
+ path.pop();
2979
+ inStack.delete(conceptId);
2980
+ }
2981
+ for (const c of concepts) {
2982
+ if (!visited.has(c.id)) {
2983
+ dfs(c.id);
2984
+ }
2985
+ }
2986
+ return cycles;
2987
+ }
2988
+ function breakPrerequisiteCycles(concepts, cycles) {
2989
+ const removed = [];
2990
+ const fixed = concepts.map((c) => ({ ...c, prerequisites: [...c.prerequisites || []] }));
2991
+ for (const cycle of cycles) {
2992
+ if (cycle.length >= 2) {
2993
+ const breaker = cycle[cycle.length - 2];
2994
+ const target = cycle[cycle.length - 1];
2995
+ for (const concept of fixed) {
2996
+ if (concept.id === breaker && target) {
2997
+ concept.prerequisites = concept.prerequisites.filter((p) => p !== target);
2998
+ removed.push({ from: breaker, to: target });
2999
+ }
3000
+ }
3001
+ }
3002
+ }
3003
+ return { fixed, removed };
3004
+ }
3005
+ function auditSyllabusCoverage(concepts, syllabus) {
3006
+ if (!syllabus || syllabus.allTopics.length === 0) {
3007
+ return { coverageRatio: 1, totalTopics: 0, coveredTopics: 0, uncovered: [] };
3008
+ }
3009
+ const topics = syllabus.allTopics;
3010
+ const coveredSet = /* @__PURE__ */ new Set();
3011
+ for (const topic of topics) {
3012
+ const topicKeywords = /* @__PURE__ */ new Set([
3013
+ ...topic.title.toLowerCase().split(/\s+/),
3014
+ ...(topic.keywords || []).map((k) => k.toLowerCase())
3015
+ ]);
3016
+ for (const concept of concepts) {
3017
+ concept.name.toLowerCase();
3018
+ (concept.description || "").toLowerCase();
3019
+ (concept.keywords || []).map((k) => k.toLowerCase());
3020
+ if (concept.id.toLowerCase() === topic.id.toLowerCase()) {
3021
+ coveredSet.add(topic.id);
3022
+ break;
3023
+ }
3024
+ const refs = concept.references || [];
3025
+ const hasTopicRef = refs.some(
3026
+ (r) => r.id === topic.id || r.title && r.title.toLowerCase() === topic.title.toLowerCase()
3027
+ );
3028
+ if (hasTopicRef) {
3029
+ coveredSet.add(topic.id);
3030
+ break;
3031
+ }
3032
+ const conceptKeywords = (concept.keywords || []).map((k) => k.toLowerCase());
3033
+ const hasOverlap = conceptKeywords.some((k) => topicKeywords.has(k)) || concept.name.toLowerCase().includes(topic.title.toLowerCase()) || topic.title.toLowerCase().includes(concept.name.toLowerCase());
3034
+ if (hasOverlap) {
3035
+ coveredSet.add(topic.id);
3036
+ break;
3037
+ }
3038
+ }
3039
+ }
3040
+ const totalTopics = topics.length;
3041
+ const coveredTopics = coveredSet.size;
3042
+ const coverageRatio = totalTopics > 0 ? coveredTopics / totalTopics : 1;
3043
+ const uncovered = topics.filter((t) => !coveredSet.has(t.id)).map((t) => `${t.id}: ${t.title}`);
3044
+ return { coverageRatio, totalTopics, coveredTopics, uncovered };
3045
+ }
3046
+ function verifyKnowledgeGraph(kg, syllabusOrOptions) {
3047
+ const syllabus = syllabusOrOptions && "parsedSyllabus" in syllabusOrOptions ? syllabusOrOptions.parsedSyllabus : syllabusOrOptions;
3048
+ const warnings = [];
3049
+ let concepts = [...kg.concepts || []];
3050
+ const conceptIds = new Set(concepts.map((c) => c.id));
3051
+ for (const c of concepts) {
3052
+ if ((c.prerequisites || []).includes(c.id)) {
3053
+ c.prerequisites = (c.prerequisites || []).filter((p) => p !== c.id);
3054
+ warnings.push(`Self-loop removed on concept ${c.id}`);
3055
+ }
3056
+ }
3057
+ const danglingPrerequisites = [];
3058
+ for (const c of concepts) {
3059
+ const validPrereqs = [];
3060
+ for (const p of c.prerequisites || []) {
3061
+ if (!conceptIds.has(p)) {
3062
+ danglingPrerequisites.push({ conceptId: c.id, missingPrereqId: p });
3063
+ warnings.push(`Dangling prerequisite removed: ${c.id} -> ${p}`);
3064
+ } else {
3065
+ validPrereqs.push(p);
3066
+ }
3067
+ }
3068
+ c.prerequisites = validPrereqs;
3069
+ }
3070
+ const initialCycles = detectPrerequisiteCycles(concepts);
3071
+ let removedEdges = [];
3072
+ if (initialCycles.length > 0) {
3073
+ const broken = breakPrerequisiteCycles(concepts, initialCycles);
3074
+ concepts = broken.fixed;
3075
+ removedEdges = broken.removed;
3076
+ for (const r of removedEdges) {
3077
+ warnings.push(`Cycle broken: removed edge ${r.from} -> ${r.to}`);
3078
+ }
3079
+ }
3080
+ const coverage = auditSyllabusCoverage(concepts, syllabus);
3081
+ if (coverage.coverageRatio < 1) {
3082
+ warnings.push(`Syllabus coverage ${(coverage.coverageRatio * 100).toFixed(1)}% (${coverage.coveredTopics}/${coverage.totalTopics} topics). Uncovered: ${coverage.uncovered.join(", ")}`);
3083
+ }
3084
+ for (const c of concepts) {
3085
+ if (!c.ulo) warnings.push(`Concept ${c.id} missing ULO (WHAT + WHY)`);
3086
+ if (!c.cio) warnings.push(`Concept ${c.id} missing CIO (HOW mechanism)`);
3087
+ if (!c.sio || c.sio.length === 0) warnings.push(`Concept ${c.id} missing SIO implementation items`);
3088
+ }
3089
+ const valid = initialCycles.length === 0 && danglingPrerequisites.length === 0 && coverage.coverageRatio >= 0.8;
3090
+ const verifiedGraph = {
3091
+ ...kg,
3092
+ concepts
3093
+ };
3094
+ const report = {
3095
+ valid,
3096
+ cycles: initialCycles,
3097
+ removedEdges,
3098
+ danglingPrerequisites,
3099
+ coverageRatio: coverage.coverageRatio,
3100
+ totalSyllabusTopics: coverage.totalTopics,
3101
+ coveredSyllabusTopics: coverage.coveredTopics,
3102
+ uncoveredTopics: coverage.uncovered,
3103
+ warnings
3104
+ };
3105
+ return { verifiedGraph, report };
3106
+ }
3107
+
3108
+ // src/graph/loExemplars.ts
3109
+ var ULO_EXEMPLARS = [
3110
+ // Calibrated against production learning_objectives (UNIVERSAL rows, neutral)
3111
+ "The learner will be able to recall the key concepts, terminology, and characteristics of layers of abstraction.",
3112
+ "The learner will be able to explain how layers of abstraction hide implementation details while exposing essential behavior.",
3113
+ "The learner will be able to explain how a program changes state through statements and control flow.",
3114
+ "The learner will be able to differentiate local from global scope and describe how return values affect data flow.",
3115
+ "The learner will be able to describe the mechanism of a loop, including its condition, step and stopping rule.",
3116
+ "The learner will be able to explain why a representation format must balance readability against processing efficiency."
3117
+ ];
3118
+ var CIO_EXEMPLARS = [
3119
+ // Production CONCEPTUAL_IMPL rows: mechanism behind the clause, Marr 2-Language neutral
3120
+ "The learner will be able to develop a conceptual model that explains the internal mechanism by which the representation produces its effect.",
3121
+ "The learner will be able to compare alternative approaches for the same operation and describe the trade-offs between them.",
3122
+ "The learner will be able to decompose the choice of representation into decisive criteria such as cost, fidelity and tolerance to change.",
3123
+ "The learner will be able to describe the sequence of steps by which a process transforms its input into its output, including the stopping condition."
3124
+ ];
3125
+ var SIO_EXEMPLARS = [
3126
+ // Production SPECIFIC_IMPL style: keyword-bound completable tasks, tech-tagged tier
3127
+ "The learner will be able to use argparse.ArgumentParser with add_subparsers to register add, done and list commands in a Python CLI.",
3128
+ "The learner will be able to define a @dataclass with type hints and a __post_init__ method that raises ValueError on invariant violation.",
3129
+ "The learner will be able to execute parameterized statements via sqlite3.Connection.execute inside a with conn: transaction to perform CRUD on a tasks table.",
3130
+ "The learner will be able to write a pytest test that runs the CLI entry point via subprocess.run and asserts the exit code and printed output."
3131
+ ];
3132
+
3133
+ // src/graph/depthAudit.ts
3134
+ var TECH_SIGNAL_RES = [
3135
+ /`[^`]+`/,
3136
+ /\b[a-zA-Z_][A-Za-z0-9_]*\(\)/,
3137
+ // call-with-arguments: Status(enum.Enum), field(default_factory=list)
3138
+ /\b[A-Za-z_][A-Za-z0-9_]*\([^)]+\)/,
3139
+ // attribute access glued to a value: enum.Enum, os.path, tasks.json
3140
+ /[A-Za-z0-9_)\]]\.[A-Za-z_][A-Za-z0-9_]+/,
3141
+ /[a-z]+_[a-z]+/,
3142
+ /[a-z]+[A-Z][A-Za-z]*/,
3143
+ /\.[a-z]{2,4}\b/,
3144
+ /\b(?:API|SDK|JSON|XML|YAML|HTTP|SQL|UUID|URL|DOM|CLI|TUI|CSV|UTF|CRUD|DSL)\b/,
3145
+ // Exception/error class names: Exception, ValueError, CustomError
3146
+ /\b[A-Z][a-zA-Z0-9_]*(?:Error|Exception|Warning|Fault)\b/,
3147
+ // class declarations or references: class Status, static methods on types
3148
+ /\bclass\s+[A-Za-z_][A-Za-z0-9_]*/,
3149
+ // inheritance/derivation mentions: inheriting from Exception, subclass of Base
3150
+ /\b(?:inherit(?:ing|s)?|subclass(?:ing)?|deriv(?:ing|ed|es)?)\s+(?:from|of)\s+[A-Z][A-Za-z0-9_]*/
3151
+ ];
3152
+ var GENERIC_STOP = /* @__PURE__ */ new Set(["e", "g", "i"]);
3153
+ function techSignalHits(text) {
3154
+ const hits = [];
3155
+ for (const re of TECH_SIGNAL_RES) {
3156
+ const found = text.match(new RegExp(re.source, "g")) ?? [];
3157
+ for (const m of found) if (!GENERIC_STOP.has(m.toLowerCase())) hits.push(m);
3158
+ }
3159
+ return hits;
3160
+ }
3161
+ function auditDepthLayers(concepts) {
3162
+ return concepts.map((c) => {
3163
+ const ulo = (c.ulo ?? "").trim();
3164
+ const cio = (c.cio ?? "").trim();
3165
+ const sioItems = (c.sio ?? []).map((s) => (s ?? "").trim()).filter(Boolean);
3166
+ const uloTechSignals = techSignalHits(ulo);
3167
+ const uloHasTechSignals = uloTechSignals.length > 0;
3168
+ const IMPL_VERBS = /\b(implement|refactor|debug|serialize)\b/i;
3169
+ const uloHasImplementationVerbs = IMPL_VERBS.test(ulo);
3170
+ const registryKeywords = (c.keywords ?? []).map((k) => String(k).toLowerCase()).filter((k) => k.length >= 3);
3171
+ const nameTokens = String(c.name ?? "").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4);
3172
+ const boundToConcept = (s) => {
3173
+ const lower = s.toLowerCase();
3174
+ return registryKeywords.some((k) => lower.includes(k)) || nameTokens.some((w) => lower.includes(w));
3175
+ };
3176
+ const sioUnboundItems = sioItems.filter((s) => {
3177
+ if (techSignalHits(s).length > 0) return false;
3178
+ return !boundToConcept(s);
3179
+ });
3180
+ const conceptKeywords = (c.keywords ?? []).map((k) => k.toLowerCase()).filter(Boolean);
3181
+ const sioOffConceptItems = conceptKeywords.length > 0 ? sioItems.filter((s) => !boundToConcept(s)) : [];
3182
+ const COMPLETABLE = /\b(build|create|implement|write|configure|add|use|apply|display|store|load|save|handle|parse|validate|test|register|compute|generate|print|format|convert|extend|define|execute|run|read|fetch|wrap|log|call|raise|install|import|export|declare|embed|model|represent|override|customize|enforce|mock|assert|query|index|cache|stream|split|join|strip|encode|decode|traverse|measure|evaluate|benchmark|diagnose|demonstrate|verify|organize|structure|choose|select|compare|distinguish|attach|annotate|expose|accept|manage|group|sort|filter|map|reduce|catch|throw|migrate|inspect|trace|parameterize|construct|design|capture)\b/i;
3183
+ const sioNotCompletable = sioItems.filter((s) => !COMPLETABLE.test(s) && !/`[^`]+`/.test(s));
3184
+ const density = (text) => {
3185
+ if (!text) return 0;
3186
+ return techSignalHits(text).length / Math.max(1, text.split(/\s+/).length) * 100;
3187
+ };
3188
+ const dUlo = density(ulo);
3189
+ density(cio);
3190
+ const dSio = sioItems.length > 0 ? Math.max(...sioItems.map(density)) : 0;
3191
+ const concretenessIncreases = dSio > dUlo;
3192
+ const clauseOk = ulo.startsWith(LEARNER_CLAUSE) && cio.startsWith(LEARNER_CLAUSE) && sioItems.every((t) => t.startsWith(LEARNER_CLAUSE));
3193
+ const codeIssues = [];
3194
+ const conceptCode = c.code;
3195
+ if (conceptCode !== void 0 && !CONCEPT_CODE_RE.test(conceptCode)) codeIssues.push("concept code not UPPER_SNAKE: " + conceptCode);
3196
+ const uloCodeVal = c.ulo_code;
3197
+ if (uloCodeVal !== void 0 && uloCodeVal !== "" && !ULO_CODE_RE.test(uloCodeVal)) codeIssues.push("ULO code malformed: " + uloCodeVal);
3198
+ const cioCodeVal = c.cio_code;
3199
+ if (cioCodeVal !== void 0 && cioCodeVal !== "" && !CIO_CODE_RE.test(cioCodeVal)) codeIssues.push("CIO code malformed: " + cioCodeVal);
3200
+ const sioCodeVals = c.sio_codes;
3201
+ if (sioCodeVals !== void 0) {
3202
+ if (sioCodeVals.length !== sioItems.length) codeIssues.push("SIO codes count mismatch");
3203
+ for (const sc of sioCodeVals) if (!SIO_CODE_RE.test(sc)) codeIssues.push("SIO code malformed: " + sc);
3204
+ }
3205
+ const codesOk = codeIssues.length === 0;
3206
+ const passed = !uloHasTechSignals && !uloHasImplementationVerbs && sioUnboundItems.length === 0 && sioOffConceptItems.length === 0 && sioNotCompletable.length === 0 && concretenessIncreases && clauseOk && codesOk;
3207
+ return {
3208
+ conceptId: c.id,
3209
+ uloHasTechSignals,
3210
+ uloTechSignals,
3211
+ uloHasImplementationVerbs,
3212
+ sioAllKeywordBound: sioUnboundItems.length === 0,
3213
+ sioUnboundItems,
3214
+ sioConceptKeywordBound: sioOffConceptItems.length === 0,
3215
+ sioOffConceptItems,
3216
+ sioAllCompletable: sioNotCompletable.length === 0,
3217
+ sioNotCompletable,
3218
+ concretenessIncreases,
3219
+ clauseOk,
3220
+ codesOk,
3221
+ codeIssues,
3222
+ passed
3223
+ };
3224
+ });
3225
+ }
3226
+
3227
+ // src/graph/hierarchicalLoAuthoring.ts
3228
+ function excerpt(text, max) {
3229
+ const t = (text || "").trim();
3230
+ return t.length > max ? t.slice(0, max) + "..." : t;
3231
+ }
3232
+ async function chatJson(client, system, user) {
3233
+ const r = await client.chat(
3234
+ [{ role: "system", content: system }, { role: "user", content: user }],
3235
+ { temperature: 0.1, maxTokens: parseInt(process.env.KG_MAX_TOKENS || "65536", 10) }
3236
+ );
3237
+ return parseJsonFromLlm(r.content);
3238
+ }
3239
+ var LO_BATCH_SIZE = 6;
3240
+ function chunkConcepts(items, size) {
3241
+ const out = [];
3242
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
3243
+ return out;
3244
+ }
3245
+ var JSON_ONLY = "Return EXACTLY JSON. No markdown, no explanations.";
3246
+ async function authorDepthLayersHierarchically(client, concepts, techStack, warnings) {
3247
+ const uloByConcept = {};
3248
+ const cioByConcept = {};
3249
+ const sioByConcept = {};
3250
+ concepts.map((c) => "- " + c.id + ": " + c.name + (c.description ? " \u2014 " + excerpt(c.description, 160) : "")).join("\n");
3251
+ const uloSystem = [
3252
+ "You are a Curriculum Design Expert writing Universal Learning Objectives (ULO).",
3253
+ "",
3254
+ "ULO = Computational Tier: the CORE COMPETENCY \u2014 WHAT the concept is and WHY it exists \u2014",
3255
+ "completely independent of any technology, language, framework, library, file format or tool.",
3256
+ "",
3257
+ "Rules:",
3258
+ "1. NEVER mention a concrete technology, API, library, file name or syntax in a ULO.",
3259
+ "2. Bind the ULO to the concept intrinsic nature: the problem it solves in principle.",
3260
+ "3. Typical assessment level: Remember or Understand. Use pragmatic, measurable verbs",
3261
+ " (Explain, Differentiate, Identify, Describe) instead of Know/Understand/Summarize.",
3262
+ "4. Cognitive Ceiling: do NOT force Analyze/Evaluate/Create ULOs \u2014 if the concept is",
3263
+ " foundational, its core competency is low-level. Higher levels are exercised later",
3264
+ " INDIRECTLY through implementation work, not in the ULO itself.",
3265
+ "5. One ULO per concept; 1-2 sentences covering WHAT + WHY.",
3266
+ "6. Statement form (mandatory): every ULO MUST read 'The learner will be able to ...' \u2014",
3267
+ " a lowercase actionable verb follows the clause (explain ..., differentiate ...).",
3268
+ " Do not prefix any other phrasing.",
3269
+ "",
3270
+ "Approved style exemplars (production bank):",
3271
+ ULO_EXEMPLARS.map((e) => "- " + e).join("\n"),
3272
+ "",
3273
+ JSON_ONLY
3274
+ ].join("\n");
3275
+ for (const batch of chunkConcepts(concepts, LO_BATCH_SIZE)) {
3276
+ const batchList = batch.map((c) => "- " + c.id + " (" + c.name + ")" + (c.description ? ": " + excerpt(c.description, 160) : "")).join("\n");
3277
+ const batchUser = "Concepts:\n" + batchList + '\n\nReturn JSON:\n{"ulos":[{"concept_id":"C1","ulo":"..."}]}';
3278
+ const uloRes = await chatJson(client, uloSystem, batchUser);
3279
+ for (const u of uloRes.ulos ?? []) {
3280
+ if (u.concept_id && u.ulo) uloByConcept[u.concept_id] = standardStatement(u.ulo);
3281
+ }
3282
+ }
3283
+ for (const c of concepts) {
3284
+ if (!uloByConcept[c.id]) {
3285
+ warnings.push("Phase A: no ULO generated for " + c.id + " \u2014 kept empty (audit will fail)");
3286
+ uloByConcept[c.id] = "";
3287
+ }
3288
+ }
3289
+ concepts.map((c) => "- " + c.id + " (" + c.name + "): " + uloByConcept[c.id]).join("\n");
3290
+ const cioSystem = [
3291
+ "You are a Curriculum Design Expert writing Concept-level Instructional Objectives (CIO).",
3292
+ "",
3293
+ "CIO = Algorithmic Tier: the MECHANISM \u2014 HOW the concept works, step by step, at a",
3294
+ "procedural level. CIOs are derived from approved ULOs.",
3295
+ "",
3296
+ "Rules:",
3297
+ "1. Still technology-agnostic: describe the neutral pattern/approach WITHOUT concrete",
3298
+ " API names, libraries, file formats or syntax.",
3299
+ "2. Marr 2-Language Test: the CIO must read as true and implementable regardless of",
3300
+ " whether the learner uses Python, Swift, JavaScript or any other language. If it",
3301
+ " only makes sense in one language, rewrite it more neutrally.",
3302
+ "3. More concrete than the ULO: reference stages, conditions, ordering or trade-offs",
3303
+ " of the underlying process.",
3304
+ "4. One CIO per concept; 1-3 sentences describing the mechanism.",
3305
+ "5. Statement form (mandatory): every CIO MUST read 'The learner will be able to ...' \u2014",
3306
+ " the mechanism is described after the clause.",
3307
+ "",
3308
+ "Approved style exemplars (production bank):",
3309
+ CIO_EXEMPLARS.map((e) => "- " + e).join("\n"),
3310
+ "",
3311
+ JSON_ONLY
3312
+ ].join("\n");
3313
+ for (const batch of chunkConcepts(concepts, LO_BATCH_SIZE)) {
3314
+ new Set(batch.map((c) => c.id));
3315
+ const batchUloContext = batch.map((c) => "- " + c.id + " (" + c.name + "): " + (uloByConcept[c.id] || "(missing)")).join("\n");
3316
+ const batchCioUser = "Approved ULOs:\n" + batchUloContext + '\n\nReturn JSON:\n{"cios":[{"concept_id":"C1","cio":"..."}]}';
3317
+ const cioRes = await chatJson(client, cioSystem, batchCioUser);
3318
+ for (const x of cioRes.cios ?? []) {
3319
+ if (x.concept_id && x.cio) cioByConcept[x.concept_id] = standardStatement(x.cio);
3320
+ }
3321
+ }
3322
+ for (const c of concepts) {
3323
+ if (!cioByConcept[c.id]) {
3324
+ warnings.push("Phase B: no CIO generated for " + c.id + " \u2014 kept empty (audit will fail)");
3325
+ cioByConcept[c.id] = "";
3326
+ }
3327
+ }
3328
+ concepts.map((c) => "- " + c.id + " (" + c.name + "): CIO=" + cioByConcept[c.id] + " | ULO=" + uloByConcept[c.id]).join("\n");
3329
+ const sioSystem = [
3330
+ "You are a Curriculum Design Expert writing Specific Instructional Objectives (SIO).",
3331
+ "",
3332
+ "SIO = Implementation Tier: completable, context-specific implementation tasks derived",
3333
+ "from approved CIOs, bound to the TARGET technology of the course.",
3334
+ "",
3335
+ "Rules:",
3336
+ "1. EVERY SIO item MUST name concrete keywords/technologies: a real API, function,",
3337
+ " decorator, module, class, file or command of the target technology.",
3338
+ "2. Each item is a completable task: an actionable verb + the concrete construct + the",
3339
+ " context where it is used. Application/Analysis/Creation skills are assessed",
3340
+ " INDIRECTLY through completing these tasks \u2014 there is no fixed Bloom ceiling.",
3341
+ " MACHINE CHECK: every item MUST contain at least one ACTION VERB from this whitelist",
3342
+ " (build, create, implement, write, configure, add, use, apply, display, store, load,",
3343
+ " save, handle, parse, validate, test, register, compute, generate, print, format,",
3344
+ " convert, extend, define, execute, run, read, fetch, wrap, log, call, raise, install,",
3345
+ " import, export, declare, embed, model, represent, override, customize, enforce, mock,",
3346
+ " assert, query, index, cache, stream, split, join, strip, encode, decode, traverse,",
3347
+ " measure, evaluate, benchmark, diagnose, demonstrate, verify, organize, structure,",
3348
+ " choose, select, compare, distinguish, attach, annotate, expose, accept, manage, group,",
3349
+ " sort, filter, map, reduce, catch, throw, migrate, inspect, trace, parameterize,",
3350
+ " construct, design, capture) \u2014 OR wrap the concrete construct in backticks.",
3351
+ " Items with abstract verbs (understand, learn, know, grasp, appreciate, explore) and",
3352
+ " no backtick construct FAIL the audit. Prefer starting the task phrase with the verb.",
3353
+ "3. 2-3 items per concept, each referencing a DIFFERENT concrete construct.",
3354
+ "4. Statement form (mandatory): every SIO MUST read 'The learner will be able to ...' \u2014",
3355
+ " the concrete task follows the clause.",
3356
+ "",
3357
+ "Approved style exemplars (production bank):",
3358
+ SIO_EXEMPLARS.map((e) => "- " + e).join("\n"),
3359
+ "",
3360
+ JSON_ONLY
3361
+ ].join("\n");
3362
+ concepts.map((c) => "- " + c.id + " (" + c.name + "): " + ((c.keywords ?? []).slice(0, 10).join(", ") || "(none listed \u2014 use concrete constructs of " + techStack + ")")).join("\n");
3363
+ for (const batch of chunkConcepts(concepts, LO_BATCH_SIZE)) {
3364
+ const batchKw = batch.map((c) => "- " + c.id + " (" + c.name + "): " + ((c.keywords ?? []).slice(0, 10).join(", ") || "(none listed \u2014 use concrete constructs of " + techStack + ")")).join("\n");
3365
+ const batchCioCtx = batch.map((c) => "- " + c.id + " (" + c.name + "): CIO=" + (cioByConcept[c.id] || "(missing)") + " | ULO=" + (uloByConcept[c.id] || "(missing)")).join("\n");
3366
+ const batchSioUser = [
3367
+ "Target technology: " + techStack,
3368
+ "",
3369
+ "Concept keywords (each SIO item MUST contain at least one of its concept's keywords):",
3370
+ batchKw,
3371
+ "",
3372
+ "Approved CIOs and ULOs:",
3373
+ batchCioCtx,
3374
+ "",
3375
+ "Return JSON:",
3376
+ '{"sios":[{"concept_id":"C1","sio":["...","..."]}]}'
3377
+ ].join("\n");
3378
+ const sioRes = await chatJson(client, sioSystem, batchSioUser);
3379
+ for (const x of sioRes.sios ?? []) {
3380
+ if (x.concept_id && Array.isArray(x.sio)) sioByConcept[x.concept_id] = x.sio.map((item) => standardStatement(String(item)));
3381
+ }
3382
+ }
3383
+ for (const c of concepts) {
3384
+ if (!sioByConcept[c.id] || sioByConcept[c.id].length === 0) {
3385
+ warnings.push("Phase C: no SIO generated for " + c.id + " \u2014 kept empty (audit will fail)");
3386
+ sioByConcept[c.id] = [];
3387
+ }
3388
+ }
3389
+ concepts.map((c) => c.id);
3390
+ const auditOnce = () => auditDepthLayers(concepts.map((c) => ({
3391
+ id: c.id,
3392
+ name: c.name,
3393
+ keywords: c.keywords ?? [],
3394
+ ulo: uloByConcept[c.id],
3395
+ cio: cioByConcept[c.id],
3396
+ sio: sioByConcept[c.id]
3397
+ })));
3398
+ let audits = auditOnce();
3399
+ const repairRound = async (round) => {
3400
+ const failingNow = audits.filter((a) => !a.passed);
3401
+ if (failingNow.length === 0) return false;
3402
+ if (round > 1) warnings.push("Depth audit repair round " + round + ": " + failingNow.length + " concept(s) still failing");
3403
+ const failing = failingNow;
3404
+ {
3405
+ warnings.push("Depth audit: " + failing.length + "/" + concepts.length + " concepts failed first audit \u2014 targeted repair");
3406
+ const repairTargets = failing.map((a) => {
3407
+ const c = concepts.find((x) => x.id === a.conceptId);
3408
+ const issues = [];
3409
+ if (a.uloHasTechSignals) issues.push("ULO names concrete technology (remove: " + a.uloTechSignals.join(", ") + ")");
3410
+ if (a.uloHasImplementationVerbs) issues.push("ULO reads as an implementation task (rewrite as WHAT+WHY competency)");
3411
+ for (const item of a.sioUnboundItems) issues.push("SIO not keyword-bound: " + excerpt(item, 120));
3412
+ for (const item of a.sioOffConceptItems) issues.push("SIO does not reference any of this concept's keywords (" + (c?.keywords ?? []).join(", ") + "): " + excerpt(item, 120));
3413
+ for (const item of a.sioNotCompletable) issues.push("SIO not a completable task: " + excerpt(item, 120));
3414
+ if (!a.concretenessIncreases) issues.push("SIO is not more concrete than the ULO \u2014 bind it to concrete constructs");
3415
+ return {
3416
+ concept_id: a.conceptId,
3417
+ name: c?.name ?? a.conceptId,
3418
+ keywords: c?.keywords ?? [],
3419
+ current: { ulo: uloByConcept[a.conceptId], cio: cioByConcept[a.conceptId], sio: sioByConcept[a.conceptId] },
3420
+ issues
3421
+ };
3422
+ });
3423
+ const repairSystem = [
3424
+ "You repair depth-layer learning objectives that failed a deterministic contract audit.",
3425
+ "For each concept, rewrite ONLY the layers mentioned in issues, keeping the others as-is.",
3426
+ "Contract:",
3427
+ "- ULO: WHAT+WHY, concept-bound, technology-agnostic, Remember/Understand verbs.",
3428
+ "- CIO: HOW mechanism, technology-agnostic, more concrete than ULO, Marr 2-Language neutral.",
3429
+ "- SIO: keyword-bound completable implementation tasks naming real constructs of " + techStack + ".",
3430
+ "- BINDING RULE: every SIO item must contain at least one of the concept's listed keywords VERBATIM (copy the exact keyword text into the sentence).",
3431
+ "- COMPLETABILITY RULE: every SIO item must contain an actionable verb (build, create, implement, write, configure, parse, validate, handle, store, load, run, read, fetch, display, generate, format, convert, test, debug, trace, measure, design, structure, select, filter, sort, map, reduce, catch, throw, migrate, deploy, query, index, cache, stream, inspect) or wrap a concrete construct in backticks \u2014 abstract verbs (understand, learn, know, grasp, explore, appreciate) always fail.",
3432
+ "",
3433
+ JSON_ONLY
3434
+ ].join("\n");
3435
+ const repairUser = [
3436
+ "Failing concepts:",
3437
+ JSON.stringify(repairTargets, null, 2),
3438
+ "",
3439
+ "Return the FULL repaired set for these concepts in the same 3-layer JSON shape:",
3440
+ '{"repairs":[{"concept_id":"C1","ulo":"...","cio":"...","sio":["..."]}]}'
3441
+ ].join("\n");
3442
+ try {
3443
+ const repairRes = await chatJson(client, repairSystem, repairUser);
3444
+ for (const r of repairRes.repairs ?? []) {
3445
+ if (!r.concept_id) continue;
3446
+ if (r.ulo) uloByConcept[r.concept_id] = standardStatement(r.ulo);
3447
+ if (r.cio) cioByConcept[r.concept_id] = standardStatement(r.cio);
3448
+ if (Array.isArray(r.sio) && r.sio.length > 0) sioByConcept[r.concept_id] = r.sio.map((t) => standardStatement(String(t)));
3449
+ }
3450
+ audits = auditOnce();
3451
+ } catch (err) {
3452
+ warnings.push("Depth repair round failed: " + (err instanceof Error ? err.message : String(err)));
3453
+ }
3454
+ }
3455
+ return audits.every((a) => a.passed);
3456
+ };
3457
+ for (let round = 1; round <= 2; round++) {
3458
+ const allPass = await repairRound(round);
3459
+ if (allPass) break;
3460
+ }
3461
+ const stillFailing = audits.filter((a) => !a.passed);
3462
+ if (stillFailing.length > 0) {
3463
+ try {
3464
+ const { mkdirSync: mkdirSync3, writeFileSync: writeFileSync3 } = await import('fs');
3465
+ const dumpDir = (await import('os')).tmpdir() + "/domain-kit-debug/";
3466
+ mkdirSync3(dumpDir, { recursive: true });
3467
+ writeFileSync3(dumpDir + "depth-audit-debug.json", JSON.stringify(
3468
+ stillFailing.map((a) => ({ audit: a, ulo: uloByConcept[a.conceptId], cio: cioByConcept[a.conceptId], sio: sioByConcept[a.conceptId] })),
3469
+ null,
3470
+ 2
3471
+ ));
3472
+ } catch {
3473
+ }
3474
+ const detail = stillFailing.map((a) => a.conceptId + "[" + (a.uloHasTechSignals ? "ulo-tech " : "") + (a.uloHasImplementationVerbs ? "ulo-impl " : "") + (a.sioUnboundItems.length ? "sio-unbound " : "") + (a.sioNotCompletable.length ? "sio-incomplete" : "") + "]").join(", ");
3475
+ throw new Error(
3476
+ "Depth-layer contract audit failed after repair for: " + detail + " \u2014 regenerate or fix the prompt contract (fail-closed, no silent fallback)"
3477
+ );
3478
+ }
3479
+ return { uloByConcept, cioByConcept, sioByConcept, audits, warnings };
3480
+ }
3481
+ function cell(v) {
3482
+ return (v || "").split(",").map((s) => s.trim()).filter(Boolean);
3483
+ }
3484
+ function loadKnowledgeTreeCatalog(tsvPath) {
3485
+ const text = fs.readFileSync(tsvPath, "utf8");
3486
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
3487
+ if (lines.length === 0) throw new Error("Knowledge tree TSV is empty: " + tsvPath);
3488
+ const header = lines[0].split(" ").map((h) => h.trim());
3489
+ const col = (name) => header.indexOf(name);
3490
+ const iT = col("item_type");
3491
+ const iC = col("code");
3492
+ const iN = col("name");
3493
+ const iD = col("description");
3494
+ const iCat = col("category_codes");
3495
+ const iTpc = col("topic_codes");
3496
+ const iPre = col("prerequisite_concept_codes");
3497
+ const iKw = col("keywords");
3498
+ for (const [name, idx] of [["item_type", iT], ["code", iC], ["name", iN]]) {
3499
+ if (idx < 0) throw new Error("Knowledge tree TSV missing column: " + name);
3500
+ }
3501
+ const categories = /* @__PURE__ */ new Map();
3502
+ const topics = /* @__PURE__ */ new Map();
3503
+ const concepts = [];
3504
+ for (const line of lines.slice(1)) {
3505
+ const c = line.split(" ");
3506
+ const type = (c[iT] || "").trim();
3507
+ const code = (c[iC] || "").trim();
3508
+ const name = (c[iN] || "").trim();
3509
+ if (!code || !name) continue;
3510
+ if (type === "CATEGORY") categories.set(code, name);
3511
+ if (type === "TOPIC") topics.set(code, { name, categoryCodes: cell(c[iCat]) });
3512
+ if (type !== "CONCEPT") continue;
3513
+ const topicCodes = cell(c[iTpc]);
3514
+ const categoryCodes = /* @__PURE__ */ new Set();
3515
+ for (const t of topicCodes) for (const cc of topics.get(t)?.categoryCodes || []) categoryCodes.add(cc);
3516
+ concepts.push({
3517
+ code,
3518
+ name,
3519
+ description: (c[iD] || "").trim(),
3520
+ keywords: cell(c[iKw]),
3521
+ topicCodes,
3522
+ categoryCodes: [...categoryCodes],
3523
+ prerequisiteCodes: cell(c[iPre])
3524
+ });
3525
+ }
3526
+ if (concepts.length === 0) throw new Error("Knowledge tree TSV has no CONCEPT rows: " + tsvPath);
3527
+ return { path: tsvPath, concepts, categories, topics };
3528
+ }
3529
+ function tokens(s) {
3530
+ return new Set(
3531
+ s.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4)
3532
+ );
3533
+ }
3534
+ function filterBySelection(catalog, selectedCategories, selectedTopics, analysisConcepts, maxConcepts = 120) {
3535
+ const tps = new Set(selectedTopics.map((c) => c.trim().toUpperCase()).filter(Boolean));
3536
+ const cats = new Set(selectedCategories.map((c) => c.trim().toUpperCase()).filter(Boolean));
3537
+ const probe = analysisConcepts.map((c) => tokens(c.name + " " + c.keywords.join(" ")));
3538
+ const probeAll = /* @__PURE__ */ new Set();
3539
+ for (const p of probe) for (const w of p) probeAll.add(w);
3540
+ const inSelection = (tc) => tc.topicCodes.some((t) => tps.has(t)) || tc.categoryCodes.some((c) => cats.has(c));
3541
+ const overlapScore = (tc) => {
3542
+ const tcTok = tokens(tc.name + " " + tc.description.slice(0, 300) + " " + tc.keywords.join(" "));
3543
+ let overlap = 0;
3544
+ for (const w of tcTok) if (probeAll.has(w)) overlap++;
3545
+ const kwDirect = tc.keywords.filter((k) => {
3546
+ const kl = k.toLowerCase();
3547
+ return [...probeAll].some((w) => w === kl || w.length >= 5 && kl.includes(w));
3548
+ }).length;
3549
+ return overlap * 2 + kwDirect * 3;
3550
+ };
3551
+ const kept = catalog.concepts.filter((tc) => inSelection(tc) || overlapScore(tc) >= 6);
3552
+ return kept.map((tc) => ({ tc, s: overlapScore(tc) })).sort((a, b) => b.s - a.s).slice(0, maxConcepts).map((x) => x.tc);
3553
+ }
3554
+ function renderShortlist(shortlist) {
3555
+ return shortlist.map((tc) => "- " + tc.code + " | " + tc.name + " | kw: " + tc.keywords.slice(0, 8).join(", ") + (tc.prerequisiteCodes.length ? " | prereqs: " + tc.prerequisiteCodes.join(",") : "")).join("\n");
3556
+ }
3557
+ function renderCategoryTopicIndex(catalog) {
3558
+ const topicToConcepts = /* @__PURE__ */ new Map();
3559
+ for (const tc of catalog.concepts) for (const t of tc.topicCodes) topicToConcepts.set(t, (topicToConcepts.get(t) || 0) + 1);
3560
+ const byCat = /* @__PURE__ */ new Map();
3561
+ for (const [code, info] of catalog.topics) {
3562
+ for (const cat of info.categoryCodes) {
3563
+ const arr2 = byCat.get(cat) || [];
3564
+ arr2.push(code + " " + info.name + " (" + (topicToConcepts.get(code) || 0) + " concepts)");
3565
+ byCat.set(cat, arr2);
3566
+ }
3567
+ }
3568
+ const out = [];
3569
+ for (const [code, name] of catalog.categories) {
3570
+ out.push("CATEGORY " + code + " \u2014 " + name);
3571
+ for (const t of byCat.get(code) || []) out.push(" TOPIC: " + t);
3572
+ }
3573
+ return out.join("\n");
3574
+ }
3575
+ var JSON_ONLY2 = "Return ONLY a JSON object, no markdown fences, no commentary.";
3576
+ function parseJsonBlock(text) {
3577
+ const start = text.indexOf("{");
3578
+ const end = text.lastIndexOf("}");
3579
+ if (start < 0 || end <= start) throw new Error("LLM standardization response has no JSON object");
3580
+ return JSON.parse(text.slice(start, end + 1));
3581
+ }
3582
+ async function standardizeConcepts(opts) {
3583
+ const { client, catalog, techStack = "python", proposalsPath } = opts;
3584
+ const log = opts.onProgress ?? (() => {
3585
+ });
3586
+ const warnings = [];
3587
+ const hasTopics = catalog.topics.size > 0;
3588
+ const selSystem = [
3589
+ "You pre-select relevant areas of an institutional knowledge tree before a detailed concept alignment pass.",
3590
+ hasTopics ? "Given the analysis concepts, pick the TOPICS that could contain matching concepts. Concepts are filed under topics, so topics are the filter dimension." : "Given the analysis concepts, pick the CATEGORIES that could contain matching concepts.",
3591
+ "- Be generous: missing a relevant topic corrupts the alignment; an extra topic only costs attention.",
3592
+ "- Only codes that appear in the index are valid.",
3593
+ JSON_ONLY2
3594
+ ].join("\n");
3595
+ const selUser = [
3596
+ "ANALYSIS CONCEPTS:",
3597
+ opts.concepts.map((c) => "- " + c.name + ": " + c.description.slice(0, 160)).join("\n"),
3598
+ "",
3599
+ "KNOWLEDGE TREE INDEX:",
3600
+ renderCategoryTopicIndex(catalog),
3601
+ "",
3602
+ hasTopics ? 'Return JSON: {"relevant_topics":["TOPIC_CODE"]}' : 'Return JSON: {"relevant_categories":["CATEGORY_CODE"]}'
3603
+ ].join("\n");
3604
+ const selRes = await client.chat(
3605
+ [{ role: "system", content: selSystem }, { role: "user", content: selUser }],
3606
+ { temperature: 0, maxTokens: 4096 }
3607
+ );
3608
+ const sel = parseJsonBlock(selRes.content);
3609
+ const selTps = hasTopics ? (sel.relevant_topics || []).map(String).filter(Boolean) : [];
3610
+ const selCats = hasTopics ? [] : (sel.relevant_categories || []).map(String).filter(Boolean);
3611
+ log("STEP_2_5", "LLM relevance pre-selection (" + (hasTopics ? "topics" : "categories") + "): " + (hasTopics ? selTps.join(",") : selCats.join(",")));
3612
+ const shortlist = filterBySelection(catalog, selCats, selTps, opts.concepts);
3613
+ log("STEP_2_5", "Filtered tree concepts: " + shortlist.length + "/" + catalog.concepts.length + " shortlisted for alignment");
3614
+ if (shortlist.length === 0) throw new Error("Topic/category pre-selection eliminated every tree concept \u2014 fail-closed");
3615
+ const system = [
3616
+ "You align auto-derived learning concepts with an institutional knowledge tree.",
3617
+ "For EACH input concept decide exactly one of:",
3618
+ ` 1. "standardized" - the concept teaches the SAME underlying knowledge as one tree concept. Applying a tree concept inside a specific product context (e.g. "File-based persistence in a CLI app") is STILL the tree concept if the core knowledge matches. Set tree_code to that concept's code.`,
3619
+ " Adopt the tree's terminology. Only propose new when the tree genuinely has NO concept teaching that knowledge (broader AND narrower both fail).",
3620
+ ' 2. "proposed_new" - no tree concept covers it at the same granularity. Propose an UPPER_SNAKE_CASE code (descriptive, stable, max 5 words) and a one-sentence definition.',
3621
+ " If between 1 and 3 tree concepts are CLOSE but not exact, list their codes in possible_tree_matches for human review.",
3622
+ `For a "standardized" decision ALSO return "adopted_keywords": the subset of that tree concept's keywords (from the shortlist) that make sense for the course TECH STACK. Drop keywords that name tooling from other ecosystems (e.g. JS test runners for a Python course) \u2014 keep conceptual keywords even when spelled generically. Maximum 8, ordered most-relevant first.`,
3623
+ "Rules:",
3624
+ "- Judge by MEANING (definition + keywords), not by string similarity.",
3625
+ '- COVERAGE RULE: a "standardized" match must cover ALL primary dimensions of the input concept \u2014 every distinct aspect named in its title or description. A tree concept covering only ONE of several aspects (e.g. "interpreting error messages" for a concept about error handling AND user experience) is a PARTIAL match: decide "proposed_new" and list that tree code in possible_tree_matches.',
3626
+ "- Context-specific wording does NOT make a new concept: new concept = knowledge absent from the tree, not knowledge applied elsewhere.",
3627
+ "- Every input concept MUST receive exactly one decision. Never merge two input concepts into one tree code.",
3628
+ "- Prefer proposed_new over a wrong standardized match: a false match corrupts the curriculum.",
3629
+ JSON_ONLY2
3630
+ ].join("\n");
3631
+ const user = [
3632
+ "TECH STACK: " + techStack,
3633
+ "",
3634
+ "ANALYSIS CONCEPTS (to standardize):",
3635
+ JSON.stringify(opts.concepts.map((c) => ({ id: c.id, name: c.name, description: c.description.slice(0, 300), keywords: c.keywords.slice(0, 12) })), null, 1),
3636
+ "",
3637
+ "KNOWLEDGE TREE SHORTLIST (pre-filtered, " + shortlist.length + " candidate concepts):",
3638
+ renderShortlist(shortlist),
3639
+ "",
3640
+ 'Return JSON: {"decisions":[{"id":"<input id>","decision":"standardized","tree_code":"TREE_CODE","adopted_keywords":["kw1","kw2"],"note":"one sentence"},',
3641
+ ' {"id":"<input id>","decision":"proposed_new","proposed_code":"PROPOSED_CODE","proposed_definition":"...","possible_tree_matches":["CODE"]}]}'
3642
+ ].join("\n");
3643
+ const res = await client.chat(
3644
+ [{ role: "system", content: system }, { role: "user", content: user }],
3645
+ { temperature: 0, maxTokens: 8192 }
3646
+ );
3647
+ log("STEP_2_5", "Standardization response received (provider " + res.provider + ")");
3648
+ const parsed = parseJsonBlock(res.content);
3649
+ const decisions = parsed.decisions ?? [];
3650
+ const byId = new Map(decisions.map((d) => [String(d.id), d]));
3651
+ const finalDecisions = [];
3652
+ const proposals = [];
3653
+ for (const c of opts.concepts) {
3654
+ const d = byId.get(c.id);
3655
+ if (!d || d.decision !== "standardized" && d.decision !== "proposed_new") {
3656
+ throw new Error("Concept " + c.id + " (" + c.name + ") has no standardization decision \u2014 fail-closed");
3657
+ }
3658
+ if (d.decision === "standardized") {
3659
+ const tc = catalog.concepts.find((x) => x.code === d.tree_code);
3660
+ if (!tc) throw new Error("Concept " + c.id + " standardized to unknown tree code " + d.tree_code + " \u2014 fail-closed");
3661
+ const treeKw = new Set(tc.keywords.map((k) => k.toLowerCase()));
3662
+ const adopted = (d.adopted_keywords || []).map((k) => String(k)).filter((k) => {
3663
+ if (treeKw.has(k.toLowerCase())) return true;
3664
+ throw new Error("Concept " + c.id + ' adopted keyword "' + k + '" not in tree concept ' + tc.code + " \u2014 fail-closed");
3665
+ });
3666
+ if (tc.keywords.length > 0 && adopted.length === 0) {
3667
+ throw new Error("Concept " + c.id + " standardized to " + tc.code + " without adopted_keywords \u2014 fail-closed");
3668
+ }
3669
+ finalDecisions.push({ ...d, id: c.id, tree_code: tc.code, adopted_keywords: adopted });
3670
+ warnings.push("Concept " + c.id + " standardized as " + tc.code + " (" + tc.name + "), adopted keywords: " + adopted.join(", ") + (d.note ? " \u2014 " + d.note : ""));
3671
+ } else {
3672
+ const code = (d.proposed_code || "").toUpperCase().replace(/[^A-Z0-9_]/g, "_");
3673
+ if (!/^[A-Z][A-Z0-9_]*$/.test(code)) {
3674
+ throw new Error("Concept " + c.id + ' proposed invalid code "' + d.proposed_code + '" \u2014 fail-closed');
3675
+ }
3676
+ finalDecisions.push({ ...d, id: c.id, proposed_code: code });
3677
+ proposals.push({
3678
+ code,
3679
+ name: c.name,
3680
+ description: d.proposed_definition || c.description.slice(0, 300),
3681
+ keywords: c.keywords,
3682
+ subject_codes: "PY",
3683
+ category_codes: "",
3684
+ topic_codes: "",
3685
+ prerequisite_concept_codes: [],
3686
+ status: "proposed",
3687
+ source_concept_id: c.id,
3688
+ possible_tree_matches: (d.possible_tree_matches || []).filter(Boolean)
3689
+ });
3690
+ warnings.push("Concept " + c.id + " proposed as NEW: " + code + (d.possible_tree_matches?.length ? " (near matches: " + d.possible_tree_matches.join(", ") + ")" : ""));
3691
+ }
3692
+ }
3693
+ const outPath = proposalsPath || path.join(os.tmpdir(), "domain-kit-debug", "concept-proposals.json");
3694
+ try {
3695
+ fs.mkdirSync(outPath.slice(0, outPath.lastIndexOf("/")), { recursive: true });
3696
+ fs.writeFileSync(outPath, JSON.stringify({ generated_at: (/* @__PURE__ */ new Date()).toISOString(), proposals }, null, 2));
3697
+ log("STEP_2_5", "Proposals written: " + proposals.length + " -> " + outPath);
3698
+ } catch (e) {
3699
+ warnings.push("Could not write proposals artifact: " + (e instanceof Error ? e.message : String(e)));
3700
+ }
3701
+ return { decisions: finalDecisions, proposals, warnings, shortlistSize: shortlist.length, catalogSize: catalog.concepts.length };
3702
+ }
3703
+
3704
+ // src/graph/knowledgeGraphPipeline.ts
3705
+ function topologicalSort(concepts) {
3706
+ const inDegree = /* @__PURE__ */ new Map();
3707
+ const adjacency = /* @__PURE__ */ new Map();
3708
+ for (const concept of concepts) {
3709
+ inDegree.set(concept.id, 0);
3710
+ adjacency.set(concept.id, []);
3711
+ }
3712
+ for (const concept of concepts) {
3713
+ for (const prereq of concept.prerequisites || []) {
3714
+ adjacency.get(prereq)?.push(concept.id);
3715
+ inDegree.set(concept.id, (inDegree.get(concept.id) || 0) + 1);
3716
+ }
3717
+ }
3718
+ const queue = [];
3719
+ for (const [id, degree] of inDegree) {
3720
+ if (degree === 0) queue.push(id);
3721
+ }
3722
+ const sorted = [];
3723
+ while (queue.length > 0) {
3724
+ const current = queue.shift();
3725
+ sorted.push(current);
3726
+ for (const neighbor of adjacency.get(current) || []) {
3727
+ const newDegree = (inDegree.get(neighbor) || 1) - 1;
3728
+ inDegree.set(neighbor, newDegree);
3729
+ if (newDegree === 0) queue.push(neighbor);
3730
+ }
3731
+ }
3732
+ if (sorted.length !== concepts.length) {
3733
+ console.warn(`[WARN] Topological sort incomplete: ${sorted.length}/${concepts.length} concepts sorted`);
3734
+ }
3735
+ return sorted;
3736
+ }
3737
+ async function generateKnowledgeGraph(options) {
3738
+ const { subject, description, syllabusText, gradeBand, domain, techStack, llmConfig, onProgress } = options;
3739
+ const warnings = [];
3740
+ const log = (step, msg) => {
3741
+ console.log(`[KG-${step}] ${msg}`);
3742
+ onProgress?.(step, msg);
3743
+ };
3744
+ log("STEP_1", "Decomposing subject into concepts...");
3745
+ let parsedSyllabus = options.parsedSyllabus;
3746
+ if (!parsedSyllabus && syllabusText) {
3747
+ parsedSyllabus = parseSyllabus(syllabusText);
3748
+ log("STEP_1", `Parsed syllabus: ${parsedSyllabus.units.length} units, ${parsedSyllabus.allTopics.length} topics`);
3749
+ }
3750
+ const client = createLlmClient(llmConfig);
3751
+ const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "16000", 10);
3752
+ let truncatedSyllabus = "";
3753
+ if (syllabusText) {
3754
+ if (syllabusText.length > maxSyllabusChars) {
3755
+ truncatedSyllabus = syllabusText.slice(0, maxSyllabusChars);
3756
+ warnings.push(`Syllabus truncated from ${syllabusText.length} to ${maxSyllabusChars} chars \u2014 may miss some concepts`);
3757
+ } else {
3758
+ truncatedSyllabus = syllabusText;
3759
+ }
3760
+ }
3761
+ const structuredTopicsGuide = parsedSyllabus && parsedSyllabus.allTopics.length > 0 ? `
3762
+ STRUCTURED SYLLABUS TOPICS (Every topic MUST map to at least one concept to ensure 100% syllabus coverage):
3763
+ ` + parsedSyllabus.allTopics.map((t) => `- [${t.unitTitle}] ${t.id}: ${t.title} (Keywords: ${t.keywords.join(", ")})`).join("\n") + "\n" : "";
3764
+ const systemPrompt = "You are an expert curriculum designer. Decompose the given subject into a structured knowledge graph with concepts, prerequisites, and problem types.\n\nCRITICAL RULES:\n1. Each concept MUST have the three depth layers, authored against this CONTRACT:\n - ulo (WHAT + WHY): bound to the CONCEPT's intrinsic NATURE. It explains what the\n concept is and why it exists \u2014 the problem it solves in principle. It must be\n TECHNOLOGY-AGNOSTIC: never name a concrete library, API, function, file format,\n tool or language feature. Typical assessment level: Remember / Understand.\n - cio (HOW): the MECHANISM \u2014 how the concept works and solves that problem, step\n by step. Still bound to the concept's nature: more concrete than the ULO, but\n NOT tied to one specific technology (no concrete API names). CIO capability is\n assessed INDIRECTLY, through completing contextual work, not in isolation.\n - sio (IMPLEMENTATION): an array of 2-4 items. EVERY item MUST reference concrete\n KEYWORDS/technologies (a real API, function, module, file, or named pattern) and\n describe a completable, context-specific implementation task. Application,\n Analysis and Creation skills are assessed INDIRECTLY through completing these\n items. There is NO fixed Bloom ceiling for a layer \u2014 but concreteness MUST\n strictly increase: SIO more technology-bound than CIO, CIO more concrete than ULO.\n - prerequisites: concept IDs that must be learned first (MUST NOT create cycles)\n - problem_types: 2-4 problem types with bloom_level and difficulty. Remember and\n Understand problem types test the ULO/CIO directly; Apply-and-above types are\n proxies for SIO completion in context.\n - techniques: methods, formulas, tools\n2. Prerequisite chains MUST be acyclic (no concept can depend on itself or create a loop)\n3. Concepts should be ordered by prerequisite dependency (topological sort)\n4. Problem types should range from Remember to Apply (not all at same level)\n5. Group concepts into categories (chapters/sections)\n6. ALL text in ENGLISH (even if input is Vietnamese)\n7. NEVER invent concepts not present in the syllabus\n8. Return JSON matching the KnowledgeGraphSchema";
3765
+ const userPrompt = `Subject: ${subject}
3766
+ ${description ? `Description: ${description}` : ""}
3767
+ ${structuredTopicsGuide}${truncatedSyllabus ? `Syllabus:
3768
+ ${truncatedSyllabus}` : ""}
3769
+ ${gradeBand ? `Grade band: ${gradeBand[0]}-${gradeBand[1]}` : ""}
3770
+ ${domain ? `Domain: ${domain}` : ""}
3771
+
3772
+ Return JSON:
3773
+ {
3774
+ "subject": {
3775
+ "id": "${subject.toLowerCase().replace(/\s+/g, "-").slice(0, 30)}",
3776
+ "name": "${subject}",
3777
+ "description": "...",
3778
+ "domain": "${domain || "other"}",
3779
+ "grade_band": ${gradeBand ? `[${gradeBand[0]}, ${gradeBand[1]}]` : "[6, 12]"}
3780
+ },
3781
+ "concepts": [
3782
+ {
3783
+ "id": "C1",
3784
+ "name": "Concept name",
3785
+ "description": "Brief description",
3786
+ "category": "category_id",
3787
+ "ulo": "WHAT + WHY in technology-agnostic language: what the concept is and the problem it exists to solve. No library/API/tool names.",
3788
+ "cio": "HOW the mechanism works, still technology-agnostic but more concrete than the ULO.",
3789
+ "sio": ["Concrete task naming a REAL API/function/module/file from the target technology, completable end to end", "Another technology-bound task using a different concrete keyword"],
3790
+ "prerequisites": [],
3791
+ "estimated_minutes": 30,
3792
+ "problem_types": [
3793
+ {
3794
+ "id": "PT-C1-01",
3795
+ "name": "Problem type name",
3796
+ "description": "What this problem type tests",
3797
+ "bloom_level": "Remember",
3798
+ "difficulty": "easy",
3799
+ "estimated_minutes": 10
3800
+ }
3801
+ ],
3802
+ "techniques": ["technique 1", "technique 2"],
3803
+ "common_mistakes": ["mistake 1"]
3804
+ }
3805
+ ],
3806
+ "categories": [
3807
+ {
3808
+ "id": "cat1",
3809
+ "name": "Category name",
3810
+ "description": "Category description",
3811
+ "concept_ids": ["C1", "C2"],
3812
+ "order": 1
3813
+ }
3814
+ ],
3815
+ "learning_path": [
3816
+ {"concept_id": "C1", "order": 1, "depends_on": []},
3817
+ {"concept_id": "C2", "order": 2, "depends_on": ["C1"]}
3818
+ ]
3819
+ }`;
3820
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
3821
+ temperature: 0.1,
3822
+ maxTokens: parseInt(process.env.KG_MAX_TOKENS || "65536", 10)
3823
+ });
3824
+ log("STEP_2", "Validating and cleaning knowledge graph...");
3825
+ const concepts = result.concepts || [];
3826
+ const categories = result.categories || [];
3827
+ {
3828
+ const STOP = /* @__PURE__ */ new Set(["and", "the", "for", "with", "from", "that", "this", "into", "when", "while", "using", "their", "your", "each", "how", "why", "what", "between", "through", "about"]);
3829
+ const tokensFrom = (text) => text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP.has(w));
3830
+ for (const c of concepts) {
3831
+ const existing = (c.keywords || []).map((k) => String(k).trim()).filter(Boolean);
3832
+ if (existing.length === 0) {
3833
+ const derived = [.../* @__PURE__ */ new Set([...tokensFrom(c.name), ...tokensFrom(c.description || "")])].slice(0, 5);
3834
+ if (derived.length === 0) {
3835
+ throw new Error("Concept " + c.id + " (" + c.name + ") has no keywords and none could be derived from name/description \u2014 fail-closed");
3836
+ }
3837
+ c.keywords = derived;
3838
+ warnings.push("Concept " + c.id + ": empty keyword set - derived " + derived.join(", ") + " from name/description");
3839
+ } else {
3840
+ c.keywords = existing;
3841
+ }
3842
+ }
3843
+ }
3844
+ const rawPath = result.learning_path;
3845
+ let learningPath = (rawPath || []).map((s) => ({
3846
+ concept_id: String(s.concept_id || ""),
3847
+ order: Number(s.order || 0),
3848
+ depends_on: Array.isArray(s.depends_on) ? s.depends_on.map(String) : [],
3849
+ milestone_name: String(s.milestone_name || "")
3850
+ }));
3851
+ const conceptIds = new Set(concepts.map((c) => c.id));
3852
+ for (const concept of concepts) {
3853
+ for (const prereq of concept.prerequisites || []) {
3854
+ if (!conceptIds.has(prereq)) {
3855
+ warnings.push(`Concept ${concept.id} references non-existent prerequisite ${prereq} \u2014 removed`);
3856
+ concept.prerequisites = (concept.prerequisites || []).filter((p) => p !== prereq);
3857
+ }
3858
+ }
3859
+ }
3860
+ if (options.knowledgeTreePath) {
3861
+ const catalog = loadKnowledgeTreeCatalog(options.knowledgeTreePath);
3862
+ log("STEP_2_5", "Knowledge tree loaded: " + catalog.concepts.length + " concepts from " + catalog.path.split("/").pop());
3863
+ const std = await standardizeConcepts({
3864
+ client,
3865
+ concepts: concepts.map((c) => ({
3866
+ id: c.id,
3867
+ name: c.name,
3868
+ description: c.description || "",
3869
+ keywords: c.keywords || []
3870
+ })),
3871
+ catalog,
3872
+ techStack,
3873
+ proposalsPath: options.proposalsPath,
3874
+ onProgress: log
3875
+ });
3876
+ warnings.push(...std.warnings);
3877
+ const codeByStd = new Map(std.decisions.map((d) => [d.id, d]));
3878
+ for (const c of concepts) {
3879
+ const d = codeByStd.get(c.id);
3880
+ if (!d) throw new Error("Concept " + c.id + " missing standardization decision \u2014 fail-closed");
3881
+ if (d.decision === "standardized") {
3882
+ c.standardization = "standardized";
3883
+ c.tree_code = d.tree_code;
3884
+ c.code = d.tree_code;
3885
+ const tc = catalog.concepts.find((x) => x.code === d.tree_code);
3886
+ if (tc) {
3887
+ for (const k of d.adopted_keywords || []) {
3888
+ if (!(c.keywords || []).some((x) => x.toLowerCase() === k.toLowerCase())) {
3889
+ c.keywords = [...c.keywords || [], k];
3890
+ }
3891
+ }
3892
+ c.tree_prerequisites = tc.prerequisiteCodes;
3893
+ }
3894
+ } else {
3895
+ c.standardization = "proposed_new";
3896
+ c.code = d.proposed_code;
3897
+ }
3898
+ }
3899
+ const idByTreeCode = new Map(concepts.filter((c) => c.tree_code).map((c) => [c.tree_code, c.id]));
3900
+ for (const c of concepts) {
3901
+ for (const tp of c.tree_prerequisites || []) {
3902
+ const target = idByTreeCode.get(tp);
3903
+ if (target && target !== c.id && !(c.prerequisites || []).includes(target)) {
3904
+ c.prerequisites = [...c.prerequisites || [], target];
3905
+ warnings.push("Prereq seeded from tree: " + c.id + " <- " + tp + " (" + target + ")");
3906
+ }
3907
+ }
3908
+ }
3909
+ log("STEP_2_5", "Standardized: " + std.decisions.filter((d) => d.decision === "standardized").length + ", proposed new: " + std.decisions.filter((d) => d.decision === "proposed_new").length);
3910
+ }
3911
+ log("STEP_3", "Checking for prerequisite cycles...");
3912
+ const cycles = detectPrerequisiteCycles(concepts);
3913
+ if (cycles.length > 0) {
3914
+ log("STEP_3", `Found ${cycles.length} cycle(s) in prerequisite chain \u2014 breaking cycles`);
3915
+ for (const cycle of cycles) {
3916
+ warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 breaking at weakest edge`);
3917
+ }
3918
+ const { fixed, removed } = breakPrerequisiteCycles(concepts, cycles);
3919
+ for (let i = 0; i < concepts.length; i++) {
3920
+ const fixedConcept = fixed.find((f) => f.id === concepts[i].id);
3921
+ if (fixedConcept) {
3922
+ concepts[i] = { ...concepts[i], prerequisites: fixedConcept.prerequisites };
3923
+ }
3924
+ }
3925
+ for (const { from, to } of removed) {
3926
+ warnings.push(`Removed prerequisite edge: ${from} \u2192 ${to} (cycle breaker)`);
3927
+ }
3928
+ }
3929
+ log("STEP_4", "Topological sort of concepts...");
3930
+ const sortedIds = topologicalSort(concepts);
3931
+ learningPath = sortedIds.map((id, idx) => {
3932
+ const concept = concepts.find((c) => c.id === id);
3933
+ return {
3934
+ concept_id: id,
3935
+ order: idx + 1,
3936
+ depends_on: concept?.prerequisites || [],
3937
+ milestone_name: ""
3938
+ };
3939
+ });
3940
+ log("STEP_5", "Authoring ULO/CIO/SIO depth layers hierarchically...");
3941
+ {
3942
+ const lo = await authorDepthLayersHierarchically(
3943
+ client,
3944
+ concepts.map((c) => ({ id: c.id, name: c.name, description: c.description, keywords: c.keywords })),
3945
+ techStack || (domain === "software" || domain === "technology" ? "python" : domain || "general"),
3946
+ warnings
3947
+ );
3948
+ const codeByConceptId = assignConceptCodes(concepts.map((c) => ({ id: c.id, name: c.name })));
3949
+ const techTag = techTagFor(techStack || (domain === "software" || domain === "technology" ? "python" : domain || "general"));
3950
+ for (const concept of concepts) {
3951
+ concept.ulo = lo.uloByConcept[concept.id] || concept.ulo || "";
3952
+ concept.cio = lo.cioByConcept[concept.id] || concept.cio || "";
3953
+ if (lo.sioByConcept[concept.id] && lo.sioByConcept[concept.id].length > 0) {
3954
+ concept.sio = lo.sioByConcept[concept.id];
3955
+ }
3956
+ concept.code = concept.code || codeByConceptId[concept.id];
3957
+ concept.ulo_code = concept.ulo ? uloCode(concept.code) : concept.ulo_code;
3958
+ concept.cio_code = concept.cio ? cioCode(concept.code, 1, cioActionSlug(concept.cio)) : concept.cio_code;
3959
+ concept.sio_codes = concept.sio.length > 0 ? concept.sio.map((_, i) => sioCode(techTag, concept.code, i + 1)) : concept.sio_codes;
3960
+ }
3961
+ log("STEP_5", "Depth layers authored: " + concepts.length + " concepts, audit " + lo.audits.filter((a) => a.passed).length + "/" + concepts.length + " passed");
3962
+ }
3963
+ for (const step of learningPath) {
3964
+ if (!conceptIds.has(step.concept_id)) {
3965
+ warnings.push(`Learning path references non-existent concept ${step.concept_id}`);
3966
+ }
3967
+ }
3968
+ log("STEP_2", `Validated ${concepts.length} concepts, ${categories.length} categories, ${learningPath.length} learning path steps`);
3969
+ const rawGraph = {
3970
+ schema_version: 1,
3971
+ type: "knowledge_graph",
3972
+ subject: result.subject,
3973
+ concepts,
3974
+ categories,
3975
+ learning_path: learningPath
3976
+ };
3977
+ log("STEP_6", "Running Knowledge Graph verification & reverse coverage audit...");
3978
+ const { verifiedGraph, report } = verifyKnowledgeGraph(rawGraph, parsedSyllabus);
3979
+ warnings.push(...report.warnings);
3980
+ return {
3981
+ knowledgeGraph: verifiedGraph,
3982
+ warnings,
3983
+ verificationReport: report
3984
+ };
3985
+ }
3986
+
3987
+ // src/pipeline/knowledgeGraphPipeline.ts
3988
+ init_curriculumFeedEmitter();
3989
+ async function runKnowledgeGraphPipeline(options) {
3990
+ const warnings = [];
3991
+ let parsedSyllabus = options.parsedSyllabus;
3992
+ if (!parsedSyllabus && options.syllabusText) {
3993
+ parsedSyllabus = parseSyllabus(options.syllabusText);
3994
+ }
3995
+ const effectiveSubject = options.subject || parsedSyllabus?.subject.title || "Untitled Subject";
3996
+ const effectiveDomain = options.domain || parsedSyllabus?.subject.domain || "other";
3997
+ const effectiveGradeBand = options.gradeBand || parsedSyllabus?.subject.gradeBand;
3998
+ const effectiveDesc = options.description || parsedSyllabus?.subject.description;
3999
+ const result = await generateKnowledgeGraph({
4000
+ ...options,
4001
+ subject: effectiveSubject,
4002
+ domain: effectiveDomain,
4003
+ gradeBand: effectiveGradeBand,
4004
+ description: effectiveDesc,
4005
+ parsedSyllabus
4006
+ });
4007
+ warnings.push(...result.warnings);
4008
+ const curriculumFeed = emitCurriculumFeed(result.knowledgeGraph, {
4009
+ warnings
4010
+ });
4011
+ return {
4012
+ knowledgeGraph: result.knowledgeGraph,
4013
+ parsedSyllabus,
4014
+ curriculumFeed,
4015
+ verificationReport: result.verificationReport,
4016
+ warnings
4017
+ };
4018
+ }
4019
+
4020
+ // src/graph/hybridGraphPipeline.ts
4021
+ async function generateHybridGraph(options) {
4022
+ const { projectGraph, knowledgeGraph, llmConfig, onProgress } = options;
4023
+ const warnings = [];
4024
+ const log = (step, msg) => {
4025
+ console.log("[HG-" + step + "] " + msg);
4026
+ onProgress?.(step, msg);
4027
+ };
4028
+ const features = projectGraph.features || [];
4029
+ const concepts = knowledgeGraph.concepts || [];
4030
+ if (features.length === 0 || concepts.length === 0) {
4031
+ warnings.push("No features or concepts to link");
4032
+ return {
4033
+ hybridGraph: {
4034
+ schema_version: 1,
4035
+ type: "hybrid_graph",
4036
+ project_graph: projectGraph,
4037
+ knowledge_graph: knowledgeGraph,
4038
+ links: [],
4039
+ phases: [],
4040
+ description: "",
4041
+ total_project_minutes: 0,
4042
+ total_concept_minutes: 0,
4043
+ estimated_total_minutes: 0
4044
+ },
4045
+ warnings
4046
+ };
4047
+ }
4048
+ const client = createLlmClient(llmConfig);
4049
+ const featuresList = features.map(
4050
+ (f) => "- " + f.id + ": " + f.name + " - " + (f.description || "") + " (keywords: " + (f.steps || []).flatMap((s) => s.keywords || []).slice(0, 5).join(", ") + ")"
4051
+ ).join("\n");
4052
+ const conceptsList = concepts.map(
4053
+ (c) => "- " + c.id + ": " + c.name + " - ULO: " + (c.ulo || "").slice(0, 100) + " (prerequisites: " + (c.prerequisites || []).join(", ") + ")"
4054
+ ).join("\n");
4055
+ const systemPrompt = "You are a curriculum architect. Link project features to knowledge concepts.\nFor each feature, determine which concepts it requires or demonstrates.\n\nRULES:\n1. Each link must have a relationship type: requires | demonstrates | applies | extends\n2. depth indicates how deeply the feature needs the concept:\n - ulo: feature only needs WHAT+WHY understanding\n - cio: feature needs HOW understanding\n - sio: feature needs specific implementation knowledge\n3. estimated_prereq_minutes: how many minutes of concept learning before this feature\n4. Not every feature needs every concept - only create meaningful links\n5. Return JSON object with a links array";
4056
+ const userPrompt = "FEATURES:\n" + featuresList + "\n\nCONCEPTS:\n" + conceptsList + "\n\nCreate links between features and concepts. Return JSON.\n";
4057
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
4058
+ temperature: 0.1,
4059
+ maxTokens: 16384
4060
+ });
4061
+ const rawLinks = result.links || [];
4062
+ const featureIds = new Set(features.map((f) => f.id));
4063
+ const conceptIds = new Set(concepts.map((c) => c.id));
4064
+ const validLinks = [];
4065
+ for (const link of rawLinks) {
4066
+ const fid = String(link.feature_id || "");
4067
+ const cid = String(link.concept_id || "");
4068
+ if (!featureIds.has(fid)) {
4069
+ warnings.push("Link references non-existent feature " + fid);
4070
+ continue;
4071
+ }
4072
+ if (!conceptIds.has(cid)) {
4073
+ warnings.push("Link references non-existent concept " + cid);
4074
+ continue;
4075
+ }
4076
+ validLinks.push({
4077
+ feature_id: fid,
4078
+ concept_id: cid,
4079
+ relationship: link.relationship || "requires",
4080
+ depth: link.depth || "cio",
4081
+ description: String(link.description || ""),
4082
+ estimated_prereq_minutes: Number(link.estimated_prereq_minutes) || 0,
4083
+ phase_id: ""
4084
+ });
4085
+ }
4086
+ log("STEP_1", "Generated " + validLinks.length + " feature-concept links");
4087
+ const phases = await decomposePhases({
4088
+ features,
4089
+ concepts,
4090
+ links: validLinks,
4091
+ client,
4092
+ log,
4093
+ warnings
4094
+ });
4095
+ const phaseByFeature = /* @__PURE__ */ new Map();
4096
+ for (const ph of phases) {
4097
+ for (const fid of ph.feature_ids) phaseByFeature.set(fid, ph.id);
4098
+ }
4099
+ for (const link of validLinks) link.phase_id = phaseByFeature.get(link.feature_id) || "";
4100
+ log("STEP_3", "Calculating totals...");
4101
+ const totalProjectMinutes = features.reduce((sum, f) => {
4102
+ const featureMinutes = (f.steps || []).reduce((s, step) => s + (step.effort?.estimated_minutes || 15), 0);
4103
+ return sum + featureMinutes;
4104
+ }, 0);
4105
+ const totalConceptMinutes = concepts.reduce((sum, c) => sum + (c.estimated_minutes || 30), 0);
4106
+ const prereqMinutesByConcept = /* @__PURE__ */ new Map();
4107
+ for (const link of validLinks) {
4108
+ const mins = link.estimated_prereq_minutes || 0;
4109
+ if (mins > 0) {
4110
+ const existing = prereqMinutesByConcept.get(link.concept_id) || 0;
4111
+ prereqMinutesByConcept.set(link.concept_id, Math.max(existing, mins));
4112
+ }
4113
+ }
4114
+ const totalPrereqMinutes = Array.from(prereqMinutesByConcept.values()).reduce((a, b) => a + b, 0);
4115
+ return {
4116
+ hybridGraph: {
4117
+ schema_version: 1,
4118
+ type: "hybrid_graph",
4119
+ project_graph: projectGraph,
4120
+ knowledge_graph: knowledgeGraph,
4121
+ links: validLinks,
4122
+ phases,
4123
+ description: "Progressive completion path: " + phases.length + " phases, " + features.length + " features, " + concepts.length + " concepts",
4124
+ total_project_minutes: totalProjectMinutes,
4125
+ total_concept_minutes: totalConceptMinutes,
4126
+ estimated_total_minutes: totalProjectMinutes + totalPrereqMinutes
4127
+ },
4128
+ warnings
4129
+ };
4130
+ }
4131
+ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
4132
+ const knownFeatureIds = new Set(features.map((f) => f.id));
4133
+ const knownConceptIds = new Set(concepts.map((c) => c.id));
4134
+ const prereqOf = /* @__PURE__ */ new Map();
4135
+ for (const c of concepts) prereqOf.set(c.id, (c.prerequisites || []).filter((p) => knownConceptIds.has(p)));
4136
+ const phases = [];
4137
+ const featurePhase = /* @__PURE__ */ new Map();
4138
+ const newIntroPhase = /* @__PURE__ */ new Map();
4139
+ const errors = [];
4140
+ plan.forEach((rp, idx) => {
4141
+ const id = String(rp.id || "PH" + (idx + 1));
4142
+ const order = idx + 1;
4143
+ const name = String(rp.name || "Phase " + order).slice(0, 80);
4144
+ const productCompletion = String(rp.product_completion || "").trim();
4145
+ const rawFeatureIds = Array.isArray(rp.feature_ids) ? rp.feature_ids.map(String) : [];
4146
+ const rawIntros = Array.isArray(rp.introduces) ? rp.introduces : [];
4147
+ if (!productCompletion) {
4148
+ errors.push("Phase " + id + ": product_completion is empty - every phase must describe the product completion level");
4149
+ }
4150
+ const featureIdsForPhase = [];
4151
+ for (const fid of rawFeatureIds) {
4152
+ if (!knownFeatureIds.has(fid)) {
4153
+ errors.push("Phase " + id + ": unknown feature " + fid);
4154
+ continue;
4155
+ }
4156
+ if (featurePhase.has(fid)) {
4157
+ errors.push("Feature " + fid + " assigned to both " + featurePhase.get(fid) + " and " + id + " - each feature must belong to exactly one phase");
4158
+ continue;
4159
+ }
4160
+ featurePhase.set(fid, id);
4161
+ featureIdsForPhase.push(fid);
4162
+ }
4163
+ const introductions = [];
4164
+ for (const ri of rawIntros) {
4165
+ const cid = String(ri.concept_id || "");
4166
+ if (!knownConceptIds.has(cid)) {
4167
+ errors.push("Phase " + id + ": introduces unknown concept " + cid);
4168
+ continue;
4169
+ }
4170
+ const aspect = String(ri.aspect || "new") === "advanced" ? "advanced" : "new";
4171
+ if (aspect === "advanced") {
4172
+ const first = newIntroPhase.get(cid);
4173
+ if (first === void 0) {
4174
+ errors.push("Phase " + id + ": advanced introduction of " + cid + " before any new introduction - revisit requires prior introduction");
4175
+ continue;
4176
+ }
4177
+ if (first >= order) {
4178
+ errors.push("Phase " + id + ": advanced introduction of " + cid + " must come after its new introduction in phase " + first);
4179
+ continue;
4180
+ }
4181
+ } else if (newIntroPhase.has(cid)) {
4182
+ errors.push("Phase " + id + ": duplicate new introduction of " + cid + " (already introduced in phase " + newIntroPhase.get(cid) + ")");
4183
+ continue;
4184
+ } else {
4185
+ newIntroPhase.set(cid, order);
4186
+ }
4187
+ for (const pre of prereqOf.get(cid) || []) {
4188
+ if (!newIntroPhase.has(pre)) {
4189
+ errors.push("Phase " + id + ": concept " + cid + " introduced before its prerequisite " + pre + " is introduced - violates the prerequisite chain");
4190
+ }
4191
+ }
4192
+ introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
4193
+ }
4194
+ phases.push({
4195
+ id,
4196
+ order,
4197
+ name,
4198
+ product_completion: productCompletion,
4199
+ feature_ids: featureIdsForPhase,
4200
+ introduces: introductions
4201
+ });
4202
+ });
4203
+ const missing = features.map((f) => f.id).filter((fid) => !featurePhase.has(fid));
4204
+ if (missing.length > 0) {
4205
+ errors.push("Features not assigned to any phase: " + missing.join(", "));
4206
+ }
4207
+ const firstConsumerPhase = /* @__PURE__ */ new Map();
4208
+ const phaseOrderByFeature = /* @__PURE__ */ new Map();
4209
+ for (const ph of phases) for (const fid of ph.feature_ids) phaseOrderByFeature.set(fid, ph.order);
4210
+ for (const l of links) {
4211
+ const po = phaseOrderByFeature.get(l.feature_id);
4212
+ if (po === void 0) continue;
4213
+ const cur = firstConsumerPhase.get(l.concept_id);
4214
+ if (cur === void 0 || po < cur) firstConsumerPhase.set(l.concept_id, po);
4215
+ }
4216
+ for (const [cid, consumerOrder] of firstConsumerPhase) {
4217
+ const intro = newIntroPhase.get(cid);
4218
+ if (intro === void 0) {
4219
+ errors.push("Concept " + cid + " is consumed from phase " + consumerOrder + " but never introduced in any phase");
4220
+ } else if (intro > consumerOrder) {
4221
+ errors.push("Concept " + cid + " introduced in phase " + intro + " but first consumed in phase " + consumerOrder + " - introduction must come no later than first consumption");
4222
+ }
4223
+ }
4224
+ return { phases, errors };
4225
+ }
4226
+ async function decomposePhases(args) {
4227
+ const { features, concepts, links, client, log, warnings } = args;
4228
+ const featureList = features.map(
4229
+ (f) => "- " + f.id + ": " + f.name + " - " + (f.description || "").slice(0, 120)
4230
+ ).join("\n");
4231
+ const conceptList = concepts.map(
4232
+ (c) => "- " + c.id + ": " + c.name + " (prerequisites: " + ((c.prerequisites || []).join(", ") || "none") + ")"
4233
+ ).join("\n");
4234
+ const linkList = links.map(
4235
+ (l) => "- " + l.feature_id + " -> " + l.concept_id + " (" + l.depth + ")"
4236
+ ).join("\n") || "(none)";
4237
+ const systemPrompt = "You decompose a software project build into PROGRESSIVE COMPLETION PHASES for learning purposes.\n\nA phase is NOT a feature and NOT a milestone list: it is a cross-feature slice of the\nproduct whose completion yields a DEMONSTRABLY more complete product. Build up gradually:\nthe product grows feature by feature across phases, and each phase adds its features to\nwhat previous phases already built.\n\nRULES:\n1. Every feature must be assigned to EXACTLY ONE phase (no feature reused, none skipped).\n2. Phases are ordered; later phases depend on earlier ones and yield a more complete product.\n3. product_completion describes concretely what the product can DO once the phase is done.\n4. introduces lists which concepts the phase must teach BEFORE its feature work starts:\n - aspect new: first time the concept appears in the course\n - aspect advanced: a concept introduced in an EARLIER phase, revisited here at a\n higher depth (an advanced aspect of the same concept, e.g. validation rules on top\n of basic data modeling). Use the note field to say which aspect.\n5. A concept MUST be introduced (new) no later than the first phase that consumes it.\n6. A concept must not be introduced before its own prerequisites are introduced.\n7. Typically 3-6 phases for a small project; keep phases balanced.\n8. Phase 1 usually covers project setup/foundation features plus the first minimal slice.\n9. Return JSON only.";
4238
+ const baseUserPrompt = "FEATURES:\n" + featureList + "\n\nCONCEPTS:\n" + conceptList + "\n\nFEATURE-CONCEPT LINKS:\n" + linkList + "\n\nReturn JSON with a phases array. Each phase: { id: PH1, name, product_completion, feature_ids: [F0, F1], introduces: [{ concept_id: C1, aspect: new or advanced, note }] }.\n";
4239
+ const MAX_DECOMPOSE_ATTEMPTS = 3;
4240
+ let auditErrors = [];
4241
+ let phases = [];
4242
+ let rawPhases = [];
4243
+ for (let attempt = 1; attempt <= MAX_DECOMPOSE_ATTEMPTS; attempt++) {
4244
+ const userPrompt = attempt === 1 ? baseUserPrompt : baseUserPrompt + "\nYOUR PREVIOUS PLAN VIOLATED THESE CONTRACT RULES (fix ALL of them):\n" + auditErrors.map((e) => "- " + e).join("\n") + "\nReturn a corrected full phases array.\n";
4245
+ const result = await llmChatJson(client, systemPrompt, userPrompt, {
4246
+ temperature: 0.1,
4247
+ maxTokens: 16384
4248
+ });
4249
+ rawPhases = result.phases || [];
4250
+ if (rawPhases.length === 0) {
4251
+ if (attempt === MAX_DECOMPOSE_ATTEMPTS) {
4252
+ throw new Error("Phase decomposition returned zero phases - fail-closed abort");
4253
+ }
4254
+ auditErrors = ["Returned zero phases"];
4255
+ continue;
4256
+ }
4257
+ ({ phases, errors: auditErrors } = auditPhasePlan(rawPhases, features, concepts, links));
4258
+ if (auditErrors.length === 0) break;
4259
+ if (attempt < MAX_DECOMPOSE_ATTEMPTS) {
4260
+ log("STEP_2", "Contract violations (" + auditErrors.length + ") - repairing (attempt " + (attempt + 1) + "/" + MAX_DECOMPOSE_ATTEMPTS + ")");
4261
+ }
4262
+ }
4263
+ if (auditErrors.length > 0) {
4264
+ throw new Error(
4265
+ "Phase decomposition violated the progressive-completion contract (" + auditErrors.length + " issue(s)):\n" + auditErrors.map((e) => " - " + e).join("\n") + "\nRe-run the pipeline - a phase model that cannot be trusted is worse than no output."
4266
+ );
4267
+ }
4268
+ if (phases.length === 1 && features.length > 2) {
4269
+ warnings.push("Only one phase for " + features.length + " features - progressive decomposition likely too coarse");
4270
+ }
4271
+ log("STEP_2", "Decomposed into " + phases.length + " phases: " + phases.map((p) => p.id + "(" + p.feature_ids.length + "F/" + p.introduces.length + "K)").join(", "));
4272
+ return phases;
4273
+ }
4274
+
4275
+ // src/pipeline/hybridGraphPipeline.ts
4276
+ init_curriculumFeedEmitter();
4277
+ async function runHybridGraphPipeline(options) {
4278
+ const warnings = [];
4279
+ const result = await generateHybridGraph(options);
4280
+ warnings.push(...result.warnings);
4281
+ const curriculumFeed = emitCurriculumFeed(result.hybridGraph, {
4282
+ warnings
4283
+ });
4284
+ return {
4285
+ hybridGraph: result.hybridGraph,
4286
+ curriculumFeed,
4287
+ warnings
4288
+ };
4289
+ }
4290
+
4291
+ // src/detector/inputTypeDetector.ts
4292
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
4293
+ ".swift",
4294
+ ".ts",
4295
+ ".tsx",
4296
+ ".js",
4297
+ ".jsx",
4298
+ ".py",
4299
+ ".ino",
4300
+ ".cpp",
4301
+ ".c",
4302
+ ".h",
4303
+ ".hpp",
4304
+ ".kt",
4305
+ ".kts",
4306
+ ".java",
4307
+ ".cs",
4308
+ ".rb",
4309
+ ".go",
4310
+ ".rs",
4311
+ ".php",
4312
+ ".lua",
4313
+ ".r",
4314
+ ".m"
4315
+ ]);
4316
+ var NON_CODE_EXTENSIONS = /* @__PURE__ */ new Set([
4317
+ ".stl",
4318
+ ".obj",
4319
+ ".step",
4320
+ ".stp",
4321
+ ".iges",
4322
+ ".igs",
4323
+ // 3D models
4324
+ ".png",
4325
+ ".jpg",
4326
+ ".jpeg",
4327
+ ".gif",
4328
+ ".svg",
4329
+ ".bmp",
4330
+ // Images
4331
+ ".pdf",
4332
+ ".doc",
4333
+ ".docx",
4334
+ ".pptx",
4335
+ ".xlsx",
4336
+ // Documents
4337
+ ".tc3",
4338
+ ".f3d",
4339
+ // CAD files
4340
+ ".mp3",
4341
+ ".wav",
4342
+ ".midi",
4343
+ // Audio
4344
+ ".mp4",
4345
+ ".avi",
4346
+ ".mov"
4347
+ // Video
4348
+ ]);
4349
+ function detectInputType(options) {
4350
+ const evidence = [];
4351
+ let repoScore = 0;
4352
+ let syllabusScore = 0;
4353
+ let filesScore = 0;
4354
+ let descriptionScore = 0;
4355
+ if (options.repoUrl) {
4356
+ repoScore += 0.5;
4357
+ evidence.push(`repoUrl provided: ${options.repoUrl}`);
4358
+ }
4359
+ if (options.repoDir) {
4360
+ repoScore += 0.5;
4361
+ evidence.push(`repoDir provided: ${options.repoDir}`);
4362
+ }
4363
+ if (options.syllabusText && options.syllabusText.length > 50) {
4364
+ syllabusScore += 0.6;
4365
+ evidence.push(`syllabusText provided (${options.syllabusText.length} chars)`);
4366
+ }
4367
+ if (options.chapters && options.chapters.length > 0) {
4368
+ syllabusScore += 0.4;
4369
+ evidence.push(`${options.chapters.length} chapters provided`);
4370
+ }
4371
+ if (options.fileExtensions && options.fileExtensions.length > 0) {
4372
+ const codeCount = options.fileExtensions.filter((ext) => CODE_EXTENSIONS.has(ext)).length;
4373
+ const nonCodeCount = options.fileExtensions.filter((ext) => NON_CODE_EXTENSIONS.has(ext)).length;
4374
+ if (nonCodeCount > 0) {
4375
+ filesScore += 0.7;
4376
+ evidence.push(`${nonCodeCount} non-code files detected: ${options.fileExtensions.filter((ext) => NON_CODE_EXTENSIONS.has(ext)).join(", ")}`);
4377
+ }
4378
+ if (codeCount > 0 && repoScore === 0) {
4379
+ repoScore += 0.3;
4380
+ evidence.push(`${codeCount} source code files detected`);
4381
+ }
4382
+ }
4383
+ if (options.description && options.description.length > 100) {
4384
+ descriptionScore += 0.5;
4385
+ evidence.push(`description provided (${options.description.length} chars)`);
4386
+ }
4387
+ if (options.goal && options.goal.length > 20) {
4388
+ descriptionScore += 0.3;
4389
+ evidence.push(`goal provided (${options.goal.length} chars)`);
4390
+ }
4391
+ const scores = [
4392
+ { type: "repository", score: repoScore },
4393
+ { type: "syllabus", score: syllabusScore },
4394
+ { type: "files", score: filesScore },
4395
+ { type: "description", score: descriptionScore }
4396
+ ];
4397
+ scores.sort((a, b) => b.score - a.score);
4398
+ const winner = scores[0];
4399
+ const activeTypes = scores.filter((s) => s.score >= 0.3);
4400
+ if (activeTypes.length >= 2) {
4401
+ return {
4402
+ inputType: "mixed",
4403
+ confidence: 0.7,
4404
+ evidence: [...evidence, `Mixed input detected: ${activeTypes.map((t) => t.type).join(" + ")}`]
4405
+ };
4406
+ }
4407
+ if (winner.score === 0) {
4408
+ return {
4409
+ inputType: "description",
4410
+ confidence: 0.3,
4411
+ evidence: [...evidence, "No strong signals \u2014 defaulting to description"]
4412
+ };
4413
+ }
4414
+ return {
4415
+ inputType: winner.type,
4416
+ confidence: Math.min(winner.score, 1),
4417
+ evidence
4418
+ };
4419
+ }
4420
+
4421
+ // src/detector/domainDetector.ts
4422
+ var DOMAIN_KEYWORDS = {
4423
+ software: [
4424
+ "app",
4425
+ "web",
4426
+ "api",
4427
+ "database",
4428
+ "frontend",
4429
+ "backend",
4430
+ "mobile",
4431
+ "swift",
4432
+ "react",
4433
+ "vue",
4434
+ "angular",
4435
+ "node",
4436
+ "python",
4437
+ "java",
4438
+ "typescript",
4439
+ "javascript",
4440
+ "html",
4441
+ "css",
4442
+ "sql",
4443
+ "rest",
4444
+ "graphql",
4445
+ "git",
4446
+ "docker",
4447
+ "kubernetes",
4448
+ "ci/cd",
4449
+ "deploy",
4450
+ "swiftui",
4451
+ "uikit",
4452
+ "flutter",
4453
+ "kotlin",
4454
+ "android",
4455
+ "ios",
4456
+ "nextjs",
4457
+ "nuxt",
4458
+ "svelte",
4459
+ "django",
4460
+ "flask",
4461
+ "express"
4462
+ ],
4463
+ hardware_iot: [
4464
+ "arduino",
4465
+ "robot",
4466
+ "sensor",
4467
+ "motor",
4468
+ "esp32",
4469
+ "gpio",
4470
+ "raspberry pi",
4471
+ "iot",
4472
+ "firmware",
4473
+ "embedded",
4474
+ "microcontroller",
4475
+ "led",
4476
+ "servo",
4477
+ "potentiometer",
4478
+ "breadboard",
4479
+ "soldering",
4480
+ "i2c",
4481
+ "spi",
4482
+ "pwm",
4483
+ "uart",
4484
+ "analog",
4485
+ "digital",
4486
+ "ultrasonic",
4487
+ "infrared",
4488
+ "bluetooth",
4489
+ "wifi module",
4490
+ "pin",
4491
+ "voltage",
4492
+ "resistor",
4493
+ "capacitor",
4494
+ "circuit"
4495
+ ],
4496
+ "3d_design": [
4497
+ "tinkercad",
4498
+ "3d model",
4499
+ "cad",
4500
+ "parametric",
4501
+ "stl",
4502
+ "obj",
4503
+ "fusion360",
4504
+ "blender",
4505
+ "solidworks",
4506
+ "autocad",
4507
+ "freecad",
4508
+ "shape",
4509
+ "dimension",
4510
+ "extrude",
4511
+ "revolve",
4512
+ "fillet",
4513
+ "chamfer",
4514
+ "assembly",
4515
+ "constraint",
4516
+ "sketch",
4517
+ "plane",
4518
+ "axis",
4519
+ "render",
4520
+ "mesh",
4521
+ "nurbs",
4522
+ "surface",
4523
+ "volume"
4524
+ ],
4525
+ math: [
4526
+ // Vietnamese
4527
+ "h\xECnh h\u1ECDc",
4528
+ "\u0111\u1EA1i s\u1ED1",
4529
+ "gi\u1EA3i t\xEDch",
4530
+ "x\xE1c su\u1EA5t",
4531
+ "th\u1ED1ng k\xEA",
4532
+ "tam gi\xE1c",
4533
+ "\u0111\u01B0\u1EDDng tr\xF2n",
4534
+ "ph\u01B0\u01A1ng tr\xECnh",
4535
+ "b\u1EA5t ph\u01B0\u01A1ng tr\xECnh",
4536
+ "s\u1ED1 h\u1ECDc",
4537
+ "t\u1ED5 h\u1EE3p",
4538
+ "ho\xE1n v\u1ECB",
4539
+ "\u0111\u1ED9 \u0111o",
4540
+ "g\xF3c",
4541
+ "c\u1EA1nh",
4542
+ "di\u1EC7n t\xEDch",
4543
+ "th\u1EC3 t\xEDch",
4544
+ "chu vi",
4545
+ "\u0111\u01B0\u1EDDng cao",
4546
+ "trung tuy\u1EBFn",
4547
+ // English
4548
+ "geometry",
4549
+ "algebra",
4550
+ "calculus",
4551
+ "trigonometry",
4552
+ "logarithm",
4553
+ "probability",
4554
+ "statistics",
4555
+ "derivative",
4556
+ "integral",
4557
+ "matrix",
4558
+ "vector",
4559
+ "theorem",
4560
+ "proof",
4561
+ "angle",
4562
+ "perimeter",
4563
+ "area",
4564
+ // Chinese
4565
+ "\u51E0\u4F55",
4566
+ "\u4EE3\u6570",
4567
+ "\u5FAE\u79EF\u5206",
4568
+ "\u6982\u7387",
4569
+ "\u7EDF\u8BA1",
4570
+ "\u4E09\u89D2\u5F62",
4571
+ "\u5706",
4572
+ "\u65B9\u7A0B",
4573
+ // Japanese
4574
+ "\u5E7E\u4F55",
4575
+ "\u4EE3\u6570",
4576
+ "\u5FAE\u5206",
4577
+ "\u7A4D\u5206",
4578
+ "\u78BA\u7387",
4579
+ "\u7D71\u8A08",
4580
+ // French
4581
+ "g\xE9om\xE9trie",
4582
+ "alg\xE8bre",
4583
+ "calcul",
4584
+ "trigonom\xE9trie",
4585
+ "probabilit\xE9"
4586
+ ],
4587
+ science: [
4588
+ // Vietnamese
4589
+ "l\xFD thuy\u1EBFt",
4590
+ "nguy\xEAn l\xFD",
4591
+ "th\xED nghi\u1EC7m",
4592
+ "\u0111i\u1EC7n",
4593
+ "l\u1EF1c",
4594
+ "h\xF3a h\u1ECDc",
4595
+ "sinh h\u1ECDc",
4596
+ "v\u1EADt l\xFD",
4597
+ "\u0111\u1ECBa l\xFD",
4598
+ "ph\u1EA3n \u1EE9ng",
4599
+ "nguy\xEAn t\u1ED1",
4600
+ "ph\xE2n t\u1EED",
4601
+ "n\u0103ng l\u01B0\u1EE3ng",
4602
+ "c\u1EA3m \u1EE9ng",
4603
+ "t\u1EEB tr\u01B0\u1EDDng",
4604
+ "quang h\u1ECDc",
4605
+ "m\xF4i tr\u01B0\u1EDDng",
4606
+ "sinh th\xE1i",
4607
+ // English
4608
+ "physics",
4609
+ "chemistry",
4610
+ "biology",
4611
+ "earth science",
4612
+ "cell",
4613
+ "gene",
4614
+ "evolution",
4615
+ "ecosystem",
4616
+ "molecule",
4617
+ "atom",
4618
+ // Chinese
4619
+ "\u7269\u7406",
4620
+ "\u5316\u5B66",
4621
+ "\u751F\u7269",
4622
+ "\u5730\u7406",
4623
+ // Japanese
4624
+ "\u7269\u7406",
4625
+ "\u5316\u5B66",
4626
+ "\u751F\u7269",
4627
+ // French
4628
+ "physique",
4629
+ "chimie",
4630
+ "biologie"
4631
+ ],
4632
+ language: [
4633
+ // Vietnamese
4634
+ "ti\u1EBFng anh",
4635
+ "ti\u1EBFng vi\u1EC7t",
4636
+ "ng\u1EEF ph\xE1p",
4637
+ "t\u1EEB v\u1EF1ng",
4638
+ "\u0111\u1ECDc",
4639
+ "vi\u1EBFt",
4640
+ "n\xF3i",
4641
+ "nghe",
4642
+ // English
4643
+ "grammar",
4644
+ "vocabulary",
4645
+ "writing",
4646
+ "reading",
4647
+ "speaking",
4648
+ "listening",
4649
+ "pronunciation",
4650
+ "essay",
4651
+ "paragraph",
4652
+ "sentence",
4653
+ "verb",
4654
+ "noun",
4655
+ "adjective",
4656
+ "ielts",
4657
+ "toeic",
4658
+ "toefl",
4659
+ // Chinese
4660
+ "\u82F1\u8BED",
4661
+ "\u8BED\u6CD5",
4662
+ "\u8BCD\u6C47",
4663
+ "\u9605\u8BFB",
4664
+ "\u5199\u4F5C",
4665
+ // Japanese
4666
+ "\u82F1\u8A9E",
4667
+ "\u6587\u6CD5",
4668
+ "\u8A9E\u5F59",
4669
+ "\u8AAD\u89E3",
4670
+ "\u4F5C\u6587",
4671
+ // French
4672
+ "anglais",
4673
+ "grammaire",
4674
+ "vocabulaire",
4675
+ "lecture",
4676
+ "\xE9criture"
4677
+ ],
4678
+ arts: [
4679
+ // Vietnamese
4680
+ "\xE2m nh\u1EA1c",
4681
+ "nh\u1EA1c l\xFD",
4682
+ "solf\xE8ge",
4683
+ "\u0111\xE0n",
4684
+ "piano",
4685
+ "guitar",
4686
+ "h\u1ED9i h\u1ECDa",
4687
+ "thi\u1EBFt k\u1EBF",
4688
+ "nhi\u1EBFp \u1EA3nh",
4689
+ "tranh",
4690
+ "m\xE0u",
4691
+ "s\xE1ng t\xE1c",
4692
+ "bi\xEAn kh\xFAc",
4693
+ // English
4694
+ "music",
4695
+ "art",
4696
+ "painting",
4697
+ "drawing",
4698
+ "sketch",
4699
+ "photography",
4700
+ "color theory",
4701
+ "composition",
4702
+ "typography",
4703
+ "layout",
4704
+ "digital art",
4705
+ "illustration",
4706
+ "figma",
4707
+ // Chinese
4708
+ "\u97F3\u4E50",
4709
+ "\u7F8E\u672F",
4710
+ "\u7ED8\u753B",
4711
+ "\u6444\u5F71",
4712
+ "\u8BBE\u8BA1",
4713
+ // Japanese
4714
+ "\u97F3\u697D",
4715
+ "\u7F8E\u8853",
4716
+ "\u7D75\u753B",
4717
+ "\u5199\u771F",
4718
+ "\u30C7\u30B6\u30A4\u30F3",
4719
+ // French
4720
+ "musique",
4721
+ "art",
4722
+ "peinture",
4723
+ "dessin",
4724
+ "photographie"
4725
+ ],
4726
+ research: [
4727
+ // Vietnamese
4728
+ "nghi\xEAn c\u1EE9u",
4729
+ "th\u1ED1ng k\xEA",
4730
+ "ph\u01B0\u01A1ng ph\xE1p lu\u1EADn",
4731
+ "ph\xE2n t\xEDch d\u1EEF li\u1EC7u",
4732
+ "\u0111\u1ECBnh t\xEDnh",
4733
+ "\u0111\u1ECBnh l\u01B0\u1EE3ng",
4734
+ "gi\u1EA3 thuy\u1EBFt",
4735
+ "m\u1EABu",
4736
+ "t\u1ED5ng th\u1EC3",
4737
+ "t\u01B0\u01A1ng quan",
4738
+ "h\u1ED3i quy",
4739
+ "t\xE0i li\u1EC7u tham kh\u1EA3o",
4740
+ "tr\xEDch d\u1EABn",
4741
+ "t\xF3m t\u1EAFt",
4742
+ // English
4743
+ "research",
4744
+ "survey",
4745
+ "methodology",
4746
+ "data analysis",
4747
+ "qualitative",
4748
+ "quantitative",
4749
+ "hypothesis",
4750
+ "experiment",
4751
+ "sample",
4752
+ "population",
4753
+ "correlation",
4754
+ "regression",
4755
+ "literature review",
4756
+ "citation",
4757
+ "abstract",
4758
+ // Chinese
4759
+ "\u7814\u7A76",
4760
+ "\u8C03\u67E5",
4761
+ "\u65B9\u6CD5\u8BBA",
4762
+ "\u6570\u636E\u5206\u6790",
4763
+ // Japanese
4764
+ "\u7814\u7A76",
4765
+ "\u8ABF\u67FB",
4766
+ "\u65B9\u6CD5\u8AD6",
4767
+ "\u30C7\u30FC\u30BF\u5206\u6790",
4768
+ // French
4769
+ "recherche",
4770
+ "enqu\xEAte",
4771
+ "m\xE9thodologie",
4772
+ "analyse de donn\xE9es"
4773
+ ],
4774
+ business: [
4775
+ // Vietnamese
4776
+ "kinh doanh",
4777
+ "t\xE0i ch\xEDnh",
4778
+ "qu\u1EA3n l\xFD",
4779
+ "marketing",
4780
+ "b\xE1n h\xE0ng",
4781
+ "kh\u1EDFi nghi\u1EC7p",
4782
+ "k\u1EBF ho\u1EA1ch kinh doanh",
4783
+ "doanh thu",
4784
+ "l\u1EE3i nhu\u1EADn",
4785
+ "qu\u1EA3n tr\u1ECB",
4786
+ "t\xE0i kho\u1EA3n",
4787
+ "\u0111\u1EA7u t\u01B0",
4788
+ "ng\xE2n s\xE1ch",
4789
+ // English
4790
+ "business",
4791
+ "finance",
4792
+ "accounting",
4793
+ "investment",
4794
+ "budget",
4795
+ "startup",
4796
+ "revenue",
4797
+ "profit",
4798
+ "roi",
4799
+ "sales",
4800
+ "product management",
4801
+ "agile",
4802
+ "scrum",
4803
+ "kanban",
4804
+ // Chinese
4805
+ "\u5546\u4E1A",
4806
+ "\u91D1\u878D",
4807
+ "\u4F1A\u8BA1",
4808
+ "\u6295\u8D44",
4809
+ "\u8425\u9500",
4810
+ // Japanese
4811
+ "\u30D3\u30B8\u30CD\u30B9",
4812
+ "\u91D1\u878D",
4813
+ "\u4F1A\u8A08",
4814
+ "\u6295\u8CC7",
4815
+ "\u30DE\u30FC\u30B1\u30C6\u30A3\u30F3\u30B0",
4816
+ // French
4817
+ "affaires",
4818
+ "finance",
4819
+ "comptabilit\xE9",
4820
+ "investissement"
4821
+ ],
4822
+ other: []
4823
+ };
4824
+ var EXTENSION_DOMAIN = {
4825
+ ".swift": "software",
4826
+ ".ts": "software",
4827
+ ".tsx": "software",
4828
+ ".js": "software",
4829
+ ".jsx": "software",
4830
+ ".py": "software",
4831
+ ".kt": "software",
4832
+ ".kts": "software",
4833
+ ".java": "software",
4834
+ ".cs": "software",
4835
+ ".go": "software",
4836
+ ".rs": "software",
4837
+ ".rb": "software",
4838
+ ".php": "software",
4839
+ ".lua": "software",
4840
+ ".r": "software",
4841
+ // .ino is ALWAYS hardware
4842
+ ".ino": "hardware_iot",
4843
+ // .cpp/.c/.h are CONTEXT-DEPENDENT → resolve in detection logic
4844
+ ".stl": "3d_design",
4845
+ ".obj": "3d_design",
4846
+ ".step": "3d_design",
4847
+ ".stp": "3d_design",
4848
+ ".tc3": "3d_design",
4849
+ ".f3d": "3d_design"
4850
+ };
4851
+ var AMBIGUOUS_EXTENSIONS = /* @__PURE__ */ new Set([".cpp", ".c", ".h", ".hpp"]);
4852
+ var LANG_PATTERNS = [
4853
+ { lang: "vi", patterns: [/[ạảãàáạảãạ]/i, /\bgiải\b/, /\btính\b/] },
4854
+ { lang: "zh", patterns: [/[\u4e00-\u9fff]{2,}/] },
4855
+ { lang: "ja", patterns: [/[\u3040-\u309f\u30a0-\u30ff]{2,}/] },
4856
+ { lang: "ko", patterns: [/[\uac00-\ud7af]{2,}/] },
4857
+ { lang: "fr", patterns: [/\b(la|le|les|des|une|est|sont|avec|pour|dans)\b/i] }
4858
+ ];
4859
+ function detectSyllabusLanguage(text) {
4860
+ for (const { lang, patterns } of LANG_PATTERNS) {
4861
+ if (patterns.some((p) => p.test(text))) return lang;
4862
+ }
4863
+ return "en";
4864
+ }
4865
+ function detectDomain(options) {
4866
+ const evidence = [];
4867
+ const detectedTechStack = [];
4868
+ let detectedSubject = "";
4869
+ let ambiguous;
4870
+ const scores = {
4871
+ software: 0,
4872
+ hardware_iot: 0,
4873
+ "3d_design": 0,
4874
+ math: 0,
4875
+ science: 0,
4876
+ language: 0,
4877
+ arts: 0,
4878
+ research: 0,
4879
+ business: 0,
4880
+ other: 0
4881
+ };
4882
+ const hasHardwareContext = !!(options.techStack?.toLowerCase().match(/arduino|esp32|raspberry|embedded|iot/) || options.description?.toLowerCase().match(/arduino|robot|sensor|motor|circuit/) || options.goal?.toLowerCase().match(/arduino|robot|sensor|motor|circuit/));
4883
+ const hasSoftwareContext = !!(options.techStack?.toLowerCase().match(/swift|react|vue|angular|python|java|typescript|javascript/) || options.description?.toLowerCase().match(/app|web|api|frontend|backend|mobile/) || options.goal?.toLowerCase().match(/app|web|api|frontend|backend|mobile/));
4884
+ if (options.fileExtensions) {
4885
+ for (const ext of options.fileExtensions) {
4886
+ const extLower = ext.toLowerCase();
4887
+ if (extLower === ".ino") {
4888
+ scores.hardware_iot += 0.5;
4889
+ evidence.push(`File extension ${ext} \u2192 hardware_iot (always)`);
4890
+ continue;
4891
+ }
4892
+ if (AMBIGUOUS_EXTENSIONS.has(extLower)) {
4893
+ if (hasHardwareContext && !hasSoftwareContext) {
4894
+ scores.hardware_iot += 0.3;
4895
+ evidence.push(`File extension ${ext} \u2192 hardware_iot (hardware context)`);
4896
+ } else if (hasSoftwareContext && !hasHardwareContext) {
4897
+ scores.software += 0.3;
4898
+ evidence.push(`File extension ${ext} \u2192 software (software context)`);
4899
+ } else if (hasHardwareContext && hasSoftwareContext) {
4900
+ scores.software += 0.15;
4901
+ scores.hardware_iot += 0.15;
4902
+ evidence.push(`File extension ${ext} \u2192 ambiguous (both software and hardware context)`);
4903
+ ambiguous = {
4904
+ domain1: "software",
4905
+ domain2: "hardware_iot",
4906
+ reason: `.cpp/.c/.h files present with both software and hardware context keywords`
4907
+ };
4908
+ } else {
4909
+ scores.software += 0.2;
4910
+ evidence.push(`File extension ${ext} \u2192 software (default, no context)`);
4911
+ }
4912
+ continue;
4913
+ }
4914
+ const domain = EXTENSION_DOMAIN[extLower];
4915
+ if (domain) {
4916
+ scores[domain] += 0.3;
4917
+ evidence.push(`File extension ${ext} \u2192 ${domain}`);
4918
+ }
4919
+ }
4920
+ }
4921
+ const textContent = [
4922
+ options.description || "",
4923
+ options.goal || "",
4924
+ options.syllabusText || ""
4925
+ ].join(" ").toLowerCase();
4926
+ for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
4927
+ const cat = domain;
4928
+ for (const kw of keywords) {
4929
+ if (textContent.includes(kw.toLowerCase())) {
4930
+ scores[cat] += 0.1;
4931
+ if (!evidence.some((e) => e.includes(`"${kw}"`))) {
4932
+ evidence.push(`Keyword "${kw}" \u2192 ${cat}`);
4933
+ }
4934
+ }
4935
+ }
4936
+ }
4937
+ if (options.techStack) {
4938
+ const techItems = options.techStack.split(",").map((t) => t.trim());
4939
+ for (const tech of techItems) {
4940
+ const techL = tech.toLowerCase();
4941
+ if (["swift", "swiftui", "uikit", "ios"].some((k) => techL.includes(k))) {
4942
+ scores.software += 0.4;
4943
+ detectedTechStack.push(tech);
4944
+ } else if (["react", "vue", "angular", "next", "nuxt", "typescript", "javascript"].some((k) => techL.includes(k))) {
4945
+ scores.software += 0.4;
4946
+ detectedTechStack.push(tech);
4947
+ } else if (["python", "django", "flask"].some((k) => techL.includes(k))) {
4948
+ scores.software += 0.3;
4949
+ detectedTechStack.push(tech);
4950
+ } else if (["arduino", "esp32", "raspberry"].some((k) => techL.includes(k))) {
4951
+ scores.hardware_iot += 0.5;
4952
+ detectedTechStack.push(tech);
4953
+ } else if (["tinkercad", "fusion360", "blender"].some((k) => techL.includes(k))) {
4954
+ scores["3d_design"] += 0.5;
4955
+ detectedTechStack.push(tech);
4956
+ } else if (["c++", "c", "cpp"].some((k) => techL === k || techL.endsWith(k))) {
4957
+ if (hasHardwareContext) {
4958
+ scores.hardware_iot += 0.3;
4959
+ } else {
4960
+ scores.software += 0.3;
4961
+ }
4962
+ detectedTechStack.push(tech);
4963
+ }
4964
+ }
4965
+ }
4966
+ if (options.syllabusText) {
4967
+ const syllabusLang = detectSyllabusLanguage(options.syllabusText);
4968
+ const syllabusLower = options.syllabusText.toLowerCase();
4969
+ if (syllabusLower.includes("h\xECnh h\u1ECDc") || syllabusLower.includes("\u0111\u1EA1i s\u1ED1") || syllabusLower.includes("geometry") || syllabusLower.includes("algebra") || syllabusLower.includes("\u51E0\u4F55") || syllabusLower.includes("\u4EE3\u6570") || syllabusLower.includes("\u5E7E\u4F55") || syllabusLower.includes("\u4EE3\u6570") || syllabusLower.includes("g\xE9om\xE9trie") || syllabusLower.includes("alg\xE8bre")) {
4970
+ scores.math += 0.5;
4971
+ detectedSubject = "Mathematics";
4972
+ } else if (syllabusLower.includes("v\u1EADt l\xFD") || syllabusLower.includes("h\xF3a") || syllabusLower.includes("sinh") || syllabusLower.includes("physics") || syllabusLower.includes("chemistry") || syllabusLower.includes("biology") || syllabusLower.includes("\u7269\u7406") || syllabusLower.includes("\u5316\u5B66") || syllabusLower.includes("\u7269\u7406") || syllabusLower.includes("\u5316\u5B66") || syllabusLower.includes("physique") || syllabusLower.includes("chimie")) {
4973
+ scores.science += 0.5;
4974
+ detectedSubject = "Natural Science";
4975
+ } else if (syllabusLower.includes("ti\u1EBFng anh") || syllabusLower.includes("english") || syllabusLower.includes("grammar") || syllabusLower.includes("\u82F1\u8BED") || syllabusLower.includes("\u82F1\u8A9E") || syllabusLower.includes("anglais")) {
4976
+ scores.language += 0.5;
4977
+ detectedSubject = "English Language";
4978
+ }
4979
+ evidence.push(`Syllabus language detected: ${syllabusLang}`);
4980
+ }
4981
+ const entries = Object.entries(scores).filter(([k]) => k !== "other");
4982
+ entries.sort((a, b) => b[1] - a[1]);
4983
+ const [winner, winnerScore] = entries[0];
4984
+ if (winnerScore === 0) {
4985
+ return {
4986
+ domainCategory: "other",
4987
+ confidence: 0.2,
4988
+ evidence: [...evidence, "No domain signals detected"],
4989
+ detectedTechStack,
4990
+ detectedSubject
4991
+ };
4992
+ }
4993
+ const second = entries[1];
4994
+ if (second && winnerScore - second[1] < 0.15 && winnerScore > 0) {
4995
+ ambiguous = {
4996
+ domain1: winner,
4997
+ domain2: second[0],
4998
+ reason: `Scores very close: ${winner}=${winnerScore.toFixed(2)}, ${second[0]}=${second[1].toFixed(2)}`
4999
+ };
5000
+ }
5001
+ const totalScore = entries.reduce((sum, [, s]) => sum + s, 0);
5002
+ const confidence = totalScore > 0 ? Math.min(winnerScore / totalScore + 0.3, 1) : 0.3;
5003
+ return {
5004
+ domainCategory: winner,
5005
+ confidence,
5006
+ evidence: evidence.slice(0, 20),
5007
+ detectedTechStack,
5008
+ detectedSubject,
5009
+ ambiguous
5010
+ };
5011
+ }
5012
+
5013
+ // src/detector/learningModeDetector.ts
5014
+ var BUILD_VERBS = [
5015
+ "build",
5016
+ "create",
5017
+ "develop",
5018
+ "implement",
5019
+ "construct",
5020
+ "make",
5021
+ "x\xE2y d\u1EF1ng",
5022
+ "t\u1EA1o",
5023
+ "ph\xE1t tri\u1EC3n",
5024
+ "tri\u1EC3n khai",
5025
+ "l\u1EAFp r\xE1p",
5026
+ "code",
5027
+ "program",
5028
+ "design a",
5029
+ "set up",
5030
+ "deploy"
5031
+ ];
5032
+ var UNDERSTAND_VERBS = [
5033
+ "understand",
5034
+ "learn",
5035
+ "grasp",
5036
+ "comprehend",
5037
+ "master",
5038
+ "hi\u1EC3u",
5039
+ "n\u1EAFm v\u1EEFng",
5040
+ "h\u1ECDc",
5041
+ "ti\u1EBFp thu",
5042
+ "am hi\u1EC3u",
5043
+ "v\u1EADn d\u1EE5ng",
5044
+ "ph\xE2n t\xEDch",
5045
+ "\u0111\xE1nh gi\xE1",
5046
+ "so s\xE1nh",
5047
+ "explain",
5048
+ "describe",
5049
+ "analyze",
5050
+ "evaluate",
5051
+ "compare"
5052
+ ];
5053
+ var CONCEPT_DOMAINS = ["math", "science", "language", "arts", "research", "business"];
5054
+ var PRODUCT_DOMAINS = ["software", "hardware_iot", "3d_design"];
5055
+ function detectLearningMode(options) {
5056
+ const evidence = [];
5057
+ const text = [options.goal || "", options.description || ""].join(" ").toLowerCase();
5058
+ let productScore = 0;
5059
+ let conceptScore = 0;
5060
+ if (options.hasRepo) {
5061
+ productScore += 0.3;
5062
+ evidence.push("Repository present \u2192 product signal");
5063
+ }
5064
+ if (options.hasSyllabus) {
5065
+ conceptScore += 0.3;
5066
+ evidence.push("Syllabus present \u2192 concept signal");
5067
+ }
5068
+ if (PRODUCT_DOMAINS.includes(options.domainCategory)) {
5069
+ productScore += 0.2;
5070
+ evidence.push(`Domain ${options.domainCategory} \u2192 product\u503E\u5411`);
5071
+ }
5072
+ if (CONCEPT_DOMAINS.includes(options.domainCategory)) {
5073
+ conceptScore += 0.2;
5074
+ evidence.push(`Domain ${options.domainCategory} \u2192 concept\u503E\u5411`);
5075
+ }
5076
+ const hasBuildVerbs = BUILD_VERBS.some((v) => text.includes(v));
5077
+ const hasUnderstandVerbs = UNDERSTAND_VERBS.some((v) => text.includes(v));
5078
+ if (hasBuildVerbs) {
5079
+ productScore += 0.3;
5080
+ evidence.push("Build verbs detected in goal/description");
5081
+ }
5082
+ if (hasUnderstandVerbs) {
5083
+ conceptScore += 0.3;
5084
+ evidence.push("Understand verbs detected in goal/description");
5085
+ }
5086
+ const total = productScore + conceptScore;
5087
+ if (total === 0) {
5088
+ return {
5089
+ learningMode: "product",
5090
+ confidence: 0.3,
5091
+ evidence: [...evidence, "No signals \u2014 defaulting to product"]
5092
+ };
5093
+ }
5094
+ if (productScore >= 0.3 && conceptScore >= 0.3) {
5095
+ const hybridConf = Math.min((productScore + conceptScore) / 2, 1);
5096
+ return {
5097
+ learningMode: "hybrid",
5098
+ confidence: hybridConf,
5099
+ evidence: [...evidence, `Mixed signals: product=${productScore.toFixed(2)}, concept=${conceptScore.toFixed(2)} \u2192 hybrid`]
5100
+ };
5101
+ }
5102
+ if (productScore > conceptScore) {
5103
+ const raw2 = total > 0 ? productScore / total : 0.5;
5104
+ return {
5105
+ learningMode: "product",
5106
+ confidence: Math.min(Math.max(raw2, 0) + 0.2, 1),
5107
+ evidence
5108
+ };
5109
+ }
5110
+ const raw = total > 0 ? conceptScore / total : 0.5;
5111
+ return {
5112
+ learningMode: "concept",
5113
+ confidence: Math.min(Math.max(raw, 0) + 0.2, 1),
5114
+ evidence
5115
+ };
5116
+ }
5117
+
5118
+ // src/detector/domainProfileDetector.ts
5119
+ function detectDomainProfile(options) {
5120
+ const effectiveSyllabus = options.syllabusText || options.parsedSyllabus?.rawText || "";
5121
+ const inputResult = detectInputType({
5122
+ ...options,
5123
+ syllabusText: effectiveSyllabus
5124
+ });
5125
+ const domainResult = detectDomain({
5126
+ description: options.description,
5127
+ goal: options.goal,
5128
+ syllabusText: effectiveSyllabus,
5129
+ techStack: options.techStack,
5130
+ fileExtensions: options.fileExtensions,
5131
+ repoUrl: options.repoUrl
5132
+ });
5133
+ const modeResult = detectLearningMode({
5134
+ inputType: inputResult.inputType,
5135
+ domainCategory: domainResult.domainCategory,
5136
+ goal: options.goal,
5137
+ description: options.description,
5138
+ hasRepo: !!(options.repoUrl || options.repoDir),
5139
+ hasSyllabus: !!(effectiveSyllabus || options.chapters && options.chapters.length > 0 || options.parsedSyllabus)
5140
+ });
5141
+ const graphType = determineGraphType(modeResult.learningMode);
5142
+ return {
5143
+ inputType: inputResult.inputType,
5144
+ domainCategory: domainResult.domainCategory === "other" && options.parsedSyllabus?.subject.domain ? options.parsedSyllabus.subject.domain : domainResult.domainCategory,
5145
+ learningMode: modeResult.learningMode,
5146
+ graphType,
5147
+ confidence: {
5148
+ inputType: inputResult.confidence,
5149
+ domainCategory: domainResult.confidence,
5150
+ learningMode: modeResult.confidence
5151
+ },
5152
+ evidence: [...inputResult.evidence, ...domainResult.evidence, ...modeResult.evidence],
5153
+ detectedTechStack: domainResult.detectedTechStack,
5154
+ detectedSubject: options.parsedSyllabus?.subject.title || domainResult.detectedSubject,
5155
+ detectedDomain: domainResult.domainCategory,
5156
+ goal: options.goal || "",
5157
+ description: options.description || "",
5158
+ repoUrl: options.repoUrl || "",
5159
+ repoDir: options.repoDir || "",
5160
+ syllabusText: effectiveSyllabus
5161
+ };
5162
+ }
5163
+ function determineGraphType(learningMode) {
5164
+ switch (learningMode) {
5165
+ case "product":
5166
+ return "project_graph";
5167
+ case "concept":
5168
+ return "knowledge_graph";
5169
+ case "hybrid":
5170
+ return "hybrid_graph";
5171
+ default:
5172
+ return "project_graph";
5173
+ }
5174
+ }
5175
+
5176
+ // src/pipeline/domainRouter.ts
5177
+ async function runDomainPipeline(options) {
5178
+ const warnings = [];
5179
+ const log = (step, msg) => {
5180
+ console.log(`[ROUTER-${step}] ${msg}`);
5181
+ options.onProgress?.(step, msg);
5182
+ };
5183
+ log("LAYER_1", "Detecting domain profile...");
5184
+ const domainProfile = detectDomainProfile(options);
5185
+ const effectiveMode = options.forcedLearningMode || domainProfile.learningMode;
5186
+ log("LAYER_2", `Routing to pipeline for learning mode: ${effectiveMode} (graphType: ${domainProfile.graphType})`);
5187
+ if (effectiveMode === "concept") {
5188
+ log("LAYER_3", "Executing Knowledge Graph Pipeline (Concept-driven)...");
5189
+ const knowledgeResult = await runKnowledgeGraphPipeline({
5190
+ subject: options.subject || domainProfile.detectedSubject || options.goal || options.description || "General Subject",
5191
+ description: options.description,
5192
+ syllabusText: options.syllabusText,
5193
+ parsedSyllabus: options.parsedSyllabus,
5194
+ gradeBand: options.gradeBand,
5195
+ domain: domainProfile.domainCategory,
5196
+ techStack: options.techStack || domainProfile.detectedTechStack?.[0],
5197
+ llmConfig: options.llmConfig,
5198
+ onProgress: options.onProgress,
5199
+ knowledgeTreePath: options.knowledgeTreePath,
5200
+ proposalsPath: options.proposalsPath
5201
+ });
5202
+ warnings.push(...knowledgeResult.warnings);
5203
+ return {
5204
+ domainProfile,
5205
+ graphType: "knowledge_graph",
5206
+ curriculumFeed: knowledgeResult.curriculumFeed,
5207
+ knowledgeResult,
5208
+ warnings
5209
+ };
5210
+ }
5211
+ if (effectiveMode === "hybrid") {
5212
+ log("LAYER_3", "Executing Hybrid Graph Pipeline...");
5213
+ if (options.repoDir) {
5214
+ const projectResult2 = await runProjectGraphPipeline({
5215
+ repoDir: options.repoDir,
5216
+ goal: options.goal || "Build complete project",
5217
+ techStack: options.techStack || "typescript",
5218
+ llmConfig: options.llmConfig,
5219
+ embeddings: options.embeddings,
5220
+ onProgress: options.onProgress
5221
+ });
5222
+ const knowledgeResult = await runKnowledgeGraphPipeline({
5223
+ subject: options.subject || domainProfile.detectedSubject || options.goal || "Subject Theory",
5224
+ description: options.description,
5225
+ syllabusText: options.syllabusText,
5226
+ parsedSyllabus: options.parsedSyllabus,
5227
+ domain: domainProfile.domainCategory,
5228
+ techStack: options.techStack,
5229
+ llmConfig: options.llmConfig,
5230
+ onProgress: options.onProgress
5231
+ });
5232
+ const hybridResult = await runHybridGraphPipeline({
5233
+ projectGraph: projectResult2.projectGraph,
5234
+ knowledgeGraph: knowledgeResult.knowledgeGraph,
5235
+ llmConfig: options.llmConfig,
5236
+ onProgress: options.onProgress
5237
+ });
5238
+ warnings.push(...hybridResult.warnings);
5239
+ return {
5240
+ domainProfile,
5241
+ graphType: "hybrid_graph",
5242
+ curriculumFeed: hybridResult.curriculumFeed,
5243
+ projectResult: projectResult2,
5244
+ knowledgeResult,
5245
+ hybridResult,
5246
+ warnings
5247
+ };
5248
+ } else {
5249
+ warnings.push("Hybrid learning mode requested but repoDir was not provided \u2014 falling back to concept-driven knowledge graph pipeline");
5250
+ const knowledgeResult = await runKnowledgeGraphPipeline({
5251
+ subject: options.subject || domainProfile.detectedSubject || options.goal || "Subject Theory",
5252
+ description: options.description,
5253
+ syllabusText: options.syllabusText,
5254
+ parsedSyllabus: options.parsedSyllabus,
5255
+ domain: domainProfile.domainCategory,
5256
+ techStack: options.techStack,
5257
+ llmConfig: options.llmConfig,
5258
+ onProgress: options.onProgress
5259
+ });
5260
+ warnings.push(...knowledgeResult.warnings);
5261
+ return {
5262
+ domainProfile,
5263
+ graphType: "knowledge_graph",
5264
+ curriculumFeed: knowledgeResult.curriculumFeed,
5265
+ knowledgeResult,
5266
+ warnings
5267
+ };
5268
+ }
5269
+ }
5270
+ log("LAYER_3", "Executing Project Graph Pipeline (Product-driven)...");
5271
+ if (!options.repoDir) {
5272
+ throw new Error("repoDir is required for product-driven project graph pipeline");
5273
+ }
5274
+ const projectResult = await runProjectGraphPipeline({
5275
+ repoDir: options.repoDir,
5276
+ goal: options.goal || "Build complete project",
5277
+ techStack: options.techStack || domainProfile.detectedTechStack?.[0] || "typescript",
5278
+ llmConfig: options.llmConfig,
5279
+ embeddings: options.embeddings,
5280
+ onProgress: options.onProgress
5281
+ });
5282
+ const { emitCurriculumFeed: emitCurriculumFeed2 } = await Promise.resolve().then(() => (init_curriculumFeedEmitter(), curriculumFeedEmitter_exports));
5283
+ const curriculumFeed = emitCurriculumFeed2(projectResult.projectGraph, {
5284
+ warnings
5285
+ });
5286
+ return {
5287
+ domainProfile,
5288
+ graphType: "project_graph",
5289
+ curriculumFeed,
5290
+ projectResult,
5291
+ warnings
5292
+ };
5293
+ }
5294
+
5295
+ exports.runDomainPipeline = runDomainPipeline;
5296
+ exports.runHybridGraphPipeline = runHybridGraphPipeline;
5297
+ exports.runKnowledgeGraphPipeline = runKnowledgeGraphPipeline;
2164
5298
  exports.runProjectGraphPipeline = runProjectGraphPipeline;
2165
5299
  //# sourceMappingURL=index.cjs.map
2166
5300
  //# sourceMappingURL=index.cjs.map