@thanh01.pmt/curriculum-kit 1.4.24 → 1.4.26

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
@@ -1336,6 +1336,10 @@ function parseLessonFlow(lessonMarkdown) {
1336
1336
  }
1337
1337
  const durMatch = lessonMarkdown.match(/Estimated Duration:\s*([^\n]+)/i);
1338
1338
  if (durMatch) result.estimatedDuration = durMatch[1].trim();
1339
+ if (!result.lessonTitle) {
1340
+ const h1 = lessonMarkdown.match(/^#\s+(?:LESSON[^:]*:\s*)?(.+)$/m);
1341
+ if (h1 && h1[1]) result.lessonTitle = h1[1].replace(/\*+/g, "").trim();
1342
+ }
1339
1343
  const contractMatch = lessonMarkdown.match(/SLIDE must visualize:?\s*([^\n]+)/i);
1340
1344
  if (contractMatch) result.slideContract = contractMatch[1].trim();
1341
1345
  const actSeqSectionMatch = lessonMarkdown.match(/###\s*(?:\d+\.\s*)?Activity Sequence[\s\S]*?(?=(?:###|\n##\s+|$))/i);
@@ -1417,7 +1421,7 @@ var init_lessonFlowParser = __esm({
1417
1421
 
1418
1422
  // src/ai/prompts/slideBlueprintPrompt.ts
1419
1423
  function buildSlideBlueprintPrompt(params) {
1420
- const { lessonFlow, targetSlideCount, stylePresetName = "Blue Professional" } = params;
1424
+ const { lessonFlow, targetSlideCount, stylePresetName = "Blue Professional", productSpecBlock } = params;
1421
1425
  const activitiesSummary = lessonFlow.activities.map(
1422
1426
  (a) => `Seq ${a.seq} [${a.phase}]: ${a.purpose} (Teacher: ${a.teacherMove} | Student: ${a.studentAction} | Time: ${a.time})`
1423
1427
  ).join("\n");
@@ -1439,7 +1443,9 @@ ${countGuidance}
1439
1443
  - Target Duration: ${lessonFlow.estimatedDuration}
1440
1444
  - Active Visual Style: "${stylePresetName}"
1441
1445
  ${lessonFlow.slideContract ? `- Slide Artifact Contract: "${lessonFlow.slideContract}"` : ""}
1442
-
1446
+ ${productSpecBlock ? `
1447
+ ${productSpecBlock}
1448
+ ` : ""}
1443
1449
  ### \u{1F5FA}\uFE0F CANONICAL ACTIVITY SEQUENCE:
1444
1450
  ${activitiesSummary || "Standard phased sequence"}
1445
1451
 
@@ -1513,7 +1519,7 @@ function buildSlideBatchPrompt(params) {
1513
1519
  skillPrompt,
1514
1520
  language = "Vietnamese",
1515
1521
  languageDirective = "",
1516
- headingDirective = "",
1522
+ // deprecated — see interface doc
1517
1523
  groundContext = ""
1518
1524
  } = params;
1519
1525
  const slidesSpec = clusterSlides.map((s) => `
@@ -1534,7 +1540,6 @@ ${groundContext}
1534
1540
  ---` : ""}
1535
1541
  ### OPERATIONAL GROUND RULES FOR CHUNK GENERATION:
1536
1542
  ${languageDirective}
1537
- ${headingDirective}
1538
1543
  1. **FOCUS ON CLUSTER ${clusterId}: "${clusterTitle}"**:
1539
1544
  Generate EXACTLY the ${clusterSlides.length} slides requested in the user prompt.
1540
1545
  2. **ZERO ABBREVIATION / NO "// TODO"**:
@@ -1725,7 +1730,7 @@ async function inferStructured(schema, systemPrompt, userPrompt, options, label)
1725
1730
  const messages = [
1726
1731
  { role: "user", content: [systemPrompt, userPrompt].filter(Boolean).join("\n\n") }
1727
1732
  ];
1728
- const raw = await runCurriculumAIInference(messages, options.satelliteContext || "", options.runnerOptions, (token, type) => {
1733
+ const raw = await runCurriculumAIInference(messages, "", options.runnerOptions, (token, type) => {
1729
1734
  if (type === "usage") options.onProgress?.("@illustrator", token, { type: "usage" });
1730
1735
  });
1731
1736
  const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
@@ -1911,7 +1916,8 @@ async function executeSlideProductionWorkflow(options) {
1911
1916
  const blueprintPrompt = buildSlideBlueprintPrompt({
1912
1917
  lessonFlow,
1913
1918
  targetSlideCount,
1914
- stylePresetName: stylePreset?.name || "Blue Professional"
1919
+ stylePresetName: stylePreset?.name || "Blue Professional",
1920
+ productSpecBlock: options.productSpecBlock
1915
1921
  });
1916
1922
  let blueprintItems;
1917
1923
  let allGeneratedSlides;
@@ -1994,7 +2000,6 @@ ${p.content}`).join("\n\n") : lessonFlow.rawContent.slice(0, 3500);
1994
2000
  skillPrompt,
1995
2001
  language,
1996
2002
  languageDirective,
1997
- headingDirective,
1998
2003
  groundContext: options.groundContext
1999
2004
  });
2000
2005
  try {
@@ -11719,19 +11724,30 @@ function extractToolchainFacts(refPack) {
11719
11724
  const factLines = [];
11720
11725
  for (const rawLine of excerpt.split("\n")) {
11721
11726
  const line = rawLine.trim();
11722
- if (!line || line.startsWith("#")) continue;
11723
- if (/\d{1,2}(?:\.\d{1,2})?/.test(line) || /Xcode|Swift|macOS|iOS|SDK|Node|Python|JDK|\.NET/i.test(line)) {
11724
- factLines.push(line.replace(/^[-*•]\s*/, "- "));
11725
- }
11727
+ if (!line || line.startsWith("#") || line.startsWith("<!--")) continue;
11728
+ if (!/\d{1,2}(?:\.\d{1,2})?/.test(line) && !/Xcode|Swift|macOS|iOS|SDK|Node|Python|JDK|\.NET/i.test(line)) continue;
11729
+ factLines.push(line.replace(/^[*\-•]\s*/, "- "));
11726
11730
  if (factLines.length >= 8) break;
11727
11731
  }
11728
11732
  return factLines;
11729
11733
  }
11734
+ function derivePrimaryTechnology(facts, briefing) {
11735
+ const explicit = (briefing?.coreTechnology || briefing?.techStack || "").trim();
11736
+ if (explicit && explicit.length <= 80) return explicit;
11737
+ const found = [];
11738
+ const CANON = ["Swift", "SwiftUI", "macOS", "iOS", "Xcode", "SwiftData", "Node.js", "TypeScript", "Python", "React"];
11739
+ for (const name of CANON) {
11740
+ if (facts.some((f) => f.includes(name)) && !found.includes(name)) found.push(name);
11741
+ }
11742
+ if (found.length > 0) return found.slice(0, 4).join(" + ");
11743
+ const topic = (briefing?.topic || "").trim();
11744
+ return topic && topic.length <= 60 ? topic : "";
11745
+ }
11730
11746
  function buildProductSpecBlock(input) {
11731
11747
  const b = input.briefing || {};
11732
- const platform = (input.overrides?.primaryLanguage || b.coreTechnology || b.techStack || b.topic || "").trim();
11733
- const hardware = (b.hardwarePlatform || b.studentEquipment || b.classDynamic || "").trim();
11734
11748
  const toolchainFacts = input.refPack ? extractToolchainFacts(input.refPack) : [];
11749
+ const platform = (input.overrides?.primaryLanguage || derivePrimaryTechnology(toolchainFacts, b) || "").trim();
11750
+ const hardware = (b.hardwarePlatform || b.studentEquipment || b.classDynamic || "").trim();
11735
11751
  const lines = [];
11736
11752
  lines.push("[PRODUCT SPEC \u2014 CANONICAL, BINDING FOR EVERY ARTIFACT OF THIS PROJECT]:");
11737
11753
  lines.push("All decisions below are PROJECT-WIDE. Every artifact (LESSON, satellites, KX)");
@@ -11747,7 +11763,7 @@ function buildProductSpecBlock(input) {
11747
11763
  if (input.overrides?.deliverableTemplate) {
11748
11764
  lines.push(`- Deliverable template: ${input.overrides.deliverableTemplate}`);
11749
11765
  }
11750
- if (toolchainFacts.length > 0) {
11766
+ if (toolchainFacts.length > 0 && !input.skipToolchainList) {
11751
11767
  lines.push("- Toolchain & versions (ground truth):");
11752
11768
  for (const f of toolchainFacts) lines.push(` ${f}`);
11753
11769
  }
@@ -11875,7 +11891,9 @@ async function ensureKnowledgeExposition(options) {
11875
11891
  const productSpecBlock = buildProductSpecBlock({
11876
11892
  refPack,
11877
11893
  briefing: techStack || hardwarePlatform ? { techStack, hardwarePlatform } : void 0,
11878
- overrides: options.productSpec
11894
+ overrides: options.productSpec,
11895
+ skipToolchainList: true
11896
+ // same RefPack excerpt already in [GROUND TRUTH]
11879
11897
  });
11880
11898
  const originalUserPrompt = buildUserPrompt(session, plan, glossary, targetLanguage, options.slcMarkdown, refPack) + (productSpecBlock ? "\n\n" + productSpecBlock : "");
11881
11899
  const content = (await llmFn(
@@ -11975,17 +11993,29 @@ async function ensureExpositionForLesson(storage, projectId, lessonCode, options
11975
11993
  const lpRaw = await storage.readSotDocument(projectId, "LEARNER_PROFILE.md");
11976
11994
  if (lpRaw) {
11977
11995
  const hwPatterns = [
11996
+ // "Cơ sở phần cứng (Hardware Ecosystem): Học sinh thực hành trên máy..."
11997
+ // — the audit: the old pattern captured the parenthetical label
11998
+ // "(Hardware Ecosystem):" itself. Capture AFTER the closing paren + colon.
11999
+ /\([^)]*Ecosystem[^)]*\)\s*:\s*([^\n\r]+)/i,
11978
12000
  /(?:Student Equipment|Hardware|Platform|Thiết bị|Thiết bị học tập)\s*(?:\(|:|\*)*\s*([^\n\r]+?)(?:\s*\*\*|[.).]?\s*$)/i,
11979
12001
  /(?:máy|device|machine)\s+([^,.;\n]{4,60}(?:M\d|Intel|PC|computer|mini)[^,.;\n]{0,30})/i
11980
12002
  ];
11981
12003
  for (const p of hwPatterns) {
11982
12004
  const m = lpRaw.match(p);
11983
- if (m && !hw) {
11984
- hw = m[1].trim();
12005
+ if (m && m[1] && !hw) {
12006
+ const firstSentence = m[1].split(/(?<=[.!?])\s/)[0] || m[1];
12007
+ hw = firstSentence.replace(/\*+/g, "").replace(/\s+/g, " ").trim().slice(0, 140);
11985
12008
  break;
11986
12009
  }
11987
12010
  }
11988
12011
  }
12012
+ if (!hw) {
12013
+ const rpRaw = await storage.readSotDocument(projectId, "REFERENCE_PACK.md");
12014
+ if (rpRaw) {
12015
+ const rp = rpRaw.match(/(?:School Lab|Thiết bị lớp học)[^\n]*\*\*([^*]+(?:M\d|Mac)[^*]*)\*\*/i);
12016
+ if (rp && rp[1]) hw = rp[1].trim();
12017
+ }
12018
+ }
11989
12019
  } catch {
11990
12020
  }
11991
12021
  }
@@ -12722,11 +12752,11 @@ function buildSessionSliceContext(plan, lessonCode) {
12722
12752
  lines.push("- Time split: knowledge " + s.knowledge_minutes + "m / practice " + s.practice_minutes + "m / overhead " + s.overhead_minutes + "m");
12723
12753
  lines.push("- Depth assignments: " + s.depth_assignments.map((d) => d.node_id + "=" + d.depth.toUpperCase() + "(" + d.source + ")").join("; "));
12724
12754
  if (s.prerequisite_decisions.length > 0) {
12725
- lines.push("- Prerequisite decisions [7]:");
12755
+ lines.push("- Prerequisite decisions [" + s.prerequisite_decisions.length + "]:");
12726
12756
  for (const d of s.prerequisite_decisions) lines.push(" * " + d.node_id + ": " + d.decision + " \u2014 " + d.reason);
12727
12757
  }
12728
12758
  if (s.scaffold_decisions.length > 0) {
12729
- lines.push("- Scaffold decisions [7]:");
12759
+ lines.push("- Scaffold decisions [" + s.scaffold_decisions.length + "]:");
12730
12760
  for (const d of s.scaffold_decisions) lines.push(" * " + d.item + ": " + d.decision + (d.minutes_saved > 0 ? " (saves " + d.minutes_saved + "m)" : "") + " \u2014 " + d.reason);
12731
12761
  }
12732
12762
  lines.push("- Exit evidence: " + s.exit_evidence.join("; "));
@@ -25961,8 +25991,11 @@ function buildFrameworkExcerptForLesson(framework, lessonId) {
25961
25991
  });
25962
25992
  const rowIdx = scopeRows[0] ?? -1;
25963
25993
  const currentRow = rowIdx >= 0 ? lines[rowIdx] : "";
25964
- const prevRow = rowIdx > 0 && (lines[rowIdx - 1] || "").trim().startsWith("|") ? lines[rowIdx - 1] : "";
25965
- const nextRow = (lines[rowIdx + 1] || "").trim().startsWith("|") ? lines[rowIdx + 1] : "";
25994
+ const isSeparatorRow = (l) => !!l && /^\s*\|?[\s:|-]+\|?\s*$/.test(l) && l.includes("-");
25995
+ const prevRaw = rowIdx > 0 ? lines[rowIdx - 1] : "";
25996
+ const prevRow = prevRaw.trim().startsWith("|") && !isSeparatorRow(prevRaw) ? prevRaw : "";
25997
+ const nextRaw = lines[rowIdx + 1] || "";
25998
+ const nextRow = nextRaw.trim().startsWith("|") && !isSeparatorRow(nextRaw) ? nextRaw : "";
25966
25999
  const hierStart = lines.findIndex((l) => /Structural Hierarchy/i.test(l));
25967
26000
  let hierarchyBlock = "";
25968
26001
  if (hierStart >= 0) {
@@ -26368,6 +26401,7 @@ ${renderHorizonPromptBlock(horizon)}`;
26368
26401
  } catch (hErr) {
26369
26402
  console.warn(`[produceSingleLesson] Notice: Curriculum Horizon extraction:`, hErr?.message || hErr);
26370
26403
  }
26404
+ let productSpecBlock = "";
26371
26405
  const buildGroundTruthBlock = () => {
26372
26406
  const parts = [];
26373
26407
  if (expositionContext) {
@@ -26378,9 +26412,9 @@ ${expositionContext}`);
26378
26412
  parts.push(`### REFERENCE PACK GROUND TRUTH
26379
26413
  ${effectiveRefPack}`);
26380
26414
  }
26381
- const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26382
- if (productSpec) {
26383
- parts.push(productSpec);
26415
+ productSpecBlock = buildProductSpecBlock({ refPack, briefing: options.briefing, skipToolchainList: true });
26416
+ if (productSpecBlock) {
26417
+ parts.push(productSpecBlock);
26384
26418
  }
26385
26419
  return parts.length > 0 ? `
26386
26420
 
@@ -26397,10 +26431,17 @@ ${standardsContext}` : "";
26397
26431
  const sessionSliceBlock = sessionSliceContext ? `
26398
26432
 
26399
26433
  ${sessionSliceContext}` : "";
26434
+ const platformGuardrail = buildDomainLexiconGuardrail(
26435
+ options.briefing?.techStack || options.briefing?.coreTechnology,
26436
+ options.briefing?.hardwarePlatform
26437
+ );
26438
+ const guardrailBlock = `
26439
+
26440
+ ${platformGuardrail}`;
26400
26441
  const assembleCommonContext = (sg) => `${baseContextPrefix}
26401
26442
 
26402
26443
  [CONTENT STYLE GUIDE EXCERPT]:
26403
- ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBlock}${horizonBlock}`;
26444
+ ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBlock}${horizonBlock}${guardrailBlock}`;
26404
26445
  let commonContext = assembleCommonContext(effectiveStyleGuide);
26405
26446
  const MAX_COMPOSITE_PROMPT_CHARS = 3e4;
26406
26447
  const runnerOptions = options.customInference ? { customInference: options.customInference } : {};
@@ -26926,6 +26967,11 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26926
26967
  const quizPrompt = `You are @assessor (Lead Psychometrician & Competency Assessment Specialist).
26927
26968
  Based on Master Lesson Plan \`LESSON_${lessonCode}.md\`, author the scenario-based diagnostic question bank: \`QUIZ_${lessonCode}.md\`.
26928
26969
 
26970
+ SCOPE & PACING (prompt audit S-6):
26971
+ - This is a FORMATIVE CHECKPOINT for ONE 90-minute session, NOT an exam bank.
26972
+ - Size the bank to fit the session's Wrap-up (~10 minutes of answering): the MCQ bank, code-tracing question, and rubric together must be completable by a student in that time.
26973
+ - Question ONLY concepts inside this session's scope (see SESSION SLICE + ALLOWED DESIGN SPACE in the context).
26974
+
26929
26975
  Mandatory Format Requirements:
26930
26976
  1. EXACT YAML Frontmatter \u2014 copy verbatim, then fill in the blanks:
26931
26977
  ---
@@ -27010,6 +27056,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
27010
27056
  languageDirective,
27011
27057
  headingDirective: buildHeadingDirective("SLIDE", slcMarkdown, targetLang),
27012
27058
  groundContext: slideGroundContext,
27059
+ productSpecBlock: productSpecBlock || void 0,
27013
27060
  satelliteContext,
27014
27061
  runnerOptions,
27015
27062
  onProgress: (agent, msg, meta) => {