@thanh01.pmt/curriculum-kit 1.4.22 → 1.4.24

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
@@ -11624,6 +11624,137 @@ function extractSymbolLedger(lessonMarkdown) {
11624
11624
  return result;
11625
11625
  }
11626
11626
 
11627
+ // src/services/artifactValidators.ts
11628
+ var CJK_RE = /[\u3400-\u4DBF\u4E00-\u9FFF\u3040-\u30FF\uAC00-\uD7AF]/g;
11629
+ function scanCjkLeaks(content) {
11630
+ const matches = content.match(CJK_RE);
11631
+ if (!matches || matches.length === 0) return null;
11632
+ const lines = [];
11633
+ for (const line of content.split("\n")) {
11634
+ if (CJK_RE.test(line)) lines.push(line.trim().slice(0, 160));
11635
+ }
11636
+ return {
11637
+ code: "cjk-leak",
11638
+ detail: `Ph\xE1t hi\u1EC7n ${matches.length} k\xFD t\u1EF1 CJK (Trung/Nh\u1EADt/H\xE0n) trong artifact ng\xF4n ng\u1EEF Vi\u1EC7t/Anh. B\u1ECB c\u1EA5m tuy\u1EC7t \u0111\u1ED1i.`,
11639
+ evidence: lines.slice(0, 5)
11640
+ };
11641
+ }
11642
+ var VERSION_CLAIM_RE = /\b([A-Z][A-Za-z+#.\-]{1,24})\s+(\d{1,2}(?:\.\d{1,2})?)\b/g;
11643
+ function checkVersionGroundTruth(content, refPack) {
11644
+ if (!refPack || !refPack.trim()) return null;
11645
+ const packLower = refPack.toLowerCase();
11646
+ const claims = /* @__PURE__ */ new Map();
11647
+ for (const m of content.matchAll(VERSION_CLAIM_RE)) {
11648
+ const tool = m[1];
11649
+ if (claims.size === 0 || !claims.has(tool)) claims.set(tool, m[2]);
11650
+ }
11651
+ const contradictions = [];
11652
+ for (const [tool, claimed] of claims) {
11653
+ const toolRe = new RegExp(`\\b${tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+(\\d{1,2}(?:\\.\\d{1,2})?)\\b`, "i");
11654
+ const packMatch = packLower.match(toolRe);
11655
+ if (packMatch && packMatch[1] !== claimed) {
11656
+ contradictions.push(`${tool}: artifact n\xF3i ${claimed}, REFERENCE_PACK ch\u1ED1t ${packMatch[1]}`);
11657
+ }
11658
+ }
11659
+ if (contradictions.length === 0) return null;
11660
+ return {
11661
+ code: "version-contradiction",
11662
+ detail: `Version m\xE2u thu\u1EABn v\u1EDBi ground truth REFERENCE_PACK: ${contradictions.join("; ")}. S\u1EEDa theo b\u1EA3n trong REFERENCE_PACK.`,
11663
+ evidence: contradictions
11664
+ };
11665
+ }
11666
+ function checkScopeDrift(content, scopedKeywords, tolerance = 0) {
11667
+ const conceptHeaders = /* @__PURE__ */ new Set();
11668
+ for (const m of content.matchAll(/\*\*([^*\n]{3,60}?)\s*\((?:WHAT|CIO|SIO|ULO)[^)]*\):\*\*/g)) {
11669
+ conceptHeaders.add(m[1].toLowerCase());
11670
+ }
11671
+ if (conceptHeaders.size === 0) return null;
11672
+ const drift = [];
11673
+ for (const header of conceptHeaders) {
11674
+ const hit = scopedKeywords.some((kw) => {
11675
+ const k = kw.toLowerCase().trim();
11676
+ if (!k) return false;
11677
+ const tokens = k.split(/\s+/);
11678
+ const present = tokens.filter((t) => header.includes(t)).length;
11679
+ return present >= Math.max(1, tokens.length - tolerance);
11680
+ });
11681
+ if (!hit) drift.push(header);
11682
+ }
11683
+ if (drift.length === 0) return null;
11684
+ return {
11685
+ code: "scope-drift",
11686
+ detail: `C\xE1c kh\xE1i ni\u1EC7m sau KH\xD4NG thu\u1ED9c scope c\u1EE7a bu\u1ED5i h\u1ECDc (kh\xF4ng c\xF3 trong new_keywords/graph node keywords): ${drift.join(" | ")}. Xo\xE1 ho\xE0n to\xE0n ho\u1EB7c thay b\u1EB1ng kh\xE1i ni\u1EC7m trong scope.`,
11687
+ evidence: drift
11688
+ };
11689
+ }
11690
+ function validateArtifactDeterministic(input) {
11691
+ const issues = [];
11692
+ const cjk = scanCjkLeaks(input.content);
11693
+ if (cjk) issues.push(cjk);
11694
+ if (input.refPack) {
11695
+ const ver = checkVersionGroundTruth(input.content, input.refPack);
11696
+ if (ver) issues.push(ver);
11697
+ }
11698
+ if (input.scopedKeywords && input.scopedKeywords.length > 0) {
11699
+ const scope = checkScopeDrift(input.content, input.scopedKeywords);
11700
+ if (scope) issues.push(scope);
11701
+ }
11702
+ return { ok: issues.length === 0, issues };
11703
+ }
11704
+ function validatorRepairPrompt(issues) {
11705
+ return [
11706
+ "DETERMINISTIC VALIDATOR FAILURES (mechanically detected \u2014 non-negotiable, fix ALL):",
11707
+ ...issues.map((i, n) => `${n + 1}. [${i.code}] ${i.detail}${i.evidence.length ? `
11708
+ Evidence: ${i.evidence.join(" | ").slice(0, 400)}` : ""}`)
11709
+ ].join("\n");
11710
+ }
11711
+
11712
+ // src/services/productSpec.ts
11713
+ function extractToolchainFacts(refPack) {
11714
+ const { excerpt } = buildSectionAwareExcerpt(refPack, {
11715
+ priorities: ["Technical Overview & Architecture Blueprint", "Hardware Pinout & Wiring Configuration Matrix"],
11716
+ budget: 4e3
11717
+ });
11718
+ if (!excerpt) return [];
11719
+ const factLines = [];
11720
+ for (const rawLine of excerpt.split("\n")) {
11721
+ 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
+ }
11726
+ if (factLines.length >= 8) break;
11727
+ }
11728
+ return factLines;
11729
+ }
11730
+ function buildProductSpecBlock(input) {
11731
+ 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
+ const toolchainFacts = input.refPack ? extractToolchainFacts(input.refPack) : [];
11735
+ const lines = [];
11736
+ lines.push("[PRODUCT SPEC \u2014 CANONICAL, BINDING FOR EVERY ARTIFACT OF THIS PROJECT]:");
11737
+ lines.push("All decisions below are PROJECT-WIDE. Every artifact (LESSON, satellites, KX)");
11738
+ lines.push("must describe the SAME product. Contradicting or re-deciding any line here is a");
11739
+ lines.push("consistency failure \u2014 if a needed decision is absent, stay GENERIC, never invent.");
11740
+ lines.push("");
11741
+ if (platform) {
11742
+ lines.push(`- Primary technology: ${platform}`);
11743
+ }
11744
+ if (hardware) {
11745
+ lines.push(`- Hardware / classroom setup: ${hardware}`);
11746
+ }
11747
+ if (input.overrides?.deliverableTemplate) {
11748
+ lines.push(`- Deliverable template: ${input.overrides.deliverableTemplate}`);
11749
+ }
11750
+ if (toolchainFacts.length > 0) {
11751
+ lines.push("- Toolchain & versions (ground truth):");
11752
+ for (const f of toolchainFacts) lines.push(` ${f}`);
11753
+ }
11754
+ if (lines.length <= 5) return "";
11755
+ return lines.join("\n");
11756
+ }
11757
+
11627
11758
  // src/services/knowledgeExpositionService.ts
11628
11759
  var EXPOSITION_REL = (lessonCode) => "_sot/expositions/" + lessonCode + ".md";
11629
11760
  var ExpositionApprovalError = class extends Error {
@@ -11741,7 +11872,12 @@ async function ensureKnowledgeExposition(options) {
11741
11872
  }
11742
11873
  const targetLanguage = options.targetLanguage || plan.translation?.target_language || "vi";
11743
11874
  const originalSystemPrompt = buildSystemPrompt(targetLanguage, techStack, hardwarePlatform);
11744
- const originalUserPrompt = buildUserPrompt(session, plan, glossary, targetLanguage, options.slcMarkdown, refPack);
11875
+ const productSpecBlock = buildProductSpecBlock({
11876
+ refPack,
11877
+ briefing: techStack || hardwarePlatform ? { techStack, hardwarePlatform } : void 0,
11878
+ overrides: options.productSpec
11879
+ });
11880
+ const originalUserPrompt = buildUserPrompt(session, plan, glossary, targetLanguage, options.slcMarkdown, refPack) + (productSpecBlock ? "\n\n" + productSpecBlock : "");
11745
11881
  const content = (await llmFn(
11746
11882
  originalSystemPrompt,
11747
11883
  originalUserPrompt
@@ -11775,7 +11911,17 @@ async function ensureKnowledgeExposition(options) {
11775
11911
  const parsed = JSON.parse(match[0]);
11776
11912
  return { verdict: parsed.verdict ?? "NEEDS_REVISION", score: parsed.score ?? 0, critique: parsed.critique ?? "" };
11777
11913
  };
11778
- let verdict = await judgeOnce(finalContent);
11914
+ const scopedKeywords = [
11915
+ ...session.new_keywords,
11916
+ ...glossary.map((g) => g.term)
11917
+ ];
11918
+ const runDeterministic = (candidate) => validateArtifactDeterministic({
11919
+ content: candidate,
11920
+ refPack,
11921
+ scopedKeywords
11922
+ });
11923
+ let det = runDeterministic(finalContent);
11924
+ let verdict = det.ok ? await judgeOnce(finalContent) : { verdict: "NEEDS_REVISION", score: 0, critique: validatorRepairPrompt(det.issues) };
11779
11925
  if (verdict.verdict !== "APPROVED") {
11780
11926
  const repairPrompt = [
11781
11927
  originalUserPrompt,
@@ -11783,11 +11929,12 @@ async function ensureKnowledgeExposition(options) {
11783
11929
  "--- YOUR PREVIOUS DRAFT (REJECTED, fix ALL issues below) ---",
11784
11930
  finalContent,
11785
11931
  "",
11786
- "--- JUDGE CRITIQUE (fix ALL issues, output the COMPLETE corrected document) ---",
11932
+ "--- CRITIQUE (fix ALL issues, output the COMPLETE corrected document) ---",
11787
11933
  verdict.critique
11788
11934
  ].join("\n");
11789
11935
  finalContent = (await llmFn(originalSystemPrompt, repairPrompt)).trim();
11790
- verdict = await judgeOnce(finalContent);
11936
+ det = runDeterministic(finalContent);
11937
+ verdict = det.ok ? await judgeOnce(finalContent) : { verdict: "NEEDS_REVISION", score: 0, critique: validatorRepairPrompt(det.issues) };
11791
11938
  }
11792
11939
  if (verdict.verdict !== "APPROVED") {
11793
11940
  throw new Error("EXPOSITION failed judge for " + lessonCode + " (verdict " + verdict.verdict + ", score " + verdict.score + "): " + verdict.critique);
@@ -26056,7 +26203,22 @@ async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
26056
26203
  const framework = await storage.readSotDocument(projectId, "CURRICULUM_FRAMEWORK.md") || "";
26057
26204
  const styleGuide = await storage.readSotDocument(projectId, "CONTENT_STYLE_GUIDE.md") || "";
26058
26205
  const refPack = await storage.readSotDocument(projectId, "REFERENCE_PACK.md") || "";
26206
+ let currentPlanHash;
26207
+ try {
26208
+ const planRaw0 = await storage.readArtifact(projectId, "_sot/CURRICULUM_PLAN.json");
26209
+ if (planRaw0) currentPlanHash = JSON.parse(planRaw0).plan_hash;
26210
+ } catch {
26211
+ }
26059
26212
  await storage.readSotDocument(projectId, "LEARNER_PROFILE.md") || "";
26213
+ const isStaleVsPlan = (content) => {
26214
+ if (!content || !currentPlanHash) return false;
26215
+ const m = content.match(/plan_hash:\s*"([0-9a-f]+)"/);
26216
+ return !!m && m[1] !== currentPlanHash;
26217
+ };
26218
+ const stampPlanHash = (content) => {
26219
+ if (!currentPlanHash || content.includes('plan_hash: "' + currentPlanHash + '"')) return content;
26220
+ return content + '\n---\nplan_hash: "' + currentPlanHash + '"\n- **Plan reference:** ' + currentPlanHash + "\n";
26221
+ };
26060
26222
  const slcMarkdown = options.sectionLanguageContract || await storage.readSotDocument(projectId, "SECTION_LANGUAGE_CONTRACT.md") || "";
26061
26223
  let targetLang = options.targetLanguage;
26062
26224
  if (!targetLang) {
@@ -26216,6 +26378,10 @@ ${expositionContext}`);
26216
26378
  parts.push(`### REFERENCE PACK GROUND TRUTH
26217
26379
  ${effectiveRefPack}`);
26218
26380
  }
26381
+ const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26382
+ if (productSpec) {
26383
+ parts.push(productSpec);
26384
+ }
26219
26385
  return parts.length > 0 ? `
26220
26386
 
26221
26387
  [GROUND TRUTH]:
@@ -26263,7 +26429,10 @@ ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBl
26263
26429
  const lessonRelPath = `_content/${unitCode}/LESSON_${lessonCode}.md`;
26264
26430
  const existingLessonContent = await storage.readArtifact(projectId, lessonRelPath);
26265
26431
  let lessonContent = existingLessonContent || "";
26266
- if (artifactScope.includes("LESSON") && (!existingLessonContent || force)) {
26432
+ if (artifactScope.includes("LESSON") && (!existingLessonContent || force || isStaleVsPlan(existingLessonContent))) {
26433
+ if (existingLessonContent && !force && isStaleVsPlan(existingLessonContent)) {
26434
+ onProgress?.("@content", `[STALENESS] LESSON_${lessonCode} was generated under a different plan_hash \u2014 regenerating against the current plan.`);
26435
+ }
26267
26436
  onProgress?.("@content", `[1/4] Authoring Master Lesson Plan (${pedagogyLabel}): \`LESSON_${lessonCode}.md\` (${lessonTitle})...`);
26268
26437
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26269
26438
  const mediaDirective = options.mediaPolicy && options.mediaPolicy.maxGenImagesPerArtifact > 0 ? `
@@ -26334,7 +26503,7 @@ Fix ALL issues above and output the complete corrected document.` }],
26334
26503
  } else {
26335
26504
  lessonContent = lintReport.autoFixedContent || rawLesson;
26336
26505
  }
26337
- await storage.saveArtifact(projectId, lessonRelPath, lessonContent);
26506
+ await storage.saveArtifact(projectId, lessonRelPath, stampPlanHash(lessonContent));
26338
26507
  producedArtifacts.push(`LESSON_${lessonCode}.md`);
26339
26508
  }
26340
26509
  const contentHash = lessonContent ? computeContentHash(lessonContent) : void 0;
@@ -26374,6 +26543,23 @@ ${yamlBlock.trim()}
26374
26543
  `);
26375
26544
  };
26376
26545
  var injectYamlReviewMetadata = injectYamlReviewMetadata2;
26546
+ const lessonDet = validateArtifactDeterministic({ content: lessonContent, refPack });
26547
+ if (!lessonDet.ok) {
26548
+ const detCritique = validatorRepairPrompt(lessonDet.issues);
26549
+ onProgress?.("@reviewer", `\u26D4 LESSON deterministic validator FAIL: ${detCritique.slice(0, 200)}`);
26550
+ await storage.updateArtifactState(projectId, lessonCode, "LESSON", {
26551
+ state: "rejected",
26552
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26553
+ contentHash: computeContentHash(lessonContent),
26554
+ review: {
26555
+ decision: "NEEDS_REVISION",
26556
+ reviewedBy: "@heuristic-linter",
26557
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
26558
+ score: 0,
26559
+ critique: detCritique
26560
+ }
26561
+ });
26562
+ }
26377
26563
  const judgeObjectives = parseLessonObjectives(lessonContent, concept, bloomLevel);
26378
26564
  const judgeExpositionExcerpt = expositionContext ? buildExpositionExcerpt(expositionContext, { budget: 3e3, sectionLanguageContract: slcMarkdown, artifactType: "KNOWLEDGE_EXPOSITION" }).excerpt : void 0;
26379
26565
  const judgeResult = await LLMJudgeEngine.evaluateArtifactWithLLM({
@@ -26629,6 +26815,23 @@ ${currentContent}` }],
26629
26815
  ${lessonExcerpt}${symbolLedgerBlock}`;
26630
26816
  const judgeSat = (sat, content) => {
26631
26817
  if (gateModeFor(gates, sat) !== "LLM_JUDGE") return Promise.resolve();
26818
+ const det = validateArtifactDeterministic({ content, refPack });
26819
+ if (!det.ok) {
26820
+ const critique = validatorRepairPrompt(det.issues);
26821
+ onProgress?.("@reviewer", `\u26D4 ${sat} deterministic validator FAIL: ${critique.slice(0, 200)}`);
26822
+ return storage.updateArtifactState(projectId, lessonCode, sat, {
26823
+ state: "rejected",
26824
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26825
+ contentHash: computeContentHash(content),
26826
+ review: {
26827
+ decision: "NEEDS_REVISION",
26828
+ reviewedBy: "@heuristic-linter",
26829
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
26830
+ score: 0,
26831
+ critique
26832
+ }
26833
+ });
26834
+ }
26632
26835
  return judgeSatelliteArtifact({
26633
26836
  storage,
26634
26837
  projectId,
@@ -26645,7 +26848,8 @@ ${lessonExcerpt}${symbolLedgerBlock}`;
26645
26848
  });
26646
26849
  };
26647
26850
  const actRelPath = `_content/${unitCode}/ACT_${lessonCode}.md`;
26648
- if (artifactScope.includes("ACT") && (!await storage.readArtifact(projectId, actRelPath) || force)) {
26851
+ const existingAct = await storage.readArtifact(projectId, actRelPath);
26852
+ if (artifactScope.includes("ACT") && (!existingAct || force || isStaleVsPlan(existingAct))) {
26649
26853
  onProgress?.("@activity", `[2/4] Authoring Hands-on Lab: \`ACT_${lessonCode}.md\`...`);
26650
26854
  const actDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26651
26855
  const actPrompt = `You are @activity (Lead Hands-on STEM & Laboratory Exercise Designer).
@@ -26705,7 +26909,7 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26705
26909
  rawError: rawAct || "Empty AI response"
26706
26910
  });
26707
26911
  }
26708
- await storage.saveArtifact(projectId, actRelPath, rawAct);
26912
+ await storage.saveArtifact(projectId, actRelPath, stampPlanHash(rawAct));
26709
26913
  await storage.updateArtifactState(projectId, lessonCode, "ACT", {
26710
26914
  state: "completed",
26711
26915
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26715,7 +26919,8 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26715
26919
  await judgeSat("ACT", rawAct);
26716
26920
  }
26717
26921
  const quizRelPath = `_content/${unitCode}/QUIZ_${lessonCode}.md`;
26718
- if (artifactScope.includes("QUIZ") && (!await storage.readArtifact(projectId, quizRelPath) || force)) {
26922
+ const existingQuiz = await storage.readArtifact(projectId, quizRelPath);
26923
+ if (artifactScope.includes("QUIZ") && (!existingQuiz || force || isStaleVsPlan(existingQuiz))) {
26719
26924
  onProgress?.("@assessor", `[3/4] Authoring Assessment Bank: \`QUIZ_${lessonCode}.md\`...`);
26720
26925
  const quizDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26721
26926
  const quizPrompt = `You are @assessor (Lead Psychometrician & Competency Assessment Specialist).
@@ -26769,7 +26974,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26769
26974
  rawError: rawQuiz || "Empty AI response"
26770
26975
  });
26771
26976
  }
26772
- await storage.saveArtifact(projectId, quizRelPath, rawQuiz);
26977
+ await storage.saveArtifact(projectId, quizRelPath, stampPlanHash(rawQuiz));
26773
26978
  await storage.updateArtifactState(projectId, lessonCode, "QUIZ", {
26774
26979
  state: "completed",
26775
26980
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26779,7 +26984,8 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26779
26984
  await judgeSat("QUIZ", rawQuiz);
26780
26985
  }
26781
26986
  const slideRelPath = `_content/${unitCode}/SLIDE_${lessonCode}.md`;
26782
- if (artifactScope.includes("SLIDE") && (!await storage.readArtifact(projectId, slideRelPath) || force)) {
26987
+ const existingSlide = await storage.readArtifact(projectId, slideRelPath);
26988
+ if (artifactScope.includes("SLIDE") && (!existingSlide || force || isStaleVsPlan(existingSlide))) {
26783
26989
  onProgress?.("@illustrator", `[4/4] Authoring Presentation Slide Deck: \`SLIDE_${lessonCode}.md\`...`);
26784
26990
  const isHtmlEngine = options.slidesEngine !== "marp";
26785
26991
  if (isHtmlEngine) {
@@ -26813,7 +27019,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26813
27019
  let deckJson = workflowResult.deckJson;
26814
27020
  const markdownWrapper = workflowResult.markdownWrapper;
26815
27021
  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" }]};
26816
- await storage.saveArtifact(projectId, slideRelPath, markdownWrapper);
27022
+ await storage.saveArtifact(projectId, slideRelPath, stampPlanHash(markdownWrapper));
26817
27023
  producedArtifacts.push(`SLIDE_${lessonCode}.md`);
26818
27024
  if (deckJson) {
26819
27025
  const deckJsonStr = JSON.stringify(deckJson, null, 2);
@@ -26904,7 +27110,7 @@ footer: "Curriculum OS \u2022 ${lessonCode} (${pedagogyLabel})"
26904
27110
  rawError: rawSlide || "Empty AI response"
26905
27111
  });
26906
27112
  }
26907
- await storage.saveArtifact(projectId, slideRelPath, rawSlide);
27113
+ await storage.saveArtifact(projectId, slideRelPath, stampPlanHash(rawSlide));
26908
27114
  await storage.updateArtifactState(projectId, lessonCode, "SLIDE", {
26909
27115
  state: "completed",
26910
27116
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26915,7 +27121,8 @@ footer: "Curriculum OS \u2022 ${lessonCode} (${pedagogyLabel})"
26915
27121
  }
26916
27122
  }
26917
27123
  const guideRelPath = `_content/${unitCode}/GUIDE_${lessonCode}.md`;
26918
- if (artifactScope.includes("GUIDE") && (!await storage.readArtifact(projectId, guideRelPath) || force)) {
27124
+ const existingGuide = await storage.readArtifact(projectId, guideRelPath);
27125
+ if (artifactScope.includes("GUIDE") && (!existingGuide || force || isStaleVsPlan(existingGuide))) {
26919
27126
  onProgress?.("@content", `Authoring Teacher Guide: \`GUIDE_${lessonCode}.md\`...`, { artifactType: "GUIDE" });
26920
27127
  const guideDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26921
27128
  const guidePrompt = `You are @content (Senior Curriculum Developer & Teacher Facilitation Specialist).
@@ -26955,7 +27162,7 @@ ${buildHeadingDirective("GUIDE", slcMarkdown, targetLang)}`;
26955
27162
  }
26956
27163
  );
26957
27164
  if (rawGuide && !rawGuide.startsWith("\u26A0\uFE0F") && rawGuide.length >= 200) {
26958
- await storage.saveArtifact(projectId, guideRelPath, rawGuide);
27165
+ await storage.saveArtifact(projectId, guideRelPath, stampPlanHash(rawGuide));
26959
27166
  await storage.updateArtifactState(projectId, lessonCode, "GUIDE", {
26960
27167
  state: "completed",
26961
27168
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26966,7 +27173,8 @@ ${buildHeadingDirective("GUIDE", slcMarkdown, targetLang)}`;
26966
27173
  }
26967
27174
  }
26968
27175
  const handoutRelPath = `_content/${unitCode}/HANDOUT_${lessonCode}.md`;
26969
- if (artifactScope.includes("HANDOUT") && (!await storage.readArtifact(projectId, handoutRelPath) || force)) {
27176
+ const existingHandout = await storage.readArtifact(projectId, handoutRelPath);
27177
+ if (artifactScope.includes("HANDOUT") && (!existingHandout || force || isStaleVsPlan(existingHandout))) {
26970
27178
  onProgress?.("@content", `Authoring Student Handout: \`HANDOUT_${lessonCode}.md\`...`, { artifactType: "HANDOUT" });
26971
27179
  const handoutDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26972
27180
  const handoutPrompt = `You are @content (Educational Content Specialist).
@@ -27006,7 +27214,7 @@ ${buildHeadingDirective("HANDOUT", slcMarkdown, targetLang)}`;
27006
27214
  }
27007
27215
  );
27008
27216
  if (rawHandout && !rawHandout.startsWith("\u26A0\uFE0F") && rawHandout.length >= 200) {
27009
- await storage.saveArtifact(projectId, handoutRelPath, rawHandout);
27217
+ await storage.saveArtifact(projectId, handoutRelPath, stampPlanHash(rawHandout));
27010
27218
  await storage.updateArtifactState(projectId, lessonCode, "HANDOUT", {
27011
27219
  state: "completed",
27012
27220
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27017,7 +27225,8 @@ ${buildHeadingDirective("HANDOUT", slcMarkdown, targetLang)}`;
27017
27225
  }
27018
27226
  }
27019
27227
  const wksRelPath = `_content/${unitCode}/WKS_${lessonCode}.md`;
27020
- if (artifactScope.includes("WKS") && (!await storage.readArtifact(projectId, wksRelPath) || force)) {
27228
+ const existingWks = await storage.readArtifact(projectId, wksRelPath);
27229
+ if (artifactScope.includes("WKS") && (!existingWks || force || isStaleVsPlan(existingWks))) {
27021
27230
  onProgress?.("@content", `Authoring Student Worksheet: \`WKS_${lessonCode}.md\`...`, { artifactType: "WKS" });
27022
27231
  const wksDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27023
27232
  const wksPrompt = `You are @content (Instructional Activity & Worksheet Designer).
@@ -27061,7 +27270,7 @@ ${buildHeadingDirective("WKS", slcMarkdown, targetLang)}`;
27061
27270
  }
27062
27271
  );
27063
27272
  if (rawWks && !rawWks.startsWith("\u26A0\uFE0F") && rawWks.length >= 200) {
27064
- await storage.saveArtifact(projectId, wksRelPath, rawWks);
27273
+ await storage.saveArtifact(projectId, wksRelPath, stampPlanHash(rawWks));
27065
27274
  await storage.updateArtifactState(projectId, lessonCode, "WKS", {
27066
27275
  state: "completed",
27067
27276
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27072,7 +27281,8 @@ ${buildHeadingDirective("WKS", slcMarkdown, targetLang)}`;
27072
27281
  }
27073
27282
  }
27074
27283
  const codeRelPath = `_content/${unitCode}/CODE_${lessonCode}.md`;
27075
- if ((artifactScope.includes("CODE") || artifactScope.includes("CODE_LAB")) && (!await storage.readArtifact(projectId, codeRelPath) || force)) {
27284
+ const existingCode = await storage.readArtifact(projectId, codeRelPath);
27285
+ if ((artifactScope.includes("CODE") || artifactScope.includes("CODE_LAB")) && (!existingCode || force || isStaleVsPlan(existingCode))) {
27076
27286
  onProgress?.("@activity", `Authoring Executable Code Lab: \`CODE_${lessonCode}.md\`...`, { artifactType: "CODE" });
27077
27287
  const codeDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27078
27288
  const codePrompt = `You are @activity & @tech_sme (Lead Software Engineer & Technical SME).
@@ -27120,7 +27330,7 @@ ${buildHeadingDirective("CODE", slcMarkdown, targetLang)}`;
27120
27330
  }
27121
27331
  );
27122
27332
  if (rawCode && !rawCode.startsWith("\u26A0\uFE0F") && rawCode.length >= 200) {
27123
- await storage.saveArtifact(projectId, codeRelPath, rawCode);
27333
+ await storage.saveArtifact(projectId, codeRelPath, stampPlanHash(rawCode));
27124
27334
  await storage.updateArtifactState(projectId, lessonCode, "CODE", {
27125
27335
  state: "completed",
27126
27336
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27136,7 +27346,8 @@ ${buildHeadingDirective("CODE", slcMarkdown, targetLang)}`;
27136
27346
  }
27137
27347
  }
27138
27348
  const extRelPath = `_content/${unitCode}/EXT_${lessonCode}.md`;
27139
- if (artifactScope.includes("EXT") && (!await storage.readArtifact(projectId, extRelPath) || force)) {
27349
+ const existingExt = await storage.readArtifact(projectId, extRelPath);
27350
+ if (artifactScope.includes("EXT") && (!existingExt || force || isStaleVsPlan(existingExt))) {
27140
27351
  onProgress?.("@activity", `Authoring Advanced Extension Challenge: \`EXT_${lessonCode}.md\`...`, { artifactType: "EXT" });
27141
27352
  const extDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27142
27353
  const isVi = targetLang === "vi";
@@ -27206,7 +27417,7 @@ ${buildHeadingDirective("EXT", slcMarkdown, targetLang)}`;
27206
27417
  }
27207
27418
  );
27208
27419
  if (rawExt && !rawExt.startsWith("\u26A0\uFE0F") && rawExt.length >= 200) {
27209
- await storage.saveArtifact(projectId, extRelPath, rawExt);
27420
+ await storage.saveArtifact(projectId, extRelPath, stampPlanHash(rawExt));
27210
27421
  await storage.updateArtifactState(projectId, lessonCode, "EXT", {
27211
27422
  state: "completed",
27212
27423
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),