@thanh01.pmt/curriculum-kit 1.4.24 → 1.4.25

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,28 @@ 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
+ hw = m[1].trim().replace(/\*+/g, "").slice(0, 300);
11985
12007
  break;
11986
12008
  }
11987
12009
  }
11988
12010
  }
12011
+ if (!hw) {
12012
+ const rpRaw = await storage.readSotDocument(projectId, "REFERENCE_PACK.md");
12013
+ if (rpRaw) {
12014
+ const rp = rpRaw.match(/(?:School Lab|Thiết bị lớp học)[^\n]*\*\*([^*]+(?:M\d|Mac)[^*]*)\*\*/i);
12015
+ if (rp && rp[1]) hw = rp[1].trim();
12016
+ }
12017
+ }
11989
12018
  } catch {
11990
12019
  }
11991
12020
  }
@@ -12722,11 +12751,11 @@ function buildSessionSliceContext(plan, lessonCode) {
12722
12751
  lines.push("- Time split: knowledge " + s.knowledge_minutes + "m / practice " + s.practice_minutes + "m / overhead " + s.overhead_minutes + "m");
12723
12752
  lines.push("- Depth assignments: " + s.depth_assignments.map((d) => d.node_id + "=" + d.depth.toUpperCase() + "(" + d.source + ")").join("; "));
12724
12753
  if (s.prerequisite_decisions.length > 0) {
12725
- lines.push("- Prerequisite decisions [7]:");
12754
+ lines.push("- Prerequisite decisions [" + s.prerequisite_decisions.length + "]:");
12726
12755
  for (const d of s.prerequisite_decisions) lines.push(" * " + d.node_id + ": " + d.decision + " \u2014 " + d.reason);
12727
12756
  }
12728
12757
  if (s.scaffold_decisions.length > 0) {
12729
- lines.push("- Scaffold decisions [7]:");
12758
+ lines.push("- Scaffold decisions [" + s.scaffold_decisions.length + "]:");
12730
12759
  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
12760
  }
12732
12761
  lines.push("- Exit evidence: " + s.exit_evidence.join("; "));
@@ -25961,8 +25990,11 @@ function buildFrameworkExcerptForLesson(framework, lessonId) {
25961
25990
  });
25962
25991
  const rowIdx = scopeRows[0] ?? -1;
25963
25992
  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] : "";
25993
+ const isSeparatorRow = (l) => !!l && /^\s*\|?[\s:|-]+\|?\s*$/.test(l) && l.includes("-");
25994
+ const prevRaw = rowIdx > 0 ? lines[rowIdx - 1] : "";
25995
+ const prevRow = prevRaw.trim().startsWith("|") && !isSeparatorRow(prevRaw) ? prevRaw : "";
25996
+ const nextRaw = lines[rowIdx + 1] || "";
25997
+ const nextRow = nextRaw.trim().startsWith("|") && !isSeparatorRow(nextRaw) ? nextRaw : "";
25966
25998
  const hierStart = lines.findIndex((l) => /Structural Hierarchy/i.test(l));
25967
25999
  let hierarchyBlock = "";
25968
26000
  if (hierStart >= 0) {
@@ -26368,6 +26400,7 @@ ${renderHorizonPromptBlock(horizon)}`;
26368
26400
  } catch (hErr) {
26369
26401
  console.warn(`[produceSingleLesson] Notice: Curriculum Horizon extraction:`, hErr?.message || hErr);
26370
26402
  }
26403
+ let productSpecBlock = "";
26371
26404
  const buildGroundTruthBlock = () => {
26372
26405
  const parts = [];
26373
26406
  if (expositionContext) {
@@ -26378,9 +26411,9 @@ ${expositionContext}`);
26378
26411
  parts.push(`### REFERENCE PACK GROUND TRUTH
26379
26412
  ${effectiveRefPack}`);
26380
26413
  }
26381
- const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26382
- if (productSpec) {
26383
- parts.push(productSpec);
26414
+ productSpecBlock = buildProductSpecBlock({ refPack, briefing: options.briefing, skipToolchainList: true });
26415
+ if (productSpecBlock) {
26416
+ parts.push(productSpecBlock);
26384
26417
  }
26385
26418
  return parts.length > 0 ? `
26386
26419
 
@@ -26397,10 +26430,17 @@ ${standardsContext}` : "";
26397
26430
  const sessionSliceBlock = sessionSliceContext ? `
26398
26431
 
26399
26432
  ${sessionSliceContext}` : "";
26433
+ const platformGuardrail = buildDomainLexiconGuardrail(
26434
+ options.briefing?.techStack || options.briefing?.coreTechnology,
26435
+ options.briefing?.hardwarePlatform
26436
+ );
26437
+ const guardrailBlock = `
26438
+
26439
+ ${platformGuardrail}`;
26400
26440
  const assembleCommonContext = (sg) => `${baseContextPrefix}
26401
26441
 
26402
26442
  [CONTENT STYLE GUIDE EXCERPT]:
26403
- ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBlock}${horizonBlock}`;
26443
+ ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBlock}${horizonBlock}${guardrailBlock}`;
26404
26444
  let commonContext = assembleCommonContext(effectiveStyleGuide);
26405
26445
  const MAX_COMPOSITE_PROMPT_CHARS = 3e4;
26406
26446
  const runnerOptions = options.customInference ? { customInference: options.customInference } : {};
@@ -26926,6 +26966,11 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26926
26966
  const quizPrompt = `You are @assessor (Lead Psychometrician & Competency Assessment Specialist).
26927
26967
  Based on Master Lesson Plan \`LESSON_${lessonCode}.md\`, author the scenario-based diagnostic question bank: \`QUIZ_${lessonCode}.md\`.
26928
26968
 
26969
+ SCOPE & PACING (prompt audit S-6):
26970
+ - This is a FORMATIVE CHECKPOINT for ONE 90-minute session, NOT an exam bank.
26971
+ - 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.
26972
+ - Question ONLY concepts inside this session's scope (see SESSION SLICE + ALLOWED DESIGN SPACE in the context).
26973
+
26929
26974
  Mandatory Format Requirements:
26930
26975
  1. EXACT YAML Frontmatter \u2014 copy verbatim, then fill in the blanks:
26931
26976
  ---
@@ -27010,6 +27055,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
27010
27055
  languageDirective,
27011
27056
  headingDirective: buildHeadingDirective("SLIDE", slcMarkdown, targetLang),
27012
27057
  groundContext: slideGroundContext,
27058
+ productSpecBlock: productSpecBlock || void 0,
27013
27059
  satelliteContext,
27014
27060
  runnerOptions,
27015
27061
  onProgress: (agent, msg, meta) => {