@thanh01.pmt/domain-kit 0.5.0 → 0.6.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 (69) hide show
  1. package/README.md +682 -168
  2. package/dist/assembly/index.d.cts +2 -2
  3. package/dist/assembly/index.d.ts +2 -2
  4. package/dist/{chunk-TYXSJUQU.mjs → chunk-2OGNQUXC.mjs} +81 -32
  5. package/dist/chunk-2OGNQUXC.mjs.map +1 -0
  6. package/dist/{chunk-G7HLFMD2.mjs → chunk-5OMSYKQP.mjs} +43 -23
  7. package/dist/chunk-5OMSYKQP.mjs.map +1 -0
  8. package/dist/{chunk-BL5NBKPH.mjs → chunk-67GXK3ZE.mjs} +18 -8
  9. package/dist/chunk-67GXK3ZE.mjs.map +1 -0
  10. package/dist/{chunk-E4GCCWJO.mjs → chunk-CGNXGVHV.mjs} +37 -4
  11. package/dist/chunk-CGNXGVHV.mjs.map +1 -0
  12. package/dist/{chunk-VCPIWJ7R.mjs → chunk-ERDTHXQA.mjs} +21 -3
  13. package/dist/chunk-ERDTHXQA.mjs.map +1 -0
  14. package/dist/chunk-OYSZQVPY.mjs +10 -0
  15. package/dist/chunk-OYSZQVPY.mjs.map +1 -0
  16. package/dist/{chunk-TWKIVUTQ.mjs → chunk-WKTH3JPA.mjs} +197 -36
  17. package/dist/chunk-WKTH3JPA.mjs.map +1 -0
  18. package/dist/{conceptEscalator-YfMKIKCZ.d.ts → conceptEscalator-BCsw0ys_.d.ts} +10 -1
  19. package/dist/{conceptEscalator-ltRLtPhf.d.cts → conceptEscalator-DPrs4PwC.d.cts} +10 -1
  20. package/dist/concepts/index.d.cts +1 -1
  21. package/dist/concepts/index.d.ts +1 -1
  22. package/dist/{curriculumFeedSchema-DUFwFyr2.d.ts → curriculumFeedSchema-DYayelAA.d.cts} +156 -1
  23. package/dist/{curriculumFeedSchema-DUFwFyr2.d.cts → curriculumFeedSchema-DYayelAA.d.ts} +156 -1
  24. package/dist/detector/index.cjs +21 -6
  25. package/dist/detector/index.cjs.map +1 -1
  26. package/dist/detector/index.mjs +2 -1
  27. package/dist/feed/index.cjs +229 -33
  28. package/dist/feed/index.cjs.map +1 -1
  29. package/dist/feed/index.d.cts +32 -2
  30. package/dist/feed/index.d.ts +32 -2
  31. package/dist/feed/index.mjs +2 -2
  32. package/dist/graph/index.cjs +73 -29
  33. package/dist/graph/index.cjs.map +1 -1
  34. package/dist/graph/index.d.cts +14 -5
  35. package/dist/graph/index.d.ts +14 -5
  36. package/dist/graph/index.mjs +2 -1
  37. package/dist/{hybridGraphPipeline-DXQDbBEj.d.ts → hybridGraphPipeline-9dRKJj7r.d.ts} +6 -2
  38. package/dist/{hybridGraphPipeline-dT-bio-3.d.cts → hybridGraphPipeline-CocfYe4d.d.cts} +6 -2
  39. package/dist/{hybridGraphSchema-nrUeSwpU.d.ts → hybridGraphSchema-C37qxdTM.d.cts} +16 -3
  40. package/dist/{hybridGraphSchema-nrUeSwpU.d.cts → hybridGraphSchema-C37qxdTM.d.ts} +16 -3
  41. package/dist/index.cjs +2634 -2359
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +6 -6
  44. package/dist/index.d.ts +6 -6
  45. package/dist/index.mjs +7 -6
  46. package/dist/llmClient-CX5uUiQ1.d.cts +60 -0
  47. package/dist/llmClient-CX5uUiQ1.d.ts +60 -0
  48. package/dist/pipeline/index.cjs +2207 -1958
  49. package/dist/pipeline/index.cjs.map +1 -1
  50. package/dist/pipeline/index.d.cts +13 -5
  51. package/dist/pipeline/index.d.ts +13 -5
  52. package/dist/pipeline/index.mjs +6 -5
  53. package/dist/schemas/index.cjs +55 -2
  54. package/dist/schemas/index.cjs.map +1 -1
  55. package/dist/schemas/index.d.cts +2 -2
  56. package/dist/schemas/index.d.ts +2 -2
  57. package/dist/schemas/index.mjs +2 -2
  58. package/package.json +10 -9
  59. package/LICENSE +0 -21
  60. package/dist/chunk-BL5NBKPH.mjs.map +0 -1
  61. package/dist/chunk-E4GCCWJO.mjs.map +0 -1
  62. package/dist/chunk-G7HLFMD2.mjs.map +0 -1
  63. package/dist/chunk-TWKIVUTQ.mjs.map +0 -1
  64. package/dist/chunk-TYXSJUQU.mjs.map +0 -1
  65. package/dist/chunk-VCPIWJ7R.mjs.map +0 -1
  66. package/dist/curriculumFeedEmitter-T7NUBLAM.mjs +0 -4
  67. package/dist/curriculumFeedEmitter-T7NUBLAM.mjs.map +0 -1
  68. package/dist/llmClient-ysPhLjcH.d.cts +0 -16
  69. package/dist/llmClient-ysPhLjcH.d.ts +0 -16
@@ -1,7 +1,7 @@
1
1
  import { u as ProjectGraph, c as Step } from '../projectGraphSchema-DnD7orZV.cjs';
2
- import { C as ConceptMapping } from '../conceptEscalator-ltRLtPhf.cjs';
2
+ import { C as ConceptMapping } from '../conceptEscalator-DPrs4PwC.cjs';
3
3
  import 'zod';
4
- import '../llmClient-ysPhLjcH.cjs';
4
+ import '../llmClient-CX5uUiQ1.cjs';
5
5
 
6
6
  /**
7
7
  * Roadmap assembly — assembles final roadmap with keyword tracking and time awareness.
@@ -1,7 +1,7 @@
1
1
  import { u as ProjectGraph, c as Step } from '../projectGraphSchema-DnD7orZV.js';
2
- import { C as ConceptMapping } from '../conceptEscalator-YfMKIKCZ.js';
2
+ import { C as ConceptMapping } from '../conceptEscalator-BCsw0ys_.js';
3
3
  import 'zod';
4
- import '../llmClient-ysPhLjcH.js';
4
+ import '../llmClient-CX5uUiQ1.js';
5
5
 
6
6
  /**
7
7
  * Roadmap assembly — assembles final roadmap with keyword tracking and time awareness.
@@ -1,9 +1,21 @@
1
1
  import { parseSyllabus } from './chunk-T4Y2DE2W.mjs';
2
+ import { stripDiacritics } from './chunk-OYSZQVPY.mjs';
2
3
  import { readFileSync, mkdirSync, writeFileSync } from 'fs';
3
4
  import { tmpdir } from 'os';
4
5
  import { join } from 'path';
5
6
 
6
7
  // src/utils/llmClient.ts
8
+ function summarizeProvenance(callLog) {
9
+ const providers = [];
10
+ const models = [];
11
+ let failed = 0;
12
+ for (const r of callLog) {
13
+ if (!providers.includes(r.provider)) providers.push(r.provider);
14
+ if (!models.includes(r.model)) models.push(r.model);
15
+ if (!r.ok) failed++;
16
+ }
17
+ return { providers, models, calls: callLog.length, failed_calls: failed, degraded: failed > 0 };
18
+ }
7
19
  var LlmClientError = class extends Error {
8
20
  constructor(message, status, cause) {
9
21
  super(message);
@@ -36,9 +48,10 @@ function resolveProviderChain(config) {
36
48
  return chain;
37
49
  }
38
50
  var MIN_REQUEST_INTERVAL_MS = 2500;
51
+ var requestIntervalMs = Number(process.env.LLM_MIN_REQUEST_INTERVAL_MS || MIN_REQUEST_INTERVAL_MS);
39
52
  var lastRequestAt = 0;
40
53
  async function pacedDelay() {
41
- const wait = lastRequestAt + MIN_REQUEST_INTERVAL_MS - Date.now();
54
+ const wait = lastRequestAt + requestIntervalMs - Date.now();
42
55
  if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
43
56
  lastRequestAt = Date.now();
44
57
  }
@@ -53,6 +66,10 @@ function createLlmClient(config) {
53
66
  primary.apiKey;
54
67
  primary.baseUrl;
55
68
  const model = primary.model;
69
+ if (config?.minRequestIntervalMs !== void 0) {
70
+ requestIntervalMs = Math.max(0, config.minRequestIntervalMs);
71
+ }
72
+ const callLog = [];
56
73
  async function chat(messages, options) {
57
74
  const temperature = options?.temperature ?? config?.temperature ?? 0.1;
58
75
  const maxTokens = options?.maxTokens ?? config?.maxTokens ?? parseInt(process.env.LLM_MAX_TOKENS || "65536", 10);
@@ -67,6 +84,7 @@ function createLlmClient(config) {
67
84
  await new Promise((resolve) => setTimeout(resolve, 2e4));
68
85
  }
69
86
  for (const provider of chain) {
87
+ const recIdx = callLog.push({ provider: provider.label, model: provider.model, ok: false }) - 1;
70
88
  try {
71
89
  const headers = {
72
90
  "Content-Type": "application/json",
@@ -127,6 +145,7 @@ function createLlmClient(config) {
127
145
  const retryData = await retry.json();
128
146
  const retryContent2 = retryData.choices?.[0]?.message?.content || "";
129
147
  if (!retryContent2.trim()) throw err;
148
+ callLog[recIdx] = { ...callLog[recIdx], ok: true };
130
149
  return {
131
150
  content: retryContent2,
132
151
  model: retryData.model || provider.model,
@@ -155,6 +174,7 @@ function createLlmClient(config) {
155
174
  if (!retry.ok) throw err;
156
175
  const retryData = await retry.json();
157
176
  const retryContent = retryData.choices?.[0]?.message?.content || "";
177
+ callLog[recIdx] = { ...callLog[recIdx], ok: true };
158
178
  return {
159
179
  content: retryContent,
160
180
  model: retryData.model || provider.model,
@@ -185,6 +205,7 @@ function createLlmClient(config) {
185
205
  };
186
206
  } catch (err) {
187
207
  const msg = err instanceof Error ? err.message : String(err);
208
+ callLog[recIdx] = { ...callLog[recIdx], ok: false, error: msg.slice(0, 200) };
188
209
  attempts.push(`${provider.label}: ${msg}`);
189
210
  lastError = err instanceof LlmClientError ? err : new LlmClientError(`[${provider.label}] ${msg}`);
190
211
  }
@@ -194,7 +215,7 @@ function createLlmClient(config) {
194
215
  }
195
216
  throw lastError ?? new LlmClientError("All LLM providers failed after " + MAX_CHAIN_ROUNDS + " rounds.");
196
217
  }
197
- return { chat, model };
218
+ return { chat, model, callLog };
198
219
  }
199
220
  function parseJsonFromLlm(text) {
200
221
  let cleaned = text.trim();
@@ -258,8 +279,8 @@ async function llmChatJson(client, systemPrompt, userPrompt, options) {
258
279
 
259
280
  // src/graph/scaffoldExtractor.ts
260
281
  async function extractScaffold(options) {
261
- const { goal, techStack, fileList, llmConfig } = options;
262
- const client = createLlmClient(llmConfig);
282
+ const { goal, techStack, fileList, llmConfig, sharedClient } = options;
283
+ const client = sharedClient ?? createLlmClient(llmConfig);
263
284
  const fileListStr = fileList.length > 0 ? fileList.join("\n") : "(empty)";
264
285
  const systemPrompt = "You are a programming pedagogy expert. For ONE concrete project, extract the 'FOUNDATION & SETUP' feature (id F0) \u2014 the COMPLETE set of 'MUST-KNOW' steps the learner needs before studying the first feature. Break it into concrete ACTION steps (5-8 steps), each with completion_level='base'. Include:\n1. Tools required for this tech stack (IDE, simulator/emulator, terminal, git, package manager) + basic operations (open project, build, run, debug, commit).\n2. Create the initial project: from a template OR clone/open the base-project (the provided repo).\n3. MINIMAL programming knowledge of the language (variables, types, functions, if/for, view declaration) + build one small demo (e.g. a single-screen Hello World app) BEFORE touching the real project.\n4. Repo map of the actual project: folder structure, entry point, how to build/run, main architecture (MVVM/Flux...), important packages/modules.\n5. Development loop (build->run->see result->debug), how to read/fix basic compile errors, minimal git workflow (clone/branch/commit/push).\nkeywords[]: real tool/language terms (e.g. 'Xcode', 'Simulator', 'Git', 'Swift', '@main', 'MVVM'). files[]: leave [] or use real paths if the step touches specific files. description: ONE concise sentence, MAX 140 characters. intent: WHY this step matters. outcome: {user_visible: string, technical: string}. acceptance[]: 2 verifiable criteria. effort: {estimated_minutes: number, complexity: low|medium|high}. NEVER invent. ALL text must be in ENGLISH (technical terms stay as-is). Return JSON containing only feature F0.";
265
286
  const userPrompt = `App goal: ${goal}
@@ -344,8 +365,8 @@ ${content}`);
344
365
  return blocks.join("\n\n");
345
366
  }
346
367
  async function extractProjectOverview(options) {
347
- const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig } = options;
348
- const client = createLlmClient(llmConfig);
368
+ const { goal, techStack, sdkApiIndex, fileContentsMap, astKeywords, llmConfig, sharedClient } = options;
369
+ const client = sharedClient ?? createLlmClient(llmConfig);
349
370
  const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
350
371
  const allFilesStr = buildSourceText(fileContentsMap);
351
372
  let keywordsAnchor = "";
@@ -414,8 +435,8 @@ ${content}`);
414
435
  return blocks.join("\n\n");
415
436
  }
416
437
  async function extractFeatureSteps(options) {
417
- const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig } = options;
418
- const client = createLlmClient(llmConfig);
438
+ const { goal, techStack, sdkApiIndex, fileContentsMap, feature, llmConfig, sharedClient } = options;
439
+ const client = sharedClient ?? createLlmClient(llmConfig);
419
440
  const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
420
441
  const allFilesStr = buildSourceText2(fileContentsMap);
421
442
  const fid = feature.id || "F_UNK";
@@ -482,8 +503,8 @@ Split this feature into 2-6 action steps. Return JSON:
482
503
  concept_codes: []
483
504
  }));
484
505
  }
485
- async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig) {
486
- const client = createLlmClient(llmConfig);
506
+ async function extractFeatureStepsBatched(goal, techStack, sdkApiIndex, fileContentsMap, featuresMeta, llmConfig, sharedClient) {
507
+ const client = sharedClient ?? createLlmClient(llmConfig);
487
508
  const usedSdkApis = sdkApiIndex.sdk_apis.filter((api) => api.used_in_demo).map((api) => api.name).slice(0, 2e3);
488
509
  const sourceText = buildSourceText2(fileContentsMap);
489
510
  const featuresListing = featuresMeta.map(
@@ -750,7 +771,7 @@ function inferDepthFromContext(keyword, fileContentsMap) {
750
771
  return { depth: "sio", rationale: `Default: treating '${keyword}' as SIO (implementation-level)` };
751
772
  }
752
773
  async function escalateAndMapConcepts(options) {
753
- const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig } = options;
774
+ const { projectGraph, fileContentsMap, availableConcepts, conceptMapOverride, llmConfig, sharedClient } = options;
754
775
  const features = projectGraph.features || [];
755
776
  const allTerms = /* @__PURE__ */ new Set();
756
777
  for (const feature of features) {
@@ -766,7 +787,7 @@ async function escalateAndMapConcepts(options) {
766
787
  llmResults = conceptMapOverride;
767
788
  } else {
768
789
  try {
769
- const client = createLlmClient(llmConfig);
790
+ const client = sharedClient ?? createLlmClient(llmConfig);
770
791
  const conceptBank = (availableConcepts || []).sort().join(", ") || "(empty)";
771
792
  const termEntries = termList.map((term) => {
772
793
  const snippets = extractTermSnippets(term, fileContentsMap);
@@ -819,13 +840,16 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
819
840
  const cName = info.concept_name || kw;
820
841
  let depth = "sio";
821
842
  let depthRationale = "Default: SIO";
843
+ let depthSource = "override";
822
844
  if (info.depth && ["ulo", "cio", "sio"].includes(info.depth)) {
823
845
  depth = info.depth;
824
846
  depthRationale = `LLM classified as ${depth}`;
847
+ depthSource = "llm";
825
848
  } else {
826
849
  const inferred = inferDepthFromContext(kw, fileContentsMap);
827
850
  depth = inferred.depth;
828
851
  depthRationale = inferred.rationale;
852
+ depthSource = "heuristic";
829
853
  }
830
854
  const kwKey = `${cCode}::${kw}`;
831
855
  if (seen.has(kwKey)) continue;
@@ -843,6 +867,7 @@ Return JSON: {"results": {"<keyword>": {"concept_code": "...", "concept_name": "
843
867
  keyword: kw,
844
868
  depth,
845
869
  depth_rationale: depthRationale,
870
+ depth_source: depthSource,
846
871
  evidence_files: evidenceFiles
847
872
  });
848
873
  }
@@ -1545,7 +1570,7 @@ function loadKnowledgeTreeCatalog(tsvPath) {
1545
1570
  }
1546
1571
  function tokens(s) {
1547
1572
  return new Set(
1548
- s.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4)
1573
+ stripDiacritics(s).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4)
1549
1574
  );
1550
1575
  }
1551
1576
  function filterBySelection(catalog, selectedCategories, selectedTopics, analysisConcepts, maxConcepts = 120) {
@@ -1719,6 +1744,15 @@ async function standardizeConcepts(opts) {
1719
1744
  }
1720
1745
 
1721
1746
  // src/graph/knowledgeGraphPipeline.ts
1747
+ var MIN_CALIBRATION_MINUTES = 15;
1748
+ var MAX_CALIBRATION_MINUTES = 60;
1749
+ var DEFAULT_CONCEPT_MINUTES = 30;
1750
+ var MIN_CALIBRATION_DIVERGENCE = 0.5;
1751
+ function calibrateConceptMinutes(sioCount, problemTypeCount, prereqCount, hasHighBloom) {
1752
+ const ratio = (value, cap) => Math.min(1, Math.max(0, value) / cap);
1753
+ const score = 0.3 * ratio(sioCount, 4) + 0.25 * ratio(problemTypeCount, 4) + 0.2 * ratio(prereqCount, 4) + 0.25 * (hasHighBloom ? 1 : 0);
1754
+ return Math.round(MIN_CALIBRATION_MINUTES + score * (MAX_CALIBRATION_MINUTES - MIN_CALIBRATION_MINUTES));
1755
+ }
1722
1756
  function topologicalSort(concepts) {
1723
1757
  const inDegree = /* @__PURE__ */ new Map();
1724
1758
  const adjacency = /* @__PURE__ */ new Map();
@@ -1884,7 +1918,7 @@ Return JSON:
1884
1918
  }
1885
1919
  {
1886
1920
  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"]);
1887
- const tokensFrom = (text) => text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP.has(w));
1921
+ const tokensFrom = (text) => stripDiacritics(text).replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP.has(w));
1888
1922
  for (const c of concepts) {
1889
1923
  const existing = (c.keywords || []).map((k) => String(k).trim()).filter(Boolean);
1890
1924
  if (existing.length === 0) {
@@ -2052,14 +2086,11 @@ Return JSON:
2052
2086
  const hasHighBloom = (concept.problem_types || []).some(
2053
2087
  (pt) => ["Analyze", "Evaluate", "Create"].includes(pt.bloom_level || "")
2054
2088
  );
2055
- const score = Math.min(1, sioCount * 0.2 + ptCount * 0.15 + prereqCount * 0.15 + (hasHighBloom ? 0.2 : 0));
2056
- const calibrated = Math.round(15 + score * 45);
2057
- const llmEstimate = concept.estimated_minutes || 30;
2058
- if (llmEstimate === 30 || Math.abs(calibrated - llmEstimate) / llmEstimate > 0.5) {
2089
+ const calibrated = calibrateConceptMinutes(sioCount, ptCount, prereqCount, hasHighBloom);
2090
+ const llmEstimate = concept.estimated_minutes || DEFAULT_CONCEPT_MINUTES;
2091
+ if (Math.abs(calibrated - llmEstimate) / llmEstimate > MIN_CALIBRATION_DIVERGENCE) {
2059
2092
  concept.estimated_minutes = calibrated;
2060
- if (llmEstimate !== calibrated) {
2061
- warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m (score=" + score.toFixed(2) + ")");
2062
- }
2093
+ warnings.push("Concept " + concept.id + ": minutes calibrated " + llmEstimate + "m \u2192 " + calibrated + "m");
2063
2094
  }
2064
2095
  }
2065
2096
  log("STEP_5c", "Minute calibration complete");
@@ -2084,7 +2115,8 @@ Return JSON:
2084
2115
  return {
2085
2116
  knowledgeGraph: verifiedGraph,
2086
2117
  warnings,
2087
- verificationReport: report
2118
+ verificationReport: report,
2119
+ provenance: client.callLog
2088
2120
  };
2089
2121
  }
2090
2122
 
@@ -2111,9 +2143,10 @@ async function generateHybridGraph(options) {
2111
2143
  description: "",
2112
2144
  total_project_minutes: 0,
2113
2145
  total_concept_minutes: 0,
2114
- estimated_total_minutes: 0
2146
+ linked_prereq_minutes: 0
2115
2147
  },
2116
- warnings
2148
+ warnings,
2149
+ provenance: []
2117
2150
  };
2118
2151
  }
2119
2152
  const client = createLlmClient(llmConfig);
@@ -2194,9 +2227,14 @@ async function generateHybridGraph(options) {
2194
2227
  description: "Progressive completion path: " + phases.length + " phases, " + features.length + " features, " + concepts.length + " concepts",
2195
2228
  total_project_minutes: totalProjectMinutes,
2196
2229
  total_concept_minutes: totalConceptMinutes,
2197
- estimated_total_minutes: totalProjectMinutes + totalPrereqMinutes
2230
+ // HG-1 (2026-10-05): renamed from estimated_total_minutes. It counts only
2231
+ // concepts that HAVE a feature link (max per concept, not summed), so it
2232
+ // is NOT the course total — see HybridGraphSchema.linked_prereq_minutes.
2233
+ // The phase planner below commits to totalProjectMinutes + totalConceptMinutes.
2234
+ linked_prereq_minutes: totalProjectMinutes + totalPrereqMinutes
2198
2235
  },
2199
- warnings
2236
+ warnings,
2237
+ provenance: client.callLog
2200
2238
  };
2201
2239
  }
2202
2240
  function auditPhasePlan(plan, features, concepts, links, warnings, log) {
@@ -2272,12 +2310,20 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
2272
2310
  } else {
2273
2311
  newIntroPhase.set(cid, order);
2274
2312
  }
2313
+ introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
2314
+ }
2315
+ for (const intro of introductions) {
2316
+ const cid = intro.concept_id;
2317
+ if (intro.aspect === "advanced") continue;
2318
+ const introOrder = newIntroPhase.get(cid);
2319
+ if (introOrder === void 0 || introOrder !== order) continue;
2275
2320
  for (const pre of prereqOf.get(cid) || []) {
2276
- if (!newIntroPhase.has(pre)) {
2277
- errors.push("Phase " + id + ": concept " + cid + " introduced before its prerequisite " + pre + " is introduced - violates the prerequisite chain");
2321
+ const previewOrder = previewPhase.get(pre);
2322
+ const covered = newIntroPhase.has(pre) || previewOrder !== void 0 && previewOrder < order;
2323
+ if (!covered) {
2324
+ errors.push("Phase " + id + ": concept " + cid + " introduced before its prerequisite " + pre + " is introduced (or previewed in an earlier phase) - violates the prerequisite chain");
2278
2325
  }
2279
2326
  }
2280
- introductions.push({ concept_id: cid, aspect, note: String(ri.note || "") });
2281
2327
  }
2282
2328
  phases.push({
2283
2329
  id,
@@ -2315,6 +2361,9 @@ function auditPhasePlan(plan, features, concepts, links, warnings, log) {
2315
2361
  if (anchor !== void 0 && anchor <= pOrder) {
2316
2362
  errors.push("Concept " + cid + " previewed in phase " + pOrder + " but fully introduced in phase " + anchor + " - preview must precede the anchor introduction");
2317
2363
  }
2364
+ if (anchor === void 0) {
2365
+ errors.push("Concept " + cid + " is previewed in phase " + pOrder + " but never fully introduced (aspect new) in any phase - a preview must have an anchor teach");
2366
+ }
2318
2367
  }
2319
2368
  return { phases, errors };
2320
2369
  }
@@ -2371,6 +2420,6 @@ async function decomposePhases(args) {
2371
2420
  return phases;
2372
2421
  }
2373
2422
 
2374
- export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditPhasePlan, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName, createLlmClient, detectPrerequisiteCycles, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, generateHybridGraph, generateKnowledgeGraph, llmChatJson, sioCode, standardStatement, techTagFor, uloCode, verifyKnowledgeGraph, verifyProjectGraph };
2375
- //# sourceMappingURL=chunk-TYXSJUQU.mjs.map
2376
- //# sourceMappingURL=chunk-TYXSJUQU.mjs.map
2423
+ export { CIO_CODE_RE, CONCEPT_CODE_RE, LEARNER_CLAUSE, SIO_CODE_RE, ULO_CODE_RE, assignConceptCodes, auditDepthLayers, auditPhasePlan, auditSyllabusCoverage, breakPrerequisiteCycles, cioActionSlug, cioCode, conceptCodeFromName, createLlmClient, detectPrerequisiteCycles, escalateAndMapConcepts, extractFeatureSteps, extractFeatureStepsBatched, extractProjectOverview, extractScaffold, generateHybridGraph, generateKnowledgeGraph, llmChatJson, sioCode, standardStatement, summarizeProvenance, techTagFor, uloCode, verifyKnowledgeGraph, verifyProjectGraph };
2424
+ //# sourceMappingURL=chunk-2OGNQUXC.mjs.map
2425
+ //# sourceMappingURL=chunk-2OGNQUXC.mjs.map