@thanh01.pmt/curriculum-kit 1.0.22 → 1.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.
package/dist/index.cjs CHANGED
@@ -85,6 +85,8 @@ function getSuggestedActionForCode(code) {
85
85
  return "N\u1ED9i dung sinh ra thi\u1EBFu c\xE1c ph\u1EA7n b\u1EAFt bu\u1ED9c [REQUIRED]. H\xE3y k\xEDch ho\u1EA1t ki\u1EC3m th\u1EED ho\u1EB7c b\u1EA5m Rerun.";
86
86
  case "ERR_STORAGE_IO_FAILED":
87
87
  return "L\u1ED7i \u0111\u1ECDc/ghi t\u1EC7p tin tr\xEAn h\u1EC7 th\u1ED1ng l\u01B0u tr\u1EEF. Ki\u1EC3m tra quy\u1EC1n ghi th\u01B0 m\u1EE5c output/projects.";
88
+ case "ERR_CONTEXT_MISSING":
89
+ return "Thi\u1EBFu ngu\u1ED3n tri th\u1EE9c b\u1EAFt bu\u1ED9c (KNOWLEDGE_EXPOSITION ho\u1EB7c CANONICAL LESSON) theo Dependency Routing Matrix. Vui l\xF2ng t\u1EA1o ho\u1EB7c \u0111\u1ED3ng b\u1ED9 t\xE0i li\u1EC7u ngu\u1ED3n tr\u01B0\u1EDBc.";
88
90
  default:
89
91
  return "B\u1EA5m Rerun \u0111\u1EC3 th\u1EED l\u1EA1i ho\u1EB7c ki\u1EC3m tra nh\u1EADt k\xFD h\u1EC7 th\u1ED1ng.";
90
92
  }
@@ -102,11 +104,23 @@ function isCodeRetryable(code) {
102
104
  case "ERR_TASK_GATE_BLOCKED":
103
105
  case "ERR_METERED_LIMIT_EXCEEDED":
104
106
  case "ERR_SCHEMA_CONTRACT_VIOLATION":
107
+ case "ERR_CONTEXT_MISSING":
105
108
  return false;
106
109
  default:
107
110
  return true;
108
111
  }
109
112
  }
113
+ function createContextMissingError(params) {
114
+ return new exports.CurriculumError({
115
+ errorCode: "ERR_CONTEXT_MISSING",
116
+ artifactType: params.artifactType,
117
+ lessonId: params.lessonId,
118
+ agent: params.agent,
119
+ message: `Required context source "${params.missingSource}" is missing for artifact "${params.artifactType}"`,
120
+ suggestedAction: `Generate or provide the authoritative "${params.missingSource}" source before producing "${params.artifactType}".`,
121
+ retryable: false
122
+ });
123
+ }
110
124
  exports.CurriculumError = void 0;
111
125
  var init_errors = __esm({
112
126
  "src/errors/index.ts"() {
@@ -2432,9 +2446,20 @@ var SessionPlanSchema = zod.z.object({
2432
2446
  differentiation: zod.z.object({ bronze: zod.z.string(), silver: zod.z.string(), gold: zod.z.string() })
2433
2447
  });
2434
2448
  var TranslationPolicySchema = zod.z.object({
2435
- policy: zod.z.enum(["after_artifact", "end_of_unit", "at_publish"]).default("at_publish"),
2449
+ policy: zod.z.enum(["after_artifact", "end_of_unit", "at_publish", "native"]).default("native"),
2436
2450
  target_language: zod.z.string().nullable().default(null)
2437
- });
2451
+ }).refine(
2452
+ (data) => {
2453
+ if (data.policy === "native") {
2454
+ return typeof data.target_language === "string" && data.target_language.trim().length > 0;
2455
+ }
2456
+ return true;
2457
+ },
2458
+ {
2459
+ message: "When translation policy is 'native', target_language must be a non-empty string",
2460
+ path: ["target_language"]
2461
+ }
2462
+ );
2438
2463
  var CoverageReportSchema = zod.z.object({
2439
2464
  assigned_node_ids: zod.z.array(zod.z.string()),
2440
2465
  unassigned_node_ids: zod.z.array(zod.z.string()),
@@ -5368,6 +5393,45 @@ Return a single valid JSON object matching SlideDeckSchema with these exact keys
5368
5393
  }
5369
5394
  `.trim();
5370
5395
  }
5396
+ var SLIDE_CANONICAL_METHODOLOGY = `
5397
+ MANDATORY 12-14 SLIDE MICRO-CYCLE STRUCTURE (Delimited by '---'):
5398
+ 1. **Slide 1 [Title & Mission]:** Real-world inquiry hook and overarching build mission
5399
+ 2. **Slide 2 [Agenda & Roadmap]:** Timing and milestone breakdown table
5400
+ 3. **Slide 3 [Warm-Up / Prior Knowledge]:** 2-column recall + 60-second diagnostic challenge card
5401
+ 4. **Slide 4 [High-Stakes Hook Scenario]:** Authentic engineering/domain crisis card + dilemma constraint
5402
+ 5. **Slide 5 [Core Concept 1: Mental Model]:** Definition, intuitive everyday analogy, and golden rule card
5403
+ 6. **Slide 6 [Architecture & Visual Flow]:** Clean Mermaid diagram (flowchart or sequenceDiagram) + 3 flow steps
5404
+ 7. **Slide 7 [Core Concept 2: Syntax / Mechanism Anatomy]:** Side-by-side runnable code + anatomy breakdown & syntax trap card
5405
+ 8. **Slide 8 [Guided Practice: Try It Now]:** 3-minute active desk task card with starter hint and success criteria
5406
+ 9. **Slide 9 [Integrated Hands-On Lab]:** 3-tier differentiated challenge cards (Bronze / Silver / Gold) + gotcha alert card
5407
+ 10. **Slide 10 [Traffic Light Checkpoint & Pitfalls]:** Formative self-check cards (\u{1F7E2}/\u{1F7E1}/\u{1F534}) & diagnostic test question
5408
+ 11. **Slide 11 [Exit Ticket & Metacognition]:** 2 scenario-based check questions + 1 metacognitive reflection prompt
5409
+ 12. **Slide 12 [Summary & Next Steps]:** 3 core golden rules + preview card of next upcoming milestone
5410
+
5411
+ Mandatory Canvas & Presentation Invariants:
5412
+ - A slide is an INTERACTIVE PROJECTION CANVAS, NOT a textbook! Absolutely NO dense paragraphs or walls of text.
5413
+ - Follow the 6x6 rule: Maximum 4-6 bullet points per slide, maximum 8-12 words per bullet.
5414
+ - Structure slides using \`<div class="columns"><div>...</div><div>...</div></div>\` and \`<div class="card">\`, \`<div class="card highlight">\`, \`<div class="card warning">\`, \`<div class="card success">\`.
5415
+ - MANDATORY PRESENTER NOTES ON EVERY SLIDE: Every slide MUST end with \`<!-- Presenter Notes: ... -->\` containing:
5416
+ * SCRIPT: 60-90s teacher talk track with intuitive analogies.
5417
+ * COLD CALL / CHECK: Specific diagnostic question to ask students.
5418
+ * SCAFFOLDING / GOTCHA: Guidance for students who get stuck.
5419
+ * PACING: Recommended minutes for the slide.
5420
+ - Output 100% valid Marp Markdown.
5421
+ `.trim();
5422
+ function buildMarpMarkdownSlidePrompt(input) {
5423
+ return `You are @illustrator & @content (Pedagogical Slide Deck Presentation Specialists).
5424
+ Based on Master Lesson Plan \`LESSON_${input.lessonCode}.md\`, lesson context, and the MARP SLIDE TEMPLATE below, author the complete, highly engaging Marp presentation deck: \`SLIDE_${input.lessonCode}.md\`.
5425
+
5426
+ [MARP SLIDE TEMPLATE SCAFFOLD]:
5427
+ ${input.templateScaffold}
5428
+
5429
+ ${SLIDE_CANONICAL_METHODOLOGY}
5430
+
5431
+ ${input.languageDirective}
5432
+
5433
+ ${input.headingDirective}`.trim();
5434
+ }
5371
5435
 
5372
5436
  // src/ai/prompts/teacherGuidePrompt.ts
5373
5437
  function buildTeacherGuidePrompt(input) {
@@ -9700,6 +9764,224 @@ function resolveGateSettings(raw) {
9700
9764
  function gateModeFor(resolved, type) {
9701
9765
  return resolved[type] ?? "OFF";
9702
9766
  }
9767
+
9768
+ // src/services/contextBuilder.ts
9769
+ var DEFAULT_LESSON_PRIORITIES = [
9770
+ "A. Lesson Design Plan",
9771
+ "B. Lesson Flow",
9772
+ "Learning Objectives & Evidence",
9773
+ "Activity Sequence"
9774
+ ];
9775
+ var DEFAULT_KX_PRIORITIES = [
9776
+ "Key Terms",
9777
+ "Concept Narratives",
9778
+ "Worked Micro-Examples"
9779
+ ];
9780
+ function buildSectionAwareExcerpt(markdown, opts = {}) {
9781
+ const budget = opts.budget ?? 12e3;
9782
+ const minContentChars = opts.minContentChars ?? 400;
9783
+ const issues = [];
9784
+ const source = (markdown || "").trim();
9785
+ if (!source) {
9786
+ return {
9787
+ excerpt: "",
9788
+ verified: false,
9789
+ sectionAware: false,
9790
+ includedSections: [],
9791
+ issues: ["empty-source"],
9792
+ tokenEstimate: 0
9793
+ };
9794
+ }
9795
+ const slcMap = normalizeSlcContract(opts.sectionLanguageContract);
9796
+ const sections = parseMarkdownSections(source, slcMap, opts.artifactType);
9797
+ if (sections.length === 0) {
9798
+ issues.push("parse:no-sections");
9799
+ const excerpt2 = truncateByBudget(source, budget);
9800
+ const verified2 = excerpt2.length >= minContentChars;
9801
+ if (excerpt2.length < minContentChars) issues.push("content:insufficient");
9802
+ return {
9803
+ excerpt: excerpt2,
9804
+ verified: verified2,
9805
+ sectionAware: false,
9806
+ includedSections: [],
9807
+ issues,
9808
+ tokenEstimate: Math.round(excerpt2.length / 4)
9809
+ };
9810
+ }
9811
+ const byCanonical = /* @__PURE__ */ new Map();
9812
+ const unmatched = [];
9813
+ for (const s of sections) {
9814
+ if (s.canonicalKey && !byCanonical.has(s.canonicalKey)) {
9815
+ byCanonical.set(s.canonicalKey, s);
9816
+ } else {
9817
+ unmatched.push(s);
9818
+ }
9819
+ }
9820
+ const priorities = opts.priorities ?? DEFAULT_LESSON_PRIORITIES;
9821
+ const includedSections = [];
9822
+ const blocks = [];
9823
+ let used = 0;
9824
+ const tryAdd = (heading, body, key) => {
9825
+ const trimmedBody = body.trim();
9826
+ if (!trimmedBody) return false;
9827
+ const block = `## ${heading}
9828
+
9829
+ ${trimmedBody}`;
9830
+ if (used + block.length > budget && blocks.length > 0) return false;
9831
+ blocks.push(block);
9832
+ used += block.length;
9833
+ includedSections.push(key);
9834
+ return true;
9835
+ };
9836
+ let sectionAware = false;
9837
+ for (const key of priorities) {
9838
+ const sec = byCanonical.get(key);
9839
+ if (!sec) continue;
9840
+ sectionAware = true;
9841
+ if (!tryAdd(sec.cleanTitle, sec.body, key)) break;
9842
+ }
9843
+ for (const [key, sec] of byCanonical) {
9844
+ if (includedSections.includes(key)) continue;
9845
+ if (used >= budget) break;
9846
+ if (!tryAdd(sec.cleanTitle, sec.body, key)) break;
9847
+ }
9848
+ for (const sec of unmatched) {
9849
+ if (used >= budget) break;
9850
+ if (!tryAdd(sec.cleanTitle, sec.body, sec.cleanTitle)) break;
9851
+ }
9852
+ if (priorities.length > 0 && !priorities.some((k) => includedSections.includes(k))) {
9853
+ issues.push("parse:no-priority-resolution");
9854
+ }
9855
+ let excerpt = blocks.join("\n\n");
9856
+ if (excerpt.length > budget) {
9857
+ excerpt = truncateByBudget(excerpt, budget);
9858
+ }
9859
+ const effectiveMinChars = Math.min(minContentChars, source.length);
9860
+ if (excerpt.length < effectiveMinChars) {
9861
+ issues.push("content:insufficient");
9862
+ }
9863
+ const verified = !issues.includes("parse:no-priority-resolution") && excerpt.length >= effectiveMinChars;
9864
+ return {
9865
+ excerpt,
9866
+ verified,
9867
+ sectionAware: sectionAware || priorities.length === 0 && sections.length > 0,
9868
+ includedSections,
9869
+ issues,
9870
+ tokenEstimate: Math.round(excerpt.length / 4)
9871
+ };
9872
+ }
9873
+ function buildExpositionExcerpt(markdown, opts = {}) {
9874
+ return buildSectionAwareExcerpt(markdown, {
9875
+ ...opts,
9876
+ priorities: DEFAULT_KX_PRIORITIES
9877
+ });
9878
+ }
9879
+ function buildLessonExcerpt(markdown, opts = {}) {
9880
+ return buildSectionAwareExcerpt(markdown, {
9881
+ ...opts,
9882
+ priorities: DEFAULT_LESSON_PRIORITIES
9883
+ });
9884
+ }
9885
+ function normalizeSlcContract(slc) {
9886
+ if (!slc) return void 0;
9887
+ if (typeof slc !== "string") return slc;
9888
+ const out = {};
9889
+ const blocks = slc.split(/\n(?=#{2,3}\s+[A-Z0-9_]+)/g);
9890
+ for (const block of blocks) {
9891
+ const header = block.match(/^#{2,3}\s+([A-Z0-9_]+)/);
9892
+ if (!header?.[1]) continue;
9893
+ const mapping = {};
9894
+ for (const line of block.split("\n")) {
9895
+ const t = line.trim();
9896
+ if (!t.startsWith("|") || t.includes("---")) continue;
9897
+ const cells = t.split("|").map((c) => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1);
9898
+ const [c0, c1] = cells;
9899
+ if (c0 && c1 && !c0.toLowerCase().includes("canonical") && !c1.toLowerCase().includes("localized")) {
9900
+ mapping[c0] = c1;
9901
+ }
9902
+ }
9903
+ if (Object.keys(mapping).length > 0) out[header[1]] = mapping;
9904
+ }
9905
+ return Object.keys(out).length > 0 ? out : void 0;
9906
+ }
9907
+ function parseMarkdownSections(markdown, slcMap, artifactType) {
9908
+ const sections = [];
9909
+ const lines = markdown.split("\n");
9910
+ let currentHeading = "";
9911
+ let currentBody = [];
9912
+ const reverseMap = /* @__PURE__ */ new Map();
9913
+ if (slcMap) {
9914
+ const maps = artifactType && slcMap[artifactType] ? [slcMap[artifactType]] : Object.values(slcMap);
9915
+ for (const m of maps) {
9916
+ for (const [canonical, localized] of Object.entries(m)) {
9917
+ reverseMap.set(normalizeHeading(localized), canonical);
9918
+ reverseMap.set(normalizeHeading(canonical), canonical);
9919
+ }
9920
+ }
9921
+ }
9922
+ const flush = () => {
9923
+ if (!currentHeading && currentBody.length === 0) return;
9924
+ const cleanTitle = currentHeading.replace(/^#{1,4}\s+/, "").trim();
9925
+ const norm = normalizeHeading(cleanTitle);
9926
+ const canonicalKey = reverseMap.get(norm) || findCanonicalFuzzy(norm);
9927
+ sections.push({
9928
+ rawHeading: currentHeading,
9929
+ cleanTitle,
9930
+ canonicalKey,
9931
+ body: currentBody.join("\n")
9932
+ });
9933
+ currentBody = [];
9934
+ };
9935
+ const hasH2 = lines.some((l) => /^##\s+/.test(l));
9936
+ const headingRegex = hasH2 ? /^##\s+(.*)$/ : /^#{2,3}\s+(.*)$/;
9937
+ for (const line of lines) {
9938
+ const headingMatch = line.match(headingRegex);
9939
+ if (headingMatch) {
9940
+ flush();
9941
+ currentHeading = line.trim();
9942
+ } else {
9943
+ currentBody.push(line);
9944
+ }
9945
+ }
9946
+ flush();
9947
+ return sections;
9948
+ }
9949
+ function normalizeHeading(str) {
9950
+ return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
9951
+ }
9952
+ function findCanonicalFuzzy(norm) {
9953
+ if (norm.includes("lesson design plan") || norm.includes("ke hoach thiet ke")) {
9954
+ return "A. Lesson Design Plan";
9955
+ }
9956
+ if (norm.includes("lesson flow") || norm.includes("tien trinh giang day")) {
9957
+ return "B. Lesson Flow";
9958
+ }
9959
+ if (norm.includes("learning objective") || norm.includes("muc tieu bai hoc")) {
9960
+ return "Learning Objectives & Evidence";
9961
+ }
9962
+ if (norm.includes("activity sequence") || norm.includes("chuoi hoat dong")) {
9963
+ return "Activity Sequence";
9964
+ }
9965
+ if (norm.includes("key term") || norm.includes("thuat ngu")) {
9966
+ return "Key Terms";
9967
+ }
9968
+ if (norm.includes("concept narrative") || norm.includes("dien giai khai niem")) {
9969
+ return "Concept Narratives";
9970
+ }
9971
+ if (norm.includes("worked example") || norm.includes("vi du")) {
9972
+ return "Worked Micro-Examples";
9973
+ }
9974
+ return void 0;
9975
+ }
9976
+ function truncateByBudget(source, budget) {
9977
+ if (source.length <= budget) return source;
9978
+ let cut = source.slice(0, budget);
9979
+ const fenceCount = (cut.match(/^```/gm) || []).length;
9980
+ if (fenceCount % 2 === 1) cut += "\n```";
9981
+ return cut;
9982
+ }
9983
+
9984
+ // src/services/knowledgeExpositionService.ts
9703
9985
  var EXPOSITION_REL = (lessonCode) => "_sot/expositions/" + lessonCode + ".md";
9704
9986
  var ExpositionApprovalError = class extends Error {
9705
9987
  constructor(message) {
@@ -9806,15 +10088,8 @@ async function ensureKnowledgeExposition(options) {
9806
10088
  return { relPath: EXPOSITION_REL(lessonCode), content: stamped, reused: false, gateMode: gateModeFor(options.gates ?? {}, "KNOWLEDGE_EXPOSITION") };
9807
10089
  }
9808
10090
  function buildExpositionContext(expositionContent, maxChars = 6e3) {
9809
- const trimmed = expositionContent.trim();
9810
- if (trimmed.length <= maxChars) return trimmed;
9811
- const sections = trimmed.split(/\n(?=## )/);
9812
- let out = sections[0];
9813
- for (let i = 1; i < sections.length && out.length < maxChars; i++) {
9814
- const s = sections[i];
9815
- out += "\n" + s.slice(0, Math.max(400, maxChars - out.length));
9816
- }
9817
- return out.slice(0, maxChars);
10091
+ if (!expositionContent || !expositionContent.trim()) return "";
10092
+ return buildExpositionExcerpt(expositionContent, { budget: maxChars }).excerpt;
9818
10093
  }
9819
10094
  function expositionCacheKey(planHash, lessonCode) {
9820
10095
  return crypto.createHash("sha256").update(planHash + "|" + lessonCode).digest("hex").slice(0, 16);
@@ -9855,6 +10130,7 @@ async function ensureExpositionForLesson(storage, projectId, lessonCode, options
9855
10130
  });
9856
10131
  return { relPath: result.relPath, context: buildExpositionContext(result.content), reused: result.reused };
9857
10132
  }
10133
+ init_errors();
9858
10134
  var asArr = (v) => Array.isArray(v) ? v : [];
9859
10135
  var asStr = (v, dflt = "") => typeof v === "string" ? v : dflt;
9860
10136
  var asNum = (v, dflt) => typeof v === "number" && Number.isFinite(v) ? v : dflt;
@@ -10497,7 +10773,15 @@ function extractSessionSlice(plan, lessonCode) {
10497
10773
  }
10498
10774
  function buildSessionSliceContext(plan, lessonCode) {
10499
10775
  const s = plan.sessions.find((x) => x.id === lessonCode);
10500
- if (!s) return "";
10776
+ if (!s) {
10777
+ throw new exports.CurriculumError({
10778
+ errorCode: "ERR_LESSON_NOT_IN_SOT",
10779
+ message: `Session "${lessonCode}" not found in CURRICULUM_PLAN.json (${plan.plan_hash})`,
10780
+ suggestedAction: "\u0110\u1ED3ng b\u1ED9 CURRICULUM_PLAN r\u1ED3i ch\u1EA1y l\u1EA1i",
10781
+ retryable: false,
10782
+ lessonId: lessonCode
10783
+ });
10784
+ }
10501
10785
  const lines = [];
10502
10786
  lines.push("[SESSION SLICE (from CURRICULUM_PLAN " + plan.plan_hash + ")]");
10503
10787
  lines.push("- Objective: " + s.prose_objective);
@@ -11064,6 +11348,90 @@ function resolveStandardsPacks(packIds) {
11064
11348
  return packs;
11065
11349
  }
11066
11350
 
11351
+ // src/services/languageDirective.ts
11352
+ function resolveTargetLanguageCode(language) {
11353
+ const raw = String(language || "vi").trim().toLowerCase();
11354
+ if (raw.startsWith("vi")) return "vi";
11355
+ if (raw.startsWith("en")) return "en";
11356
+ return raw || "vi";
11357
+ }
11358
+ function targetLanguageDisplayName(code) {
11359
+ const map = {
11360
+ vi: "Vietnamese (Ti\u1EBFng Vi\u1EC7t)",
11361
+ en: "English (US)"
11362
+ };
11363
+ return map[code] || code;
11364
+ }
11365
+ function buildLanguageDirective(targetLanguage) {
11366
+ const code = resolveTargetLanguageCode(targetLanguage);
11367
+ const name = targetLanguageDisplayName(code);
11368
+ const perLanguageExamples = {
11369
+ vi: `- V\xED d\u1EE5 ti\xEAu \u0111\u1EC1 b\xE0i h\u1ECDc \u0111\xFAng chu\u1EA9n: "B\xE0i 1: Kh\xE1m ph\xE1 m\u1EA1ch \u0111i\u1EC7n c\u01A1 b\u1EA3n", "B\xE0i 2: \u0110i\u1EC1u khi\u1EC3n \u0111\xE8n LED nh\u1EA5p nh\xE1y" \u2014 KH\xD4NG d\xF9ng ti\xEAu \u0111\u1EC1 ti\u1EBFng Anh.`,
11370
+ en: `- Example compliant lesson title: "Lesson 1: Exploring Basic Circuits" \u2014 natural, idiomatic English throughout.`
11371
+ };
11372
+ const exampleLine = perLanguageExamples[code] || "";
11373
+ return `MANDATORY LANGUAGE DIRECTIVE (TARGET OUTPUT LANGUAGE: ${name}):
11374
+ - The TARGET OUTPUT LANGUAGE for this course is ${name}. You MUST author the ENTIRE document in ${name}.
11375
+ - All section titles, table headers, table cells, lesson names, learning objectives, pedagogical narratives, and cognitive analyses MUST be written in natural, fluent, academic ${name}.
11376
+ - Technical identifiers, code keywords (e.g. setup(), loop(), pinMode()), standard protocols (I2C, SPI, UART, GPIO), and component model numbers (ESP32, Arduino Uno) remain in standard technical notation, but all surrounding explanations MUST be in ${name}.
11377
+ - DO NOT mix in paragraphs, tables, or titles written in any other language. Strict adherence is required.${exampleLine ? `
11378
+ ${exampleLine}` : ""}`;
11379
+ }
11380
+ function extractSectionHeadingsFromSLC(slcInput, artifactType) {
11381
+ if (!slcInput) return {};
11382
+ const upperType = artifactType.trim().toUpperCase();
11383
+ if (typeof slcInput === "object") {
11384
+ if (slcInput[upperType]) return slcInput[upperType];
11385
+ for (const [key, mapping] of Object.entries(slcInput)) {
11386
+ if (key.toUpperCase() === upperType || upperType.startsWith("LESSON") && key.toUpperCase().startsWith("LESSON")) {
11387
+ return mapping;
11388
+ }
11389
+ }
11390
+ return {};
11391
+ }
11392
+ const slcMarkdown = slcInput;
11393
+ const blocks = slcMarkdown.split(/\n(?=#{2,3}\s+[A-Z0-9_]+)/g);
11394
+ for (const block of blocks) {
11395
+ const headerMatch = block.match(/^#{2,3}\s+([A-Z0-9_]+)/);
11396
+ if (!headerMatch) continue;
11397
+ const blockType = headerMatch[1].trim().toUpperCase();
11398
+ if (blockType === upperType || upperType.startsWith("LESSON") && blockType.startsWith("LESSON")) {
11399
+ const mapping = {};
11400
+ const lines = block.split("\n");
11401
+ for (const line of lines) {
11402
+ const trimmed = line.trim();
11403
+ if (trimmed.startsWith("|") && !trimmed.includes("---")) {
11404
+ const cells = trimmed.split("|").map((c) => c.trim()).filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
11405
+ if (cells.length >= 2 && !cells[0].toLowerCase().includes("canonical") && !cells[1].toLowerCase().includes("localized")) {
11406
+ mapping[cells[0]] = cells[1];
11407
+ }
11408
+ } else {
11409
+ const bullet = line.match(/^[-*]\s+([^:]+):\s+(.+)$/);
11410
+ if (bullet) {
11411
+ mapping[bullet[1].trim()] = bullet[2].trim();
11412
+ }
11413
+ }
11414
+ }
11415
+ if (Object.keys(mapping).length > 0) return mapping;
11416
+ }
11417
+ }
11418
+ return {};
11419
+ }
11420
+ function buildHeadingDirective(artifactType, slcMarkdown, targetLanguage = "vi") {
11421
+ const headings = extractSectionHeadingsFromSLC(slcMarkdown, artifactType);
11422
+ if (Object.keys(headings).length === 0) {
11423
+ return "";
11424
+ }
11425
+ const lines = Object.entries(headings).map(
11426
+ ([canonical, localized]) => ` - "${canonical}" -> Use Display Heading: "${localized}"`
11427
+ );
11428
+ return `MANDATORY SECTION HEADINGS FROM SECTION_LANGUAGE_CONTRACT:
11429
+ - The YAML frontmatter \`template_required_sections\` MUST preserve the exact English canonical keys.
11430
+ - In the markdown body, section headers MUST use the EXACT Localized Display Labels below (do NOT translate or paraphrase):
11431
+ ${lines.join("\n")}
11432
+ - For example: if canonical section is "Computational Thinking Focus", your heading must be "## [REQUIRED] ${headings["Computational Thinking Focus"] || "Computational Thinking Focus"}".`;
11433
+ }
11434
+
11067
11435
  // src/services/lessonProductionService.ts
11068
11436
  var __filename2 = typeof (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)) === "string" ? url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))) : "";
11069
11437
  var _resolvedDir = __filename2 ? path3__default.default.dirname(__filename2) : typeof __dirname !== "undefined" ? __dirname : process.cwd();
@@ -11105,11 +11473,44 @@ function detectProjectPedagogy(frameworkText, styleGuideText, explicitPedagogy)
11105
11473
  return "5e";
11106
11474
  }
11107
11475
  async function getLessonMetadata(storage, projectId, lessonId) {
11108
- const framework = await storage.readSotDocument(projectId, "CURRICULUM_FRAMEWORK.md") || "";
11109
11476
  const parts = lessonId.split("_");
11110
11477
  const unitCode = parts[0] || "U01";
11111
11478
  const moduleCode = parts.length >= 2 ? `${parts[0]}_${parts[1]}` : `${unitCode}_M01`;
11112
11479
  const lessonCode = lessonId;
11480
+ let planStr = null;
11481
+ try {
11482
+ planStr = await storage.readSotDocument(projectId, "CURRICULUM_PLAN.json") || await storage.readArtifact(projectId, "_sot/CURRICULUM_PLAN.json") || await storage.readArtifact(projectId, "CURRICULUM_PLAN.json");
11483
+ } catch {
11484
+ }
11485
+ if (planStr) {
11486
+ try {
11487
+ const plan = JSON.parse(planStr);
11488
+ if (Array.isArray(plan.sessions)) {
11489
+ const session = plan.sessions.find((s) => s.id === lessonId);
11490
+ if (session) {
11491
+ const lessonTitle2 = session.title || `Lesson ${lessonId}`;
11492
+ const keywords = Array.isArray(session.new_keywords) && session.new_keywords.length > 0 ? ` T\u1EEB kh\xF3a: ${session.new_keywords.join(", ")}` : "";
11493
+ const objective2 = session.prose_objective || session.title || "";
11494
+ const concept2 = objective2 ? `${objective2}.${keywords}` : session.title || "Core Technical Competency";
11495
+ const prerequisites2 = session.anchor_type ? `Anchor: ${session.anchor_type}` : "None";
11496
+ const depth = Array.isArray(session.depth_assignments) ? session.depth_assignments[0]?.depth : session.depth_assignments?.knowledge_depth;
11497
+ const bloomLevel2 = depth === "sio" ? "Create" : depth === "cio" ? "Apply" : "Understand";
11498
+ return {
11499
+ unitCode: session.unit_id || unitCode,
11500
+ moduleCode,
11501
+ lessonCode,
11502
+ lessonTitle: lessonTitle2,
11503
+ concept: `M\u1EE5c ti\xEAu bu\u1ED5i h\u1ECDc: ${concept2}`,
11504
+ prerequisites: prerequisites2,
11505
+ bloomLevel: bloomLevel2
11506
+ };
11507
+ }
11508
+ }
11509
+ } catch (planErr) {
11510
+ console.warn(`[getLessonMetadata] Error parsing CURRICULUM_PLAN.json for ${projectId}:`, planErr);
11511
+ }
11512
+ }
11513
+ const framework = await storage.readSotDocument(projectId, "CURRICULUM_FRAMEWORK.md") || "";
11113
11514
  let lessonTitle = `Lesson ${lessonId}`;
11114
11515
  let concept = "Core Technical Competency";
11115
11516
  let prerequisites = "None";
@@ -11154,15 +11555,24 @@ async function getLessonMetadata(storage, projectId, lessonId) {
11154
11555
  }
11155
11556
  if (found) break;
11156
11557
  }
11157
- return {
11158
- unitCode,
11159
- moduleCode,
11160
- lessonCode,
11161
- lessonTitle,
11162
- concept: objective ? `${concept}. M\u1EE5c ti\xEAu bu\u1ED5i h\u1ECDc: ${objective}` : concept,
11163
- prerequisites: deliverable ? `${prerequisites}. S\u1EA3n ph\u1EA9m hands-on c\u1EE7a bu\u1ED5i: ${deliverable}` : prerequisites,
11164
- bloomLevel
11165
- };
11558
+ if (found) {
11559
+ return {
11560
+ unitCode,
11561
+ moduleCode,
11562
+ lessonCode,
11563
+ lessonTitle,
11564
+ concept: objective ? `${concept}. M\u1EE5c ti\xEAu bu\u1ED5i h\u1ECDc: ${objective}` : concept,
11565
+ prerequisites: deliverable ? `${prerequisites}. S\u1EA3n ph\u1EA9m hands-on c\u1EE7a bu\u1ED5i: ${deliverable}` : prerequisites,
11566
+ bloomLevel
11567
+ };
11568
+ }
11569
+ throw new exports.CurriculumError({
11570
+ errorCode: "ERR_LESSON_NOT_IN_SOT",
11571
+ lessonId,
11572
+ message: `ERR_LESSON_NOT_IN_SOT: B\xE0i h\u1ECDc "${lessonId}" kh\xF4ng t\u1ED3n t\u1EA1i trong CURRICULUM_PLAN.json ho\u1EB7c CURRICULUM_FRAMEWORK.md c\u1EE7a d\u1EF1 \xE1n "${projectId}".`,
11573
+ suggestedAction: "\u0110\u1ED3ng b\u1ED9 CURRICULUM_PLAN r\u1ED3i ch\u1EA1y l\u1EA1i",
11574
+ retryable: false
11575
+ });
11166
11576
  }
11167
11577
  function buildFrameworkExcerptForLesson(framework, lessonId) {
11168
11578
  if (!framework) return "";
@@ -11204,7 +11614,7 @@ ${nextRow}`
11204
11614
  ].filter(Boolean);
11205
11615
  return parts.join("\n\n") || framework.slice(0, 1500);
11206
11616
  }
11207
- function buildLessonPrompt(pedagogy, lessonCode, unitCode, lessonTitle, concept, bloomLevel, dateStr) {
11617
+ function buildLessonPrompt(pedagogy, lessonCode, unitCode, lessonTitle, concept, bloomLevel, dateStr, targetLanguage = "vi", sectionLanguageContract) {
11208
11618
  const commonFrontmatter = `id: "LESSON_${lessonCode}"
11209
11619
  title: "${lessonTitle}"
11210
11620
  created_by: "Curriculum OS Builder"
@@ -11224,8 +11634,9 @@ template_required_sections:
11224
11634
  - "Artifact Contract"
11225
11635
  - "B. Lesson Flow"
11226
11636
  - "Artifact Linkage"`;
11637
+ let prompt = "";
11227
11638
  if (pedagogy === "edp") {
11228
- return `You are @content (Educational Content Editor & EDP Engineering Design Process Specialist).
11639
+ prompt = `You are @content (Educational Content Editor & EDP Engineering Design Process Specialist).
11229
11640
  Author the comprehensive Master Lesson Plan adhering strictly to \`LESSON_EDP_template.md\` standards for:
11230
11641
  - Lesson Code: ${lessonCode}
11231
11642
  - Title: ${lessonTitle}
@@ -11274,9 +11685,8 @@ Linkage ledger mapping all satellite artifacts (\`ACT_...\`, \`QUIZ_...\`, \`SLI
11274
11685
 
11275
11686
  3. Ensure all sample code and schemas are 100% syntax-valid and executable.
11276
11687
  4. Output 100% clean Markdown directly.`;
11277
- }
11278
- if (pedagogy === "pbl" || pedagogy === "general") {
11279
- return `You are @content (Educational Content Editor & PBL/General Pedagogy Specialist).
11688
+ } else if (pedagogy === "pbl" || pedagogy === "general") {
11689
+ prompt = `You are @content (Educational Content Editor & PBL/General Pedagogy Specialist).
11280
11690
  Author the comprehensive Master Lesson Plan adhering strictly to \`LESSON_GENERAL_template.md\` standards for:
11281
11691
  - Lesson Code: ${lessonCode}
11282
11692
  - Title: ${lessonTitle}
@@ -11323,8 +11733,8 @@ type: "LESSON_GENERAL"
11323
11733
  Linkage ledger mapping all satellite artifacts (\`ACT_...\`, \`QUIZ_...\`, \`SLIDE_...\`).
11324
11734
 
11325
11735
  3. Output 100% clean Markdown directly.`;
11326
- }
11327
- return `You are @content (Educational Content Editor & 5E Instructional Model Specialist).
11736
+ } else {
11737
+ prompt = `You are @content (Educational Content Editor & 5E Instructional Model Specialist).
11328
11738
  Author the comprehensive Master Lesson Plan adhering strictly to \`LESSON_5E_template.md\` standards for:
11329
11739
  - Lesson Code: ${lessonCode}
11330
11740
  - Title: ${lessonTitle}
@@ -11372,6 +11782,18 @@ Linkage ledger mapping all satellite artifacts (\`GUIDE_...\`, \`SLIDE_...\`, \`
11372
11782
 
11373
11783
  3. Ensure all sample code and syntax are 100% verified and runnable.
11374
11784
  4. Output 100% clean Markdown directly.`;
11785
+ }
11786
+ const langDirective = buildLanguageDirective(targetLanguage);
11787
+ const headingDirective = buildHeadingDirective(
11788
+ pedagogy === "edp" ? "LESSON_EDP" : pedagogy === "general" || pedagogy === "pbl" ? "LESSON_GENERAL" : "LESSON_5E",
11789
+ sectionLanguageContract,
11790
+ targetLanguage
11791
+ );
11792
+ return `${prompt}
11793
+
11794
+ ${langDirective}${headingDirective ? `
11795
+
11796
+ ${headingDirective}` : ""}`;
11375
11797
  }
11376
11798
  async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
11377
11799
  const {
@@ -11402,6 +11824,20 @@ async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
11402
11824
  const styleGuide = await storage.readSotDocument(projectId, "CONTENT_STYLE_GUIDE.md") || "";
11403
11825
  const refPack = await storage.readSotDocument(projectId, "REFERENCE_PACK.md") || "";
11404
11826
  await storage.readSotDocument(projectId, "LEARNER_PROFILE.md") || "";
11827
+ const slcMarkdown = options.sectionLanguageContract || await storage.readSotDocument(projectId, "SECTION_LANGUAGE_CONTRACT.md") || "";
11828
+ let targetLang = options.targetLanguage;
11829
+ if (!targetLang) {
11830
+ try {
11831
+ const planRaw = await storage.readArtifact(projectId, "_sot/CURRICULUM_PLAN.json");
11832
+ if (planRaw) {
11833
+ const p = JSON.parse(planRaw);
11834
+ targetLang = p?.translation?.target_language;
11835
+ }
11836
+ } catch {
11837
+ }
11838
+ }
11839
+ targetLang = targetLang || "vi";
11840
+ const languageDirective = buildLanguageDirective(targetLang);
11405
11841
  let expositionContext = "";
11406
11842
  let sessionSliceContext = "";
11407
11843
  try {
@@ -11436,6 +11872,18 @@ async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
11436
11872
  }
11437
11873
  const pedagogy = detectProjectPedagogy(framework, styleGuide, options.pedagogy);
11438
11874
  const pedagogyLabel = pedagogy.toUpperCase();
11875
+ const styleGuideExcerpt = styleGuide ? buildSectionAwareExcerpt(styleGuide, {
11876
+ priorities: ["Voice & Tone", "Standard Terminology (Glossary)", "Lesson Content Standards"],
11877
+ budget: 2e3
11878
+ }).excerpt : "";
11879
+ const refPackExcerpt = refPack ? buildSectionAwareExcerpt(refPack, {
11880
+ priorities: [
11881
+ "Technical Overview & Architecture Blueprint",
11882
+ "Hardware Pinout & Wiring Configuration Matrix",
11883
+ "Core Pedagogical Concept Anchor & Real-World Domain Bridge"
11884
+ ],
11885
+ budget: 2500
11886
+ }).excerpt : "";
11439
11887
  const commonContext = `PROJECT & ACADEMIC CONTEXT:
11440
11888
  - Project ID: ${projectId}
11441
11889
  - Pedagogical Model: ${pedagogyLabel}
@@ -11449,10 +11897,10 @@ async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
11449
11897
  ${buildFrameworkExcerptForLesson(framework, lessonId)}
11450
11898
 
11451
11899
  [CONTENT STYLE GUIDE EXCERPT]:
11452
- ${styleGuide.slice(0, 1500)}
11900
+ ${styleGuideExcerpt}
11453
11901
 
11454
11902
  [REFERENCE PACK GROUND TRUTH]:
11455
- ${refPack.slice(0, 1e3)}${standardsContext ? "\n\n" + standardsContext : ""}${expositionContext ? "\n\n[KNOWLEDGE_EXPOSITION (CANONICAL KNOWLEDGE \u2014 teach from this, do not contradict)]:\n" + expositionContext : ""}${sessionSliceContext ? "\n\n" + sessionSliceContext : ""}`;
11903
+ ${refPackExcerpt}${standardsContext ? "\n\n" + standardsContext : ""}${expositionContext ? "\n\n[KNOWLEDGE_EXPOSITION (CANONICAL KNOWLEDGE \u2014 teach from this, do not contradict)]:\n" + expositionContext : ""}${sessionSliceContext ? "\n\n" + sessionSliceContext : ""}`;
11456
11904
  const producedArtifacts = [];
11457
11905
  const lessonRelPath = `_content/${unitCode}/LESSON_${lessonCode}.md`;
11458
11906
  const existingLessonContent = await storage.readArtifact(projectId, lessonRelPath);
@@ -11471,7 +11919,9 @@ Khi n\u1ED9i dung c\u1EA7n minh h\u1ECDa (diagram quy tr\xECnh, s\u01A1 \u0111\u
11471
11919
  lessonTitle,
11472
11920
  concept,
11473
11921
  bloomLevel,
11474
- dateStr
11922
+ dateStr,
11923
+ targetLang,
11924
+ slcMarkdown
11475
11925
  ) + mediaDirective;
11476
11926
  const rawLesson = await runCurriculumAIInference(
11477
11927
  [{ role: "user", content: lessonPrompt }],
@@ -11570,14 +12020,15 @@ ${yamlBlock.trim()}
11570
12020
  const judgeResult = await LLMJudgeEngine.evaluateArtifactWithLLM({
11571
12021
  targetArtifactType: "LESSON",
11572
12022
  lessonId: lessonCode,
11573
- expectedLanguage: "en",
12023
+ expectedLanguage: targetLang || "en",
11574
12024
  targetObjectives: judgeObjectives,
11575
12025
  generatedContentJson: JSON.stringify({
11576
12026
  lessonContent,
11577
12027
  lessonId: lessonCode,
11578
12028
  title: lessonTitle,
11579
- frameworkExcerpt: framework ? framework.slice(0, 2500) : void 0,
11580
- styleGuideExcerpt: styleGuide ? styleGuide.slice(0, 1500) : void 0
12029
+ frameworkExcerpt: framework ? buildFrameworkExcerptForLesson(framework, lessonId) : void 0,
12030
+ styleGuideExcerpt: styleGuideExcerpt || void 0,
12031
+ canonicalExposition: expositionContext ? expositionContext.slice(0, 3e3) : void 0
11581
12032
  }),
11582
12033
  standardStatements: Object.keys(standardStatements).length > 0 ? standardStatements : void 0
11583
12034
  });
@@ -11642,14 +12093,15 @@ ${currentContent}` }],
11642
12093
  const reJudge = await LLMJudgeEngine.evaluateArtifactWithLLM({
11643
12094
  targetArtifactType: "LESSON",
11644
12095
  lessonId: lessonCode,
11645
- expectedLanguage: "en",
12096
+ expectedLanguage: targetLang || "en",
11646
12097
  targetObjectives: parseLessonObjectives(currentContent, concept, bloomLevel),
11647
12098
  generatedContentJson: JSON.stringify({
11648
12099
  lessonContent: currentContent,
11649
12100
  lessonId: lessonCode,
11650
12101
  title: lessonTitle,
11651
- frameworkExcerpt: framework ? framework.slice(0, 2500) : void 0,
11652
- styleGuideExcerpt: styleGuide ? styleGuide.slice(0, 1500) : void 0
12102
+ frameworkExcerpt: framework ? buildFrameworkExcerptForLesson(framework, lessonId) : void 0,
12103
+ styleGuideExcerpt: styleGuideExcerpt || void 0,
12104
+ canonicalExposition: expositionContext ? expositionContext.slice(0, 3e3) : void 0
11653
12105
  }),
11654
12106
  standardStatements: Object.keys(standardStatements).length > 0 ? standardStatements : void 0
11655
12107
  });
@@ -11770,10 +12222,34 @@ ${currentContent}` }],
11770
12222
  contentHash
11771
12223
  });
11772
12224
  }
12225
+ const lessonExcerptResult = buildSectionAwareExcerpt(lessonContent, {
12226
+ priorities: DEFAULT_LESSON_PRIORITIES,
12227
+ budget: 12e3,
12228
+ sectionLanguageContract: slcMarkdown,
12229
+ artifactType: "LESSON"
12230
+ });
12231
+ const lessonExcerpt = lessonExcerptResult.excerpt;
11773
12232
  const satelliteContext = `${commonContext}
11774
12233
 
11775
12234
  [CANONICAL LESSON PLAN (${pedagogyLabel})]:
11776
- ${lessonContent.slice(0, 4e3)}`;
12235
+ ${lessonExcerpt}`;
12236
+ const judgeSat = (sat, content) => {
12237
+ if (gateModeFor(gates, sat) !== "LLM_JUDGE") return Promise.resolve();
12238
+ return judgeSatelliteArtifact({
12239
+ storage,
12240
+ projectId,
12241
+ lessonCode,
12242
+ sat,
12243
+ content,
12244
+ concept,
12245
+ bloomLevel,
12246
+ standardStatements,
12247
+ onProgress,
12248
+ targetLang,
12249
+ canonicalLessonContent: lessonContent,
12250
+ canonicalLessonExcerpt: lessonExcerpt
12251
+ });
12252
+ };
11777
12253
  const actRelPath = `_content/${unitCode}/ACT_${lessonCode}.md`;
11778
12254
  if (artifactScope.includes("ACT") && (!await storage.readArtifact(projectId, actRelPath) || force)) {
11779
12255
  onProgress?.("@activity", `[2/4] Authoring Hands-on Lab: \`ACT_${lessonCode}.md\`...`);
@@ -11812,7 +12288,11 @@ template_required_sections:
11812
12288
  - ### Phase 3: Debugging & Testing
11813
12289
  - ## [REQUIRED] 3-Tier Differentiation (Bronze/Silver/Gold tasks)
11814
12290
  - ## [REQUIRED] Reflection (Discussion question)
11815
- 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.`;
12291
+ 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.
12292
+
12293
+ ${languageDirective}
12294
+
12295
+ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
11816
12296
  const rawAct = await runCurriculumAIInference(
11817
12297
  [{ role: "user", content: actPrompt }],
11818
12298
  satelliteContext,
@@ -11838,19 +12318,7 @@ template_required_sections:
11838
12318
  contentHash: computeContentHash(rawAct)
11839
12319
  });
11840
12320
  producedArtifacts.push(`ACT_${lessonCode}.md`);
11841
- if (gateModeFor(gates, "ACT") === "LLM_JUDGE") {
11842
- await judgeSatelliteArtifact({
11843
- storage,
11844
- projectId,
11845
- lessonCode,
11846
- sat: "ACT",
11847
- content: rawAct,
11848
- concept,
11849
- bloomLevel,
11850
- standardStatements,
11851
- onProgress
11852
- });
11853
- }
12321
+ await judgeSat("ACT", rawAct);
11854
12322
  }
11855
12323
  const quizRelPath = `_content/${unitCode}/QUIZ_${lessonCode}.md`;
11856
12324
  if (artifactScope.includes("QUIZ") && (!await storage.readArtifact(projectId, quizRelPath) || force)) {
@@ -11884,7 +12352,11 @@ template_required_sections:
11884
12352
  - ## [REQUIRED] Multiple-Choice Question Bank (\u22655 scenario-based 4-option questions with: Question text, A/B/C/D options, Correct answer, Bloom level, Detailed distractor rationale)
11885
12353
  - ## [REQUIRED] Code Analysis / Debugging Question (1 deep code tracing question requiring students to predict output or identify bugs)
11886
12354
  - ## [REQUIRED] Competency Rubric (4-tier matrix: Developing \u2192 Proficient \u2192 Advanced \u2192 Exemplary)
11887
- 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.`;
12355
+ 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.
12356
+
12357
+ ${languageDirective}
12358
+
12359
+ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
11888
12360
  const rawQuiz = await runCurriculumAIInference(
11889
12361
  [{ role: "user", content: quizPrompt }],
11890
12362
  satelliteContext,
@@ -11910,60 +12382,25 @@ template_required_sections:
11910
12382
  contentHash: computeContentHash(rawQuiz)
11911
12383
  });
11912
12384
  producedArtifacts.push(`QUIZ_${lessonCode}.md`);
11913
- if (gateModeFor(gates, "QUIZ") === "LLM_JUDGE") {
11914
- await judgeSatelliteArtifact({
11915
- storage,
11916
- projectId,
11917
- lessonCode,
11918
- sat: "QUIZ",
11919
- content: rawQuiz,
11920
- concept,
11921
- bloomLevel,
11922
- standardStatements,
11923
- onProgress
11924
- });
11925
- }
12385
+ await judgeSat("QUIZ", rawQuiz);
11926
12386
  }
11927
12387
  const slideRelPath = `_content/${unitCode}/SLIDE_${lessonCode}.md`;
11928
12388
  if (artifactScope.includes("SLIDE") && (!await storage.readArtifact(projectId, slideRelPath) || force)) {
11929
12389
  onProgress?.("@illustrator", `[4/4] Authoring Presentation Slide Deck: \`SLIDE_${lessonCode}.md\`...`);
11930
12390
  const canonicalSlideTemplate = loadCurriculumTemplate("SLIDE_template.md");
11931
- const slidePrompt = `You are @illustrator & @content (Pedagogical Slide Deck Presentation Specialists).
11932
- Based on Master Lesson Plan \`LESSON_${lessonCode}.md\`, lesson context, and the MARP SLIDE TEMPLATE below, author the complete, highly engaging Marp presentation deck: \`SLIDE_${lessonCode}.md\`.
11933
-
11934
- [MARP SLIDE TEMPLATE SCAFFOLD]:
11935
- ${canonicalSlideTemplate || `---
12391
+ const templateScaffold = canonicalSlideTemplate || `---
11936
12392
  marp: true
11937
12393
  theme: default
11938
12394
  paginate: true
11939
12395
  header: "${titleHeader(lessonTitle)}"
11940
12396
  footer: "Curriculum OS \u2022 ${lessonCode} (${pedagogyLabel})"
11941
- ---`}
11942
-
11943
- MANDATORY 12-14 SLIDE MICRO-CYCLE STRUCTURE (Delimited by '---'):
11944
- 1. **Slide 1 [Title & Mission]:** ${lessonCode}: ${lessonTitle} + Real-world inquiry hook
11945
- 2. **Slide 2 [Agenda & Roadmap]:** Timing and milestone breakdown table (${pedagogyLabel})
11946
- 3. **Slide 3 [Warm-Up / Prior Knowledge]:** 2-column recall + 60-second diagnostic challenge card
11947
- 4. **Slide 4 [High-Stakes Hook Scenario]:** Authentic engineering/domain crisis card + dilemma constraint
11948
- 5. **Slide 5 [Core Concept 1: Mental Model]:** Definition, intuitive everyday analogy, and golden rule card
11949
- 6. **Slide 6 [Architecture & Visual Flow]:** Clean Mermaid diagram (flowchart or sequenceDiagram) + 3 flow steps
11950
- 7. **Slide 7 [Core Concept 2: Syntax / Mechanism Anatomy]:** Side-by-side 6-10 lines runnable code + anatomy breakdown & syntax trap card
11951
- 8. **Slide 8 [Guided Practice: Try It Now]:** 3-minute active desk task card with starter hint and success criteria
11952
- 9. **Slide 9 [Integrated Hands-On Lab]:** 3-tier differentiated challenge cards (Bronze / Silver / Gold) + gotcha alert card
11953
- 10. **Slide 10 [Traffic Light Checkpoint & Pitfalls]:** Formative self-check cards (\u{1F7E2}/\u{1F7E1}/\u{1F534}) & diagnostic test question
11954
- 11. **Slide 11 [Exit Ticket & Metacognition]:** 2 scenario-based check questions + 1 metacognitive reflection prompt
11955
- 12. **Slide 12 [Summary & Next Steps]:** 3 core golden rules + preview card of next upcoming milestone
11956
-
11957
- Mandatory Canvas & Presentation Invariants:
11958
- - A slide is an INTERACTIVE PROJECTION CANVAS, NOT a textbook! Absolutely NO dense paragraphs or walls of text.
11959
- - Follow the 6x6 rule: Maximum 4-6 bullet points per slide, maximum 8-12 words per bullet.
11960
- - Structure slides using \`<div class="columns"><div>...</div><div>...</div></div>\` and \`<div class="card">\`, \`<div class="card highlight">\`, \`<div class="card warning">\`, \`<div class="card success">\`.
11961
- - MANDATORY PRESENTER NOTES ON EVERY SLIDE: Every slide MUST end with \`<!-- Presenter Notes: ... -->\` containing:
11962
- * SCRIPT: 60-90s teacher talk track with intuitive analogies.
11963
- * COLD CALL / CHECK: Specific diagnostic question to ask students.
11964
- * SCAFFOLDING / GOTCHA: Guidance for students who get stuck.
11965
- * PACING: Recommended minutes for the slide.
11966
- - Output 100% valid Marp Markdown.`;
12397
+ ---`;
12398
+ const slidePrompt = buildMarpMarkdownSlidePrompt({
12399
+ lessonCode,
12400
+ templateScaffold,
12401
+ languageDirective,
12402
+ headingDirective: buildHeadingDirective("SLIDE", slcMarkdown, targetLang)
12403
+ });
11967
12404
  const rawSlide = await runCurriculumAIInference(
11968
12405
  [{ role: "user", content: slidePrompt }],
11969
12406
  satelliteContext,
@@ -11989,19 +12426,7 @@ Mandatory Canvas & Presentation Invariants:
11989
12426
  contentHash: computeContentHash(rawSlide)
11990
12427
  });
11991
12428
  producedArtifacts.push(`SLIDE_${lessonCode}.md`);
11992
- if (gateModeFor(gates, "SLIDE") === "LLM_JUDGE") {
11993
- await judgeSatelliteArtifact({
11994
- storage,
11995
- projectId,
11996
- lessonCode,
11997
- sat: "SLIDE",
11998
- content: rawSlide,
11999
- concept,
12000
- bloomLevel,
12001
- standardStatements,
12002
- onProgress
12003
- });
12004
- }
12429
+ await judgeSat("SLIDE", rawSlide);
12005
12430
  }
12006
12431
  const guideRelPath = `_content/${unitCode}/GUIDE_${lessonCode}.md`;
12007
12432
  if (artifactScope.includes("GUIDE") && (!await storage.readArtifact(projectId, guideRelPath) || force)) {
@@ -12030,7 +12455,11 @@ template_contract: "artifact-template-v1"
12030
12455
  - ## [REQUIRED] Facilitation Script & Timeline (Phase-by-phase teacher moves, inquiry questions, transitions)
12031
12456
  - ## [REQUIRED] Common Misconceptions & Diagnostic Remediation Matrix
12032
12457
  - ## [REQUIRED] Differentiated Support Strategies (Support scaffolds vs Extension challenges)
12033
- 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.`;
12458
+ 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.
12459
+
12460
+ ${languageDirective}
12461
+
12462
+ ${buildHeadingDirective("GUIDE", slcMarkdown, targetLang)}`;
12034
12463
  const rawGuide = await runCurriculumAIInference(
12035
12464
  [{ role: "user", content: guidePrompt }],
12036
12465
  satelliteContext,
@@ -12047,19 +12476,7 @@ template_contract: "artifact-template-v1"
12047
12476
  contentHash: computeContentHash(rawGuide)
12048
12477
  });
12049
12478
  producedArtifacts.push(`GUIDE_${lessonCode}.md`);
12050
- if (gateModeFor(gates, "GUIDE") === "LLM_JUDGE") {
12051
- await judgeSatelliteArtifact({
12052
- storage,
12053
- projectId,
12054
- lessonCode,
12055
- sat: "GUIDE",
12056
- content: rawGuide,
12057
- concept,
12058
- bloomLevel,
12059
- standardStatements,
12060
- onProgress
12061
- });
12062
- }
12479
+ await judgeSat("GUIDE", rawGuide);
12063
12480
  }
12064
12481
  }
12065
12482
  const handoutRelPath = `_content/${unitCode}/HANDOUT_${lessonCode}.md`;
@@ -12089,7 +12506,11 @@ template_contract: "artifact-template-v1"
12089
12506
  - ## [REQUIRED] Visual Mental Model / Architecture Diagram (Mermaid or ASCII diagram)
12090
12507
  - ## [REQUIRED] Step-by-Step Practical Quick-Start Guide
12091
12508
  - ## [REQUIRED] Self-Check Diagnostic Checklist (Can-do statements for student self-assessment)
12092
- 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.`;
12509
+ 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.
12510
+
12511
+ ${languageDirective}
12512
+
12513
+ ${buildHeadingDirective("HANDOUT", slcMarkdown, targetLang)}`;
12093
12514
  const rawHandout = await runCurriculumAIInference(
12094
12515
  [{ role: "user", content: handoutPrompt }],
12095
12516
  satelliteContext,
@@ -12106,19 +12527,7 @@ template_contract: "artifact-template-v1"
12106
12527
  contentHash: computeContentHash(rawHandout)
12107
12528
  });
12108
12529
  producedArtifacts.push(`HANDOUT_${lessonCode}.md`);
12109
- if (gateModeFor(gates, "HANDOUT") === "LLM_JUDGE") {
12110
- await judgeSatelliteArtifact({
12111
- storage,
12112
- projectId,
12113
- lessonCode,
12114
- sat: "HANDOUT",
12115
- content: rawHandout,
12116
- concept,
12117
- bloomLevel,
12118
- standardStatements,
12119
- onProgress
12120
- });
12121
- }
12530
+ await judgeSat("HANDOUT", rawHandout);
12122
12531
  }
12123
12532
  }
12124
12533
  const wksRelPath = `_content/${unitCode}/WKS_${lessonCode}.md`;
@@ -12152,7 +12561,11 @@ template_contract: "artifact-template-v1"
12152
12561
  - ## [REQUIRED] Part 3: Code Analysis & Bug Hunting Challenge
12153
12562
  - ## [REQUIRED] Part 4: Synthesis & Problem-Solving Application
12154
12563
  - ## [REQUIRED] Part 5: Self-Reflection & Learning Log
12155
- 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.`;
12564
+ 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.
12565
+
12566
+ ${languageDirective}
12567
+
12568
+ ${buildHeadingDirective("WKS", slcMarkdown, targetLang)}`;
12156
12569
  const rawWks = await runCurriculumAIInference(
12157
12570
  [{ role: "user", content: wksPrompt }],
12158
12571
  satelliteContext,
@@ -12169,19 +12582,7 @@ template_contract: "artifact-template-v1"
12169
12582
  contentHash: computeContentHash(rawWks)
12170
12583
  });
12171
12584
  producedArtifacts.push(`WKS_${lessonCode}.md`);
12172
- if (gateModeFor(gates, "WKS") === "LLM_JUDGE") {
12173
- await judgeSatelliteArtifact({
12174
- storage,
12175
- projectId,
12176
- lessonCode,
12177
- sat: "WKS",
12178
- content: rawWks,
12179
- concept,
12180
- bloomLevel,
12181
- standardStatements,
12182
- onProgress
12183
- });
12184
- }
12585
+ await judgeSat("WKS", rawWks);
12185
12586
  }
12186
12587
  }
12187
12588
  const codeRelPath = `_content/${unitCode}/CODE_${lessonCode}.md`;
@@ -12219,7 +12620,11 @@ template_required_sections:
12219
12620
  - ## [REQUIRED] Verified Reference Solution Code (100% syntactically valid, production-ready, fully commented)
12220
12621
  - ## [REQUIRED] Automated Test / Verification Script (How to compile, flash, or unit test)
12221
12622
  - ## [REQUIRED] Common Syntax & Runtime Pitfalls Matrix
12222
- 3. Output 100% clean Markdown directly with runnable code snippets \u2014 NO outer code fences around the document.`;
12623
+ 3. Output 100% clean Markdown directly with runnable code snippets \u2014 NO outer code fences around the document.
12624
+
12625
+ ${languageDirective}
12626
+
12627
+ ${buildHeadingDirective("CODE", slcMarkdown, targetLang)}`;
12223
12628
  const rawCode = await runCurriculumAIInference(
12224
12629
  [{ role: "user", content: codePrompt }],
12225
12630
  satelliteContext,
@@ -12241,19 +12646,7 @@ template_required_sections:
12241
12646
  contentHash: computeContentHash(rawCode)
12242
12647
  });
12243
12648
  producedArtifacts.push(`CODE_${lessonCode}.md`);
12244
- if (gateModeFor(gates, "CODE") === "LLM_JUDGE") {
12245
- await judgeSatelliteArtifact({
12246
- storage,
12247
- projectId,
12248
- lessonCode,
12249
- sat: "CODE",
12250
- content: rawCode,
12251
- concept,
12252
- bloomLevel,
12253
- standardStatements,
12254
- onProgress
12255
- });
12256
- }
12649
+ await judgeSat("CODE", rawCode);
12257
12650
  }
12258
12651
  }
12259
12652
  const extRelPath = `_content/${unitCode}/EXT_${lessonCode}.md`;
@@ -12285,7 +12678,11 @@ template_contract: "artifact-template-v1"
12285
12678
  - ### Challenge 2: [Name] (Difficulty: \u2B50\u2B50\u2B50\u2B50) \u2014 Advanced Architectural Constraints & Edge Cases
12286
12679
  - ### Challenge 3: [Name] (Difficulty: \u2B50\u2B50\u2B50\u2B50\u2B50) \u2014 Extension Milestones (Level 1: Ninja \u2192 Level 2: Guru \u2192 Level 3: Master)
12287
12680
  - ## [REQUIRED] Metacognitive Deep Dive & Engineering Trade-offs
12288
- 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.`;
12681
+ 3. Output 100% clean Markdown directly \u2014 NO code fences around the document.
12682
+
12683
+ ${languageDirective}
12684
+
12685
+ ${buildHeadingDirective("EXT", slcMarkdown, targetLang)}`;
12289
12686
  const rawExt = await runCurriculumAIInference(
12290
12687
  [{ role: "user", content: extPrompt }],
12291
12688
  satelliteContext,
@@ -12302,19 +12699,7 @@ template_contract: "artifact-template-v1"
12302
12699
  contentHash: computeContentHash(rawExt)
12303
12700
  });
12304
12701
  producedArtifacts.push(`EXT_${lessonCode}.md`);
12305
- if (gateModeFor(gates, "EXT") === "LLM_JUDGE") {
12306
- await judgeSatelliteArtifact({
12307
- storage,
12308
- projectId,
12309
- lessonCode,
12310
- sat: "EXT",
12311
- content: rawExt,
12312
- concept,
12313
- bloomLevel,
12314
- standardStatements,
12315
- onProgress
12316
- });
12317
- }
12702
+ await judgeSat("EXT", rawExt);
12318
12703
  }
12319
12704
  }
12320
12705
  onProgress?.("@reviewer", `\u2705 Successfully authored all requested artifacts for \`${lessonCode}\` (${pedagogyLabel})!`);
@@ -12380,12 +12765,22 @@ async function judgeSatelliteArtifact(ctx) {
12380
12765
  const { storage, projectId, lessonCode, sat, content } = ctx;
12381
12766
  try {
12382
12767
  ctx.onProgress?.("@reviewer", `\u{1F916} Judging ${sat}_${lessonCode}...`);
12768
+ const targetObjectives = parseLessonObjectives(
12769
+ ctx.canonicalLessonContent || content,
12770
+ ctx.concept,
12771
+ ctx.bloomLevel
12772
+ );
12383
12773
  const result = await LLMJudgeEngine.evaluateArtifactWithLLM({
12384
12774
  targetArtifactType: sat,
12385
12775
  lessonId: lessonCode,
12386
- expectedLanguage: "en",
12387
- targetObjectives: parseLessonObjectives(content, ctx.concept, ctx.bloomLevel),
12388
- generatedContentJson: JSON.stringify({ content, artifactType: sat, lessonId: lessonCode }),
12776
+ expectedLanguage: ctx.targetLang || "en",
12777
+ targetObjectives,
12778
+ generatedContentJson: JSON.stringify({
12779
+ content,
12780
+ artifactType: sat,
12781
+ lessonId: lessonCode,
12782
+ canonicalLessonExcerpt: ctx.canonicalLessonExcerpt
12783
+ }),
12389
12784
  standardStatements: Object.keys(ctx.standardStatements).length > 0 ? ctx.standardStatements : void 0
12390
12785
  });
12391
12786
  const passed = result.passed && result.score >= 85;
@@ -12508,6 +12903,87 @@ async function produceBatchLessons(storage, projectId, lessonIds, options = {})
12508
12903
  // src/services/singleArtifactService.ts
12509
12904
  init_streamRunner();
12510
12905
  init_errors();
12906
+
12907
+ // src/services/contextRouting.ts
12908
+ init_errors();
12909
+ var ARTIFACT_DEPENDENCY_ROUTING = {
12910
+ LESSON: {
12911
+ required: ["KNOWLEDGE_EXPOSITION", "SESSION_SLICE", "FRAMEWORK_EXCERPT"],
12912
+ optional: ["STYLE_GUIDE", "REFERENCE_PACK"],
12913
+ rationale: "D\u1EA1y theo ki\u1EBFn th\u1EE9c chu\u1EA9n c\u1EE7a plan, ph\xE2n b\u1ED5 m\u1EE5c ti\xEAu v\xE0 khung ch\u01B0\u01A1ng tr\xECnh"
12914
+ },
12915
+ HANDOUT: {
12916
+ required: ["KNOWLEDGE_EXPOSITION"],
12917
+ optional: ["SESSION_SLICE"],
12918
+ rationale: "T\xE0i li\u1EC7u t\u1EF1 h\u1ECDc \u2014 b\xE1m kh\xE1i ni\u1EC7m chu\u1EA9n, kh\xF4ng c\u1EA7n activity timeline"
12919
+ },
12920
+ EXT: {
12921
+ required: ["KNOWLEDGE_EXPOSITION"],
12922
+ optional: ["SESSION_SLICE"],
12923
+ rationale: "Th\u1EED th\xE1ch m\u1EDF r\u1ED9ng chuy\xEAn s\xE2u b\xE1m kh\xE1i ni\u1EC7m"
12924
+ },
12925
+ CODE: {
12926
+ required: ["KNOWLEDGE_EXPOSITION"],
12927
+ optional: ["LESSON_OBJECTIVES"],
12928
+ rationale: "Code lab th\u1EF1c h\xE0nh b\xE1m kh\xE1i ni\u1EC7m k\u1EF9 thu\u1EADt v\xE0 v\xED d\u1EE5 worked examples"
12929
+ },
12930
+ ACT: {
12931
+ required: ["CANONICAL_LESSON"],
12932
+ optional: ["KNOWLEDGE_EXPOSITION", "SESSION_SLICE"],
12933
+ rationale: "STEM Activity Lab b\u1EAFt bu\u1ED9c ph\u1EA3i kh\u1EDBp 100% Activity Sequence v\xE0 Lesson Flow c\u1EE7a LESSON"
12934
+ },
12935
+ WKS: {
12936
+ required: ["CANONICAL_LESSON"],
12937
+ optional: ["KNOWLEDGE_EXPOSITION"],
12938
+ rationale: "Phi\u1EBFu h\u1ECDc t\u1EADp (Worksheet) luy\u1EC7n t\u1EADp theo c\xE1c ch\u1EB7ng ho\u1EA1t \u0111\u1ED9ng c\u1EE7a gi\xE1o \xE1n"
12939
+ },
12940
+ GUIDE: {
12941
+ required: ["CANONICAL_LESSON"],
12942
+ optional: ["SESSION_SLICE"],
12943
+ rationale: "K\u1ECBch b\u1EA3n s\u01B0 ph\u1EA1m gi\xE1o vi\xEAn (Teacher Guide) b\xE1m s\xE1t timeline t\u1EEBng ph\xFAt c\u1EE7a LESSON"
12944
+ },
12945
+ SLIDE: {
12946
+ required: ["CANONICAL_LESSON"],
12947
+ optional: ["KNOWLEDGE_EXPOSITION"],
12948
+ rationale: "Slide b\xE0i gi\u1EA3ng tr\xECnh chi\u1EBFu theo c\u1EA5u tr\xFAc ph\xE2n ph\u1ED1i n\u1ED9i dung c\u1EE7a gi\xE1o \xE1n"
12949
+ },
12950
+ QUIZ: {
12951
+ required: ["KNOWLEDGE_EXPOSITION", "CANONICAL_LESSON"],
12952
+ optional: ["SESSION_SLICE"],
12953
+ rationale: "Ng\xE2n h\xE0ng c\xE2u h\u1ECFi \u0111\xE1nh gi\xE1 c\u1EA3 ki\u1EBFn th\u1EE9c chu\u1EA9n m\u1EF1c (KX) l\u1EABn m\u1EE5c ti\xEAu b\xE0i d\u1EA1y (LESSON)"
12954
+ }
12955
+ };
12956
+ function validateArtifactDependencies(artifactType, availableSources, opts = {}) {
12957
+ const normType = (artifactType || "").toUpperCase().trim();
12958
+ const rule = ARTIFACT_DEPENDENCY_ROUTING[normType];
12959
+ const failClosed = opts.failClosed ?? true;
12960
+ if (!rule) {
12961
+ return { valid: true, missing: [] };
12962
+ }
12963
+ const missing = [];
12964
+ for (const req of rule.required) {
12965
+ const val = availableSources[req];
12966
+ const isPresent = val !== void 0 && val !== null && (typeof val === "string" ? val.trim().length > 0 : true);
12967
+ if (!isPresent) {
12968
+ missing.push(req);
12969
+ }
12970
+ }
12971
+ if (missing.length > 0) {
12972
+ const err = createContextMissingError({
12973
+ artifactType: normType,
12974
+ missingSource: missing[0],
12975
+ lessonId: opts.lessonId,
12976
+ agent: opts.agent
12977
+ });
12978
+ if (failClosed) {
12979
+ throw err;
12980
+ }
12981
+ return { valid: false, missing, error: err };
12982
+ }
12983
+ return { valid: true, missing: [] };
12984
+ }
12985
+
12986
+ // src/services/singleArtifactService.ts
12511
12987
  function getArtifactMetadata(type, lessonId, pedagogy) {
12512
12988
  const id = lessonId || "ON_DEMAND";
12513
12989
  switch (type) {
@@ -12587,9 +13063,24 @@ async function generateSingleArtifact(req) {
12587
13063
  const templateContent = loadedTemplate || getDefaultFallbackTemplate(artifactType, displayTitle, lessonId || "ON_DEMAND");
12588
13064
  const hwStr = hardwarePlatform.join(", ");
12589
13065
  const selectedDevices = (config?.devices || (hardwarePlatform[0]?.toLowerCase().includes("paper") ? "paper-only" : "laptop")).toLowerCase();
12590
- const isBilingual = language.toLowerCase().includes("bilingual") || language.toLowerCase().includes("song");
12591
- const isVietnamese = !isBilingual && language.toLowerCase().includes("viet");
12592
- const langRule = isBilingual ? `1. AUTHORING LANGUAGE: Bilingual (Vietnamese - English). Headings and core conceptual definitions should be presented in Vietnamese with English technical terms in parentheses. Explanations and instructional text are in Vietnamese. Key code terms, comments, and challenge summaries are presented bilingually.` : isVietnamese ? `1. AUTHORING LANGUAGE: Vietnamese for all instructional text, explanations, questions, rubrics, and presenter notes. Programming code, syntax keywords, and standard API identifiers remain in standard English (with Vietnamese explanation comments/notes).` : `1. AUTHORING LANGUAGE: 100% English (Plan C Canonical Specification).`;
13066
+ language.toLowerCase().includes("bilingual") || language.toLowerCase().includes("song");
13067
+ const targetLang = resolveTargetLanguageCode(language);
13068
+ const langDirective = buildLanguageDirective(language);
13069
+ const headingDirective = buildHeadingDirective(
13070
+ artifactType.toUpperCase(),
13071
+ req.sectionLanguageContract,
13072
+ targetLang
13073
+ );
13074
+ const availableSources = {
13075
+ KNOWLEDGE_EXPOSITION: contextSot.exposition || researchContext,
13076
+ CANONICAL_LESSON: contextSot.lessonPlan,
13077
+ FRAMEWORK_EXCERPT: contextSot.framework
13078
+ };
13079
+ validateArtifactDependencies(artifactType.toUpperCase(), availableSources, {
13080
+ lessonId,
13081
+ agent: meta.persona,
13082
+ failClosed: req.failClosedContext ?? false
13083
+ });
12593
13084
  const isPaperOnly = selectedDevices === "paper-only" || hwStr.toLowerCase().includes("paper") || hwStr.toLowerCase().includes("print-ready");
12594
13085
  const deviceRule = isPaperOnly ? `
12595
13086
  - \u{1F4C4} PAPER-ONLY / NO-DEVICE ENVIRONMENT: The learning space has NO computers or screens. All exercises, worksheets, task cards, exit tickets, and code analyses MUST be 100% printable. Provide write-in blank boxes, printed code snippets with line numbers, tracing tables, and physical manipulative instructions.` : selectedDevices === "tablet" || selectedDevices === "smartphone" ? `
@@ -12610,7 +13101,8 @@ async function generateSingleArtifact(req) {
12610
13101
  Your task is to author a canonical, publication-grade learning artifact of type "${artifactType.toUpperCase()}".
12611
13102
 
12612
13103
  ### CORE OPERATIONAL INVARIANTS:
12613
- ${langRule}
13104
+ ${langDirective}
13105
+ ${headingDirective}
12614
13106
  2. NON-MOCK CODE INVARIANT: Any code snippet provided MUST be 100% syntactically valid, idiomatic, and executable without placeholders or fake functions.
12615
13107
  3. PEDAGOGICAL GROUNDING: Strictly align with the ${pedagogy.toUpperCase()} model, Bloom's Revised Taxonomy, and IEEE/ACM CS2023 guidelines.
12616
13108
  4. SCAFFOLDING LEVEL: "${scaffoldingLevel.toUpperCase()}".${scaffoldingRule}
@@ -12619,6 +13111,11 @@ ${langRule}
12619
13111
 
12620
13112
  IMAGE PLANNING (media ledger) \u2014 QUOTA GEN: t\u1ED1i \u0111a ${mediaPolicy.maxGenImagesPerArtifact} \u1EA3nh AI cho artifact n\xE0y (\u1EA3nh search kho kh\xF4ng gi\u1EDBi h\u1EA1n).
12621
13113
  Khi n\u1ED9i dung c\u1EA7n minh h\u1ECDa (diagram quy tr\xECnh, s\u01A1 \u0111\u1ED3 kh\xE1i ni\u1EC7m, step-by-step visual), ch\xE8n placeholder \u0111\xFAng format [IMAGE: slug] (slug ch\u1EEF th\u01B0\u1EDDng-g\u1EA1ch ngang, unique trong b\xE0i, vd img-for-loop-diagram) T\u1EA0I \u0110\xDANG CH\u1ED6 c\u1EA7n \u1EA3nh, tr\xEAn d\xF2ng ri\xEAng. KH\xD4NG t\u1EF1 sinh \u1EA3nh, KH\xD4NG d\xF9ng markdown image \u2014 placeholder s\u1EBD \u0111\u01B0\u1EE3c thay b\u1EB1ng \u1EA3nh th\u1EADt sau khi Media Curator duy\u1EC7t. Ch\u1EC9 \u0111\u1EB7t cho \u1EA3nh TH\u1EF0C S\u1EF0 c\u1EA7n thi\u1EBFt.` : ""}`;
13114
+ const slcContract = req.sectionLanguageContract;
13115
+ const frameworkExcerpt = contextSot.framework ? buildSectionAwareExcerpt(contextSot.framework, { budget: 2500, sectionLanguageContract: slcContract }).excerpt : "";
13116
+ const lessonPlanExcerpt = contextSot.lessonPlan ? buildLessonExcerpt(contextSot.lessonPlan, { budget: 8e3, sectionLanguageContract: slcContract, artifactType: "LESSON" }).excerpt : "";
13117
+ const handoutExcerpt = contextSot.handout ? buildSectionAwareExcerpt(contextSot.handout, { budget: 2e3, sectionLanguageContract: slcContract }).excerpt : "";
13118
+ const expositionExcerpt = contextSot.exposition ? buildExpositionExcerpt(contextSot.exposition, { budget: 6e3, sectionLanguageContract: slcContract }).excerpt : "";
12622
13119
  const userPrompt = `### CONTEXT GROUND TRUTH:
12623
13120
  - Topic / Concept Domain: "${topic}"
12624
13121
  - Lesson ID: "${lessonId || "ON_DEMAND"}"
@@ -12636,14 +13133,17 @@ ${selectedDevices ? `- Device Mode: "${selectedDevices}"
12636
13133
  ${researchContext ? `### RESEARCH GROUND TRUTH (fact-checked context \u2014 \u01B0u ti\xEAn d\xF9ng, KH\xD4NG b\u1ECBa s\u1ED1 li\u1EC7u):
12637
13134
  ${researchContext.slice(0, 4e3)}
12638
13135
  ` : ""}
12639
- ${contextSot.framework ? `### SOT CURRICULUM FRAMEWORK:
12640
- ${contextSot.framework.slice(0, 1500)}
13136
+ ${expositionExcerpt ? `### SOT KNOWLEDGE EXPOSITION (CANONICAL KNOWLEDGE \u2014 teach from this, do not contradict):
13137
+ ${expositionExcerpt}
13138
+ ` : ""}
13139
+ ${frameworkExcerpt ? `### SOT CURRICULUM FRAMEWORK:
13140
+ ${frameworkExcerpt}
12641
13141
  ` : ""}
12642
- ${contextSot.lessonPlan ? `### SOT CANONICAL LESSON PLAN:
12643
- ${contextSot.lessonPlan.slice(0, 1500)}
13142
+ ${lessonPlanExcerpt ? `### SOT CANONICAL LESSON PLAN:
13143
+ ${lessonPlanExcerpt}
12644
13144
  ` : ""}
12645
- ${contextSot.handout ? `### SOT STUDENT HANDOUT:
12646
- ${contextSot.handout.slice(0, 1e3)}
13145
+ ${handoutExcerpt ? `### SOT STUDENT HANDOUT:
13146
+ ${handoutExcerpt}
12647
13147
  ` : ""}
12648
13148
 
12649
13149
  ### MANDATORY CANONICAL TEMPLATE CONTRACT:
@@ -12732,9 +13232,16 @@ ${yaml.trim()}
12732
13232
  const verdict = await LLMJudgeEngine.evaluateArtifactWithLLM({
12733
13233
  targetArtifactType: artifactType.toUpperCase(),
12734
13234
  lessonId: lessonId || "ON_DEMAND",
12735
- expectedLanguage: language.toLowerCase().includes("viet") ? "vietnamese" : "english",
13235
+ expectedLanguage: targetLang || "en",
12736
13236
  targetObjectives: judgeObjectives,
12737
- generatedContentJson: JSON.stringify({ content: finalContent, artifactType, lessonId: lessonId || "ON_DEMAND", title: displayTitle })
13237
+ generatedContentJson: JSON.stringify({
13238
+ content: finalContent,
13239
+ artifactType,
13240
+ lessonId: lessonId || "ON_DEMAND",
13241
+ title: displayTitle,
13242
+ canonicalLessonExcerpt: lessonPlanExcerpt || void 0,
13243
+ canonicalExposition: expositionExcerpt || void 0
13244
+ })
12738
13245
  });
12739
13246
  const passed = Boolean(verdict.passed) && verdict.score >= 85;
12740
13247
  if (passed) {
@@ -12776,9 +13283,16 @@ ${finalContent}` }
12776
13283
  const reVerdict = await LLMJudgeEngine.evaluateArtifactWithLLM({
12777
13284
  targetArtifactType: artifactType.toUpperCase(),
12778
13285
  lessonId: lessonId || "ON_DEMAND",
12779
- expectedLanguage: language.toLowerCase().includes("viet") ? "vietnamese" : "english",
13286
+ expectedLanguage: targetLang || "en",
12780
13287
  targetObjectives: judgeObjectives,
12781
- generatedContentJson: JSON.stringify({ content: repaired, artifactType, lessonId: lessonId || "ON_DEMAND", title: displayTitle })
13288
+ generatedContentJson: JSON.stringify({
13289
+ content: repaired,
13290
+ artifactType,
13291
+ lessonId: lessonId || "ON_DEMAND",
13292
+ title: displayTitle,
13293
+ canonicalLessonExcerpt: lessonPlanExcerpt || void 0,
13294
+ canonicalExposition: expositionExcerpt || void 0
13295
+ })
12782
13296
  });
12783
13297
  if (reVerdict.passed && reVerdict.score >= 85) {
12784
13298
  return {
@@ -16730,6 +17244,7 @@ function renderMediaPlaceholder(entry) {
16730
17244
  }
16731
17245
 
16732
17246
  exports.ACT_TEMPLATE = ACT_TEMPLATE;
17247
+ exports.ARTIFACT_DEPENDENCY_ROUTING = ARTIFACT_DEPENDENCY_ROUTING;
16733
17248
  exports.ARTIFACT_EXECUTION_ORDER = ARTIFACT_EXECUTION_ORDER;
16734
17249
  exports.AcademicAuditor = AcademicAuditor;
16735
17250
  exports.ActiveLearningWorkflowSchema = ActiveLearningWorkflowSchema;
@@ -16774,6 +17289,8 @@ exports.CurriculumPlanSchema = CurriculumPlanSchema;
16774
17289
  exports.CurriculumQualityReportSchema = CurriculumQualityReportSchema;
16775
17290
  exports.DEFAULT_ENABLED_PROVIDERS = DEFAULT_ENABLED_PROVIDERS;
16776
17291
  exports.DEFAULT_GATE_SETTINGS = DEFAULT_GATE_SETTINGS;
17292
+ exports.DEFAULT_KX_PRIORITIES = DEFAULT_KX_PRIORITIES;
17293
+ exports.DEFAULT_LESSON_PRIORITIES = DEFAULT_LESSON_PRIORITIES;
16777
17294
  exports.DEFAULT_STREAM_IDLE_MS = DEFAULT_STREAM_IDLE_MS;
16778
17295
  exports.DEFAULT_STREAM_TOTAL_MS = DEFAULT_STREAM_TOTAL_MS;
16779
17296
  exports.DependencyEdgeSchema = DependencyEdgeSchema;
@@ -16873,6 +17390,7 @@ exports.RubricCriteriaSchema = RubricCriteriaSchema;
16873
17390
  exports.RubricSchema = RubricSchema;
16874
17391
  exports.SCIENCE_LAB_TEMPLATE = SCIENCE_LAB_TEMPLATE;
16875
17392
  exports.SELF_LAB_TEMPLATE = SELF_LAB_TEMPLATE;
17393
+ exports.SLIDE_CANONICAL_METHODOLOGY = SLIDE_CANONICAL_METHODOLOGY;
16876
17394
  exports.SLIDE_LAYOUT_PRESETS = SLIDE_LAYOUT_PRESETS;
16877
17395
  exports.SLIDE_TEMPLATE = SLIDE_TEMPLATE;
16878
17396
  exports.STANDARD_SOT_FILES = STANDARD_SOT_FILES;
@@ -16927,12 +17445,18 @@ exports.buildCurriculumPlan = buildCurriculumPlan;
16927
17445
  exports.buildDeliveryPackages = buildDeliveryPackages;
16928
17446
  exports.buildDiagnosticQuizPrompt = buildDiagnosticQuizPrompt;
16929
17447
  exports.buildExpositionContext = buildExpositionContext;
17448
+ exports.buildExpositionExcerpt = buildExpositionExcerpt;
16930
17449
  exports.buildExtensionPrompt = buildExtensionPrompt;
16931
17450
  exports.buildHandoutPrompt = buildHandoutPrompt;
17451
+ exports.buildHeadingDirective = buildHeadingDirective;
16932
17452
  exports.buildImagePrompt = buildImagePrompt;
16933
17453
  exports.buildJudgePrompt = buildJudgePrompt;
17454
+ exports.buildLanguageDirective = buildLanguageDirective;
17455
+ exports.buildLessonExcerpt = buildLessonExcerpt;
16934
17456
  exports.buildLessonMasterPrompt = buildLessonMasterPrompt;
17457
+ exports.buildMarpMarkdownSlidePrompt = buildMarpMarkdownSlidePrompt;
16935
17458
  exports.buildProjectInstructionPrompt = buildProjectInstructionPrompt;
17459
+ exports.buildSectionAwareExcerpt = buildSectionAwareExcerpt;
16936
17460
  exports.buildSelfLabPrompt = buildSelfLabPrompt;
16937
17461
  exports.buildSessionSliceContext = buildSessionSliceContext;
16938
17462
  exports.buildSlidesPrompt = buildSlidesPrompt;
@@ -16948,6 +17472,7 @@ exports.conductPreliminaryResearch = conductPreliminaryResearch;
16948
17472
  exports.contentTools = contentTools;
16949
17473
  exports.convertRoadmapToFoundationSot = convertRoadmapToFoundationSot;
16950
17474
  exports.createAiInferenceError = createAiInferenceError;
17475
+ exports.createContextMissingError = createContextMissingError;
16951
17476
  exports.createCurriculumStorage = createCurriculumStorage;
16952
17477
  exports.createIdleAbortController = createIdleAbortController;
16953
17478
  exports.createStreamAbortController = createStreamAbortController;
@@ -16963,6 +17488,7 @@ exports.executeCurriculumCommand = executeCurriculumCommand;
16963
17488
  exports.exportAllProjectQuizzes = exportAllProjectQuizzes;
16964
17489
  exports.expositionCacheKey = expositionCacheKey;
16965
17490
  exports.extractScopeSequenceRows = extractScopeSequenceRows;
17491
+ exports.extractSectionHeadingsFromSLC = extractSectionHeadingsFromSLC;
16966
17492
  exports.extractSessionSlice = extractSessionSlice;
16967
17493
  exports.extractStreamChunk = extractStreamChunk;
16968
17494
  exports.extractThoughtAndContent = extractThoughtAndContent;
@@ -17014,6 +17540,7 @@ exports.lintFrameworkPack = lintFrameworkPack;
17014
17540
  exports.loadCurriculumTemplate = loadCurriculumTemplate;
17015
17541
  exports.loadSotTemplate = loadSotTemplate;
17016
17542
  exports.normalizePlanningGraph = normalizePlanningGraph;
17543
+ exports.normalizeSlcContract = normalizeSlcContract;
17017
17544
  exports.packagerTools = packagerTools;
17018
17545
  exports.parseGateSettings = parseGateSettings;
17019
17546
  exports.parseQuizMarkdown = parseQuizMarkdown;
@@ -17029,6 +17556,7 @@ exports.researcherTools = researcherTools;
17029
17556
  exports.resolveGateSettings = resolveGateSettings;
17030
17557
  exports.resolveStandardsPacks = resolveStandardsPacks;
17031
17558
  exports.resolveStreamBudget = resolveStreamBudget;
17559
+ exports.resolveTargetLanguageCode = resolveTargetLanguageCode;
17032
17560
  exports.resolveTranslationTargets = resolveTranslationTargets;
17033
17561
  exports.reviewerTools = reviewerTools;
17034
17562
  exports.runCurriculumAIInference = runCurriculumAIInference;
@@ -17052,9 +17580,11 @@ exports.streamLayerPrefillWithFallback = streamLayerPrefillWithFallback;
17052
17580
  exports.streamLessonMasterFlow = streamLessonMasterFlow;
17053
17581
  exports.streamSelfLabFlow = streamSelfLabFlow;
17054
17582
  exports.stripLlmJsonWrappers = stripLlmJsonWrappers;
17583
+ exports.targetLanguageDisplayName = targetLanguageDisplayName;
17055
17584
  exports.techSmeTools = techSmeTools;
17056
17585
  exports.topoSort = topoSort;
17057
17586
  exports.uploadAssetToBucket = uploadAssetToBucket;
17587
+ exports.validateArtifactDependencies = validateArtifactDependencies;
17058
17588
  exports.validateCurriculumPlan = validateCurriculumPlan;
17059
17589
  exports.validateFrameworkPack = validateFrameworkPack;
17060
17590
  exports.validateMarkdownTables = validateMarkdownTables;