@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
package/dist/index.cjs CHANGED
@@ -6,7 +6,492 @@ var os = require('os');
6
6
  var path = require('path');
7
7
  var child_process = require('child_process');
8
8
 
9
- // src/schemas/projectGraphSchema.ts
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 = exports.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
+ exports.FeedKeywordsSchema = void 0; exports.FeedScaffoldCandidateSchema = void 0; exports.FeedNodeSchema = void 0; exports.FeedEdgeSchema = void 0; exports.FeedGroupingSchema = void 0; exports.FeedPhaseSchema = void 0; exports.CurriculumFeedSchema = void 0;
45
+ var init_curriculumFeedSchema = __esm({
46
+ "src/schemas/curriculumFeedSchema.ts"() {
47
+ exports.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
+ exports.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
+ exports.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: exports.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(exports.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
+ exports.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
+ exports.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
+ exports.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
+ exports.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(exports.FeedNodeSchema).min(1, "curriculum feed must contain at least one learning node"),
108
+ dependency_edges: zod.z.array(exports.FeedEdgeSchema).default([]),
109
+ suggested_groupings: zod.z.array(exports.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(exports.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
+ });
10
495
  var StepEffortSchema = zod.z.object({
11
496
  estimated_minutes: zod.z.number().optional(),
12
497
  complexity: zod.z.enum(["low", "medium", "high"]).optional(),
@@ -378,6 +863,51 @@ var ThreeDesignTechStackExtensionSchema = zod.z.object({
378
863
  canvas_size: zod.z.string().optional(),
379
864
  export_formats: zod.z.array(zod.z.string()).optional().default([]).describe("STL, OBJ, STEP")
380
865
  });
866
+ var MathTheoremSchema = zod.z.object({
867
+ name: zod.z.string(),
868
+ statement: zod.z.string(),
869
+ proof_outline: zod.z.string().optional(),
870
+ converse_statement: zod.z.string().optional()
871
+ });
872
+ var MathConceptExtensionSchema = zod.z.object({
873
+ formulas: zod.z.array(zod.z.string()).optional().default([]).describe("Mathematical formulas, e.g. a^2 + b^2 = c^2"),
874
+ theorems: zod.z.array(MathTheoremSchema).optional().default([]),
875
+ constructions: zod.z.array(zod.z.string()).optional().default([]).describe("Compass and ruler constructions, auxiliary lines"),
876
+ visual_aids: zod.z.array(zod.z.string()).optional().default([]).describe("Geometric figures, function graphs"),
877
+ common_mistakes: zod.z.array(zod.z.string()).optional().default([]).describe("False intuitions or algebraic traps")
878
+ });
879
+ var ExperimentProcedureSchema = zod.z.object({
880
+ name: zod.z.string(),
881
+ apparatus: zod.z.array(zod.z.string()).optional().default([]),
882
+ steps: zod.z.array(zod.z.string()).optional().default([]),
883
+ safety_warnings: zod.z.array(zod.z.string()).optional().default([]),
884
+ expected_observation: zod.z.string().optional()
885
+ });
886
+ var ScienceConceptExtensionSchema = zod.z.object({
887
+ experiments: zod.z.array(ExperimentProcedureSchema).optional().default([]),
888
+ instruments: zod.z.array(zod.z.string()).optional().default([]).describe("Lab equipment: microscope, beaker, sensor"),
889
+ variables: zod.z.object({
890
+ independent: zod.z.string().optional(),
891
+ dependent: zod.z.string().optional(),
892
+ controlled: zod.z.array(zod.z.string()).optional().default([])
893
+ }).optional(),
894
+ hypotheses: zod.z.array(zod.z.string()).optional().default([]),
895
+ laws_and_theories: zod.z.array(zod.z.string()).optional().default([])
896
+ });
897
+ var GrammarRuleSchema = zod.z.object({
898
+ rule_name: zod.z.string(),
899
+ pattern: zod.z.string(),
900
+ example_correct: zod.z.string(),
901
+ example_incorrect: zod.z.string().optional(),
902
+ exceptions: zod.z.array(zod.z.string()).optional().default([])
903
+ });
904
+ var LanguageConceptExtensionSchema = zod.z.object({
905
+ reading_genres: zod.z.array(zod.z.string()).optional().default([]).describe("argumentative, narrative, expository"),
906
+ grammar_rules: zod.z.array(GrammarRuleSchema).optional().default([]),
907
+ writing_tasks: zod.z.array(zod.z.string()).optional().default([]),
908
+ lexicon_keywords: zod.z.array(zod.z.string()).optional().default([]),
909
+ rhetorical_devices: zod.z.array(zod.z.string()).optional().default([])
910
+ });
381
911
  var ProjectConceptLinkSchema = zod.z.object({
382
912
  feature_id: zod.z.string().describe("Feature ID from project_graph"),
383
913
  concept_id: zod.z.string().describe("Concept ID from knowledge_graph"),
@@ -516,99 +1046,9 @@ var DomainProfileSchema = zod.z.object({
516
1046
  repoDir: zod.z.string().optional().default(""),
517
1047
  syllabusText: zod.z.string().optional().default("")
518
1048
  });
519
- var FeedKeywordsSchema = zod.z.object({
520
- all: zod.z.array(zod.z.string()).default([]),
521
- new: zod.z.array(zod.z.string()).default([]),
522
- prerequisite: zod.z.array(zod.z.string()).default([])
523
- });
524
- var FeedScaffoldCandidateSchema = zod.z.object({
525
- item: zod.z.string(),
526
- reason: zod.z.string().default(""),
527
- minutes_saved: zod.z.number().int().nonnegative().default(0),
528
- suggest: zod.z.enum(["provide_full", "provide_partial", "provide_reference"]).default("provide_full")
529
- });
530
- var FeedNodeSchema = zod.z.object({
531
- id: zod.z.string().min(1),
532
- kind: zod.z.enum(["skill", "concept", "product_step"]),
533
- name: zod.z.string().min(1),
534
- concept_codes: zod.z.array(zod.z.string()).default([]),
535
- keywords: FeedKeywordsSchema,
536
- estimated_minutes: zod.z.number().int().nonnegative(),
537
- bloom_hint: zod.z.enum(["Remember", "Understand", "Apply", "Analyze", "Evaluate", "Create"]).nullable().default(null),
538
- depth_hint: zod.z.enum(["ulo", "cio", "sio"]).nullable().default(null),
539
- completion_level: zod.z.enum(["mvp", "standard", "stretch"]).default("standard"),
540
- scaffolding: zod.z.object({
541
- scaffold_candidates: zod.z.array(FeedScaffoldCandidateSchema).default([]),
542
- core_value_minutes: zod.z.number().int().nonnegative().default(0)
543
- }).default({ scaffold_candidates: [], core_value_minutes: 0 }),
544
- references: zod.z.array(zod.z.object({ file: zod.z.string().min(1), evidence: zod.z.string().default("") })).default([]),
545
- feature_id: zod.z.string().nullable().default(null),
546
- concept_ref: zod.z.string().nullable().default(null),
547
- // Progressive-completion bookkeeping (feed v2, additive):
548
- // phase_id = development phase that introduces/uses this node;
549
- // introduce_aspect = how the phase teaches a concept node (first intro vs advanced revisit).
550
- phase_id: zod.z.string().default(""),
551
- introduce_aspect: zod.z.enum(["new", "advanced"]).nullable().default(null)
552
- });
553
- var FeedEdgeSchema = zod.z.object({
554
- from: zod.z.string().min(1),
555
- to: zod.z.string().min(1),
556
- kind: zod.z.enum(["knowledge", "task"]),
557
- reason: zod.z.string().default("")
558
- });
559
- var FeedGroupingSchema = zod.z.object({
560
- id: zod.z.string().min(1),
561
- name: zod.z.string().min(1),
562
- node_ids: zod.z.array(zod.z.string()).min(1),
563
- rationale: zod.z.string().default("")
564
- });
565
- var FeedPhaseSchema = zod.z.object({
566
- id: zod.z.string().min(1),
567
- order: zod.z.number().int().positive(),
568
- name: zod.z.string().min(1),
569
- product_completion: zod.z.string().min(1),
570
- node_ids: zod.z.array(zod.z.string()).min(1)
571
- });
572
- var CurriculumFeedSchema = zod.z.object({
573
- schema_version: zod.z.literal(2),
574
- source: zod.z.object({
575
- graph_type: zod.z.enum(["project_graph", "knowledge_graph", "hybrid_graph"]),
576
- warnings: zod.z.array(zod.z.string()).default([]),
577
- hallucination_count: zod.z.number().int().nonnegative().default(0)
578
- }),
579
- learning_nodes: zod.z.array(FeedNodeSchema).min(1, "curriculum feed must contain at least one learning node"),
580
- dependency_edges: zod.z.array(FeedEdgeSchema).default([]),
581
- suggested_groupings: zod.z.array(FeedGroupingSchema).default([]),
582
- // Progressive completion phases (cross-feature). When non-empty, these — not
583
- // suggested_groupings — define the teaching sequence and unit structure.
584
- phases: zod.z.array(FeedPhaseSchema).default([])
585
- });
586
- function validateCurriculumFeed(feed) {
587
- const parsed = CurriculumFeedSchema.parse(feed);
588
- const ids = new Set(parsed.learning_nodes.map((n) => n.id));
589
- if (ids.size !== parsed.learning_nodes.length) {
590
- throw new Error("curriculum feed contains duplicate node ids");
591
- }
592
- for (const e of parsed.dependency_edges) {
593
- if (!ids.has(e.from) || !ids.has(e.to)) {
594
- throw new Error("curriculum feed edge " + e.from + " -> " + e.to + " references unknown node");
595
- }
596
- if (e.from === e.to) throw new Error("curriculum feed self-loop edge on " + e.from);
597
- }
598
- for (const g of parsed.suggested_groupings) {
599
- for (const id of g.node_ids) {
600
- if (!ids.has(id)) throw new Error("grouping " + g.id + " references unknown node " + id);
601
- }
602
- }
603
- for (const ph of parsed.phases) {
604
- for (const id of ph.node_ids) {
605
- if (!ids.has(id)) throw new Error("phase " + ph.id + " references unknown node " + id);
606
- }
607
- }
608
- const phaseIds = parsed.phases.map((p) => p.id);
609
- if (new Set(phaseIds).size !== phaseIds.length) throw new Error("curriculum feed contains duplicate phase ids");
610
- return parsed;
611
- }
1049
+
1050
+ // src/schemas/index.ts
1051
+ init_curriculumFeedSchema();
612
1052
 
613
1053
  // src/parsers/swift.ts
614
1054
  var IMPORT_RE = /^import\s+(\w+)/gm;
@@ -921,6 +1361,190 @@ function mergeCppResults(results) {
921
1361
  return merged;
922
1362
  }
923
1363
 
1364
+ // src/parsers/syllabusParser.ts
1365
+ var STOP_WORDS = /* @__PURE__ */ new Set([
1366
+ "and",
1367
+ "the",
1368
+ "for",
1369
+ "with",
1370
+ "from",
1371
+ "that",
1372
+ "this",
1373
+ "into",
1374
+ "when",
1375
+ "while",
1376
+ "using",
1377
+ "their",
1378
+ "your",
1379
+ "each",
1380
+ "how",
1381
+ "why",
1382
+ "what",
1383
+ "between",
1384
+ "through",
1385
+ "about",
1386
+ "trong",
1387
+ "nhung",
1388
+ "cac",
1389
+ "cua",
1390
+ "cho",
1391
+ "voi",
1392
+ "duoc",
1393
+ "khong",
1394
+ "theo",
1395
+ "tren",
1396
+ "duoi",
1397
+ "va",
1398
+ "hoac",
1399
+ "thi",
1400
+ "la",
1401
+ "nhu"
1402
+ ]);
1403
+ function extractTokens(text) {
1404
+ const words = text.toLowerCase().replace(/[^a-z0-9à-ỹ\s-]/gi, " ").split(/\s+/).filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
1405
+ return [...new Set(words)];
1406
+ }
1407
+ function extractGradeBand(text) {
1408
+ 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);
1409
+ if (gbMatch && gbMatch[1] && gbMatch[2]) {
1410
+ const min = parseInt(gbMatch[1], 10);
1411
+ const max = parseInt(gbMatch[2], 10);
1412
+ return [Math.min(min, max), Math.max(min, max)];
1413
+ }
1414
+ return void 0;
1415
+ }
1416
+ function extractDomain(text) {
1417
+ const lower = text.toLowerCase();
1418
+ if (lower.includes("computer science") || lower.includes("l\u1EADp tr\xECnh") || lower.includes("algorithm") || lower.includes("software")) return "software";
1419
+ if (lower.includes("to\xE1n") || lower.includes("geometry") || lower.includes("algebra") || lower.includes("h\xECnh h\u1ECDc") || lower.includes("mathematics")) return "math";
1420
+ 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";
1421
+ 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";
1422
+ return void 0;
1423
+ }
1424
+ function parseSyllabus(rawText) {
1425
+ const lines = rawText.split(/\r?\n/);
1426
+ let subjectTitle = "";
1427
+ let subjectDesc = "";
1428
+ const units = [];
1429
+ const allTopics = [];
1430
+ const globalKeywordsSet = /* @__PURE__ */ new Set();
1431
+ let currentUnit = null;
1432
+ let currentTopic = null;
1433
+ let unitIndex = 0;
1434
+ let topicIndex = 0;
1435
+ for (let i = 0; i < lines.length; i++) {
1436
+ const line = lines[i].trim();
1437
+ const lineNum = i + 1;
1438
+ if (!line) continue;
1439
+ if (line.startsWith("# ") && !subjectTitle) {
1440
+ subjectTitle = line.replace(/^#\s+/, "").trim();
1441
+ continue;
1442
+ }
1443
+ if (!currentUnit) {
1444
+ if (line.toLowerCase().startsWith("m\xF4n h\u1ECDc:") || line.toLowerCase().startsWith("subject:")) {
1445
+ const val = line.split(":")[1]?.trim() || "";
1446
+ if (val && !subjectTitle) subjectTitle = val;
1447
+ }
1448
+ if (line.toLowerCase().startsWith("m\xF4 t\u1EA3:") || line.toLowerCase().startsWith("description:")) {
1449
+ subjectDesc = line.split(":")[1]?.trim() || "";
1450
+ }
1451
+ }
1452
+ const isUnitHeading = /^##\s+(?:chương|module|unit|phần|chủ đề)\s*(\d+|[ivx]+)?[:.-]?\s*(.*)/i.test(line) || /^##\s+([^#].*)/.test(line);
1453
+ if (isUnitHeading) {
1454
+ if (currentTopic) {
1455
+ currentTopic.rawLineEnd = lineNum - 1;
1456
+ currentTopic = null;
1457
+ }
1458
+ unitIndex += 1;
1459
+ const titleMatch = line.replace(/^##\s+/, "").trim();
1460
+ currentUnit = {
1461
+ id: `U${String(unitIndex).padStart(2, "0")}`,
1462
+ title: titleMatch,
1463
+ order: unitIndex,
1464
+ topics: []
1465
+ };
1466
+ units.push(currentUnit);
1467
+ continue;
1468
+ }
1469
+ 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);
1470
+ if (isTopicItem) {
1471
+ if (currentTopic) {
1472
+ currentTopic.rawLineEnd = lineNum - 1;
1473
+ }
1474
+ if (!currentUnit) {
1475
+ unitIndex += 1;
1476
+ currentUnit = {
1477
+ id: `U${String(unitIndex).padStart(2, "0")}`,
1478
+ title: subjectTitle || "General",
1479
+ order: unitIndex,
1480
+ topics: []
1481
+ };
1482
+ units.push(currentUnit);
1483
+ }
1484
+ topicIndex += 1;
1485
+ const rawName = (isTopicItem[3] || isTopicItem[2] || isTopicItem[1] || `Topic ${topicIndex}`).trim();
1486
+ currentTopic = {
1487
+ id: `T${String(topicIndex).padStart(2, "0")}`,
1488
+ title: rawName,
1489
+ unitId: currentUnit.id,
1490
+ unitTitle: currentUnit.title,
1491
+ description: "",
1492
+ learningObjectives: [],
1493
+ keywords: [],
1494
+ rawLineStart: lineNum,
1495
+ rawLineEnd: lineNum
1496
+ };
1497
+ currentUnit.topics.push(currentTopic);
1498
+ allTopics.push(currentTopic);
1499
+ continue;
1500
+ }
1501
+ if (currentTopic) {
1502
+ currentTopic.rawLineEnd = lineNum;
1503
+ const stripped = line.replace(/^[-*•\s]+/, "").trim();
1504
+ if (stripped.toLowerCase().startsWith("m\u1EE5c ti\xEAu:") || stripped.toLowerCase().startsWith("objectives:")) {
1505
+ const content = stripped.replace(/^(?:mục tiêu|objectives):\s*/i, "").trim();
1506
+ if (content) currentTopic.learningObjectives.push(content);
1507
+ continue;
1508
+ }
1509
+ if (stripped.toLowerCase().startsWith("t\u1EEB kh\xF3a:") || stripped.toLowerCase().startsWith("keywords:")) {
1510
+ const rawKw = stripped.replace(/^(?:từ khóa|keywords):\s*/i, "").trim();
1511
+ const tokens2 = rawKw.split(/[,;]+/).map((k) => k.trim()).filter(Boolean);
1512
+ for (const tok of tokens2) {
1513
+ currentTopic.keywords.push(tok);
1514
+ globalKeywordsSet.add(tok);
1515
+ }
1516
+ continue;
1517
+ }
1518
+ if (currentTopic.description) {
1519
+ currentTopic.description += " " + stripped;
1520
+ } else {
1521
+ currentTopic.description = stripped;
1522
+ }
1523
+ for (const tok of extractTokens(stripped)) {
1524
+ if (tok.length >= 4) globalKeywordsSet.add(tok);
1525
+ }
1526
+ }
1527
+ }
1528
+ if (currentTopic) {
1529
+ currentTopic.rawLineEnd = lines.length;
1530
+ }
1531
+ if (!subjectTitle && units.length > 0) {
1532
+ subjectTitle = units[0]?.title || "Untitled Subject";
1533
+ }
1534
+ return {
1535
+ subject: {
1536
+ title: subjectTitle || "Untitled Subject",
1537
+ domain: extractDomain(rawText),
1538
+ gradeBand: extractGradeBand(rawText),
1539
+ description: subjectDesc
1540
+ },
1541
+ units,
1542
+ allTopics,
1543
+ extractedKeywords: [...globalKeywordsSet],
1544
+ rawText
1545
+ };
1546
+ }
1547
+
924
1548
  // src/extractors/keywordExtractor.ts
925
1549
  var STDLIB_MODULES = /* @__PURE__ */ new Set([
926
1550
  // Python
@@ -2128,11 +2752,15 @@ function detectLearningMode(options) {
2128
2752
 
2129
2753
  // src/detector/domainProfileDetector.ts
2130
2754
  function detectDomainProfile(options) {
2131
- const inputResult = detectInputType(options);
2755
+ const effectiveSyllabus = options.syllabusText || options.parsedSyllabus?.rawText || "";
2756
+ const inputResult = detectInputType({
2757
+ ...options,
2758
+ syllabusText: effectiveSyllabus
2759
+ });
2132
2760
  const domainResult = detectDomain({
2133
2761
  description: options.description,
2134
2762
  goal: options.goal,
2135
- syllabusText: options.syllabusText,
2763
+ syllabusText: effectiveSyllabus,
2136
2764
  techStack: options.techStack,
2137
2765
  fileExtensions: options.fileExtensions,
2138
2766
  repoUrl: options.repoUrl
@@ -2143,12 +2771,12 @@ function detectDomainProfile(options) {
2143
2771
  goal: options.goal,
2144
2772
  description: options.description,
2145
2773
  hasRepo: !!(options.repoUrl || options.repoDir),
2146
- hasSyllabus: !!(options.syllabusText || options.chapters && options.chapters.length > 0)
2774
+ hasSyllabus: !!(effectiveSyllabus || options.chapters && options.chapters.length > 0 || options.parsedSyllabus)
2147
2775
  });
2148
2776
  const graphType = determineGraphType(modeResult.learningMode);
2149
2777
  return {
2150
2778
  inputType: inputResult.inputType,
2151
- domainCategory: domainResult.domainCategory,
2779
+ domainCategory: domainResult.domainCategory === "other" && options.parsedSyllabus?.subject.domain ? options.parsedSyllabus.subject.domain : domainResult.domainCategory,
2152
2780
  learningMode: modeResult.learningMode,
2153
2781
  graphType,
2154
2782
  confidence: {
@@ -2158,13 +2786,13 @@ function detectDomainProfile(options) {
2158
2786
  },
2159
2787
  evidence: [...inputResult.evidence, ...domainResult.evidence, ...modeResult.evidence],
2160
2788
  detectedTechStack: domainResult.detectedTechStack,
2161
- detectedSubject: domainResult.detectedSubject,
2789
+ detectedSubject: options.parsedSyllabus?.subject.title || domainResult.detectedSubject,
2162
2790
  detectedDomain: domainResult.domainCategory,
2163
2791
  goal: options.goal || "",
2164
2792
  description: options.description || "",
2165
2793
  repoUrl: options.repoUrl || "",
2166
2794
  repoDir: options.repoDir || "",
2167
- syllabusText: options.syllabusText || ""
2795
+ syllabusText: effectiveSyllabus
2168
2796
  };
2169
2797
  }
2170
2798
  function determineGraphType(learningMode) {
@@ -3144,6 +3772,161 @@ var ULO_CODE_RE = /^ULO-[A-Z][A-Z0-9_]*-\d{2}$/;
3144
3772
  var CIO_CODE_RE = /^CIO-[A-Z][A-Z0-9_]*-\d{2}-[A-Z][A-Z0-9_]*$/;
3145
3773
  var SIO_CODE_RE = /^SIO-[A-Z0-9]+-[A-Z][A-Z0-9_]*-\d{2}$/;
3146
3774
 
3775
+ // src/graph/knowledgeGraphVerifier.ts
3776
+ function detectPrerequisiteCycles(concepts) {
3777
+ const cycles = [];
3778
+ const visited = /* @__PURE__ */ new Set();
3779
+ const inStack = /* @__PURE__ */ new Set();
3780
+ const path = [];
3781
+ const prereqMap = /* @__PURE__ */ new Map();
3782
+ for (const c of concepts) {
3783
+ prereqMap.set(c.id, c.prerequisites || []);
3784
+ }
3785
+ function dfs(conceptId) {
3786
+ if (inStack.has(conceptId)) {
3787
+ const cycleStart = path.indexOf(conceptId);
3788
+ if (cycleStart >= 0) {
3789
+ cycles.push([...path.slice(cycleStart), conceptId]);
3790
+ }
3791
+ return;
3792
+ }
3793
+ if (visited.has(conceptId)) return;
3794
+ visited.add(conceptId);
3795
+ inStack.add(conceptId);
3796
+ path.push(conceptId);
3797
+ for (const prereq of prereqMap.get(conceptId) || []) {
3798
+ dfs(prereq);
3799
+ }
3800
+ path.pop();
3801
+ inStack.delete(conceptId);
3802
+ }
3803
+ for (const c of concepts) {
3804
+ if (!visited.has(c.id)) {
3805
+ dfs(c.id);
3806
+ }
3807
+ }
3808
+ return cycles;
3809
+ }
3810
+ function breakPrerequisiteCycles(concepts, cycles) {
3811
+ const removed = [];
3812
+ const fixed = concepts.map((c) => ({ ...c, prerequisites: [...c.prerequisites || []] }));
3813
+ for (const cycle of cycles) {
3814
+ if (cycle.length >= 2) {
3815
+ const breaker = cycle[cycle.length - 2];
3816
+ const target = cycle[cycle.length - 1];
3817
+ for (const concept of fixed) {
3818
+ if (concept.id === breaker && target) {
3819
+ concept.prerequisites = concept.prerequisites.filter((p) => p !== target);
3820
+ removed.push({ from: breaker, to: target });
3821
+ }
3822
+ }
3823
+ }
3824
+ }
3825
+ return { fixed, removed };
3826
+ }
3827
+ function auditSyllabusCoverage(concepts, syllabus) {
3828
+ if (!syllabus || syllabus.allTopics.length === 0) {
3829
+ return { coverageRatio: 1, totalTopics: 0, coveredTopics: 0, uncovered: [] };
3830
+ }
3831
+ const topics = syllabus.allTopics;
3832
+ const coveredSet = /* @__PURE__ */ new Set();
3833
+ for (const topic of topics) {
3834
+ const topicKeywords = /* @__PURE__ */ new Set([
3835
+ ...topic.title.toLowerCase().split(/\s+/),
3836
+ ...(topic.keywords || []).map((k) => k.toLowerCase())
3837
+ ]);
3838
+ for (const concept of concepts) {
3839
+ concept.name.toLowerCase();
3840
+ (concept.description || "").toLowerCase();
3841
+ (concept.keywords || []).map((k) => k.toLowerCase());
3842
+ if (concept.id.toLowerCase() === topic.id.toLowerCase()) {
3843
+ coveredSet.add(topic.id);
3844
+ break;
3845
+ }
3846
+ const refs = concept.references || [];
3847
+ const hasTopicRef = refs.some(
3848
+ (r) => r.id === topic.id || r.title && r.title.toLowerCase() === topic.title.toLowerCase()
3849
+ );
3850
+ if (hasTopicRef) {
3851
+ coveredSet.add(topic.id);
3852
+ break;
3853
+ }
3854
+ const conceptKeywords = (concept.keywords || []).map((k) => k.toLowerCase());
3855
+ const hasOverlap = conceptKeywords.some((k) => topicKeywords.has(k)) || concept.name.toLowerCase().includes(topic.title.toLowerCase()) || topic.title.toLowerCase().includes(concept.name.toLowerCase());
3856
+ if (hasOverlap) {
3857
+ coveredSet.add(topic.id);
3858
+ break;
3859
+ }
3860
+ }
3861
+ }
3862
+ const totalTopics = topics.length;
3863
+ const coveredTopics = coveredSet.size;
3864
+ const coverageRatio = totalTopics > 0 ? coveredTopics / totalTopics : 1;
3865
+ const uncovered = topics.filter((t) => !coveredSet.has(t.id)).map((t) => `${t.id}: ${t.title}`);
3866
+ return { coverageRatio, totalTopics, coveredTopics, uncovered };
3867
+ }
3868
+ function verifyKnowledgeGraph(kg, syllabusOrOptions) {
3869
+ const syllabus = syllabusOrOptions && "parsedSyllabus" in syllabusOrOptions ? syllabusOrOptions.parsedSyllabus : syllabusOrOptions;
3870
+ const warnings = [];
3871
+ let concepts = [...kg.concepts || []];
3872
+ const conceptIds = new Set(concepts.map((c) => c.id));
3873
+ for (const c of concepts) {
3874
+ if ((c.prerequisites || []).includes(c.id)) {
3875
+ c.prerequisites = (c.prerequisites || []).filter((p) => p !== c.id);
3876
+ warnings.push(`Self-loop removed on concept ${c.id}`);
3877
+ }
3878
+ }
3879
+ const danglingPrerequisites = [];
3880
+ for (const c of concepts) {
3881
+ const validPrereqs = [];
3882
+ for (const p of c.prerequisites || []) {
3883
+ if (!conceptIds.has(p)) {
3884
+ danglingPrerequisites.push({ conceptId: c.id, missingPrereqId: p });
3885
+ warnings.push(`Dangling prerequisite removed: ${c.id} -> ${p}`);
3886
+ } else {
3887
+ validPrereqs.push(p);
3888
+ }
3889
+ }
3890
+ c.prerequisites = validPrereqs;
3891
+ }
3892
+ const initialCycles = detectPrerequisiteCycles(concepts);
3893
+ let removedEdges = [];
3894
+ if (initialCycles.length > 0) {
3895
+ const broken = breakPrerequisiteCycles(concepts, initialCycles);
3896
+ concepts = broken.fixed;
3897
+ removedEdges = broken.removed;
3898
+ for (const r of removedEdges) {
3899
+ warnings.push(`Cycle broken: removed edge ${r.from} -> ${r.to}`);
3900
+ }
3901
+ }
3902
+ const coverage = auditSyllabusCoverage(concepts, syllabus);
3903
+ if (coverage.coverageRatio < 1) {
3904
+ warnings.push(`Syllabus coverage ${(coverage.coverageRatio * 100).toFixed(1)}% (${coverage.coveredTopics}/${coverage.totalTopics} topics). Uncovered: ${coverage.uncovered.join(", ")}`);
3905
+ }
3906
+ for (const c of concepts) {
3907
+ if (!c.ulo) warnings.push(`Concept ${c.id} missing ULO (WHAT + WHY)`);
3908
+ if (!c.cio) warnings.push(`Concept ${c.id} missing CIO (HOW mechanism)`);
3909
+ if (!c.sio || c.sio.length === 0) warnings.push(`Concept ${c.id} missing SIO implementation items`);
3910
+ }
3911
+ const valid = initialCycles.length === 0 && danglingPrerequisites.length === 0 && coverage.coverageRatio >= 0.8;
3912
+ const verifiedGraph = {
3913
+ ...kg,
3914
+ concepts
3915
+ };
3916
+ const report = {
3917
+ valid,
3918
+ cycles: initialCycles,
3919
+ removedEdges,
3920
+ danglingPrerequisites,
3921
+ coverageRatio: coverage.coverageRatio,
3922
+ totalSyllabusTopics: coverage.totalTopics,
3923
+ coveredSyllabusTopics: coverage.coveredTopics,
3924
+ uncoveredTopics: coverage.uncovered,
3925
+ warnings
3926
+ };
3927
+ return { verifiedGraph, report };
3928
+ }
3929
+
3147
3930
  // src/graph/loExemplars.ts
3148
3931
  var ULO_EXEMPLARS = [
3149
3932
  // Calibrated against production learning_objectives (UNIVERSAL rows, neutral)
@@ -3741,58 +4524,6 @@ async function standardizeConcepts(opts) {
3741
4524
  }
3742
4525
 
3743
4526
  // src/graph/knowledgeGraphPipeline.ts
3744
- function detectCycles(concepts) {
3745
- const cycles = [];
3746
- const visited = /* @__PURE__ */ new Set();
3747
- const inStack = /* @__PURE__ */ new Set();
3748
- const path = [];
3749
- function dfs(conceptId, prereqMap2) {
3750
- if (inStack.has(conceptId)) {
3751
- const cycleStart = path.indexOf(conceptId);
3752
- if (cycleStart >= 0) {
3753
- cycles.push([...path.slice(cycleStart), conceptId]);
3754
- }
3755
- return;
3756
- }
3757
- if (visited.has(conceptId)) return;
3758
- visited.add(conceptId);
3759
- inStack.add(conceptId);
3760
- path.push(conceptId);
3761
- const prereqs = prereqMap2.get(conceptId) || [];
3762
- for (const prereq of prereqs) {
3763
- dfs(prereq, prereqMap2);
3764
- }
3765
- path.pop();
3766
- inStack.delete(conceptId);
3767
- }
3768
- const prereqMap = /* @__PURE__ */ new Map();
3769
- for (const concept of concepts) {
3770
- prereqMap.set(concept.id, concept.prerequisites || []);
3771
- }
3772
- for (const concept of concepts) {
3773
- if (!visited.has(concept.id)) {
3774
- dfs(concept.id, prereqMap);
3775
- }
3776
- }
3777
- return cycles;
3778
- }
3779
- function breakCycles(concepts, cycles) {
3780
- const removed = [];
3781
- const fixed = concepts.map((c) => ({ ...c, prerequisites: [...c.prerequisites || []] }));
3782
- for (const cycle of cycles) {
3783
- if (cycle.length >= 2) {
3784
- const breaker = cycle[cycle.length - 2];
3785
- const target = cycle[cycle.length - 1];
3786
- for (const concept of fixed) {
3787
- if (concept.id === breaker) {
3788
- concept.prerequisites = concept.prerequisites.filter((p) => p !== target);
3789
- removed.push({ from: breaker, to: target });
3790
- }
3791
- }
3792
- }
3793
- }
3794
- return { fixed, removed };
3795
- }
3796
4527
  function topologicalSort(concepts) {
3797
4528
  const inDegree = /* @__PURE__ */ new Map();
3798
4529
  const adjacency = /* @__PURE__ */ new Map();
@@ -3833,6 +4564,11 @@ async function generateKnowledgeGraph(options) {
3833
4564
  onProgress?.(step, msg);
3834
4565
  };
3835
4566
  log("STEP_1", "Decomposing subject into concepts...");
4567
+ let parsedSyllabus = options.parsedSyllabus;
4568
+ if (!parsedSyllabus && syllabusText) {
4569
+ parsedSyllabus = parseSyllabus(syllabusText);
4570
+ log("STEP_1", `Parsed syllabus: ${parsedSyllabus.units.length} units, ${parsedSyllabus.allTopics.length} topics`);
4571
+ }
3836
4572
  const client = createLlmClient(llmConfig);
3837
4573
  const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "16000", 10);
3838
4574
  let truncatedSyllabus = "";
@@ -3844,10 +4580,13 @@ async function generateKnowledgeGraph(options) {
3844
4580
  truncatedSyllabus = syllabusText;
3845
4581
  }
3846
4582
  }
4583
+ const structuredTopicsGuide = parsedSyllabus && parsedSyllabus.allTopics.length > 0 ? `
4584
+ STRUCTURED SYLLABUS TOPICS (Every topic MUST map to at least one concept to ensure 100% syllabus coverage):
4585
+ ` + parsedSyllabus.allTopics.map((t) => `- [${t.unitTitle}] ${t.id}: ${t.title} (Keywords: ${t.keywords.join(", ")})`).join("\n") + "\n" : "";
3847
4586
  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";
3848
4587
  const userPrompt = `Subject: ${subject}
3849
4588
  ${description ? `Description: ${description}` : ""}
3850
- ${truncatedSyllabus ? `Syllabus:
4589
+ ${structuredTopicsGuide}${truncatedSyllabus ? `Syllabus:
3851
4590
  ${truncatedSyllabus}` : ""}
3852
4591
  ${gradeBand ? `Grade band: ${gradeBand[0]}-${gradeBand[1]}` : ""}
3853
4592
  ${domain ? `Domain: ${domain}` : ""}
@@ -3992,13 +4731,13 @@ Return JSON:
3992
4731
  log("STEP_2_5", "Standardized: " + std.decisions.filter((d) => d.decision === "standardized").length + ", proposed new: " + std.decisions.filter((d) => d.decision === "proposed_new").length);
3993
4732
  }
3994
4733
  log("STEP_3", "Checking for prerequisite cycles...");
3995
- const cycles = detectCycles(concepts);
4734
+ const cycles = detectPrerequisiteCycles(concepts);
3996
4735
  if (cycles.length > 0) {
3997
4736
  log("STEP_3", `Found ${cycles.length} cycle(s) in prerequisite chain \u2014 breaking cycles`);
3998
4737
  for (const cycle of cycles) {
3999
4738
  warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 breaking at weakest edge`);
4000
4739
  }
4001
- const { fixed, removed } = breakCycles(concepts, cycles);
4740
+ const { fixed, removed } = breakPrerequisiteCycles(concepts, cycles);
4002
4741
  for (let i = 0; i < concepts.length; i++) {
4003
4742
  const fixedConcept = fixed.find((f) => f.id === concepts[i].id);
4004
4743
  if (fixedConcept) {
@@ -4049,16 +4788,21 @@ Return JSON:
4049
4788
  }
4050
4789
  }
4051
4790
  log("STEP_2", `Validated ${concepts.length} concepts, ${categories.length} categories, ${learningPath.length} learning path steps`);
4791
+ const rawGraph = {
4792
+ schema_version: 1,
4793
+ type: "knowledge_graph",
4794
+ subject: result.subject,
4795
+ concepts,
4796
+ categories,
4797
+ learning_path: learningPath
4798
+ };
4799
+ log("STEP_6", "Running Knowledge Graph verification & reverse coverage audit...");
4800
+ const { verifiedGraph, report } = verifyKnowledgeGraph(rawGraph, parsedSyllabus);
4801
+ warnings.push(...report.warnings);
4052
4802
  return {
4053
- knowledgeGraph: {
4054
- schema_version: 1,
4055
- type: "knowledge_graph",
4056
- subject: result.subject,
4057
- concepts,
4058
- categories,
4059
- learning_path: learningPath
4060
- },
4061
- warnings
4803
+ knowledgeGraph: verifiedGraph,
4804
+ warnings,
4805
+ verificationReport: report
4062
4806
  };
4063
4807
  }
4064
4808
 
@@ -4438,562 +5182,246 @@ async function resolveConcepts(options) {
4438
5182
  const effThreshold = SOURCE_THRESHOLDS[source] || threshold;
4439
5183
  const topMatch = topMatches[0];
4440
5184
  if (topMatch.score >= effThreshold) {
4441
- resolved.push({
4442
- keyword,
4443
- source,
4444
- depth,
4445
- concept_codes: topMatches.filter((m) => m.score >= effThreshold).map((m) => m.code),
4446
- matches: topMatches
4447
- });
4448
- } else {
4449
- proposed.push({
4450
- keyword,
4451
- source,
4452
- depth,
4453
- best_match: topMatch,
4454
- reason: `Low confidence (${topMatch.score.toFixed(2)} < ${effThreshold})`
4455
- });
4456
- }
4457
- }
4458
- return {
4459
- resolved,
4460
- proposed,
4461
- summary: {
4462
- total_keywords: keywords.length,
4463
- resolved_count: resolved.length,
4464
- proposed_count: proposed.length,
4465
- search_mode
4466
- }
4467
- };
4468
- }
4469
-
4470
- // src/assembly/roadmapAssembler.ts
4471
- function calculateOverhead(rawMinutes, overheadFactor) {
4472
- return Math.ceil(rawMinutes * overheadFactor);
4473
- }
4474
- function splitFeatureIntoMilestones(feature, featureConcepts, maxMinutes, overheadFactor) {
4475
- const steps = feature.steps || [];
4476
- if (steps.length === 0) return [];
4477
- const featureId = feature.id || "F_UNK";
4478
- const stepMinutes2 = steps.map((s) => s.effort?.estimated_minutes || 15);
4479
- const totalRawMinutes = stepMinutes2.reduce((a, b) => a + b, 0);
4480
- if (calculateOverhead(totalRawMinutes, overheadFactor) <= maxMinutes) {
4481
- return [buildMilestone(feature, featureConcepts, steps, stepMinutes2, featureId, 1)];
4482
- }
4483
- const milestones = [];
4484
- let currentSteps = [];
4485
- let currentMinutes = [];
4486
- let milestoneIndex = 1;
4487
- for (let i = 0; i < steps.length; i++) {
4488
- const step = steps[i];
4489
- const mins = stepMinutes2[i];
4490
- const currentTotal = currentMinutes.reduce((a, b) => a + b, 0);
4491
- if (calculateOverhead(currentTotal + mins, overheadFactor) > maxMinutes && currentSteps.length > 0) {
4492
- milestones.push(buildMilestone(
4493
- feature,
4494
- featureConcepts,
4495
- currentSteps,
4496
- currentMinutes,
4497
- featureId,
4498
- milestoneIndex
4499
- ));
4500
- milestoneIndex++;
4501
- currentSteps = [];
4502
- currentMinutes = [];
4503
- }
4504
- currentSteps.push(step);
4505
- currentMinutes.push(mins);
4506
- }
4507
- if (currentSteps.length > 0) {
4508
- milestones.push(buildMilestone(
4509
- feature,
4510
- featureConcepts,
4511
- currentSteps,
4512
- currentMinutes,
4513
- featureId,
4514
- milestoneIndex
4515
- ));
4516
- }
4517
- return milestones;
4518
- }
4519
- function buildMilestone(feature, featureConcepts, steps, stepMinutes2, featureId, milestoneIndex) {
4520
- const rawMinutes = stepMinutes2.reduce((a, b) => a + b, 0);
4521
- const levels = steps.map((s) => s.completion_level || "mvp");
4522
- const completionLevel = levels.includes("extend") ? "extend" : levels.includes("mvp") ? "mvp" : "base";
4523
- const milestoneConcepts = /* @__PURE__ */ new Set();
4524
- const milestoneKeywords = /* @__PURE__ */ new Set();
4525
- for (const step of steps) {
4526
- const stepConceptCodes = step.concept_codes || [];
4527
- for (const code of stepConceptCodes) {
4528
- milestoneConcepts.add(code);
4529
- }
4530
- const stepKws = [...step.keywords || [], ...step.api_usage || []];
4531
- for (const cm of featureConcepts) {
4532
- if (stepKws.includes(cm.keyword)) {
4533
- milestoneConcepts.add(cm.concept_code);
4534
- milestoneKeywords.add(cm.keyword);
4535
- }
4536
- }
4537
- }
4538
- const depthCounts = { ulo: 0, cio: 0, sio: 0 };
4539
- for (const cm of featureConcepts) {
4540
- if (milestoneKeywords.has(cm.keyword)) {
4541
- depthCounts[cm.depth]++;
4542
- }
4543
- }
4544
- const dominantDepth = depthCounts.sio >= depthCounts.cio && depthCounts.sio >= depthCounts.ulo ? "sio" : depthCounts.cio >= depthCounts.ulo ? "cio" : "ulo";
4545
- return {
4546
- id: milestoneIndex === 1 ? featureId : `${featureId}-M${milestoneIndex}`,
4547
- name: milestoneIndex === 1 ? feature.name || featureId : `${feature.name || featureId} (Part ${milestoneIndex})`,
4548
- description: feature.description || "",
4549
- feature_id: featureId,
4550
- feature_name: feature.name || "",
4551
- concept_code: [...milestoneConcepts].join(", "),
4552
- depth: dominantDepth,
4553
- all_keywords: [],
4554
- new_keywords: [],
4555
- prerequisite_keywords: [],
4556
- steps,
4557
- estimated_minutes: rawMinutes,
4558
- adjusted_minutes: rawMinutes,
4559
- // placeholder — assembleRoadmap() applies actual overheadFactor
4560
- completion_level: completionLevel
4561
- };
4562
- }
4563
- function buildKeywordMap(featureConcepts) {
4564
- const map = /* @__PURE__ */ new Map();
4565
- for (const mappings of featureConcepts.values()) {
4566
- for (const m of mappings) {
4567
- const existing = map.get(m.concept_code) || [];
4568
- if (!existing.includes(m.keyword)) {
4569
- existing.push(m.keyword);
4570
- map.set(m.concept_code, existing);
4571
- }
4572
- }
4573
- }
4574
- return map;
4575
- }
4576
- function attachKeywordsToMilestones(milestones, keywordMap) {
4577
- const cumulativeKeywords = /* @__PURE__ */ new Set();
4578
- for (const milestone of milestones) {
4579
- const allKw = /* @__PURE__ */ new Set();
4580
- const conceptCodes = milestone.concept_code.split(",").map((c) => c.trim()).filter(Boolean);
4581
- for (const code of conceptCodes) {
4582
- const kws = keywordMap.get(code) || [];
4583
- for (const kw of kws) allKw.add(kw);
4584
- }
4585
- milestone.all_keywords = Array.from(allKw).sort();
4586
- const newKw = [];
4587
- const prereqKw = [];
4588
- for (const kw of milestone.all_keywords) {
4589
- if (cumulativeKeywords.has(kw)) {
4590
- prereqKw.push(kw);
4591
- } else {
4592
- newKw.push(kw);
4593
- cumulativeKeywords.add(kw);
4594
- }
4595
- }
4596
- milestone.new_keywords = newKw;
4597
- milestone.prerequisite_keywords = prereqKw;
4598
- }
4599
- return milestones;
4600
- }
4601
- function groupIntoPhases(milestones, sessionMinutes) {
4602
- const phases = [];
4603
- let currentMilestones = [];
4604
- let currentMinutes = 0;
4605
- let phaseNum = 1;
4606
- for (const milestone of milestones) {
4607
- const milestoneMinutes = milestone.adjusted_minutes;
4608
- if (currentMinutes + milestoneMinutes > sessionMinutes && currentMilestones.length > 0) {
4609
- phases.push(buildPhase(currentMilestones, phaseNum));
4610
- phaseNum++;
4611
- currentMilestones = [];
4612
- currentMinutes = 0;
4613
- }
4614
- currentMilestones.push(milestone);
4615
- currentMinutes += milestoneMinutes;
4616
- }
4617
- if (currentMilestones.length > 0) {
4618
- phases.push(buildPhase(currentMilestones, phaseNum));
4619
- }
4620
- return phases;
4621
- }
4622
- function buildPhase(milestones, phaseNum) {
4623
- const totalMinutes = milestones.reduce((sum, m) => sum + m.adjusted_minutes, 0);
4624
- const depths = milestones.map((m) => m.depth);
4625
- const uniqueDepths = [...new Set(depths)];
4626
- const progression = uniqueDepths.map((d) => d.toUpperCase()).join(" \u2192 ");
4627
- return {
4628
- phase_id: phaseNum,
4629
- phase_name: `Phase ${phaseNum}`,
4630
- milestones,
4631
- total_minutes: totalMinutes,
4632
- concept_progression: progression
4633
- };
4634
- }
4635
- function assembleRoadmap(options) {
4636
- const {
4637
- projectGraph,
4638
- featureConcepts = /* @__PURE__ */ new Map(),
4639
- sessionMinutes = 90,
4640
- overheadFactor = 1.15,
4641
- maxMilestoneMinutes = 120
4642
- } = options;
4643
- const features = projectGraph.features || [];
4644
- const keywordMap = buildKeywordMap(featureConcepts);
4645
- const allMilestones = [];
4646
- for (const feature of features) {
4647
- const fid = feature.id || "F_UNK";
4648
- const fConcepts = featureConcepts.get(fid) || [];
4649
- const milestones = splitFeatureIntoMilestones(
4650
- feature,
4651
- fConcepts,
4652
- maxMilestoneMinutes,
4653
- overheadFactor
4654
- );
4655
- allMilestones.push(...milestones);
4656
- }
4657
- for (const milestone of allMilestones) {
4658
- milestone.adjusted_minutes = calculateOverhead(milestone.estimated_minutes, overheadFactor);
4659
- }
4660
- attachKeywordsToMilestones(allMilestones, keywordMap);
4661
- const phases = groupIntoPhases(allMilestones, sessionMinutes);
4662
- const totalEstimatedMinutes = allMilestones.reduce((sum, m) => sum + m.estimated_minutes, 0);
4663
- const totalAdjustedMinutes = allMilestones.reduce((sum, m) => sum + m.adjusted_minutes, 0);
4664
- return {
4665
- project: projectGraph.project,
4666
- product: projectGraph.product,
4667
- phases,
4668
- feature_concepts: featureConcepts,
4669
- time_budget: {
4670
- session_minutes: sessionMinutes,
4671
- overhead_factor: overheadFactor,
4672
- total_estimated_minutes: totalEstimatedMinutes,
4673
- total_adjusted_minutes: totalAdjustedMinutes
4674
- }
4675
- };
4676
- }
4677
-
4678
- // src/feed/curriculumFeedEmitter.ts
4679
- var BLOOM_RANK = { Remember: 0, Understand: 1, Apply: 2, Analyze: 3, Evaluate: 4, Create: 5 };
4680
- var pad22 = (n) => String(n).padStart(2, "0");
4681
- var arr = (v) => Array.isArray(v) ? v : [];
4682
- var str = (v, d = "") => typeof v === "string" ? v : d;
4683
- var num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
4684
- var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
4685
- function stepMinutes(step) {
4686
- const effort = step.effort ?? {};
4687
- return Math.max(0, Math.round(num(effort.estimated_minutes, num(step.estimated_minutes, 45))));
4688
- }
4689
- function newKeywordsInOrder(nodes) {
4690
- const seen = /* @__PURE__ */ new Set();
4691
- for (const n of nodes) {
4692
- const all = n.keywords.all;
4693
- n.keywords.new = [];
4694
- n.keywords.prerequisite = [];
4695
- for (const k of all) {
4696
- if (seen.has(k)) n.keywords.prerequisite.push(k);
4697
- else n.keywords.new.push(k);
4698
- seen.add(k);
5185
+ resolved.push({
5186
+ keyword,
5187
+ source,
5188
+ depth,
5189
+ concept_codes: topMatches.filter((m) => m.score >= effThreshold).map((m) => m.code),
5190
+ matches: topMatches
5191
+ });
5192
+ } else {
5193
+ proposed.push({
5194
+ keyword,
5195
+ source,
5196
+ depth,
5197
+ best_match: topMatch,
5198
+ reason: `Low confidence (${topMatch.score.toFixed(2)} < ${effThreshold})`
5199
+ });
4699
5200
  }
4700
5201
  }
4701
- }
4702
- function feedNodeFromStep(step, featureId) {
4703
- const id = str(step.id);
4704
- return {
4705
- id,
4706
- kind: "product_step",
4707
- name: str(step.name, id),
4708
- concept_codes: strs(step.concept_codes),
4709
- keywords: { all: strs(step.keywords), new: [], prerequisite: [] },
4710
- estimated_minutes: stepMinutes(step),
4711
- bloom_hint: "Apply",
4712
- depth_hint: null,
4713
- completion_level: "standard",
4714
- scaffolding: { scaffold_candidates: [], core_value_minutes: stepMinutes(step) },
4715
- references: arr(step.files).map((f) => ({ file: str(f), evidence: "step file" })).filter((f) => f.file),
4716
- feature_id: featureId,
4717
- concept_ref: null,
4718
- phase_id: "",
4719
- introduce_aspect: null
4720
- };
4721
- }
4722
- function feedNodeFromConcept(concept) {
4723
- const id = str(concept.id);
4724
- 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]);
4725
- const topBloom = pts[pts.length - 1] ?? "Understand";
4726
5202
  return {
4727
- id,
4728
- kind: "concept",
4729
- name: str(concept.name, id),
4730
- concept_codes: [str(concept.code) || id].filter(Boolean),
4731
- keywords: { all: strs(concept.keywords), new: [], prerequisite: [] },
4732
- estimated_minutes: Math.max(0, Math.round(num(concept.estimated_minutes, 30))),
4733
- bloom_hint: topBloom,
4734
- depth_hint: null,
4735
- completion_level: "standard",
4736
- scaffolding: { scaffold_candidates: [], core_value_minutes: 0 },
4737
- references: [],
4738
- feature_id: null,
4739
- concept_ref: id,
4740
- phase_id: "",
4741
- introduce_aspect: null
5203
+ resolved,
5204
+ proposed,
5205
+ summary: {
5206
+ total_keywords: keywords.length,
5207
+ resolved_count: resolved.length,
5208
+ proposed_count: proposed.length,
5209
+ search_mode
5210
+ }
4742
5211
  };
4743
5212
  }
4744
- function addFeatureGroup(f, fid, nodes, edges, groupings) {
4745
- const steps = arr(f.steps);
4746
- const stepIds = [];
4747
- let prev = "";
4748
- for (let i = 0; i < steps.length; i++) {
4749
- const stepNode = feedNodeFromStep(steps[i], fid);
4750
- if (!stepNode.id) continue;
4751
- stepIds.push(stepNode.id);
4752
- nodes.push(stepNode);
4753
- if (prev) edges.push({ from: prev, to: stepNode.id, kind: "task", reason: "feature build order" });
4754
- prev = stepNode.id;
4755
- }
4756
- for (const dep of strs(f.depends_on)) {
4757
- const depFirst = nodes.find((n) => n.feature_id === dep);
4758
- if (depFirst && stepIds[0]) edges.push({ from: depFirst.id, to: stepIds[0], kind: "task", reason: "feature depends_on " + dep });
4759
- }
4760
- if (stepIds.length > 0) groupings.push({ id: "G-" + fid, name: str(f.name, fid), node_ids: stepIds, rationale: str(f.description, "") });
4761
- return stepIds;
5213
+
5214
+ // src/assembly/roadmapAssembler.ts
5215
+ function calculateOverhead(rawMinutes, overheadFactor) {
5216
+ return Math.ceil(rawMinutes * overheadFactor);
4762
5217
  }
4763
- function emitFromHybrid(hybrid, opts) {
4764
- const nodes = [];
4765
- const edges = [];
4766
- const groupings = [];
4767
- const pg = hybrid.project_graph ?? {};
4768
- const kg = hybrid.knowledge_graph ?? {};
4769
- const rawPhases = arr(hybrid.phases).filter((p) => str(p.id));
4770
- const features = arr(pg.features);
4771
- const conceptRecs = arr(kg.concepts);
4772
- const conceptById = new Map(conceptRecs.map((c) => [str(c.id), c]));
4773
- if (rawPhases.length === 0) {
4774
- for (const c of conceptRecs) {
4775
- const node = feedNodeFromConcept(c);
4776
- nodes.push(node);
4777
- for (const p of strs(c.prerequisites)) edges.push({ from: p, to: node.id, kind: "knowledge", reason: "concept prerequisite" });
4778
- }
4779
- features.forEach((f, fi) => addFeatureGroup(f, str(f.id, "F" + pad22(fi + 1)), nodes, edges, groupings));
4780
- linkHybridKnowledgeEdges(hybrid, nodes, edges);
4781
- newKeywordsInOrder(nodes);
4782
- return validateCurriculumFeed({
4783
- schema_version: 2,
4784
- source: { graph_type: "hybrid_graph", warnings: opts.warnings ?? [], hallucination_count: opts.hallucinationCount ?? 0 },
4785
- learning_nodes: nodes,
4786
- dependency_edges: edges,
4787
- suggested_groupings: groupings,
4788
- phases: []
4789
- });
4790
- }
4791
- const featureOrder = /* @__PURE__ */ new Map();
4792
- features.forEach((f, fi) => featureOrder.set(str(f.id, "F" + pad22(fi + 1)), fi));
4793
- const stepIdsByFeature = /* @__PURE__ */ new Map();
4794
- for (const f of features) {
4795
- const fid = str(f.id);
4796
- stepIdsByFeature.set(fid, arr(f.steps).map((s) => str(s.id)).filter(Boolean));
4797
- }
4798
- const emitCtx = [];
4799
- const emittedConcepts = /* @__PURE__ */ new Set();
4800
- const linkList = arr(hybrid.links);
4801
- for (const rp of rawPhases) {
4802
- const pid = str(rp.id);
4803
- const order = num(rp.order, emitCtx.length + 1);
4804
- const conceptOrder = [];
4805
- const intros = arr(rp.introduces);
4806
- const news = intros.filter((i) => str(i.aspect, "new") === "new");
4807
- const advanced = intros.filter((i) => str(i.aspect) === "advanced");
4808
- for (const intro of [...news, ...advanced]) {
4809
- const cid = str(intro.concept_id);
4810
- const c = conceptById.get(cid);
4811
- if (!c) {
4812
- throw new Error("Phase " + pid + " introduces unknown concept " + cid + " - fail-closed");
4813
- }
4814
- if (str(intro.aspect, "new") === "advanced" && !emittedConcepts.has(cid)) {
4815
- throw new Error("Phase " + pid + " advanced-revisits concept " + cid + " that was never emitted - fail-closed");
4816
- }
4817
- if (!emittedConcepts.has(cid)) {
4818
- const node = feedNodeFromConcept(c);
4819
- node.phase_id = pid;
4820
- node.introduce_aspect = "new";
4821
- nodes.push(node);
4822
- for (const p of strs(c.prerequisites)) {
4823
- if (!emittedConcepts.has(p)) {
4824
- throw new Error("Concept " + cid + " (phase " + pid + ") has prerequisite " + p + " that is not introduced in an earlier phase - fail-closed");
4825
- }
4826
- edges.push({ from: p, to: cid, kind: "knowledge", reason: "concept prerequisite" });
4827
- }
4828
- emittedConcepts.add(cid);
4829
- conceptOrder.push(cid);
4830
- } else {
4831
- const revisitId = cid + "__ADV" + pad22(order);
4832
- const node = feedNodeFromConcept(c);
4833
- node.id = revisitId;
4834
- node.name = str(c.name, cid) + " (advanced)";
4835
- node.phase_id = pid;
4836
- node.introduce_aspect = "advanced";
4837
- node.estimated_minutes = Math.max(15, Math.round(node.estimated_minutes * 0.5));
4838
- node.concept_codes = [str(c.code) || cid];
4839
- edges.push({ from: cid, to: revisitId, kind: "knowledge", reason: "advanced revisit of " + cid + " at phase " + pid });
4840
- nodes.push(node);
4841
- conceptOrder.push(revisitId);
4842
- }
4843
- }
4844
- const featureIds = strs(rp.feature_ids).filter((fid) => featureOrder.has(fid)).sort((a, b) => featureOrder.get(a) - featureOrder.get(b));
4845
- emitCtx.push({ id: pid, order, name: str(rp.name, pid), productCompletion: str(rp.product_completion), conceptOrder, featureIds });
5218
+ function splitFeatureIntoMilestones(feature, featureConcepts, maxMinutes, overheadFactor) {
5219
+ const steps = feature.steps || [];
5220
+ if (steps.length === 0) return [];
5221
+ const featureId = feature.id || "F_UNK";
5222
+ const stepMinutes2 = steps.map((s) => s.effort?.estimated_minutes || 15);
5223
+ const totalRawMinutes = stepMinutes2.reduce((a, b) => a + b, 0);
5224
+ if (calculateOverhead(totalRawMinutes, overheadFactor) <= maxMinutes) {
5225
+ return [buildMilestone(feature, featureConcepts, steps, stepMinutes2, featureId, 1)];
4846
5226
  }
4847
- for (const l of linkList) {
4848
- const cid = str(l.concept_id);
4849
- if (cid && conceptById.has(cid) && !emittedConcepts.has(cid)) {
4850
- throw new Error("Concept " + cid + " is consumed by links but never introduced in any phase - fail-closed");
5227
+ const milestones = [];
5228
+ let currentSteps = [];
5229
+ let currentMinutes = [];
5230
+ let milestoneIndex = 1;
5231
+ for (let i = 0; i < steps.length; i++) {
5232
+ const step = steps[i];
5233
+ const mins = stepMinutes2[i];
5234
+ const currentTotal = currentMinutes.reduce((a, b) => a + b, 0);
5235
+ if (calculateOverhead(currentTotal + mins, overheadFactor) > maxMinutes && currentSteps.length > 0) {
5236
+ milestones.push(buildMilestone(
5237
+ feature,
5238
+ featureConcepts,
5239
+ currentSteps,
5240
+ currentMinutes,
5241
+ featureId,
5242
+ milestoneIndex
5243
+ ));
5244
+ milestoneIndex++;
5245
+ currentSteps = [];
5246
+ currentMinutes = [];
4851
5247
  }
5248
+ currentSteps.push(step);
5249
+ currentMinutes.push(mins);
4852
5250
  }
4853
- const seenFeatures = new Set(emitCtx.flatMap((e) => e.featureIds));
4854
- const missingFeatures = features.map((f) => str(f.id)).filter((fid) => fid && !seenFeatures.has(fid));
4855
- if (missingFeatures.length > 0) {
4856
- throw new Error("Features not covered by any phase: " + missingFeatures.join(", ") + " - fail-closed");
5251
+ if (currentSteps.length > 0) {
5252
+ milestones.push(buildMilestone(
5253
+ feature,
5254
+ featureConcepts,
5255
+ currentSteps,
5256
+ currentMinutes,
5257
+ featureId,
5258
+ milestoneIndex
5259
+ ));
4857
5260
  }
4858
- const phaseNodeIds = [];
4859
- for (const ctx of emitCtx) {
4860
- const ids = [...ctx.conceptOrder];
4861
- for (const fid of ctx.featureIds) {
4862
- const f = features.find((x) => str(x.id) === fid);
4863
- const stepIds = addFeatureGroup(f, fid, nodes, edges, groupings);
4864
- for (const sid of stepIds) {
4865
- const node = nodes.find((n) => n.id === sid);
4866
- node.phase_id = ctx.id;
4867
- }
4868
- ids.push(...stepIds);
5261
+ return milestones;
5262
+ }
5263
+ function buildMilestone(feature, featureConcepts, steps, stepMinutes2, featureId, milestoneIndex) {
5264
+ const rawMinutes = stepMinutes2.reduce((a, b) => a + b, 0);
5265
+ const levels = steps.map((s) => s.completion_level || "mvp");
5266
+ const completionLevel = levels.includes("extend") ? "extend" : levels.includes("mvp") ? "mvp" : "base";
5267
+ const milestoneConcepts = /* @__PURE__ */ new Set();
5268
+ const milestoneKeywords = /* @__PURE__ */ new Set();
5269
+ for (const step of steps) {
5270
+ const stepConceptCodes = step.concept_codes || [];
5271
+ for (const code of stepConceptCodes) {
5272
+ milestoneConcepts.add(code);
4869
5273
  }
4870
- phaseNodeIds.push(ids);
4871
- }
4872
- const edgeSeen = new Set(edges.map((e) => e.from + ">" + e.to));
4873
- const pushEdge = (from, to, reason) => {
4874
- const key = from + ">" + to;
4875
- if (edgeSeen.has(key)) return;
4876
- edgeSeen.add(key);
4877
- edges.push({ from, to, kind: "knowledge", reason });
4878
- };
4879
- for (const l of linkList) {
4880
- const cid = str(l.concept_id);
4881
- const fid = str(l.feature_id);
4882
- if (!conceptById.has(cid)) continue;
4883
- const firstStep = (stepIdsByFeature.get(fid) || [])[0];
4884
- if (!firstStep) continue;
4885
- const depth = str(l.depth, "cio");
4886
- pushEdge(cid, firstStep, str(l.relationship, "hybrid link") + " [" + depth + "]");
4887
- for (const n of nodes) {
4888
- if (n.kind === "concept" && n.concept_ref === cid && n.introduce_aspect === "advanced" && n.phase_id === str(l.phase_id)) {
4889
- pushEdge(n.id, firstStep, "advanced application [" + depth + "]");
5274
+ const stepKws = [...step.keywords || [], ...step.api_usage || []];
5275
+ for (const cm of featureConcepts) {
5276
+ if (stepKws.includes(cm.keyword)) {
5277
+ milestoneConcepts.add(cm.concept_code);
5278
+ milestoneKeywords.add(cm.keyword);
4890
5279
  }
4891
5280
  }
4892
5281
  }
4893
- newKeywordsInOrder(nodes);
4894
- const feedOrder = new Map(nodes.map((n, i) => [n.id, i]));
4895
- const introducer = /* @__PURE__ */ new Map();
4896
- for (const n of nodes) for (const k of n.keywords?.new ?? []) {
4897
- const key = k.toLowerCase();
4898
- if (!introducer.has(key)) introducer.set(key, n.id);
5282
+ const depthCounts = { ulo: 0, cio: 0, sio: 0 };
5283
+ for (const cm of featureConcepts) {
5284
+ if (milestoneKeywords.has(cm.keyword)) {
5285
+ depthCounts[cm.depth]++;
5286
+ }
4899
5287
  }
4900
- for (const n of nodes) {
4901
- for (const k of n.keywords?.prerequisite ?? []) {
4902
- const src = introducer.get(k.toLowerCase());
4903
- if (!src || src === n.id) continue;
4904
- if (feedOrder.get(src) < feedOrder.get(n.id)) pushEdge(src, n.id, "keyword prerequisite [" + k + "]");
5288
+ const dominantDepth = depthCounts.sio >= depthCounts.cio && depthCounts.sio >= depthCounts.ulo ? "sio" : depthCounts.cio >= depthCounts.ulo ? "cio" : "ulo";
5289
+ return {
5290
+ id: milestoneIndex === 1 ? featureId : `${featureId}-M${milestoneIndex}`,
5291
+ name: milestoneIndex === 1 ? feature.name || featureId : `${feature.name || featureId} (Part ${milestoneIndex})`,
5292
+ description: feature.description || "",
5293
+ feature_id: featureId,
5294
+ feature_name: feature.name || "",
5295
+ concept_code: [...milestoneConcepts].join(", "),
5296
+ depth: dominantDepth,
5297
+ all_keywords: [],
5298
+ new_keywords: [],
5299
+ prerequisite_keywords: [],
5300
+ steps,
5301
+ estimated_minutes: rawMinutes,
5302
+ adjusted_minutes: rawMinutes,
5303
+ // placeholder — assembleRoadmap() applies actual overheadFactor
5304
+ completion_level: completionLevel
5305
+ };
5306
+ }
5307
+ function buildKeywordMap(featureConcepts) {
5308
+ const map = /* @__PURE__ */ new Map();
5309
+ for (const mappings of featureConcepts.values()) {
5310
+ for (const m of mappings) {
5311
+ const existing = map.get(m.concept_code) || [];
5312
+ if (!existing.includes(m.keyword)) {
5313
+ existing.push(m.keyword);
5314
+ map.set(m.concept_code, existing);
5315
+ }
4905
5316
  }
4906
5317
  }
4907
- const phases = emitCtx.map((ctx, i) => ({
4908
- id: ctx.id,
4909
- order: ctx.order,
4910
- name: ctx.name,
4911
- product_completion: ctx.productCompletion || "Phase " + (i + 1) + " deliverable",
4912
- node_ids: phaseNodeIds[i]
4913
- }));
4914
- return validateCurriculumFeed({
4915
- schema_version: 2,
4916
- source: { graph_type: "hybrid_graph", warnings: opts.warnings ?? [], hallucination_count: opts.hallucinationCount ?? 0 },
4917
- learning_nodes: nodes,
4918
- dependency_edges: edges,
4919
- suggested_groupings: groupings,
4920
- phases
4921
- });
5318
+ return map;
4922
5319
  }
4923
- function linkHybridKnowledgeEdges(hybrid, nodes, edges, setStepHints) {
4924
- const conceptIds = new Set(nodes.filter((n) => n.kind === "concept").map((n) => n.concept_ref || n.id));
4925
- for (const l of arr(hybrid.links)) {
4926
- const conceptId = str(l.concept_id);
4927
- const featureId = str(l.feature_id);
4928
- if (!conceptIds.has(conceptId)) continue;
4929
- const depth = str(l.depth, "cio");
4930
- const firstStep = nodes.find((n) => n.feature_id === featureId);
4931
- if (firstStep) {
4932
- edges.push({ from: conceptId, to: firstStep.id, kind: "knowledge", reason: str(l.relationship, "hybrid link") + " [" + depth + "]" });
4933
- if (!firstStep.depth_hint) {
4934
- firstStep.depth_hint = ["ulo", "cio", "sio"].includes(depth) ? depth : "cio";
5320
+ function attachKeywordsToMilestones(milestones, keywordMap) {
5321
+ const cumulativeKeywords = /* @__PURE__ */ new Set();
5322
+ for (const milestone of milestones) {
5323
+ const allKw = /* @__PURE__ */ new Set();
5324
+ const conceptCodes = milestone.concept_code.split(",").map((c) => c.trim()).filter(Boolean);
5325
+ for (const code of conceptCodes) {
5326
+ const kws = keywordMap.get(code) || [];
5327
+ for (const kw of kws) allKw.add(kw);
5328
+ }
5329
+ milestone.all_keywords = Array.from(allKw).sort();
5330
+ const newKw = [];
5331
+ const prereqKw = [];
5332
+ for (const kw of milestone.all_keywords) {
5333
+ if (cumulativeKeywords.has(kw)) {
5334
+ prereqKw.push(kw);
5335
+ } else {
5336
+ newKw.push(kw);
5337
+ cumulativeKeywords.add(kw);
4935
5338
  }
4936
5339
  }
5340
+ milestone.new_keywords = newKw;
5341
+ milestone.prerequisite_keywords = prereqKw;
4937
5342
  }
5343
+ return milestones;
4938
5344
  }
4939
- function emitFromKnowledge(kg, opts) {
4940
- const nodes = [];
4941
- const edges = [];
4942
- const groupings = [];
4943
- for (const c of arr(kg.concepts)) {
4944
- const node = feedNodeFromConcept(c);
4945
- nodes.push(node);
4946
- for (const p of strs(c.prerequisites)) edges.push({ from: p, to: node.id, kind: "knowledge", reason: "concept prerequisite" });
5345
+ function groupIntoPhases(milestones, sessionMinutes) {
5346
+ const phases = [];
5347
+ let currentMilestones = [];
5348
+ let currentMinutes = 0;
5349
+ let phaseNum = 1;
5350
+ for (const milestone of milestones) {
5351
+ const milestoneMinutes = milestone.adjusted_minutes;
5352
+ if (currentMinutes + milestoneMinutes > sessionMinutes && currentMilestones.length > 0) {
5353
+ phases.push(buildPhase(currentMilestones, phaseNum));
5354
+ phaseNum++;
5355
+ currentMilestones = [];
5356
+ currentMinutes = 0;
5357
+ }
5358
+ currentMilestones.push(milestone);
5359
+ currentMinutes += milestoneMinutes;
4947
5360
  }
4948
- let ci = 0;
4949
- for (const cat of arr(kg.categories)) {
4950
- ci += 1;
4951
- const ids = strs(cat.concept_ids).filter((id) => nodes.some((n) => n.id === id));
4952
- if (ids.length > 0) groupings.push({ id: str(cat.id, "cat" + ci), name: str(cat.name, "Category " + ci), node_ids: ids, rationale: str(cat.description, "") });
5361
+ if (currentMilestones.length > 0) {
5362
+ phases.push(buildPhase(currentMilestones, phaseNum));
4953
5363
  }
4954
- newKeywordsInOrder(nodes);
4955
- return validateCurriculumFeed({
4956
- schema_version: 2,
4957
- source: { graph_type: "knowledge_graph", warnings: opts.warnings ?? [], hallucination_count: opts.hallucinationCount ?? 0 },
4958
- learning_nodes: nodes,
4959
- dependency_edges: edges,
4960
- suggested_groupings: groupings,
4961
- phases: []
4962
- });
5364
+ return phases;
4963
5365
  }
4964
- function emitFromProject(pg, opts) {
4965
- const nodes = [];
4966
- const edges = [];
4967
- const groupings = [];
4968
- arr(pg.features).forEach((f, fi) => addFeatureGroup(f, str(f.id, "F" + pad22(fi + 1)), nodes, edges, groupings));
4969
- if (nodes.length === 0 && arr(pg.features).length > 0) {
4970
- const hasTasks = arr(pg.implementation ?? {}).length > 0 || arr(pg.implementation?.tasks).length > 0;
4971
- throw new Error(
4972
- "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.")
4973
- );
4974
- }
4975
- newKeywordsInOrder(nodes);
4976
- return validateCurriculumFeed({
4977
- schema_version: 2,
4978
- source: { graph_type: "project_graph", warnings: opts.warnings ?? [], hallucination_count: opts.hallucinationCount ?? 0 },
4979
- learning_nodes: nodes,
4980
- dependency_edges: edges,
4981
- suggested_groupings: groupings,
4982
- phases: []
4983
- });
5366
+ function buildPhase(milestones, phaseNum) {
5367
+ const totalMinutes = milestones.reduce((sum, m) => sum + m.adjusted_minutes, 0);
5368
+ const depths = milestones.map((m) => m.depth);
5369
+ const uniqueDepths = [...new Set(depths)];
5370
+ const progression = uniqueDepths.map((d) => d.toUpperCase()).join(" \u2192 ");
5371
+ return {
5372
+ phase_id: phaseNum,
5373
+ phase_name: `Phase ${phaseNum}`,
5374
+ milestones,
5375
+ total_minutes: totalMinutes,
5376
+ concept_progression: progression
5377
+ };
4984
5378
  }
4985
- function emitCurriculumFeed(graph, opts = {}) {
4986
- const g = graph;
4987
- if (arr(g.learning_nodes).length > 0) {
4988
- return validateCurriculumFeed(g);
5379
+ function assembleRoadmap(options) {
5380
+ const {
5381
+ projectGraph,
5382
+ featureConcepts = /* @__PURE__ */ new Map(),
5383
+ sessionMinutes = 90,
5384
+ overheadFactor = 1.15,
5385
+ maxMilestoneMinutes = 120
5386
+ } = options;
5387
+ const features = projectGraph.features || [];
5388
+ const keywordMap = buildKeywordMap(featureConcepts);
5389
+ const allMilestones = [];
5390
+ for (const feature of features) {
5391
+ const fid = feature.id || "F_UNK";
5392
+ const fConcepts = featureConcepts.get(fid) || [];
5393
+ const milestones = splitFeatureIntoMilestones(
5394
+ feature,
5395
+ fConcepts,
5396
+ maxMilestoneMinutes,
5397
+ overheadFactor
5398
+ );
5399
+ allMilestones.push(...milestones);
4989
5400
  }
4990
- if (g.project_graph || g.knowledge_graph || arr(g.links).length > 0 || arr(g.phases).length > 0) {
4991
- return emitFromHybrid(g, opts);
5401
+ for (const milestone of allMilestones) {
5402
+ milestone.adjusted_minutes = calculateOverhead(milestone.estimated_minutes, overheadFactor);
4992
5403
  }
4993
- if (arr(g.concepts).length > 0 || str(g.type) === "knowledge_graph") return emitFromKnowledge(g, opts);
4994
- return emitFromProject(g, opts);
5404
+ attachKeywordsToMilestones(allMilestones, keywordMap);
5405
+ const phases = groupIntoPhases(allMilestones, sessionMinutes);
5406
+ const totalEstimatedMinutes = allMilestones.reduce((sum, m) => sum + m.estimated_minutes, 0);
5407
+ const totalAdjustedMinutes = allMilestones.reduce((sum, m) => sum + m.adjusted_minutes, 0);
5408
+ return {
5409
+ project: projectGraph.project,
5410
+ product: projectGraph.product,
5411
+ phases,
5412
+ feature_concepts: featureConcepts,
5413
+ time_budget: {
5414
+ session_minutes: sessionMinutes,
5415
+ overhead_factor: overheadFactor,
5416
+ total_estimated_minutes: totalEstimatedMinutes,
5417
+ total_adjusted_minutes: totalAdjustedMinutes
5418
+ }
5419
+ };
4995
5420
  }
4996
5421
 
5422
+ // src/feed/index.ts
5423
+ init_curriculumFeedEmitter();
5424
+
4997
5425
  // src/extractors/fileSelector.ts
4998
5426
  async function selectFilesWithLlm(filePaths, goal, techStack, llmConfig) {
4999
5427
  if (filePaths.length <= 15) {
@@ -5427,6 +5855,174 @@ async function runProjectGraphPipeline(options) {
5427
5855
  };
5428
5856
  }
5429
5857
 
5858
+ // src/pipeline/knowledgeGraphPipeline.ts
5859
+ init_curriculumFeedEmitter();
5860
+ async function runKnowledgeGraphPipeline(options) {
5861
+ const warnings = [];
5862
+ let parsedSyllabus = options.parsedSyllabus;
5863
+ if (!parsedSyllabus && options.syllabusText) {
5864
+ parsedSyllabus = parseSyllabus(options.syllabusText);
5865
+ }
5866
+ const effectiveSubject = options.subject || parsedSyllabus?.subject.title || "Untitled Subject";
5867
+ const effectiveDomain = options.domain || parsedSyllabus?.subject.domain || "other";
5868
+ const effectiveGradeBand = options.gradeBand || parsedSyllabus?.subject.gradeBand;
5869
+ const effectiveDesc = options.description || parsedSyllabus?.subject.description;
5870
+ const result = await generateKnowledgeGraph({
5871
+ ...options,
5872
+ subject: effectiveSubject,
5873
+ domain: effectiveDomain,
5874
+ gradeBand: effectiveGradeBand,
5875
+ description: effectiveDesc,
5876
+ parsedSyllabus
5877
+ });
5878
+ warnings.push(...result.warnings);
5879
+ const curriculumFeed = emitCurriculumFeed(result.knowledgeGraph, {
5880
+ warnings
5881
+ });
5882
+ return {
5883
+ knowledgeGraph: result.knowledgeGraph,
5884
+ parsedSyllabus,
5885
+ curriculumFeed,
5886
+ verificationReport: result.verificationReport,
5887
+ warnings
5888
+ };
5889
+ }
5890
+
5891
+ // src/pipeline/hybridGraphPipeline.ts
5892
+ init_curriculumFeedEmitter();
5893
+ async function runHybridGraphPipeline(options) {
5894
+ const warnings = [];
5895
+ const result = await generateHybridGraph(options);
5896
+ warnings.push(...result.warnings);
5897
+ const curriculumFeed = emitCurriculumFeed(result.hybridGraph, {
5898
+ warnings
5899
+ });
5900
+ return {
5901
+ hybridGraph: result.hybridGraph,
5902
+ curriculumFeed,
5903
+ warnings
5904
+ };
5905
+ }
5906
+
5907
+ // src/pipeline/domainRouter.ts
5908
+ async function runDomainPipeline(options) {
5909
+ const warnings = [];
5910
+ const log = (step, msg) => {
5911
+ console.log(`[ROUTER-${step}] ${msg}`);
5912
+ options.onProgress?.(step, msg);
5913
+ };
5914
+ log("LAYER_1", "Detecting domain profile...");
5915
+ const domainProfile = detectDomainProfile(options);
5916
+ const effectiveMode = options.forcedLearningMode || domainProfile.learningMode;
5917
+ log("LAYER_2", `Routing to pipeline for learning mode: ${effectiveMode} (graphType: ${domainProfile.graphType})`);
5918
+ if (effectiveMode === "concept") {
5919
+ log("LAYER_3", "Executing Knowledge Graph Pipeline (Concept-driven)...");
5920
+ const knowledgeResult = await runKnowledgeGraphPipeline({
5921
+ subject: options.subject || domainProfile.detectedSubject || options.goal || options.description || "General Subject",
5922
+ description: options.description,
5923
+ syllabusText: options.syllabusText,
5924
+ parsedSyllabus: options.parsedSyllabus,
5925
+ gradeBand: options.gradeBand,
5926
+ domain: domainProfile.domainCategory,
5927
+ techStack: options.techStack || domainProfile.detectedTechStack?.[0],
5928
+ llmConfig: options.llmConfig,
5929
+ onProgress: options.onProgress,
5930
+ knowledgeTreePath: options.knowledgeTreePath,
5931
+ proposalsPath: options.proposalsPath
5932
+ });
5933
+ warnings.push(...knowledgeResult.warnings);
5934
+ return {
5935
+ domainProfile,
5936
+ graphType: "knowledge_graph",
5937
+ curriculumFeed: knowledgeResult.curriculumFeed,
5938
+ knowledgeResult,
5939
+ warnings
5940
+ };
5941
+ }
5942
+ if (effectiveMode === "hybrid") {
5943
+ log("LAYER_3", "Executing Hybrid Graph Pipeline...");
5944
+ if (options.repoDir) {
5945
+ const projectResult2 = await runProjectGraphPipeline({
5946
+ repoDir: options.repoDir,
5947
+ goal: options.goal || "Build complete project",
5948
+ techStack: options.techStack || "typescript",
5949
+ llmConfig: options.llmConfig,
5950
+ embeddings: options.embeddings,
5951
+ onProgress: options.onProgress
5952
+ });
5953
+ const knowledgeResult = await runKnowledgeGraphPipeline({
5954
+ subject: options.subject || domainProfile.detectedSubject || options.goal || "Subject Theory",
5955
+ description: options.description,
5956
+ syllabusText: options.syllabusText,
5957
+ parsedSyllabus: options.parsedSyllabus,
5958
+ domain: domainProfile.domainCategory,
5959
+ techStack: options.techStack,
5960
+ llmConfig: options.llmConfig,
5961
+ onProgress: options.onProgress
5962
+ });
5963
+ const hybridResult = await runHybridGraphPipeline({
5964
+ projectGraph: projectResult2.projectGraph,
5965
+ knowledgeGraph: knowledgeResult.knowledgeGraph,
5966
+ llmConfig: options.llmConfig,
5967
+ onProgress: options.onProgress
5968
+ });
5969
+ warnings.push(...hybridResult.warnings);
5970
+ return {
5971
+ domainProfile,
5972
+ graphType: "hybrid_graph",
5973
+ curriculumFeed: hybridResult.curriculumFeed,
5974
+ projectResult: projectResult2,
5975
+ knowledgeResult,
5976
+ hybridResult,
5977
+ warnings
5978
+ };
5979
+ } else {
5980
+ warnings.push("Hybrid learning mode requested but repoDir was not provided \u2014 falling back to concept-driven knowledge graph pipeline");
5981
+ const knowledgeResult = await runKnowledgeGraphPipeline({
5982
+ subject: options.subject || domainProfile.detectedSubject || options.goal || "Subject Theory",
5983
+ description: options.description,
5984
+ syllabusText: options.syllabusText,
5985
+ parsedSyllabus: options.parsedSyllabus,
5986
+ domain: domainProfile.domainCategory,
5987
+ techStack: options.techStack,
5988
+ llmConfig: options.llmConfig,
5989
+ onProgress: options.onProgress
5990
+ });
5991
+ warnings.push(...knowledgeResult.warnings);
5992
+ return {
5993
+ domainProfile,
5994
+ graphType: "knowledge_graph",
5995
+ curriculumFeed: knowledgeResult.curriculumFeed,
5996
+ knowledgeResult,
5997
+ warnings
5998
+ };
5999
+ }
6000
+ }
6001
+ log("LAYER_3", "Executing Project Graph Pipeline (Product-driven)...");
6002
+ if (!options.repoDir) {
6003
+ throw new Error("repoDir is required for product-driven project graph pipeline");
6004
+ }
6005
+ const projectResult = await runProjectGraphPipeline({
6006
+ repoDir: options.repoDir,
6007
+ goal: options.goal || "Build complete project",
6008
+ techStack: options.techStack || domainProfile.detectedTechStack?.[0] || "typescript",
6009
+ llmConfig: options.llmConfig,
6010
+ embeddings: options.embeddings,
6011
+ onProgress: options.onProgress
6012
+ });
6013
+ const { emitCurriculumFeed: emitCurriculumFeed2 } = await Promise.resolve().then(() => (init_curriculumFeedEmitter(), curriculumFeedEmitter_exports));
6014
+ const curriculumFeed = emitCurriculumFeed2(projectResult.projectGraph, {
6015
+ warnings
6016
+ });
6017
+ return {
6018
+ domainProfile,
6019
+ graphType: "project_graph",
6020
+ curriculumFeed,
6021
+ projectResult,
6022
+ warnings
6023
+ };
6024
+ }
6025
+
5430
6026
  exports.ArchEdgeSchema = ArchEdgeSchema;
5431
6027
  exports.ArchNodeSchema = ArchNodeSchema;
5432
6028
  exports.ArchitectureDetailSchema = ArchitectureDetailSchema;
@@ -5436,7 +6032,6 @@ exports.CapabilitySchema = CapabilitySchema;
5436
6032
  exports.ConceptCategorySchema = ConceptCategorySchema;
5437
6033
  exports.ConceptIntroductionSchema = ConceptIntroductionSchema;
5438
6034
  exports.ConceptSchema = ConceptSchema;
5439
- exports.CurriculumFeedSchema = CurriculumFeedSchema;
5440
6035
  exports.DataIntegrationSchema = DataIntegrationSchema;
5441
6036
  exports.DecompositionSchema = DecompositionSchema;
5442
6037
  exports.DetectionConfidenceSchema = DetectionConfidenceSchema;
@@ -5446,13 +6041,9 @@ exports.DomainCategorySchema = DomainCategorySchema;
5446
6041
  exports.DomainProfileSchema = DomainProfileSchema;
5447
6042
  exports.ExperienceComponentSchema = ExperienceComponentSchema;
5448
6043
  exports.ExperienceSchema = ExperienceSchema;
6044
+ exports.ExperimentProcedureSchema = ExperimentProcedureSchema;
5449
6045
  exports.FeatureSchema = FeatureSchema;
5450
- exports.FeedEdgeSchema = FeedEdgeSchema;
5451
- exports.FeedGroupingSchema = FeedGroupingSchema;
5452
- exports.FeedKeywordsSchema = FeedKeywordsSchema;
5453
- exports.FeedNodeSchema = FeedNodeSchema;
5454
- exports.FeedPhaseSchema = FeedPhaseSchema;
5455
- exports.FeedScaffoldCandidateSchema = FeedScaffoldCandidateSchema;
6046
+ exports.GrammarRuleSchema = GrammarRuleSchema;
5456
6047
  exports.GraphTypeSchema = GraphTypeSchema;
5457
6048
  exports.HardwareFeatureExtensionSchema = HardwareFeatureExtensionSchema;
5458
6049
  exports.HardwareStepSchema = HardwareStepSchema;
@@ -5465,8 +6056,11 @@ exports.JourneySchema = JourneySchema;
5465
6056
  exports.JourneyStepSchema = JourneyStepSchema;
5466
6057
  exports.KnowledgeGraphSchema = KnowledgeGraphSchema;
5467
6058
  exports.LEARNER_CLAUSE = LEARNER_CLAUSE;
6059
+ exports.LanguageConceptExtensionSchema = LanguageConceptExtensionSchema;
5468
6060
  exports.LearningModeSchema = LearningModeSchema;
5469
6061
  exports.LearningPathStepSchema = LearningPathStepSchema;
6062
+ exports.MathConceptExtensionSchema = MathConceptExtensionSchema;
6063
+ exports.MathTheoremSchema = MathTheoremSchema;
5470
6064
  exports.MilestoneDecompositionSchema = MilestoneDecompositionSchema;
5471
6065
  exports.MissingGapSchema = MissingGapSchema;
5472
6066
  exports.NavigationSchema = NavigationSchema;
@@ -5479,6 +6073,7 @@ exports.ProjectGraphSchema = ProjectGraphSchema;
5479
6073
  exports.ProjectInfoSchema = ProjectInfoSchema;
5480
6074
  exports.RequirementSchema = RequirementSchema;
5481
6075
  exports.SIO_CODE_RE = SIO_CODE_RE;
6076
+ exports.ScienceConceptExtensionSchema = ScienceConceptExtensionSchema;
5482
6077
  exports.ScreenSchema = ScreenSchema;
5483
6078
  exports.ShapeSchema = ShapeSchema;
5484
6079
  exports.StepEffortSchema = StepEffortSchema;
@@ -5499,6 +6094,8 @@ exports.WiringSchema = WiringSchema;
5499
6094
  exports.assembleRoadmap = assembleRoadmap;
5500
6095
  exports.assignConceptCodes = assignConceptCodes;
5501
6096
  exports.auditDepthLayers = auditDepthLayers;
6097
+ exports.auditSyllabusCoverage = auditSyllabusCoverage;
6098
+ exports.breakPrerequisiteCycles = breakPrerequisiteCycles;
5502
6099
  exports.cioActionSlug = cioActionSlug;
5503
6100
  exports.cioCode = cioCode;
5504
6101
  exports.conceptCodeFromName = conceptCodeFromName;
@@ -5506,6 +6103,7 @@ exports.detectDomain = detectDomain;
5506
6103
  exports.detectDomainProfile = detectDomainProfile;
5507
6104
  exports.detectInputType = detectInputType;
5508
6105
  exports.detectLearningMode = detectLearningMode;
6106
+ exports.detectPrerequisiteCycles = detectPrerequisiteCycles;
5509
6107
  exports.emitCurriculumFeed = emitCurriculumFeed;
5510
6108
  exports.escalateAndMapConcepts = escalateAndMapConcepts;
5511
6109
  exports.extractFeatureSteps = extractFeatureSteps;
@@ -5522,8 +6120,12 @@ exports.mergeTsResults = mergeTsResults;
5522
6120
  exports.parseCppFile = parseCppFile;
5523
6121
  exports.parsePythonFile = parsePythonFile;
5524
6122
  exports.parseSwiftFile = parseSwiftFile;
6123
+ exports.parseSyllabus = parseSyllabus;
5525
6124
  exports.parseTsFile = parseTsFile;
5526
6125
  exports.resolveConcepts = resolveConcepts;
6126
+ exports.runDomainPipeline = runDomainPipeline;
6127
+ exports.runHybridGraphPipeline = runHybridGraphPipeline;
6128
+ exports.runKnowledgeGraphPipeline = runKnowledgeGraphPipeline;
5527
6129
  exports.runProjectGraphPipeline = runProjectGraphPipeline;
5528
6130
  exports.scoreByContent = scoreByContent;
5529
6131
  exports.scoreByPath = scoreByPath;
@@ -5534,6 +6136,7 @@ exports.standardStatement = standardStatement;
5534
6136
  exports.techTagFor = techTagFor;
5535
6137
  exports.uloCode = uloCode;
5536
6138
  exports.validateCurriculumFeed = validateCurriculumFeed;
6139
+ exports.verifyKnowledgeGraph = verifyKnowledgeGraph;
5537
6140
  exports.verifyProjectGraph = verifyProjectGraph;
5538
6141
  //# sourceMappingURL=index.cjs.map
5539
6142
  //# sourceMappingURL=index.cjs.map