@thanh01.pmt/domain-kit 0.3.0 → 0.5.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -11
  3. package/dist/{chunk-MSRKUZLY.mjs → chunk-E4GCCWJO.mjs} +32 -2
  4. package/dist/chunk-E4GCCWJO.mjs.map +1 -0
  5. package/dist/{chunk-MCHHEBH3.mjs → chunk-G7HLFMD2.mjs} +5 -5
  6. package/dist/{chunk-MCHHEBH3.mjs.map → chunk-G7HLFMD2.mjs.map} +1 -1
  7. package/dist/{chunk-LKSVTRQE.mjs → chunk-TWKIVUTQ.mjs} +33 -3
  8. package/dist/chunk-TWKIVUTQ.mjs.map +1 -0
  9. package/dist/{chunk-Y7MRFFDJ.mjs → chunk-TYXSJUQU.mjs} +100 -9
  10. package/dist/chunk-TYXSJUQU.mjs.map +1 -0
  11. package/dist/curriculumFeedEmitter-T7NUBLAM.mjs +4 -0
  12. package/dist/{curriculumFeedEmitter-FYAWMPE6.mjs.map → curriculumFeedEmitter-T7NUBLAM.mjs.map} +1 -1
  13. package/dist/{curriculumFeedSchema-C8ZP3XMi.d.ts → curriculumFeedSchema-DUFwFyr2.d.cts} +196 -0
  14. package/dist/{curriculumFeedSchema-C8ZP3XMi.d.cts → curriculumFeedSchema-DUFwFyr2.d.ts} +196 -0
  15. package/dist/feed/index.cjs +60 -0
  16. package/dist/feed/index.cjs.map +1 -1
  17. package/dist/feed/index.d.cts +1 -1
  18. package/dist/feed/index.d.ts +1 -1
  19. package/dist/feed/index.mjs +2 -2
  20. package/dist/graph/index.cjs +98 -6
  21. package/dist/graph/index.cjs.map +1 -1
  22. package/dist/graph/index.d.cts +1 -1
  23. package/dist/graph/index.d.ts +1 -1
  24. package/dist/graph/index.mjs +1 -1
  25. package/dist/{hybridGraphPipeline-NZAWAPt_.d.ts → hybridGraphPipeline-DXQDbBEj.d.ts} +14 -2
  26. package/dist/{hybridGraphPipeline-BhFyh3cm.d.cts → hybridGraphPipeline-dT-bio-3.d.cts} +14 -2
  27. package/dist/index.cjs +158 -6
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.mjs +4 -4
  32. package/dist/pipeline/index.cjs +157 -6
  33. package/dist/pipeline/index.cjs.map +1 -1
  34. package/dist/pipeline/index.d.cts +2 -2
  35. package/dist/pipeline/index.d.ts +2 -2
  36. package/dist/pipeline/index.mjs +4 -4
  37. package/dist/schemas/index.cjs +30 -0
  38. package/dist/schemas/index.cjs.map +1 -1
  39. package/dist/schemas/index.d.cts +1 -1
  40. package/dist/schemas/index.d.ts +1 -1
  41. package/dist/schemas/index.mjs +1 -1
  42. package/package.json +9 -10
  43. package/dist/chunk-LKSVTRQE.mjs.map +0 -1
  44. package/dist/chunk-MSRKUZLY.mjs.map +0 -1
  45. package/dist/chunk-Y7MRFFDJ.mjs.map +0 -1
  46. package/dist/curriculumFeedEmitter-FYAWMPE6.mjs +0 -4
@@ -1,7 +1,7 @@
1
1
  import { L as LlmClientConfig } from '../llmClient-ysPhLjcH.cjs';
2
2
  import { d as Feature, c as Step } from '../projectGraphSchema-DnD7orZV.cjs';
3
3
  import { K as Keyword } from '../keywordExtractor-BxlGRMHC.cjs';
4
- export { H as Hallucination, i as HybridGraphPipelineOptions, j as HybridGraphPipelineResult, K as KnowledgeGraphPipelineOptions, a as KnowledgeGraphPipelineResult, f as KnowledgeGraphVerificationReport, V as VerifyOptions, e as auditSyllabusCoverage, c as breakPrerequisiteCycles, d as detectPrerequisiteCycles, h as generateHybridGraph, g as generateKnowledgeGraph, b as verifyKnowledgeGraph, v as verifyProjectGraph } from '../hybridGraphPipeline-BhFyh3cm.cjs';
4
+ export { H as Hallucination, j as HybridGraphPipelineOptions, k as HybridGraphPipelineResult, K as KnowledgeGraphPipelineOptions, a as KnowledgeGraphPipelineResult, f as KnowledgeGraphVerificationReport, V as VerifyOptions, i as auditPhasePlan, e as auditSyllabusCoverage, c as breakPrerequisiteCycles, d as detectPrerequisiteCycles, h as generateHybridGraph, g as generateKnowledgeGraph, b as verifyKnowledgeGraph, v as verifyProjectGraph } from '../hybridGraphPipeline-dT-bio-3.cjs';
5
5
  export { C as ConceptMapping, E as EscalateOptions, e as escalateAndMapConcepts } from '../conceptEscalator-ltRLtPhf.cjs';
6
6
  import 'zod';
7
7
  import '../cpp-CZUPqJt9.cjs';
@@ -1,7 +1,7 @@
1
1
  import { L as LlmClientConfig } from '../llmClient-ysPhLjcH.js';
2
2
  import { d as Feature, c as Step } from '../projectGraphSchema-DnD7orZV.js';
3
3
  import { K as Keyword } from '../keywordExtractor-DU5XRN8-.js';
4
- export { H as Hallucination, i as HybridGraphPipelineOptions, j as HybridGraphPipelineResult, K as KnowledgeGraphPipelineOptions, a as KnowledgeGraphPipelineResult, f as KnowledgeGraphVerificationReport, V as VerifyOptions, e as auditSyllabusCoverage, c as breakPrerequisiteCycles, d as detectPrerequisiteCycles, h as generateHybridGraph, g as generateKnowledgeGraph, b as verifyKnowledgeGraph, v as verifyProjectGraph } from '../hybridGraphPipeline-NZAWAPt_.js';
4
+ export { H as Hallucination, j as HybridGraphPipelineOptions, k as HybridGraphPipelineResult, K as KnowledgeGraphPipelineOptions, a as KnowledgeGraphPipelineResult, f as KnowledgeGraphVerificationReport, V as VerifyOptions, i as auditPhasePlan, e as auditSyllabusCoverage, c as breakPrerequisiteCycles, d as detectPrerequisiteCycles, h as generateHybridGraph, g as generateKnowledgeGraph, b as verifyKnowledgeGraph, v as verifyProjectGraph } from '../hybridGraphPipeline-DXQDbBEj.js';
5
5
  export { C as ConceptMapping, E as EscalateOptions, e as escalateAndMapConcepts } from '../conceptEscalator-YfMKIKCZ.js';
6
6
  import 'zod';
7
7
  import '../cpp-CZUPqJt9.js';
@@ -1,5 +1,5 @@
1
1
  import '../chunk-ZKLT27T4.mjs';
2
- export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName, detectPrerequisiteCycles, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, generateHybridGraph, generateKnowledgeGraph, sioCode, standardStatement, techTagFor, uloCode, verifyKnowledgeGraph, verifyProjectGraph } from '../chunk-Y7MRFFDJ.mjs';
2
+ export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditPhasePlan, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName, detectPrerequisiteCycles, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, generateHybridGraph, generateKnowledgeGraph, sioCode, standardStatement, techTagFor, uloCode, verifyKnowledgeGraph, verifyProjectGraph } from '../chunk-TYXSJUQU.mjs';
3
3
  import '../chunk-T4Y2DE2W.mjs';
4
4
  //# sourceMappingURL=index.mjs.map
5
5
  //# sourceMappingURL=index.mjs.map
@@ -1,6 +1,6 @@
1
1
  import { u as ProjectGraph } from './projectGraphSchema-DnD7orZV.js';
2
2
  import { L as LlmClientConfig } from './llmClient-ysPhLjcH.js';
3
- import { g as KnowledgeGraph, b as Concept, j as HybridGraph } from './hybridGraphSchema-nrUeSwpU.js';
3
+ import { g as KnowledgeGraph, b as Concept, j as HybridGraph, l as DevelopmentPhase } from './hybridGraphSchema-nrUeSwpU.js';
4
4
  import { P as ParsedSyllabus } from './syllabusParser-MSX1fEzW.js';
5
5
 
6
6
  /**
@@ -155,5 +155,17 @@ interface HybridGraphPipelineResult {
155
155
  warnings: string[];
156
156
  }
157
157
  declare function generateHybridGraph(options: HybridGraphPipelineOptions): Promise<HybridGraphPipelineResult>;
158
+ declare function auditPhasePlan(plan: Array<Record<string, unknown>>, features: Array<{
159
+ id: string;
160
+ }>, concepts: Array<{
161
+ id: string;
162
+ prerequisites?: string[];
163
+ }>, links: Array<{
164
+ feature_id: string;
165
+ concept_id: string;
166
+ }>, warnings: string[], log: (step: string, msg: string) => void): {
167
+ phases: DevelopmentPhase[];
168
+ errors: string[];
169
+ };
158
170
 
159
- export { type Hallucination as H, type KnowledgeGraphPipelineOptions as K, type VerifyOptions as V, type KnowledgeGraphPipelineResult as a, verifyKnowledgeGraph as b, breakPrerequisiteCycles as c, detectPrerequisiteCycles as d, auditSyllabusCoverage as e, type KnowledgeGraphVerificationReport as f, generateKnowledgeGraph as g, generateHybridGraph as h, type HybridGraphPipelineOptions as i, type HybridGraphPipelineResult as j, verifyProjectGraph as v };
171
+ export { type Hallucination as H, type KnowledgeGraphPipelineOptions as K, type VerifyOptions as V, type KnowledgeGraphPipelineResult as a, verifyKnowledgeGraph as b, breakPrerequisiteCycles as c, detectPrerequisiteCycles as d, auditSyllabusCoverage as e, type KnowledgeGraphVerificationReport as f, generateKnowledgeGraph as g, generateHybridGraph as h, auditPhasePlan as i, type HybridGraphPipelineOptions as j, type HybridGraphPipelineResult as k, verifyProjectGraph as v };
@@ -1,6 +1,6 @@
1
1
  import { u as ProjectGraph } from './projectGraphSchema-DnD7orZV.cjs';
2
2
  import { L as LlmClientConfig } from './llmClient-ysPhLjcH.cjs';
3
- import { g as KnowledgeGraph, b as Concept, j as HybridGraph } from './hybridGraphSchema-nrUeSwpU.cjs';
3
+ import { g as KnowledgeGraph, b as Concept, j as HybridGraph, l as DevelopmentPhase } from './hybridGraphSchema-nrUeSwpU.cjs';
4
4
  import { P as ParsedSyllabus } from './syllabusParser-MSX1fEzW.cjs';
5
5
 
6
6
  /**
@@ -155,5 +155,17 @@ interface HybridGraphPipelineResult {
155
155
  warnings: string[];
156
156
  }
157
157
  declare function generateHybridGraph(options: HybridGraphPipelineOptions): Promise<HybridGraphPipelineResult>;
158
+ declare function auditPhasePlan(plan: Array<Record<string, unknown>>, features: Array<{
159
+ id: string;
160
+ }>, concepts: Array<{
161
+ id: string;
162
+ prerequisites?: string[];
163
+ }>, links: Array<{
164
+ feature_id: string;
165
+ concept_id: string;
166
+ }>, warnings: string[], log: (step: string, msg: string) => void): {
167
+ phases: DevelopmentPhase[];
168
+ errors: string[];
169
+ };
158
170
 
159
- export { type Hallucination as H, type KnowledgeGraphPipelineOptions as K, type VerifyOptions as V, type KnowledgeGraphPipelineResult as a, verifyKnowledgeGraph as b, breakPrerequisiteCycles as c, detectPrerequisiteCycles as d, auditSyllabusCoverage as e, type KnowledgeGraphVerificationReport as f, generateKnowledgeGraph as g, generateHybridGraph as h, type HybridGraphPipelineOptions as i, type HybridGraphPipelineResult as j, verifyProjectGraph as v };
171
+ export { type Hallucination as H, type KnowledgeGraphPipelineOptions as K, type VerifyOptions as V, type KnowledgeGraphPipelineResult as a, verifyKnowledgeGraph as b, breakPrerequisiteCycles as c, detectPrerequisiteCycles as d, auditSyllabusCoverage as e, type KnowledgeGraphVerificationReport as f, generateKnowledgeGraph as g, generateHybridGraph as h, auditPhasePlan as i, type HybridGraphPipelineOptions as j, type HybridGraphPipelineResult as k, verifyProjectGraph as v };
package/dist/index.cjs CHANGED
@@ -88,6 +88,36 @@ var init_curriculumFeedSchema = __esm({
88
88
  references: zod.z.array(zod.z.object({ file: zod.z.string().min(1), evidence: zod.z.string().default("") })).default([]),
89
89
  feature_id: zod.z.string().nullable().default(null),
90
90
  concept_ref: zod.z.string().nullable().default(null),
91
+ // P52/T2.1: outcome.user_visible projected from the ProjectGraph step — the
92
+ // tangible checkpoint the session-cutting planner aims for (empty when the
93
+ // step declares no user-visible result). Concepts carry '' by default.
94
+ user_visible_deliverable: zod.z.string().default(""),
95
+ // P52/T3.1 — depth-reconcile inputs (optional/defaulted so old feeds
96
+ // validate unchanged):
97
+ // • depth_variants: minutes to teach this node at each depth level.
98
+ // ULO (know-it-exists) ≤ CIO (mechanism) ≤ SIO (implementation). All 3
99
+ // required so a downgrade never turns an anchor teach into a zero.
100
+ // • depth_scaffold_candidates: parallel to scaffold_candidates but
101
+ // acts on DEPTH (e.g. SIO→CIO) instead of lesson time.
102
+ // • is_core: concept is a Master-Tree core concept — reconciler must
103
+ // never downgrade it; escalate instead.
104
+ depth_variants: zod.z.object({ ulo: zod.z.number().int().nonnegative(), cio: zod.z.number().int().nonnegative(), sio: zod.z.number().int().nonnegative() }).optional(),
105
+ depth_scaffold_candidates: zod.z.array(zod.z.object({
106
+ from_depth: zod.z.enum(["ulo", "cio", "sio"]),
107
+ to_depth: zod.z.enum(["ulo", "cio", "sio"]),
108
+ minutes_saved: zod.z.number().int().nonnegative(),
109
+ reason: zod.z.string().default("")
110
+ })).optional(),
111
+ is_core: zod.z.boolean().default(false),
112
+ // Depth layer text (optional, additive — old feeds validate unchanged).
113
+ // Carries the ULO/CIO/SIO authored text from the KnowledgeGraph concept
114
+ // so downstream consumers (KX generator) can inject depth-appropriate
115
+ // material into their prompts without a second LLM call.
116
+ depth_context: zod.z.object({
117
+ ulo: zod.z.string().default(""),
118
+ cio: zod.z.string().default(""),
119
+ sio: zod.z.array(zod.z.string()).default([])
120
+ }).optional(),
91
121
  // Progressive-completion bookkeeping (feed v2, additive):
92
122
  // phase_id = development phase that introduces/uses this node;
93
123
  // introduce_aspect = how the phase teaches a concept node (first intro, advanced
@@ -172,6 +202,10 @@ function feedNodeFromStep(step, featureId) {
172
202
  references: arr(step.files).map((f) => ({ file: str(f), evidence: "step file" })).filter((f) => f.file),
173
203
  feature_id: featureId,
174
204
  concept_ref: null,
205
+ // P52/T2.1: carry the step's tangible outcome so the planner can cut
206
+ // sessions at user-visible checkpoints instead of raw minute counts.
207
+ user_visible_deliverable: str(step.outcome?.user_visible),
208
+ is_core: false,
175
209
  phase_id: "",
176
210
  introduce_aspect: null
177
211
  };
@@ -190,6 +224,20 @@ function deriveConceptDepthHint(concept) {
190
224
  if (str(concept.cio) || prereqs.length >= 1) return "cio";
191
225
  return "ulo";
192
226
  }
227
+ function deriveDepthInputs(minutes, depth) {
228
+ const ulo = Math.max(5, Math.round(minutes * 0.35));
229
+ const cio = Math.max(ulo + 5, Math.round(minutes * 0.65));
230
+ const sio = Math.max(cio + 5, minutes);
231
+ const variants = { ulo, cio, sio };
232
+ const candidates = [];
233
+ if (depth === "sio") {
234
+ candidates.push({ from_depth: "sio", to_depth: "cio", minutes_saved: sio - cio, reason: "SIO implementation practice deferred \u2014 CIO mechanism suffices for downstream consumers" });
235
+ }
236
+ if (depth === "cio" || depth === "sio") {
237
+ candidates.push({ from_depth: depth, to_depth: "ulo", minutes_saved: (depth === "sio" ? sio : cio) - ulo, reason: "Mechanism deferred \u2014 ULO awareness suffices (know-it-exists); never below the ULO floor" });
238
+ }
239
+ return { depth_variants: variants, depth_scaffold_candidates: candidates };
240
+ }
193
241
  function feedNodeFromConcept(concept) {
194
242
  const id = str(concept.id);
195
243
  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]);
@@ -219,6 +267,15 @@ function feedNodeFromConcept(concept) {
219
267
  references,
220
268
  feature_id: null,
221
269
  concept_ref: id,
270
+ user_visible_deliverable: "",
271
+ // concepts are taught, not shipped — checkpoints live on product steps
272
+ ...deriveDepthInputs(Math.max(0, Math.round(num(concept.estimated_minutes, 15))), deriveConceptDepthHint(concept)),
273
+ is_core: false,
274
+ depth_context: {
275
+ ulo: str(concept.ulo),
276
+ cio: str(concept.cio),
277
+ sio: strs(concept.sio)
278
+ },
222
279
  phase_id: "",
223
280
  introduce_aspect: null
224
281
  };
@@ -325,6 +382,8 @@ function emitFromHybrid(hybrid, opts) {
325
382
  prevNode.concept_codes = [str(c.code) || cid];
326
383
  prevNode.keywords = { all: strs(c.keywords), new: [], prerequisite: [] };
327
384
  prevNode.scaffolding = { scaffold_candidates: [], core_value_minutes: 0 };
385
+ prevNode.depth_variants = { ulo: prevNode.estimated_minutes, cio: prevNode.estimated_minutes, sio: prevNode.estimated_minutes };
386
+ prevNode.depth_scaffold_candidates = [];
328
387
  nodes.push(prevNode);
329
388
  conceptOrder.push(prevNode.id);
330
389
  continue;
@@ -349,6 +408,7 @@ function emitFromHybrid(hybrid, opts) {
349
408
  node.name = str(c.name, cid) + " (advanced)";
350
409
  node.phase_id = pid;
351
410
  node.introduce_aspect = "advanced";
411
+ node.depth_scaffold_candidates = [{ from_depth: "sio", to_depth: "cio", minutes_saved: Math.round(node.estimated_minutes * 0.3), reason: "advanced revisit may drop to CIO recap without breaking first-teach chain" }];
352
412
  node.estimated_minutes = Math.max(15, Math.round(node.estimated_minutes * 0.5));
353
413
  node.concept_codes = [str(c.code) || cid];
354
414
  edges.push({ from: cid, to: revisitId, kind: "knowledge", reason: "advanced revisit of " + cid + " at phase " + pid });
@@ -4670,7 +4730,42 @@ async function generateKnowledgeGraph(options) {
4670
4730
  const structuredTopicsGuide = parsedSyllabus && parsedSyllabus.allTopics.length > 0 ? `
4671
4731
  STRUCTURED SYLLABUS TOPICS (Every topic MUST map to at least one concept to ensure 100% syllabus coverage):
4672
4732
  ` + parsedSyllabus.allTopics.map((t) => `- [${t.unitTitle}] ${t.id}: ${t.title} (Keywords: ${t.keywords.join(", ")})`).join("\n") + "\n" : "";
4673
- 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";
4733
+ const systemPrompt = `You are an expert curriculum designer. Decompose the given subject into a structured knowledge graph with concepts, prerequisites, and problem types.
4734
+
4735
+ CRITICAL RULES:
4736
+ 1. Each concept MUST have the three depth layers, authored against this CONTRACT:
4737
+ - ulo (WHAT + WHY): bound to the CONCEPT's intrinsic NATURE. It explains what the
4738
+ concept is and why it exists \u2014 the problem it solves in principle. It must be
4739
+ TECHNOLOGY-AGNOSTIC: never name a concrete library, API, function, file format,
4740
+ tool or language feature. Typical assessment level: Remember / Understand.
4741
+ - cio (HOW): the MECHANISM \u2014 how the concept works and solves that problem, step
4742
+ by step. Still bound to the concept's nature: more concrete than the ULO, but
4743
+ NOT tied to one specific technology (no concrete API names). CIO capability is
4744
+ assessed INDIRECTLY, through completing contextual work, not in isolation.
4745
+ - sio (IMPLEMENTATION): an array of 2-4 items. EVERY item MUST reference concrete
4746
+ KEYWORDS/technologies (a real API, function, module, file, or named pattern) and
4747
+ describe a completable, context-specific implementation task. Application,
4748
+ Analysis and Creation skills are assessed INDIRECTLY through completing these
4749
+ items. There is NO fixed Bloom ceiling for a layer \u2014 but concreteness MUST
4750
+ strictly increase: SIO more technology-bound than CIO, CIO more concrete than ULO.
4751
+ - prerequisites: concept IDs that must be learned first (MUST NOT create cycles)
4752
+ - problem_types: 2-4 problem types with bloom_level and difficulty. Remember and
4753
+ Understand problem types test the ULO/CIO directly; Apply-and-above types are
4754
+ proxies for SIO completion in context.
4755
+ - techniques: methods, formulas, tools
4756
+ 2. Prerequisite chains MUST be acyclic (no concept can depend on itself or create a loop)
4757
+ 3. Concepts should be ordered by prerequisite dependency (topological sort)
4758
+ 4. Problem types should range from Remember to Apply (not all at same level)
4759
+ 5. Group concepts into categories (chapters/sections)
4760
+ 6. ALL text in ENGLISH (even if input is Vietnamese)
4761
+ 7. NEVER invent concepts not present in the syllabus
4762
+ 8. KEYWORDS: Each concept MUST include 5-10 domain-specific keywords that:
4763
+ - Are concrete terms a student encounters when studying THIS concept
4764
+ - Include at least 2 technology-specific terms (APIs, functions, types, tools)
4765
+ - Are DISTINCT from keywords of other concepts (minimal overlap)
4766
+ - NEVER generic English words (e.g. "view", "data", "code" are TOO generic)
4767
+ - Example for "SwiftUI Layout": ["VStack", "HStack", "ZStack", ".padding()", ".frame()", "alignment", "Spacer", "LazyVGrid"]
4768
+ 9. Return JSON matching the KnowledgeGraphSchema`;
4674
4769
  const userPrompt = `Subject: ${subject}
4675
4770
  ${description ? `Description: ${description}` : ""}
4676
4771
  ${structuredTopicsGuide}${truncatedSyllabus ? `Syllabus:
@@ -4691,8 +4786,9 @@ Return JSON:
4691
4786
  {
4692
4787
  "id": "C1",
4693
4788
  "name": "Concept name",
4694
- "description": "Brief description",
4789
+ "description": "2-3 sentence description: what this concept covers, why a learner encounters it, and what mastering it enables. Must be specific enough that a student reading only this description understands the concept's scope.",
4695
4790
  "category": "category_id",
4791
+ "keywords": ["specific_term_1", "concrete_api_2", "method_call_3", "tool_or_type_4", "syntax_keyword_5", "concept_term_6"],
4696
4792
  "ulo": "WHAT + WHY in technology-agnostic language: what the concept is and the problem it exists to solve. No library/API/tool names.",
4697
4793
  "cio": "HOW the mechanism works, still technology-agnostic but more concrete than the ULO.",
4698
4794
  "sio": ["Concrete task naming a REAL API/function/module/file from the target technology, completable end to end", "Another technology-bound task using a different concrete keyword"],
@@ -4733,18 +4829,23 @@ Return JSON:
4733
4829
  log("STEP_2", "Validating and cleaning knowledge graph...");
4734
4830
  const concepts = result.concepts || [];
4735
4831
  const categories = result.categories || [];
4832
+ for (const c of concepts) {
4833
+ if ((c.description || "").length < 50) {
4834
+ warnings.push("Concept " + c.id + ": description too short (" + (c.description || "").length + " chars) \u2014 may produce shallow KX narratives");
4835
+ }
4836
+ }
4736
4837
  {
4737
4838
  const STOP = /* @__PURE__ */ new Set(["and", "the", "for", "with", "from", "that", "this", "into", "when", "while", "using", "their", "your", "each", "how", "why", "what", "between", "through", "about"]);
4738
4839
  const tokensFrom = (text) => text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP.has(w));
4739
4840
  for (const c of concepts) {
4740
4841
  const existing = (c.keywords || []).map((k) => String(k).trim()).filter(Boolean);
4741
4842
  if (existing.length === 0) {
4742
- const derived = [.../* @__PURE__ */ new Set([...tokensFrom(c.name), ...tokensFrom(c.description || "")])].slice(0, 5);
4843
+ const derived = [.../* @__PURE__ */ new Set([...tokensFrom(c.name), ...tokensFrom(c.description || "")])].slice(0, 8);
4743
4844
  if (derived.length === 0) {
4744
4845
  throw new Error("Concept " + c.id + " (" + c.name + ") has no keywords and none could be derived from name/description \u2014 fail-closed");
4745
4846
  }
4746
4847
  c.keywords = derived;
4747
- warnings.push("Concept " + c.id + ": empty keyword set - derived " + derived.join(", ") + " from name/description");
4848
+ warnings.push("Concept " + c.id + ": empty keyword set - derived " + derived.join(", ") + " from name/description (WARNING: LLM returned no keywords)");
4748
4849
  } else {
4749
4850
  c.keywords = existing;
4750
4851
  }
@@ -4869,6 +4970,52 @@ Return JSON:
4869
4970
  }
4870
4971
  log("STEP_5", "Depth layers authored: " + concepts.length + " concepts, audit " + lo.audits.filter((a) => a.passed).length + "/" + concepts.length + " passed");
4871
4972
  }
4973
+ {
4974
+ for (const concept of concepts) {
4975
+ const sioText = (concept.sio || []).join(" ");
4976
+ const techTokens = [];
4977
+ let m;
4978
+ const techTokenRe = /`([^`]+)`/g;
4979
+ while ((m = techTokenRe.exec(sioText)) !== null) {
4980
+ if (m[1].length >= 3 && m[1].length <= 40) techTokens.push(m[1]);
4981
+ }
4982
+ const annotationRe = /(?:^|[\s(])(@[A-Za-z]\w*)/g;
4983
+ while ((m = annotationRe.exec(sioText)) !== null) techTokens.push(m[1]);
4984
+ const dotRe = /\b\w+\.\w+\(/g;
4985
+ while ((m = dotRe.exec(sioText)) !== null) techTokens.push(m[0].replace("(", ""));
4986
+ const modifierRe = /(?:^|[\s(])(\.[a-z]\w*)\b/g;
4987
+ while ((m = modifierRe.exec(sioText)) !== null) techTokens.push(m[1]);
4988
+ const camelRe = /\b[a-z]+[A-Z][A-Za-z]+\b/g;
4989
+ while ((m = camelRe.exec(sioText)) !== null) techTokens.push(m[0]);
4990
+ const existing = new Set((concept.keywords || []).map((k) => k.toLowerCase()));
4991
+ const newTokens = [...new Set(techTokens)].filter((t) => !existing.has(t.toLowerCase())).slice(0, 8);
4992
+ if (newTokens.length > 0) {
4993
+ concept.keywords = [...concept.keywords || [], ...newTokens];
4994
+ warnings.push("Concept " + concept.id + ": enriched keywords with " + newTokens.length + " SIO-derived tokens");
4995
+ }
4996
+ }
4997
+ log("STEP_5b", "SIO-keyword enrichment complete");
4998
+ }
4999
+ {
5000
+ for (const concept of concepts) {
5001
+ const sioCount = (concept.sio || []).length;
5002
+ const ptCount = (concept.problem_types || []).length;
5003
+ const prereqCount = (concept.prerequisites || []).length;
5004
+ const hasHighBloom = (concept.problem_types || []).some(
5005
+ (pt) => ["Analyze", "Evaluate", "Create"].includes(pt.bloom_level || "")
5006
+ );
5007
+ const score = Math.min(1, sioCount * 0.2 + ptCount * 0.15 + prereqCount * 0.15 + (hasHighBloom ? 0.2 : 0));
5008
+ const calibrated = Math.round(15 + score * 45);
5009
+ const llmEstimate = concept.estimated_minutes || 30;
5010
+ if (llmEstimate === 30 || Math.abs(calibrated - llmEstimate) / llmEstimate > 0.5) {
5011
+ concept.estimated_minutes = calibrated;
5012
+ if (llmEstimate !== calibrated) {
5013
+ warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m (score=" + score.toFixed(2) + ")");
5014
+ }
5015
+ }
5016
+ }
5017
+ log("STEP_5c", "Minute calibration complete");
5018
+ }
4872
5019
  for (const step of learningPath) {
4873
5020
  if (!conceptIds.has(step.concept_id)) {
4874
5021
  warnings.push(`Learning path references non-existent concept ${step.concept_id}`);
@@ -5134,8 +5281,12 @@ async function decomposePhases(args) {
5134
5281
  const linkList = links.map(
5135
5282
  (l) => "- " + l.feature_id + " -> " + l.concept_id + " (" + l.depth + ")"
5136
5283
  ).join("\n") || "(none)";
5137
- const systemPrompt = 'You decompose a software project build into PROGRESSIVE COMPLETION PHASES for learning purposes.\n\nA phase is NOT a feature and NOT a milestone list: it is a cross-feature slice of the\nproduct whose completion yields a DEMONSTRABLY more complete product. Build up gradually:\nthe product grows feature by feature across phases, and each phase adds its features to\nwhat previous phases already built.\n\nRULES:\n1. Every feature must be assigned to EXACTLY ONE phase (no feature reused, none skipped).\n2. Phases are ordered; later phases depend on earlier ones and yield a more complete product.\n3. product_completion describes concretely what the product can DO once the phase is done.\n4. introduces lists which concepts the phase must teach BEFORE its feature work starts:\n - aspect new: first time the concept appears in the course\n - aspect advanced: a concept introduced in an EARLIER phase, revisited here at a\n higher depth (an advanced aspect of the same concept, e.g. validation rules on top\n of basic data modeling). Use the note field to say which aspect.\n - aspect preview: a light awareness pass ("know it exists", ULO depth only, ~5 min)\n placed in an EARLIER phase when its feature work needs the vocabulary before the\n full teach. The anchor phase still owns the full new introduction.\n5. A concept MUST be introduced (new) no later than the first phase that consumes it.\n6. A concept must not be introduced before its own prerequisites are introduced.\n7. Typically 3-6 phases for a small project; keep phases balanced.\n8. Phase 1 usually covers project setup/foundation features plus the first minimal slice.\n9. Return JSON only.';
5138
- const baseUserPrompt = "FEATURES:\n" + featureList + "\n\nCONCEPTS:\n" + conceptList + "\n\nFEATURE-CONCEPT LINKS:\n" + linkList + "\n\nReturn JSON with a phases array. Each phase: { id: PH1, name, product_completion, feature_ids: [F0, F1], introduces: [{ concept_id: C1, aspect: new or advanced, note }] }.\n";
5284
+ const systemPrompt = 'You decompose a software project build into PROGRESSIVE COMPLETION PHASES for learning purposes.\n\nA phase is NOT a feature and NOT a milestone list: it is a cross-feature slice of the\nproduct whose completion yields a DEMONSTRABLY more complete product. Build up gradually:\nthe product grows feature by feature across phases, and each phase adds its features to\nwhat previous phases already built.\n\nRULES:\n1. Every feature must be assigned to EXACTLY ONE phase (no feature reused, none skipped).\n2. Phases are ordered; later phases depend on earlier ones and yield a more complete product.\n3. product_completion describes concretely what the product can DO once the phase is done.\n4. introduces lists which concepts the phase must teach BEFORE its feature work starts:\n - aspect new: first time the concept appears in the course\n - aspect advanced: a concept introduced in an EARLIER phase, revisited here at a\n higher depth (an advanced aspect of the same concept, e.g. validation rules on top\n of basic data modeling). Use the note field to say which aspect.\n - aspect preview: a light awareness pass ("know it exists", ULO depth only, ~5 min)\n placed in an EARLIER phase when its feature work needs the vocabulary before the\n full teach. The anchor phase still owns the full new introduction.\n5. A concept MUST be introduced (new) no later than the first phase that consumes it.\n6. A concept must not be introduced before its own prerequisites are introduced.\n7. Typically 3-6 phases for a small project; keep phases balanced.\n8. MINUTE BALANCE: Each phase should contain approximately 120-240 total minutes of\n concept + feature work (about 3-5 sessions of 90 minutes each). If a phase\n would contain < 60 minutes (too thin) or > 400 minutes (too heavy), rebalance.\n9. Phase 1 usually covers project setup/foundation features plus the first minimal slice.\n10. Return JSON only.';
5285
+ const totalFeatureMinutes = features.reduce((s, f) => s + (f.steps || []).reduce((ss, st) => ss + (st.effort?.estimated_minutes || 15), 0), 0);
5286
+ const totalConceptMinutes = concepts.reduce((s, c) => s + (c.estimated_minutes || 30), 0);
5287
+ const totalMinutes = totalFeatureMinutes + totalConceptMinutes;
5288
+ const targetPhaseCount = Math.max(3, Math.round(totalMinutes / 180));
5289
+ const baseUserPrompt = "FEATURES:\n" + featureList + "\n\nCONCEPTS:\n" + conceptList + "\n\nFEATURE-CONCEPT LINKS:\n" + linkList + "\n\nTOTAL MINUTES: ~" + totalMinutes + "m (" + totalFeatureMinutes + "m features + " + totalConceptMinutes + "m concepts)\nTARGET: ~" + targetPhaseCount + " phases of ~180m each\n\nReturn JSON with a phases array. Each phase: { id: PH1, name, product_completion, feature_ids: [F0, F1], introduces: [{ concept_id: C1, aspect: new or advanced, note }] }.\n";
5139
5290
  const MAX_DECOMPOSE_ATTEMPTS = 3;
5140
5291
  let auditErrors = [];
5141
5292
  let phases = [];
@@ -6205,6 +6356,7 @@ exports.WiringSchema = WiringSchema;
6205
6356
  exports.assembleRoadmap = assembleRoadmap;
6206
6357
  exports.assignConceptCodes = assignConceptCodes;
6207
6358
  exports.auditDepthLayers = auditDepthLayers;
6359
+ exports.auditPhasePlan = auditPhasePlan;
6208
6360
  exports.auditSyllabusCoverage = auditSyllabusCoverage;
6209
6361
  exports.breakPrerequisiteCycles = breakPrerequisiteCycles;
6210
6362
  exports.cioActionSlug = cioActionSlug;