@thanh01.pmt/curriculum-kit 1.4.23 → 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) {
@@ -26203,7 +26235,22 @@ async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
26203
26235
  const framework = await storage.readSotDocument(projectId, "CURRICULUM_FRAMEWORK.md") || "";
26204
26236
  const styleGuide = await storage.readSotDocument(projectId, "CONTENT_STYLE_GUIDE.md") || "";
26205
26237
  const refPack = await storage.readSotDocument(projectId, "REFERENCE_PACK.md") || "";
26238
+ let currentPlanHash;
26239
+ try {
26240
+ const planRaw0 = await storage.readArtifact(projectId, "_sot/CURRICULUM_PLAN.json");
26241
+ if (planRaw0) currentPlanHash = JSON.parse(planRaw0).plan_hash;
26242
+ } catch {
26243
+ }
26206
26244
  await storage.readSotDocument(projectId, "LEARNER_PROFILE.md") || "";
26245
+ const isStaleVsPlan = (content) => {
26246
+ if (!content || !currentPlanHash) return false;
26247
+ const m = content.match(/plan_hash:\s*"([0-9a-f]+)"/);
26248
+ return !!m && m[1] !== currentPlanHash;
26249
+ };
26250
+ const stampPlanHash = (content) => {
26251
+ if (!currentPlanHash || content.includes('plan_hash: "' + currentPlanHash + '"')) return content;
26252
+ return content + '\n---\nplan_hash: "' + currentPlanHash + '"\n- **Plan reference:** ' + currentPlanHash + "\n";
26253
+ };
26207
26254
  const slcMarkdown = options.sectionLanguageContract || await storage.readSotDocument(projectId, "SECTION_LANGUAGE_CONTRACT.md") || "";
26208
26255
  let targetLang = options.targetLanguage;
26209
26256
  if (!targetLang) {
@@ -26353,6 +26400,7 @@ ${renderHorizonPromptBlock(horizon)}`;
26353
26400
  } catch (hErr) {
26354
26401
  console.warn(`[produceSingleLesson] Notice: Curriculum Horizon extraction:`, hErr?.message || hErr);
26355
26402
  }
26403
+ let productSpecBlock = "";
26356
26404
  const buildGroundTruthBlock = () => {
26357
26405
  const parts = [];
26358
26406
  if (expositionContext) {
@@ -26363,9 +26411,9 @@ ${expositionContext}`);
26363
26411
  parts.push(`### REFERENCE PACK GROUND TRUTH
26364
26412
  ${effectiveRefPack}`);
26365
26413
  }
26366
- const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26367
- if (productSpec) {
26368
- parts.push(productSpec);
26414
+ productSpecBlock = buildProductSpecBlock({ refPack, briefing: options.briefing, skipToolchainList: true });
26415
+ if (productSpecBlock) {
26416
+ parts.push(productSpecBlock);
26369
26417
  }
26370
26418
  return parts.length > 0 ? `
26371
26419
 
@@ -26382,10 +26430,17 @@ ${standardsContext}` : "";
26382
26430
  const sessionSliceBlock = sessionSliceContext ? `
26383
26431
 
26384
26432
  ${sessionSliceContext}` : "";
26433
+ const platformGuardrail = buildDomainLexiconGuardrail(
26434
+ options.briefing?.techStack || options.briefing?.coreTechnology,
26435
+ options.briefing?.hardwarePlatform
26436
+ );
26437
+ const guardrailBlock = `
26438
+
26439
+ ${platformGuardrail}`;
26385
26440
  const assembleCommonContext = (sg) => `${baseContextPrefix}
26386
26441
 
26387
26442
  [CONTENT STYLE GUIDE EXCERPT]:
26388
- ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBlock}${horizonBlock}`;
26443
+ ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBlock}${horizonBlock}${guardrailBlock}`;
26389
26444
  let commonContext = assembleCommonContext(effectiveStyleGuide);
26390
26445
  const MAX_COMPOSITE_PROMPT_CHARS = 3e4;
26391
26446
  const runnerOptions = options.customInference ? { customInference: options.customInference } : {};
@@ -26414,7 +26469,10 @@ ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBl
26414
26469
  const lessonRelPath = `_content/${unitCode}/LESSON_${lessonCode}.md`;
26415
26470
  const existingLessonContent = await storage.readArtifact(projectId, lessonRelPath);
26416
26471
  let lessonContent = existingLessonContent || "";
26417
- if (artifactScope.includes("LESSON") && (!existingLessonContent || force)) {
26472
+ if (artifactScope.includes("LESSON") && (!existingLessonContent || force || isStaleVsPlan(existingLessonContent))) {
26473
+ if (existingLessonContent && !force && isStaleVsPlan(existingLessonContent)) {
26474
+ onProgress?.("@content", `[STALENESS] LESSON_${lessonCode} was generated under a different plan_hash \u2014 regenerating against the current plan.`);
26475
+ }
26418
26476
  onProgress?.("@content", `[1/4] Authoring Master Lesson Plan (${pedagogyLabel}): \`LESSON_${lessonCode}.md\` (${lessonTitle})...`);
26419
26477
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26420
26478
  const mediaDirective = options.mediaPolicy && options.mediaPolicy.maxGenImagesPerArtifact > 0 ? `
@@ -26485,7 +26543,7 @@ Fix ALL issues above and output the complete corrected document.` }],
26485
26543
  } else {
26486
26544
  lessonContent = lintReport.autoFixedContent || rawLesson;
26487
26545
  }
26488
- await storage.saveArtifact(projectId, lessonRelPath, lessonContent);
26546
+ await storage.saveArtifact(projectId, lessonRelPath, stampPlanHash(lessonContent));
26489
26547
  producedArtifacts.push(`LESSON_${lessonCode}.md`);
26490
26548
  }
26491
26549
  const contentHash = lessonContent ? computeContentHash(lessonContent) : void 0;
@@ -26830,7 +26888,8 @@ ${lessonExcerpt}${symbolLedgerBlock}`;
26830
26888
  });
26831
26889
  };
26832
26890
  const actRelPath = `_content/${unitCode}/ACT_${lessonCode}.md`;
26833
- if (artifactScope.includes("ACT") && (!await storage.readArtifact(projectId, actRelPath) || force)) {
26891
+ const existingAct = await storage.readArtifact(projectId, actRelPath);
26892
+ if (artifactScope.includes("ACT") && (!existingAct || force || isStaleVsPlan(existingAct))) {
26834
26893
  onProgress?.("@activity", `[2/4] Authoring Hands-on Lab: \`ACT_${lessonCode}.md\`...`);
26835
26894
  const actDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26836
26895
  const actPrompt = `You are @activity (Lead Hands-on STEM & Laboratory Exercise Designer).
@@ -26890,7 +26949,7 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26890
26949
  rawError: rawAct || "Empty AI response"
26891
26950
  });
26892
26951
  }
26893
- await storage.saveArtifact(projectId, actRelPath, rawAct);
26952
+ await storage.saveArtifact(projectId, actRelPath, stampPlanHash(rawAct));
26894
26953
  await storage.updateArtifactState(projectId, lessonCode, "ACT", {
26895
26954
  state: "completed",
26896
26955
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26900,12 +26959,18 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26900
26959
  await judgeSat("ACT", rawAct);
26901
26960
  }
26902
26961
  const quizRelPath = `_content/${unitCode}/QUIZ_${lessonCode}.md`;
26903
- if (artifactScope.includes("QUIZ") && (!await storage.readArtifact(projectId, quizRelPath) || force)) {
26962
+ const existingQuiz = await storage.readArtifact(projectId, quizRelPath);
26963
+ if (artifactScope.includes("QUIZ") && (!existingQuiz || force || isStaleVsPlan(existingQuiz))) {
26904
26964
  onProgress?.("@assessor", `[3/4] Authoring Assessment Bank: \`QUIZ_${lessonCode}.md\`...`);
26905
26965
  const quizDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26906
26966
  const quizPrompt = `You are @assessor (Lead Psychometrician & Competency Assessment Specialist).
26907
26967
  Based on Master Lesson Plan \`LESSON_${lessonCode}.md\`, author the scenario-based diagnostic question bank: \`QUIZ_${lessonCode}.md\`.
26908
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
+
26909
26974
  Mandatory Format Requirements:
26910
26975
  1. EXACT YAML Frontmatter \u2014 copy verbatim, then fill in the blanks:
26911
26976
  ---
@@ -26954,7 +27019,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26954
27019
  rawError: rawQuiz || "Empty AI response"
26955
27020
  });
26956
27021
  }
26957
- await storage.saveArtifact(projectId, quizRelPath, rawQuiz);
27022
+ await storage.saveArtifact(projectId, quizRelPath, stampPlanHash(rawQuiz));
26958
27023
  await storage.updateArtifactState(projectId, lessonCode, "QUIZ", {
26959
27024
  state: "completed",
26960
27025
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26964,7 +27029,8 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26964
27029
  await judgeSat("QUIZ", rawQuiz);
26965
27030
  }
26966
27031
  const slideRelPath = `_content/${unitCode}/SLIDE_${lessonCode}.md`;
26967
- if (artifactScope.includes("SLIDE") && (!await storage.readArtifact(projectId, slideRelPath) || force)) {
27032
+ const existingSlide = await storage.readArtifact(projectId, slideRelPath);
27033
+ if (artifactScope.includes("SLIDE") && (!existingSlide || force || isStaleVsPlan(existingSlide))) {
26968
27034
  onProgress?.("@illustrator", `[4/4] Authoring Presentation Slide Deck: \`SLIDE_${lessonCode}.md\`...`);
26969
27035
  const isHtmlEngine = options.slidesEngine !== "marp";
26970
27036
  if (isHtmlEngine) {
@@ -26989,6 +27055,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26989
27055
  languageDirective,
26990
27056
  headingDirective: buildHeadingDirective("SLIDE", slcMarkdown, targetLang),
26991
27057
  groundContext: slideGroundContext,
27058
+ productSpecBlock: productSpecBlock || void 0,
26992
27059
  satelliteContext,
26993
27060
  runnerOptions,
26994
27061
  onProgress: (agent, msg, meta) => {
@@ -26998,7 +27065,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26998
27065
  let deckJson = workflowResult.deckJson;
26999
27066
  const markdownWrapper = workflowResult.markdownWrapper;
27000
27067
  let validation = presentationKitAi?.validateHtmlSlideDeck ? presentationKitAi.validateHtmlSlideDeck(deckJson, true) : deckJson && Array.isArray(deckJson.slides) ? { valid: true, score: 95, issues: [], slideCount: deckJson.slides.length} : { valid: false, score: 0, issues: [{ message: "Malformed JSON slide deck" }]};
27001
- await storage.saveArtifact(projectId, slideRelPath, markdownWrapper);
27068
+ await storage.saveArtifact(projectId, slideRelPath, stampPlanHash(markdownWrapper));
27002
27069
  producedArtifacts.push(`SLIDE_${lessonCode}.md`);
27003
27070
  if (deckJson) {
27004
27071
  const deckJsonStr = JSON.stringify(deckJson, null, 2);
@@ -27089,7 +27156,7 @@ footer: "Curriculum OS \u2022 ${lessonCode} (${pedagogyLabel})"
27089
27156
  rawError: rawSlide || "Empty AI response"
27090
27157
  });
27091
27158
  }
27092
- await storage.saveArtifact(projectId, slideRelPath, rawSlide);
27159
+ await storage.saveArtifact(projectId, slideRelPath, stampPlanHash(rawSlide));
27093
27160
  await storage.updateArtifactState(projectId, lessonCode, "SLIDE", {
27094
27161
  state: "completed",
27095
27162
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27100,7 +27167,8 @@ footer: "Curriculum OS \u2022 ${lessonCode} (${pedagogyLabel})"
27100
27167
  }
27101
27168
  }
27102
27169
  const guideRelPath = `_content/${unitCode}/GUIDE_${lessonCode}.md`;
27103
- if (artifactScope.includes("GUIDE") && (!await storage.readArtifact(projectId, guideRelPath) || force)) {
27170
+ const existingGuide = await storage.readArtifact(projectId, guideRelPath);
27171
+ if (artifactScope.includes("GUIDE") && (!existingGuide || force || isStaleVsPlan(existingGuide))) {
27104
27172
  onProgress?.("@content", `Authoring Teacher Guide: \`GUIDE_${lessonCode}.md\`...`, { artifactType: "GUIDE" });
27105
27173
  const guideDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27106
27174
  const guidePrompt = `You are @content (Senior Curriculum Developer & Teacher Facilitation Specialist).
@@ -27140,7 +27208,7 @@ ${buildHeadingDirective("GUIDE", slcMarkdown, targetLang)}`;
27140
27208
  }
27141
27209
  );
27142
27210
  if (rawGuide && !rawGuide.startsWith("\u26A0\uFE0F") && rawGuide.length >= 200) {
27143
- await storage.saveArtifact(projectId, guideRelPath, rawGuide);
27211
+ await storage.saveArtifact(projectId, guideRelPath, stampPlanHash(rawGuide));
27144
27212
  await storage.updateArtifactState(projectId, lessonCode, "GUIDE", {
27145
27213
  state: "completed",
27146
27214
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27151,7 +27219,8 @@ ${buildHeadingDirective("GUIDE", slcMarkdown, targetLang)}`;
27151
27219
  }
27152
27220
  }
27153
27221
  const handoutRelPath = `_content/${unitCode}/HANDOUT_${lessonCode}.md`;
27154
- if (artifactScope.includes("HANDOUT") && (!await storage.readArtifact(projectId, handoutRelPath) || force)) {
27222
+ const existingHandout = await storage.readArtifact(projectId, handoutRelPath);
27223
+ if (artifactScope.includes("HANDOUT") && (!existingHandout || force || isStaleVsPlan(existingHandout))) {
27155
27224
  onProgress?.("@content", `Authoring Student Handout: \`HANDOUT_${lessonCode}.md\`...`, { artifactType: "HANDOUT" });
27156
27225
  const handoutDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27157
27226
  const handoutPrompt = `You are @content (Educational Content Specialist).
@@ -27191,7 +27260,7 @@ ${buildHeadingDirective("HANDOUT", slcMarkdown, targetLang)}`;
27191
27260
  }
27192
27261
  );
27193
27262
  if (rawHandout && !rawHandout.startsWith("\u26A0\uFE0F") && rawHandout.length >= 200) {
27194
- await storage.saveArtifact(projectId, handoutRelPath, rawHandout);
27263
+ await storage.saveArtifact(projectId, handoutRelPath, stampPlanHash(rawHandout));
27195
27264
  await storage.updateArtifactState(projectId, lessonCode, "HANDOUT", {
27196
27265
  state: "completed",
27197
27266
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27202,7 +27271,8 @@ ${buildHeadingDirective("HANDOUT", slcMarkdown, targetLang)}`;
27202
27271
  }
27203
27272
  }
27204
27273
  const wksRelPath = `_content/${unitCode}/WKS_${lessonCode}.md`;
27205
- if (artifactScope.includes("WKS") && (!await storage.readArtifact(projectId, wksRelPath) || force)) {
27274
+ const existingWks = await storage.readArtifact(projectId, wksRelPath);
27275
+ if (artifactScope.includes("WKS") && (!existingWks || force || isStaleVsPlan(existingWks))) {
27206
27276
  onProgress?.("@content", `Authoring Student Worksheet: \`WKS_${lessonCode}.md\`...`, { artifactType: "WKS" });
27207
27277
  const wksDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27208
27278
  const wksPrompt = `You are @content (Instructional Activity & Worksheet Designer).
@@ -27246,7 +27316,7 @@ ${buildHeadingDirective("WKS", slcMarkdown, targetLang)}`;
27246
27316
  }
27247
27317
  );
27248
27318
  if (rawWks && !rawWks.startsWith("\u26A0\uFE0F") && rawWks.length >= 200) {
27249
- await storage.saveArtifact(projectId, wksRelPath, rawWks);
27319
+ await storage.saveArtifact(projectId, wksRelPath, stampPlanHash(rawWks));
27250
27320
  await storage.updateArtifactState(projectId, lessonCode, "WKS", {
27251
27321
  state: "completed",
27252
27322
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27257,7 +27327,8 @@ ${buildHeadingDirective("WKS", slcMarkdown, targetLang)}`;
27257
27327
  }
27258
27328
  }
27259
27329
  const codeRelPath = `_content/${unitCode}/CODE_${lessonCode}.md`;
27260
- if ((artifactScope.includes("CODE") || artifactScope.includes("CODE_LAB")) && (!await storage.readArtifact(projectId, codeRelPath) || force)) {
27330
+ const existingCode = await storage.readArtifact(projectId, codeRelPath);
27331
+ if ((artifactScope.includes("CODE") || artifactScope.includes("CODE_LAB")) && (!existingCode || force || isStaleVsPlan(existingCode))) {
27261
27332
  onProgress?.("@activity", `Authoring Executable Code Lab: \`CODE_${lessonCode}.md\`...`, { artifactType: "CODE" });
27262
27333
  const codeDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27263
27334
  const codePrompt = `You are @activity & @tech_sme (Lead Software Engineer & Technical SME).
@@ -27305,7 +27376,7 @@ ${buildHeadingDirective("CODE", slcMarkdown, targetLang)}`;
27305
27376
  }
27306
27377
  );
27307
27378
  if (rawCode && !rawCode.startsWith("\u26A0\uFE0F") && rawCode.length >= 200) {
27308
- await storage.saveArtifact(projectId, codeRelPath, rawCode);
27379
+ await storage.saveArtifact(projectId, codeRelPath, stampPlanHash(rawCode));
27309
27380
  await storage.updateArtifactState(projectId, lessonCode, "CODE", {
27310
27381
  state: "completed",
27311
27382
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27321,7 +27392,8 @@ ${buildHeadingDirective("CODE", slcMarkdown, targetLang)}`;
27321
27392
  }
27322
27393
  }
27323
27394
  const extRelPath = `_content/${unitCode}/EXT_${lessonCode}.md`;
27324
- if (artifactScope.includes("EXT") && (!await storage.readArtifact(projectId, extRelPath) || force)) {
27395
+ const existingExt = await storage.readArtifact(projectId, extRelPath);
27396
+ if (artifactScope.includes("EXT") && (!existingExt || force || isStaleVsPlan(existingExt))) {
27325
27397
  onProgress?.("@activity", `Authoring Advanced Extension Challenge: \`EXT_${lessonCode}.md\`...`, { artifactType: "EXT" });
27326
27398
  const extDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27327
27399
  const isVi = targetLang === "vi";
@@ -27391,7 +27463,7 @@ ${buildHeadingDirective("EXT", slcMarkdown, targetLang)}`;
27391
27463
  }
27392
27464
  );
27393
27465
  if (rawExt && !rawExt.startsWith("\u26A0\uFE0F") && rawExt.length >= 200) {
27394
- await storage.saveArtifact(projectId, extRelPath, rawExt);
27466
+ await storage.saveArtifact(projectId, extRelPath, stampPlanHash(rawExt));
27395
27467
  await storage.updateArtifactState(projectId, lessonCode, "EXT", {
27396
27468
  state: "completed",
27397
27469
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),