@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.d.cts CHANGED
@@ -1076,6 +1076,10 @@ interface SlideProductionWorkflowOptions {
1076
1076
  * already injected into user prompts via parseLessonFlow (no duplication).
1077
1077
  */
1078
1078
  groundContext?: string;
1079
+ /** Canonical PRODUCT_SPEC block (prompt audit S-1): injected into the
1080
+ * blueprint prompt so deck-wide content decisions see binding platform/
1081
+ * version facts even though the blueprint call carries no groundContext. */
1082
+ productSpecBlock?: string;
1079
1083
  satelliteContext?: any;
1080
1084
  /** Vercel AI SDK model resolution options (provider/model/apiKey). */
1081
1085
  modelOptions?: ModelResolutionOptions;
package/dist/index.d.ts CHANGED
@@ -1076,6 +1076,10 @@ interface SlideProductionWorkflowOptions {
1076
1076
  * already injected into user prompts via parseLessonFlow (no duplication).
1077
1077
  */
1078
1078
  groundContext?: string;
1079
+ /** Canonical PRODUCT_SPEC block (prompt audit S-1): injected into the
1080
+ * blueprint prompt so deck-wide content decisions see binding platform/
1081
+ * version facts even though the blueprint call carries no groundContext. */
1082
+ productSpecBlock?: string;
1079
1083
  satelliteContext?: any;
1080
1084
  /** Vercel AI SDK model resolution options (provider/model/apiKey). */
1081
1085
  modelOptions?: ModelResolutionOptions;
package/dist/index.mjs CHANGED
@@ -1324,6 +1324,10 @@ function parseLessonFlow(lessonMarkdown) {
1324
1324
  }
1325
1325
  const durMatch = lessonMarkdown.match(/Estimated Duration:\s*([^\n]+)/i);
1326
1326
  if (durMatch) result.estimatedDuration = durMatch[1].trim();
1327
+ if (!result.lessonTitle) {
1328
+ const h1 = lessonMarkdown.match(/^#\s+(?:LESSON[^:]*:\s*)?(.+)$/m);
1329
+ if (h1 && h1[1]) result.lessonTitle = h1[1].replace(/\*+/g, "").trim();
1330
+ }
1327
1331
  const contractMatch = lessonMarkdown.match(/SLIDE must visualize:?\s*([^\n]+)/i);
1328
1332
  if (contractMatch) result.slideContract = contractMatch[1].trim();
1329
1333
  const actSeqSectionMatch = lessonMarkdown.match(/###\s*(?:\d+\.\s*)?Activity Sequence[\s\S]*?(?=(?:###|\n##\s+|$))/i);
@@ -1405,7 +1409,7 @@ var init_lessonFlowParser = __esm({
1405
1409
 
1406
1410
  // src/ai/prompts/slideBlueprintPrompt.ts
1407
1411
  function buildSlideBlueprintPrompt(params) {
1408
- const { lessonFlow, targetSlideCount, stylePresetName = "Blue Professional" } = params;
1412
+ const { lessonFlow, targetSlideCount, stylePresetName = "Blue Professional", productSpecBlock } = params;
1409
1413
  const activitiesSummary = lessonFlow.activities.map(
1410
1414
  (a) => `Seq ${a.seq} [${a.phase}]: ${a.purpose} (Teacher: ${a.teacherMove} | Student: ${a.studentAction} | Time: ${a.time})`
1411
1415
  ).join("\n");
@@ -1427,7 +1431,9 @@ ${countGuidance}
1427
1431
  - Target Duration: ${lessonFlow.estimatedDuration}
1428
1432
  - Active Visual Style: "${stylePresetName}"
1429
1433
  ${lessonFlow.slideContract ? `- Slide Artifact Contract: "${lessonFlow.slideContract}"` : ""}
1430
-
1434
+ ${productSpecBlock ? `
1435
+ ${productSpecBlock}
1436
+ ` : ""}
1431
1437
  ### \u{1F5FA}\uFE0F CANONICAL ACTIVITY SEQUENCE:
1432
1438
  ${activitiesSummary || "Standard phased sequence"}
1433
1439
 
@@ -1501,7 +1507,7 @@ function buildSlideBatchPrompt(params) {
1501
1507
  skillPrompt,
1502
1508
  language = "Vietnamese",
1503
1509
  languageDirective = "",
1504
- headingDirective = "",
1510
+ // deprecated — see interface doc
1505
1511
  groundContext = ""
1506
1512
  } = params;
1507
1513
  const slidesSpec = clusterSlides.map((s) => `
@@ -1522,7 +1528,6 @@ ${groundContext}
1522
1528
  ---` : ""}
1523
1529
  ### OPERATIONAL GROUND RULES FOR CHUNK GENERATION:
1524
1530
  ${languageDirective}
1525
- ${headingDirective}
1526
1531
  1. **FOCUS ON CLUSTER ${clusterId}: "${clusterTitle}"**:
1527
1532
  Generate EXACTLY the ${clusterSlides.length} slides requested in the user prompt.
1528
1533
  2. **ZERO ABBREVIATION / NO "// TODO"**:
@@ -1713,7 +1718,7 @@ async function inferStructured(schema, systemPrompt, userPrompt, options, label)
1713
1718
  const messages = [
1714
1719
  { role: "user", content: [systemPrompt, userPrompt].filter(Boolean).join("\n\n") }
1715
1720
  ];
1716
- const raw = await runCurriculumAIInference(messages, options.satelliteContext || "", options.runnerOptions, (token, type) => {
1721
+ const raw = await runCurriculumAIInference(messages, "", options.runnerOptions, (token, type) => {
1717
1722
  if (type === "usage") options.onProgress?.("@illustrator", token, { type: "usage" });
1718
1723
  });
1719
1724
  const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
@@ -1899,7 +1904,8 @@ async function executeSlideProductionWorkflow(options) {
1899
1904
  const blueprintPrompt = buildSlideBlueprintPrompt({
1900
1905
  lessonFlow,
1901
1906
  targetSlideCount,
1902
- stylePresetName: stylePreset?.name || "Blue Professional"
1907
+ stylePresetName: stylePreset?.name || "Blue Professional",
1908
+ productSpecBlock: options.productSpecBlock
1903
1909
  });
1904
1910
  let blueprintItems;
1905
1911
  let allGeneratedSlides;
@@ -1982,7 +1988,6 @@ ${p.content}`).join("\n\n") : lessonFlow.rawContent.slice(0, 3500);
1982
1988
  skillPrompt,
1983
1989
  language,
1984
1990
  languageDirective,
1985
- headingDirective,
1986
1991
  groundContext: options.groundContext
1987
1992
  });
1988
1993
  try {
@@ -11707,19 +11712,30 @@ function extractToolchainFacts(refPack) {
11707
11712
  const factLines = [];
11708
11713
  for (const rawLine of excerpt.split("\n")) {
11709
11714
  const line = rawLine.trim();
11710
- if (!line || line.startsWith("#")) continue;
11711
- if (/\d{1,2}(?:\.\d{1,2})?/.test(line) || /Xcode|Swift|macOS|iOS|SDK|Node|Python|JDK|\.NET/i.test(line)) {
11712
- factLines.push(line.replace(/^[-*•]\s*/, "- "));
11713
- }
11715
+ if (!line || line.startsWith("#") || line.startsWith("<!--")) continue;
11716
+ if (!/\d{1,2}(?:\.\d{1,2})?/.test(line) && !/Xcode|Swift|macOS|iOS|SDK|Node|Python|JDK|\.NET/i.test(line)) continue;
11717
+ factLines.push(line.replace(/^[*\-•]\s*/, "- "));
11714
11718
  if (factLines.length >= 8) break;
11715
11719
  }
11716
11720
  return factLines;
11717
11721
  }
11722
+ function derivePrimaryTechnology(facts, briefing) {
11723
+ const explicit = (briefing?.coreTechnology || briefing?.techStack || "").trim();
11724
+ if (explicit && explicit.length <= 80) return explicit;
11725
+ const found = [];
11726
+ const CANON = ["Swift", "SwiftUI", "macOS", "iOS", "Xcode", "SwiftData", "Node.js", "TypeScript", "Python", "React"];
11727
+ for (const name of CANON) {
11728
+ if (facts.some((f) => f.includes(name)) && !found.includes(name)) found.push(name);
11729
+ }
11730
+ if (found.length > 0) return found.slice(0, 4).join(" + ");
11731
+ const topic = (briefing?.topic || "").trim();
11732
+ return topic && topic.length <= 60 ? topic : "";
11733
+ }
11718
11734
  function buildProductSpecBlock(input) {
11719
11735
  const b = input.briefing || {};
11720
- const platform = (input.overrides?.primaryLanguage || b.coreTechnology || b.techStack || b.topic || "").trim();
11721
- const hardware = (b.hardwarePlatform || b.studentEquipment || b.classDynamic || "").trim();
11722
11736
  const toolchainFacts = input.refPack ? extractToolchainFacts(input.refPack) : [];
11737
+ const platform = (input.overrides?.primaryLanguage || derivePrimaryTechnology(toolchainFacts, b) || "").trim();
11738
+ const hardware = (b.hardwarePlatform || b.studentEquipment || b.classDynamic || "").trim();
11723
11739
  const lines = [];
11724
11740
  lines.push("[PRODUCT SPEC \u2014 CANONICAL, BINDING FOR EVERY ARTIFACT OF THIS PROJECT]:");
11725
11741
  lines.push("All decisions below are PROJECT-WIDE. Every artifact (LESSON, satellites, KX)");
@@ -11735,7 +11751,7 @@ function buildProductSpecBlock(input) {
11735
11751
  if (input.overrides?.deliverableTemplate) {
11736
11752
  lines.push(`- Deliverable template: ${input.overrides.deliverableTemplate}`);
11737
11753
  }
11738
- if (toolchainFacts.length > 0) {
11754
+ if (toolchainFacts.length > 0 && !input.skipToolchainList) {
11739
11755
  lines.push("- Toolchain & versions (ground truth):");
11740
11756
  for (const f of toolchainFacts) lines.push(` ${f}`);
11741
11757
  }
@@ -11863,7 +11879,9 @@ async function ensureKnowledgeExposition(options) {
11863
11879
  const productSpecBlock = buildProductSpecBlock({
11864
11880
  refPack,
11865
11881
  briefing: techStack || hardwarePlatform ? { techStack, hardwarePlatform } : void 0,
11866
- overrides: options.productSpec
11882
+ overrides: options.productSpec,
11883
+ skipToolchainList: true
11884
+ // same RefPack excerpt already in [GROUND TRUTH]
11867
11885
  });
11868
11886
  const originalUserPrompt = buildUserPrompt(session, plan, glossary, targetLanguage, options.slcMarkdown, refPack) + (productSpecBlock ? "\n\n" + productSpecBlock : "");
11869
11887
  const content = (await llmFn(
@@ -11963,17 +11981,28 @@ async function ensureExpositionForLesson(storage, projectId, lessonCode, options
11963
11981
  const lpRaw = await storage.readSotDocument(projectId, "LEARNER_PROFILE.md");
11964
11982
  if (lpRaw) {
11965
11983
  const hwPatterns = [
11984
+ // "Cơ sở phần cứng (Hardware Ecosystem): Học sinh thực hành trên máy..."
11985
+ // — the audit: the old pattern captured the parenthetical label
11986
+ // "(Hardware Ecosystem):" itself. Capture AFTER the closing paren + colon.
11987
+ /\([^)]*Ecosystem[^)]*\)\s*:\s*([^\n\r]+)/i,
11966
11988
  /(?:Student Equipment|Hardware|Platform|Thiết bị|Thiết bị học tập)\s*(?:\(|:|\*)*\s*([^\n\r]+?)(?:\s*\*\*|[.).]?\s*$)/i,
11967
11989
  /(?:máy|device|machine)\s+([^,.;\n]{4,60}(?:M\d|Intel|PC|computer|mini)[^,.;\n]{0,30})/i
11968
11990
  ];
11969
11991
  for (const p of hwPatterns) {
11970
11992
  const m = lpRaw.match(p);
11971
- if (m && !hw) {
11972
- hw = m[1].trim();
11993
+ if (m && m[1] && !hw) {
11994
+ hw = m[1].trim().replace(/\*+/g, "").slice(0, 300);
11973
11995
  break;
11974
11996
  }
11975
11997
  }
11976
11998
  }
11999
+ if (!hw) {
12000
+ const rpRaw = await storage.readSotDocument(projectId, "REFERENCE_PACK.md");
12001
+ if (rpRaw) {
12002
+ const rp = rpRaw.match(/(?:School Lab|Thiết bị lớp học)[^\n]*\*\*([^*]+(?:M\d|Mac)[^*]*)\*\*/i);
12003
+ if (rp && rp[1]) hw = rp[1].trim();
12004
+ }
12005
+ }
11977
12006
  } catch {
11978
12007
  }
11979
12008
  }
@@ -12710,11 +12739,11 @@ function buildSessionSliceContext(plan, lessonCode) {
12710
12739
  lines.push("- Time split: knowledge " + s.knowledge_minutes + "m / practice " + s.practice_minutes + "m / overhead " + s.overhead_minutes + "m");
12711
12740
  lines.push("- Depth assignments: " + s.depth_assignments.map((d) => d.node_id + "=" + d.depth.toUpperCase() + "(" + d.source + ")").join("; "));
12712
12741
  if (s.prerequisite_decisions.length > 0) {
12713
- lines.push("- Prerequisite decisions [7]:");
12742
+ lines.push("- Prerequisite decisions [" + s.prerequisite_decisions.length + "]:");
12714
12743
  for (const d of s.prerequisite_decisions) lines.push(" * " + d.node_id + ": " + d.decision + " \u2014 " + d.reason);
12715
12744
  }
12716
12745
  if (s.scaffold_decisions.length > 0) {
12717
- lines.push("- Scaffold decisions [7]:");
12746
+ lines.push("- Scaffold decisions [" + s.scaffold_decisions.length + "]:");
12718
12747
  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);
12719
12748
  }
12720
12749
  lines.push("- Exit evidence: " + s.exit_evidence.join("; "));
@@ -25949,8 +25978,11 @@ function buildFrameworkExcerptForLesson(framework, lessonId) {
25949
25978
  });
25950
25979
  const rowIdx = scopeRows[0] ?? -1;
25951
25980
  const currentRow = rowIdx >= 0 ? lines[rowIdx] : "";
25952
- const prevRow = rowIdx > 0 && (lines[rowIdx - 1] || "").trim().startsWith("|") ? lines[rowIdx - 1] : "";
25953
- const nextRow = (lines[rowIdx + 1] || "").trim().startsWith("|") ? lines[rowIdx + 1] : "";
25981
+ const isSeparatorRow = (l) => !!l && /^\s*\|?[\s:|-]+\|?\s*$/.test(l) && l.includes("-");
25982
+ const prevRaw = rowIdx > 0 ? lines[rowIdx - 1] : "";
25983
+ const prevRow = prevRaw.trim().startsWith("|") && !isSeparatorRow(prevRaw) ? prevRaw : "";
25984
+ const nextRaw = lines[rowIdx + 1] || "";
25985
+ const nextRow = nextRaw.trim().startsWith("|") && !isSeparatorRow(nextRaw) ? nextRaw : "";
25954
25986
  const hierStart = lines.findIndex((l) => /Structural Hierarchy/i.test(l));
25955
25987
  let hierarchyBlock = "";
25956
25988
  if (hierStart >= 0) {
@@ -26356,6 +26388,7 @@ ${renderHorizonPromptBlock(horizon)}`;
26356
26388
  } catch (hErr) {
26357
26389
  console.warn(`[produceSingleLesson] Notice: Curriculum Horizon extraction:`, hErr?.message || hErr);
26358
26390
  }
26391
+ let productSpecBlock = "";
26359
26392
  const buildGroundTruthBlock = () => {
26360
26393
  const parts = [];
26361
26394
  if (expositionContext) {
@@ -26366,9 +26399,9 @@ ${expositionContext}`);
26366
26399
  parts.push(`### REFERENCE PACK GROUND TRUTH
26367
26400
  ${effectiveRefPack}`);
26368
26401
  }
26369
- const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26370
- if (productSpec) {
26371
- parts.push(productSpec);
26402
+ productSpecBlock = buildProductSpecBlock({ refPack, briefing: options.briefing, skipToolchainList: true });
26403
+ if (productSpecBlock) {
26404
+ parts.push(productSpecBlock);
26372
26405
  }
26373
26406
  return parts.length > 0 ? `
26374
26407
 
@@ -26385,10 +26418,17 @@ ${standardsContext}` : "";
26385
26418
  const sessionSliceBlock = sessionSliceContext ? `
26386
26419
 
26387
26420
  ${sessionSliceContext}` : "";
26421
+ const platformGuardrail = buildDomainLexiconGuardrail(
26422
+ options.briefing?.techStack || options.briefing?.coreTechnology,
26423
+ options.briefing?.hardwarePlatform
26424
+ );
26425
+ const guardrailBlock = `
26426
+
26427
+ ${platformGuardrail}`;
26388
26428
  const assembleCommonContext = (sg) => `${baseContextPrefix}
26389
26429
 
26390
26430
  [CONTENT STYLE GUIDE EXCERPT]:
26391
- ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBlock}${horizonBlock}`;
26431
+ ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBlock}${horizonBlock}${guardrailBlock}`;
26392
26432
  let commonContext = assembleCommonContext(effectiveStyleGuide);
26393
26433
  const MAX_COMPOSITE_PROMPT_CHARS = 3e4;
26394
26434
  const runnerOptions = options.customInference ? { customInference: options.customInference } : {};
@@ -26914,6 +26954,11 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26914
26954
  const quizPrompt = `You are @assessor (Lead Psychometrician & Competency Assessment Specialist).
26915
26955
  Based on Master Lesson Plan \`LESSON_${lessonCode}.md\`, author the scenario-based diagnostic question bank: \`QUIZ_${lessonCode}.md\`.
26916
26956
 
26957
+ SCOPE & PACING (prompt audit S-6):
26958
+ - This is a FORMATIVE CHECKPOINT for ONE 90-minute session, NOT an exam bank.
26959
+ - 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.
26960
+ - Question ONLY concepts inside this session's scope (see SESSION SLICE + ALLOWED DESIGN SPACE in the context).
26961
+
26917
26962
  Mandatory Format Requirements:
26918
26963
  1. EXACT YAML Frontmatter \u2014 copy verbatim, then fill in the blanks:
26919
26964
  ---
@@ -26998,6 +27043,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26998
27043
  languageDirective,
26999
27044
  headingDirective: buildHeadingDirective("SLIDE", slcMarkdown, targetLang),
27000
27045
  groundContext: slideGroundContext,
27046
+ productSpecBlock: productSpecBlock || void 0,
27001
27047
  satelliteContext,
27002
27048
  runnerOptions,
27003
27049
  onProgress: (agent, msg, meta) => {