@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.d.cts CHANGED
@@ -165,6 +165,20 @@ interface ProduceLessonOptions {
165
165
  mode?: 'search_only' | 'gen_only' | 'auto';
166
166
  };
167
167
  approvedArtifacts?: string[];
168
+ /**
169
+ * 5-Whys RC-canonicality: project briefing feeding the canonical PRODUCT_SPEC
170
+ * block (platform, hardware, deliverable template). Injected identically into
171
+ * every artifact's ground truth so no artifact re-decides product shape.
172
+ */
173
+ briefing?: {
174
+ techStack?: string;
175
+ coreTechnology?: string;
176
+ topic?: string;
177
+ hardwarePlatform?: string;
178
+ studentEquipment?: string;
179
+ classDynamic?: string;
180
+ language?: string;
181
+ };
168
182
  /**
169
183
  * P46 RC2: host-provided inference override (Vercel AI SDK gateway).
170
184
  * When supplied, ALL lesson/satellite LLM calls route through it — the kit's
@@ -658,6 +672,12 @@ interface GenerateExpositionOptions {
658
672
  /** REFERENCE_PACK.md raw content — technical ground truth (versions, APIs).
659
673
  * 5-Whys RC-vacuum: without it the KX model regresses to pretrained facts. */
660
674
  refPack?: string;
675
+ /** 5-Whys RC-canonicality: canonical PRODUCT_SPEC fields so KX does not
676
+ * re-decide the deliverable shape that LESSON/satellites already share. */
677
+ productSpec?: {
678
+ deliverableTemplate?: string;
679
+ primaryLanguage?: string;
680
+ };
661
681
  }
662
682
  interface ExpositionResult {
663
683
  relPath: string;
package/dist/index.d.ts CHANGED
@@ -165,6 +165,20 @@ interface ProduceLessonOptions {
165
165
  mode?: 'search_only' | 'gen_only' | 'auto';
166
166
  };
167
167
  approvedArtifacts?: string[];
168
+ /**
169
+ * 5-Whys RC-canonicality: project briefing feeding the canonical PRODUCT_SPEC
170
+ * block (platform, hardware, deliverable template). Injected identically into
171
+ * every artifact's ground truth so no artifact re-decides product shape.
172
+ */
173
+ briefing?: {
174
+ techStack?: string;
175
+ coreTechnology?: string;
176
+ topic?: string;
177
+ hardwarePlatform?: string;
178
+ studentEquipment?: string;
179
+ classDynamic?: string;
180
+ language?: string;
181
+ };
168
182
  /**
169
183
  * P46 RC2: host-provided inference override (Vercel AI SDK gateway).
170
184
  * When supplied, ALL lesson/satellite LLM calls route through it — the kit's
@@ -658,6 +672,12 @@ interface GenerateExpositionOptions {
658
672
  /** REFERENCE_PACK.md raw content — technical ground truth (versions, APIs).
659
673
  * 5-Whys RC-vacuum: without it the KX model regresses to pretrained facts. */
660
674
  refPack?: string;
675
+ /** 5-Whys RC-canonicality: canonical PRODUCT_SPEC fields so KX does not
676
+ * re-decide the deliverable shape that LESSON/satellites already share. */
677
+ productSpec?: {
678
+ deliverableTemplate?: string;
679
+ primaryLanguage?: string;
680
+ };
661
681
  }
662
682
  interface ExpositionResult {
663
683
  relPath: string;
package/dist/index.mjs CHANGED
@@ -11612,6 +11612,137 @@ function extractSymbolLedger(lessonMarkdown) {
11612
11612
  return result;
11613
11613
  }
11614
11614
 
11615
+ // src/services/artifactValidators.ts
11616
+ var CJK_RE = /[\u3400-\u4DBF\u4E00-\u9FFF\u3040-\u30FF\uAC00-\uD7AF]/g;
11617
+ function scanCjkLeaks(content) {
11618
+ const matches = content.match(CJK_RE);
11619
+ if (!matches || matches.length === 0) return null;
11620
+ const lines = [];
11621
+ for (const line of content.split("\n")) {
11622
+ if (CJK_RE.test(line)) lines.push(line.trim().slice(0, 160));
11623
+ }
11624
+ return {
11625
+ code: "cjk-leak",
11626
+ 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.`,
11627
+ evidence: lines.slice(0, 5)
11628
+ };
11629
+ }
11630
+ var VERSION_CLAIM_RE = /\b([A-Z][A-Za-z+#.\-]{1,24})\s+(\d{1,2}(?:\.\d{1,2})?)\b/g;
11631
+ function checkVersionGroundTruth(content, refPack) {
11632
+ if (!refPack || !refPack.trim()) return null;
11633
+ const packLower = refPack.toLowerCase();
11634
+ const claims = /* @__PURE__ */ new Map();
11635
+ for (const m of content.matchAll(VERSION_CLAIM_RE)) {
11636
+ const tool = m[1];
11637
+ if (claims.size === 0 || !claims.has(tool)) claims.set(tool, m[2]);
11638
+ }
11639
+ const contradictions = [];
11640
+ for (const [tool, claimed] of claims) {
11641
+ const toolRe = new RegExp(`\\b${tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+(\\d{1,2}(?:\\.\\d{1,2})?)\\b`, "i");
11642
+ const packMatch = packLower.match(toolRe);
11643
+ if (packMatch && packMatch[1] !== claimed) {
11644
+ contradictions.push(`${tool}: artifact n\xF3i ${claimed}, REFERENCE_PACK ch\u1ED1t ${packMatch[1]}`);
11645
+ }
11646
+ }
11647
+ if (contradictions.length === 0) return null;
11648
+ return {
11649
+ code: "version-contradiction",
11650
+ detail: `Version m\xE2u thu\u1EABn v\u1EDBi ground truth REFERENCE_PACK: ${contradictions.join("; ")}. S\u1EEDa theo b\u1EA3n trong REFERENCE_PACK.`,
11651
+ evidence: contradictions
11652
+ };
11653
+ }
11654
+ function checkScopeDrift(content, scopedKeywords, tolerance = 0) {
11655
+ const conceptHeaders = /* @__PURE__ */ new Set();
11656
+ for (const m of content.matchAll(/\*\*([^*\n]{3,60}?)\s*\((?:WHAT|CIO|SIO|ULO)[^)]*\):\*\*/g)) {
11657
+ conceptHeaders.add(m[1].toLowerCase());
11658
+ }
11659
+ if (conceptHeaders.size === 0) return null;
11660
+ const drift = [];
11661
+ for (const header of conceptHeaders) {
11662
+ const hit = scopedKeywords.some((kw) => {
11663
+ const k = kw.toLowerCase().trim();
11664
+ if (!k) return false;
11665
+ const tokens = k.split(/\s+/);
11666
+ const present = tokens.filter((t) => header.includes(t)).length;
11667
+ return present >= Math.max(1, tokens.length - tolerance);
11668
+ });
11669
+ if (!hit) drift.push(header);
11670
+ }
11671
+ if (drift.length === 0) return null;
11672
+ return {
11673
+ code: "scope-drift",
11674
+ 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.`,
11675
+ evidence: drift
11676
+ };
11677
+ }
11678
+ function validateArtifactDeterministic(input) {
11679
+ const issues = [];
11680
+ const cjk = scanCjkLeaks(input.content);
11681
+ if (cjk) issues.push(cjk);
11682
+ if (input.refPack) {
11683
+ const ver = checkVersionGroundTruth(input.content, input.refPack);
11684
+ if (ver) issues.push(ver);
11685
+ }
11686
+ if (input.scopedKeywords && input.scopedKeywords.length > 0) {
11687
+ const scope = checkScopeDrift(input.content, input.scopedKeywords);
11688
+ if (scope) issues.push(scope);
11689
+ }
11690
+ return { ok: issues.length === 0, issues };
11691
+ }
11692
+ function validatorRepairPrompt(issues) {
11693
+ return [
11694
+ "DETERMINISTIC VALIDATOR FAILURES (mechanically detected \u2014 non-negotiable, fix ALL):",
11695
+ ...issues.map((i, n) => `${n + 1}. [${i.code}] ${i.detail}${i.evidence.length ? `
11696
+ Evidence: ${i.evidence.join(" | ").slice(0, 400)}` : ""}`)
11697
+ ].join("\n");
11698
+ }
11699
+
11700
+ // src/services/productSpec.ts
11701
+ function extractToolchainFacts(refPack) {
11702
+ const { excerpt } = buildSectionAwareExcerpt(refPack, {
11703
+ priorities: ["Technical Overview & Architecture Blueprint", "Hardware Pinout & Wiring Configuration Matrix"],
11704
+ budget: 4e3
11705
+ });
11706
+ if (!excerpt) return [];
11707
+ const factLines = [];
11708
+ for (const rawLine of excerpt.split("\n")) {
11709
+ 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
+ }
11714
+ if (factLines.length >= 8) break;
11715
+ }
11716
+ return factLines;
11717
+ }
11718
+ function buildProductSpecBlock(input) {
11719
+ 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
+ const toolchainFacts = input.refPack ? extractToolchainFacts(input.refPack) : [];
11723
+ const lines = [];
11724
+ lines.push("[PRODUCT SPEC \u2014 CANONICAL, BINDING FOR EVERY ARTIFACT OF THIS PROJECT]:");
11725
+ lines.push("All decisions below are PROJECT-WIDE. Every artifact (LESSON, satellites, KX)");
11726
+ lines.push("must describe the SAME product. Contradicting or re-deciding any line here is a");
11727
+ lines.push("consistency failure \u2014 if a needed decision is absent, stay GENERIC, never invent.");
11728
+ lines.push("");
11729
+ if (platform) {
11730
+ lines.push(`- Primary technology: ${platform}`);
11731
+ }
11732
+ if (hardware) {
11733
+ lines.push(`- Hardware / classroom setup: ${hardware}`);
11734
+ }
11735
+ if (input.overrides?.deliverableTemplate) {
11736
+ lines.push(`- Deliverable template: ${input.overrides.deliverableTemplate}`);
11737
+ }
11738
+ if (toolchainFacts.length > 0) {
11739
+ lines.push("- Toolchain & versions (ground truth):");
11740
+ for (const f of toolchainFacts) lines.push(` ${f}`);
11741
+ }
11742
+ if (lines.length <= 5) return "";
11743
+ return lines.join("\n");
11744
+ }
11745
+
11615
11746
  // src/services/knowledgeExpositionService.ts
11616
11747
  var EXPOSITION_REL = (lessonCode) => "_sot/expositions/" + lessonCode + ".md";
11617
11748
  var ExpositionApprovalError = class extends Error {
@@ -11729,7 +11860,12 @@ async function ensureKnowledgeExposition(options) {
11729
11860
  }
11730
11861
  const targetLanguage = options.targetLanguage || plan.translation?.target_language || "vi";
11731
11862
  const originalSystemPrompt = buildSystemPrompt(targetLanguage, techStack, hardwarePlatform);
11732
- const originalUserPrompt = buildUserPrompt(session, plan, glossary, targetLanguage, options.slcMarkdown, refPack);
11863
+ const productSpecBlock = buildProductSpecBlock({
11864
+ refPack,
11865
+ briefing: techStack || hardwarePlatform ? { techStack, hardwarePlatform } : void 0,
11866
+ overrides: options.productSpec
11867
+ });
11868
+ const originalUserPrompt = buildUserPrompt(session, plan, glossary, targetLanguage, options.slcMarkdown, refPack) + (productSpecBlock ? "\n\n" + productSpecBlock : "");
11733
11869
  const content = (await llmFn(
11734
11870
  originalSystemPrompt,
11735
11871
  originalUserPrompt
@@ -11763,7 +11899,17 @@ async function ensureKnowledgeExposition(options) {
11763
11899
  const parsed = JSON.parse(match[0]);
11764
11900
  return { verdict: parsed.verdict ?? "NEEDS_REVISION", score: parsed.score ?? 0, critique: parsed.critique ?? "" };
11765
11901
  };
11766
- let verdict = await judgeOnce(finalContent);
11902
+ const scopedKeywords = [
11903
+ ...session.new_keywords,
11904
+ ...glossary.map((g) => g.term)
11905
+ ];
11906
+ const runDeterministic = (candidate) => validateArtifactDeterministic({
11907
+ content: candidate,
11908
+ refPack,
11909
+ scopedKeywords
11910
+ });
11911
+ let det = runDeterministic(finalContent);
11912
+ let verdict = det.ok ? await judgeOnce(finalContent) : { verdict: "NEEDS_REVISION", score: 0, critique: validatorRepairPrompt(det.issues) };
11767
11913
  if (verdict.verdict !== "APPROVED") {
11768
11914
  const repairPrompt = [
11769
11915
  originalUserPrompt,
@@ -11771,11 +11917,12 @@ async function ensureKnowledgeExposition(options) {
11771
11917
  "--- YOUR PREVIOUS DRAFT (REJECTED, fix ALL issues below) ---",
11772
11918
  finalContent,
11773
11919
  "",
11774
- "--- JUDGE CRITIQUE (fix ALL issues, output the COMPLETE corrected document) ---",
11920
+ "--- CRITIQUE (fix ALL issues, output the COMPLETE corrected document) ---",
11775
11921
  verdict.critique
11776
11922
  ].join("\n");
11777
11923
  finalContent = (await llmFn(originalSystemPrompt, repairPrompt)).trim();
11778
- verdict = await judgeOnce(finalContent);
11924
+ det = runDeterministic(finalContent);
11925
+ verdict = det.ok ? await judgeOnce(finalContent) : { verdict: "NEEDS_REVISION", score: 0, critique: validatorRepairPrompt(det.issues) };
11779
11926
  }
11780
11927
  if (verdict.verdict !== "APPROVED") {
11781
11928
  throw new Error("EXPOSITION failed judge for " + lessonCode + " (verdict " + verdict.verdict + ", score " + verdict.score + "): " + verdict.critique);
@@ -26044,7 +26191,22 @@ async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
26044
26191
  const framework = await storage.readSotDocument(projectId, "CURRICULUM_FRAMEWORK.md") || "";
26045
26192
  const styleGuide = await storage.readSotDocument(projectId, "CONTENT_STYLE_GUIDE.md") || "";
26046
26193
  const refPack = await storage.readSotDocument(projectId, "REFERENCE_PACK.md") || "";
26194
+ let currentPlanHash;
26195
+ try {
26196
+ const planRaw0 = await storage.readArtifact(projectId, "_sot/CURRICULUM_PLAN.json");
26197
+ if (planRaw0) currentPlanHash = JSON.parse(planRaw0).plan_hash;
26198
+ } catch {
26199
+ }
26047
26200
  await storage.readSotDocument(projectId, "LEARNER_PROFILE.md") || "";
26201
+ const isStaleVsPlan = (content) => {
26202
+ if (!content || !currentPlanHash) return false;
26203
+ const m = content.match(/plan_hash:\s*"([0-9a-f]+)"/);
26204
+ return !!m && m[1] !== currentPlanHash;
26205
+ };
26206
+ const stampPlanHash = (content) => {
26207
+ if (!currentPlanHash || content.includes('plan_hash: "' + currentPlanHash + '"')) return content;
26208
+ return content + '\n---\nplan_hash: "' + currentPlanHash + '"\n- **Plan reference:** ' + currentPlanHash + "\n";
26209
+ };
26048
26210
  const slcMarkdown = options.sectionLanguageContract || await storage.readSotDocument(projectId, "SECTION_LANGUAGE_CONTRACT.md") || "";
26049
26211
  let targetLang = options.targetLanguage;
26050
26212
  if (!targetLang) {
@@ -26204,6 +26366,10 @@ ${expositionContext}`);
26204
26366
  parts.push(`### REFERENCE PACK GROUND TRUTH
26205
26367
  ${effectiveRefPack}`);
26206
26368
  }
26369
+ const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26370
+ if (productSpec) {
26371
+ parts.push(productSpec);
26372
+ }
26207
26373
  return parts.length > 0 ? `
26208
26374
 
26209
26375
  [GROUND TRUTH]:
@@ -26251,7 +26417,10 @@ ${sg}${standardsBlock}${glossaryBlock}${buildGroundTruthBlock()}${sessionSliceBl
26251
26417
  const lessonRelPath = `_content/${unitCode}/LESSON_${lessonCode}.md`;
26252
26418
  const existingLessonContent = await storage.readArtifact(projectId, lessonRelPath);
26253
26419
  let lessonContent = existingLessonContent || "";
26254
- if (artifactScope.includes("LESSON") && (!existingLessonContent || force)) {
26420
+ if (artifactScope.includes("LESSON") && (!existingLessonContent || force || isStaleVsPlan(existingLessonContent))) {
26421
+ if (existingLessonContent && !force && isStaleVsPlan(existingLessonContent)) {
26422
+ onProgress?.("@content", `[STALENESS] LESSON_${lessonCode} was generated under a different plan_hash \u2014 regenerating against the current plan.`);
26423
+ }
26255
26424
  onProgress?.("@content", `[1/4] Authoring Master Lesson Plan (${pedagogyLabel}): \`LESSON_${lessonCode}.md\` (${lessonTitle})...`);
26256
26425
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26257
26426
  const mediaDirective = options.mediaPolicy && options.mediaPolicy.maxGenImagesPerArtifact > 0 ? `
@@ -26322,7 +26491,7 @@ Fix ALL issues above and output the complete corrected document.` }],
26322
26491
  } else {
26323
26492
  lessonContent = lintReport.autoFixedContent || rawLesson;
26324
26493
  }
26325
- await storage.saveArtifact(projectId, lessonRelPath, lessonContent);
26494
+ await storage.saveArtifact(projectId, lessonRelPath, stampPlanHash(lessonContent));
26326
26495
  producedArtifacts.push(`LESSON_${lessonCode}.md`);
26327
26496
  }
26328
26497
  const contentHash = lessonContent ? computeContentHash(lessonContent) : void 0;
@@ -26362,6 +26531,23 @@ ${yamlBlock.trim()}
26362
26531
  `);
26363
26532
  };
26364
26533
  var injectYamlReviewMetadata = injectYamlReviewMetadata2;
26534
+ const lessonDet = validateArtifactDeterministic({ content: lessonContent, refPack });
26535
+ if (!lessonDet.ok) {
26536
+ const detCritique = validatorRepairPrompt(lessonDet.issues);
26537
+ onProgress?.("@reviewer", `\u26D4 LESSON deterministic validator FAIL: ${detCritique.slice(0, 200)}`);
26538
+ await storage.updateArtifactState(projectId, lessonCode, "LESSON", {
26539
+ state: "rejected",
26540
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26541
+ contentHash: computeContentHash(lessonContent),
26542
+ review: {
26543
+ decision: "NEEDS_REVISION",
26544
+ reviewedBy: "@heuristic-linter",
26545
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
26546
+ score: 0,
26547
+ critique: detCritique
26548
+ }
26549
+ });
26550
+ }
26365
26551
  const judgeObjectives = parseLessonObjectives(lessonContent, concept, bloomLevel);
26366
26552
  const judgeExpositionExcerpt = expositionContext ? buildExpositionExcerpt(expositionContext, { budget: 3e3, sectionLanguageContract: slcMarkdown, artifactType: "KNOWLEDGE_EXPOSITION" }).excerpt : void 0;
26367
26553
  const judgeResult = await LLMJudgeEngine.evaluateArtifactWithLLM({
@@ -26617,6 +26803,23 @@ ${currentContent}` }],
26617
26803
  ${lessonExcerpt}${symbolLedgerBlock}`;
26618
26804
  const judgeSat = (sat, content) => {
26619
26805
  if (gateModeFor(gates, sat) !== "LLM_JUDGE") return Promise.resolve();
26806
+ const det = validateArtifactDeterministic({ content, refPack });
26807
+ if (!det.ok) {
26808
+ const critique = validatorRepairPrompt(det.issues);
26809
+ onProgress?.("@reviewer", `\u26D4 ${sat} deterministic validator FAIL: ${critique.slice(0, 200)}`);
26810
+ return storage.updateArtifactState(projectId, lessonCode, sat, {
26811
+ state: "rejected",
26812
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26813
+ contentHash: computeContentHash(content),
26814
+ review: {
26815
+ decision: "NEEDS_REVISION",
26816
+ reviewedBy: "@heuristic-linter",
26817
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
26818
+ score: 0,
26819
+ critique
26820
+ }
26821
+ });
26822
+ }
26620
26823
  return judgeSatelliteArtifact({
26621
26824
  storage,
26622
26825
  projectId,
@@ -26633,7 +26836,8 @@ ${lessonExcerpt}${symbolLedgerBlock}`;
26633
26836
  });
26634
26837
  };
26635
26838
  const actRelPath = `_content/${unitCode}/ACT_${lessonCode}.md`;
26636
- if (artifactScope.includes("ACT") && (!await storage.readArtifact(projectId, actRelPath) || force)) {
26839
+ const existingAct = await storage.readArtifact(projectId, actRelPath);
26840
+ if (artifactScope.includes("ACT") && (!existingAct || force || isStaleVsPlan(existingAct))) {
26637
26841
  onProgress?.("@activity", `[2/4] Authoring Hands-on Lab: \`ACT_${lessonCode}.md\`...`);
26638
26842
  const actDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26639
26843
  const actPrompt = `You are @activity (Lead Hands-on STEM & Laboratory Exercise Designer).
@@ -26693,7 +26897,7 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26693
26897
  rawError: rawAct || "Empty AI response"
26694
26898
  });
26695
26899
  }
26696
- await storage.saveArtifact(projectId, actRelPath, rawAct);
26900
+ await storage.saveArtifact(projectId, actRelPath, stampPlanHash(rawAct));
26697
26901
  await storage.updateArtifactState(projectId, lessonCode, "ACT", {
26698
26902
  state: "completed",
26699
26903
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26703,7 +26907,8 @@ ${buildHeadingDirective("ACT", slcMarkdown, targetLang)}`;
26703
26907
  await judgeSat("ACT", rawAct);
26704
26908
  }
26705
26909
  const quizRelPath = `_content/${unitCode}/QUIZ_${lessonCode}.md`;
26706
- if (artifactScope.includes("QUIZ") && (!await storage.readArtifact(projectId, quizRelPath) || force)) {
26910
+ const existingQuiz = await storage.readArtifact(projectId, quizRelPath);
26911
+ if (artifactScope.includes("QUIZ") && (!existingQuiz || force || isStaleVsPlan(existingQuiz))) {
26707
26912
  onProgress?.("@assessor", `[3/4] Authoring Assessment Bank: \`QUIZ_${lessonCode}.md\`...`);
26708
26913
  const quizDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26709
26914
  const quizPrompt = `You are @assessor (Lead Psychometrician & Competency Assessment Specialist).
@@ -26757,7 +26962,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26757
26962
  rawError: rawQuiz || "Empty AI response"
26758
26963
  });
26759
26964
  }
26760
- await storage.saveArtifact(projectId, quizRelPath, rawQuiz);
26965
+ await storage.saveArtifact(projectId, quizRelPath, stampPlanHash(rawQuiz));
26761
26966
  await storage.updateArtifactState(projectId, lessonCode, "QUIZ", {
26762
26967
  state: "completed",
26763
26968
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26767,7 +26972,8 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26767
26972
  await judgeSat("QUIZ", rawQuiz);
26768
26973
  }
26769
26974
  const slideRelPath = `_content/${unitCode}/SLIDE_${lessonCode}.md`;
26770
- if (artifactScope.includes("SLIDE") && (!await storage.readArtifact(projectId, slideRelPath) || force)) {
26975
+ const existingSlide = await storage.readArtifact(projectId, slideRelPath);
26976
+ if (artifactScope.includes("SLIDE") && (!existingSlide || force || isStaleVsPlan(existingSlide))) {
26771
26977
  onProgress?.("@illustrator", `[4/4] Authoring Presentation Slide Deck: \`SLIDE_${lessonCode}.md\`...`);
26772
26978
  const isHtmlEngine = options.slidesEngine !== "marp";
26773
26979
  if (isHtmlEngine) {
@@ -26801,7 +27007,7 @@ ${buildHeadingDirective("QUIZ", slcMarkdown, targetLang)}`;
26801
27007
  let deckJson = workflowResult.deckJson;
26802
27008
  const markdownWrapper = workflowResult.markdownWrapper;
26803
27009
  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" }]};
26804
- await storage.saveArtifact(projectId, slideRelPath, markdownWrapper);
27010
+ await storage.saveArtifact(projectId, slideRelPath, stampPlanHash(markdownWrapper));
26805
27011
  producedArtifacts.push(`SLIDE_${lessonCode}.md`);
26806
27012
  if (deckJson) {
26807
27013
  const deckJsonStr = JSON.stringify(deckJson, null, 2);
@@ -26892,7 +27098,7 @@ footer: "Curriculum OS \u2022 ${lessonCode} (${pedagogyLabel})"
26892
27098
  rawError: rawSlide || "Empty AI response"
26893
27099
  });
26894
27100
  }
26895
- await storage.saveArtifact(projectId, slideRelPath, rawSlide);
27101
+ await storage.saveArtifact(projectId, slideRelPath, stampPlanHash(rawSlide));
26896
27102
  await storage.updateArtifactState(projectId, lessonCode, "SLIDE", {
26897
27103
  state: "completed",
26898
27104
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26903,7 +27109,8 @@ footer: "Curriculum OS \u2022 ${lessonCode} (${pedagogyLabel})"
26903
27109
  }
26904
27110
  }
26905
27111
  const guideRelPath = `_content/${unitCode}/GUIDE_${lessonCode}.md`;
26906
- if (artifactScope.includes("GUIDE") && (!await storage.readArtifact(projectId, guideRelPath) || force)) {
27112
+ const existingGuide = await storage.readArtifact(projectId, guideRelPath);
27113
+ if (artifactScope.includes("GUIDE") && (!existingGuide || force || isStaleVsPlan(existingGuide))) {
26907
27114
  onProgress?.("@content", `Authoring Teacher Guide: \`GUIDE_${lessonCode}.md\`...`, { artifactType: "GUIDE" });
26908
27115
  const guideDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26909
27116
  const guidePrompt = `You are @content (Senior Curriculum Developer & Teacher Facilitation Specialist).
@@ -26943,7 +27150,7 @@ ${buildHeadingDirective("GUIDE", slcMarkdown, targetLang)}`;
26943
27150
  }
26944
27151
  );
26945
27152
  if (rawGuide && !rawGuide.startsWith("\u26A0\uFE0F") && rawGuide.length >= 200) {
26946
- await storage.saveArtifact(projectId, guideRelPath, rawGuide);
27153
+ await storage.saveArtifact(projectId, guideRelPath, stampPlanHash(rawGuide));
26947
27154
  await storage.updateArtifactState(projectId, lessonCode, "GUIDE", {
26948
27155
  state: "completed",
26949
27156
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -26954,7 +27161,8 @@ ${buildHeadingDirective("GUIDE", slcMarkdown, targetLang)}`;
26954
27161
  }
26955
27162
  }
26956
27163
  const handoutRelPath = `_content/${unitCode}/HANDOUT_${lessonCode}.md`;
26957
- if (artifactScope.includes("HANDOUT") && (!await storage.readArtifact(projectId, handoutRelPath) || force)) {
27164
+ const existingHandout = await storage.readArtifact(projectId, handoutRelPath);
27165
+ if (artifactScope.includes("HANDOUT") && (!existingHandout || force || isStaleVsPlan(existingHandout))) {
26958
27166
  onProgress?.("@content", `Authoring Student Handout: \`HANDOUT_${lessonCode}.md\`...`, { artifactType: "HANDOUT" });
26959
27167
  const handoutDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26960
27168
  const handoutPrompt = `You are @content (Educational Content Specialist).
@@ -26994,7 +27202,7 @@ ${buildHeadingDirective("HANDOUT", slcMarkdown, targetLang)}`;
26994
27202
  }
26995
27203
  );
26996
27204
  if (rawHandout && !rawHandout.startsWith("\u26A0\uFE0F") && rawHandout.length >= 200) {
26997
- await storage.saveArtifact(projectId, handoutRelPath, rawHandout);
27205
+ await storage.saveArtifact(projectId, handoutRelPath, stampPlanHash(rawHandout));
26998
27206
  await storage.updateArtifactState(projectId, lessonCode, "HANDOUT", {
26999
27207
  state: "completed",
27000
27208
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27005,7 +27213,8 @@ ${buildHeadingDirective("HANDOUT", slcMarkdown, targetLang)}`;
27005
27213
  }
27006
27214
  }
27007
27215
  const wksRelPath = `_content/${unitCode}/WKS_${lessonCode}.md`;
27008
- if (artifactScope.includes("WKS") && (!await storage.readArtifact(projectId, wksRelPath) || force)) {
27216
+ const existingWks = await storage.readArtifact(projectId, wksRelPath);
27217
+ if (artifactScope.includes("WKS") && (!existingWks || force || isStaleVsPlan(existingWks))) {
27009
27218
  onProgress?.("@content", `Authoring Student Worksheet: \`WKS_${lessonCode}.md\`...`, { artifactType: "WKS" });
27010
27219
  const wksDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27011
27220
  const wksPrompt = `You are @content (Instructional Activity & Worksheet Designer).
@@ -27049,7 +27258,7 @@ ${buildHeadingDirective("WKS", slcMarkdown, targetLang)}`;
27049
27258
  }
27050
27259
  );
27051
27260
  if (rawWks && !rawWks.startsWith("\u26A0\uFE0F") && rawWks.length >= 200) {
27052
- await storage.saveArtifact(projectId, wksRelPath, rawWks);
27261
+ await storage.saveArtifact(projectId, wksRelPath, stampPlanHash(rawWks));
27053
27262
  await storage.updateArtifactState(projectId, lessonCode, "WKS", {
27054
27263
  state: "completed",
27055
27264
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27060,7 +27269,8 @@ ${buildHeadingDirective("WKS", slcMarkdown, targetLang)}`;
27060
27269
  }
27061
27270
  }
27062
27271
  const codeRelPath = `_content/${unitCode}/CODE_${lessonCode}.md`;
27063
- if ((artifactScope.includes("CODE") || artifactScope.includes("CODE_LAB")) && (!await storage.readArtifact(projectId, codeRelPath) || force)) {
27272
+ const existingCode = await storage.readArtifact(projectId, codeRelPath);
27273
+ if ((artifactScope.includes("CODE") || artifactScope.includes("CODE_LAB")) && (!existingCode || force || isStaleVsPlan(existingCode))) {
27064
27274
  onProgress?.("@activity", `Authoring Executable Code Lab: \`CODE_${lessonCode}.md\`...`, { artifactType: "CODE" });
27065
27275
  const codeDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27066
27276
  const codePrompt = `You are @activity & @tech_sme (Lead Software Engineer & Technical SME).
@@ -27108,7 +27318,7 @@ ${buildHeadingDirective("CODE", slcMarkdown, targetLang)}`;
27108
27318
  }
27109
27319
  );
27110
27320
  if (rawCode && !rawCode.startsWith("\u26A0\uFE0F") && rawCode.length >= 200) {
27111
- await storage.saveArtifact(projectId, codeRelPath, rawCode);
27321
+ await storage.saveArtifact(projectId, codeRelPath, stampPlanHash(rawCode));
27112
27322
  await storage.updateArtifactState(projectId, lessonCode, "CODE", {
27113
27323
  state: "completed",
27114
27324
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -27124,7 +27334,8 @@ ${buildHeadingDirective("CODE", slcMarkdown, targetLang)}`;
27124
27334
  }
27125
27335
  }
27126
27336
  const extRelPath = `_content/${unitCode}/EXT_${lessonCode}.md`;
27127
- if (artifactScope.includes("EXT") && (!await storage.readArtifact(projectId, extRelPath) || force)) {
27337
+ const existingExt = await storage.readArtifact(projectId, extRelPath);
27338
+ if (artifactScope.includes("EXT") && (!existingExt || force || isStaleVsPlan(existingExt))) {
27128
27339
  onProgress?.("@activity", `Authoring Advanced Extension Challenge: \`EXT_${lessonCode}.md\`...`, { artifactType: "EXT" });
27129
27340
  const extDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27130
27341
  const isVi = targetLang === "vi";
@@ -27194,7 +27405,7 @@ ${buildHeadingDirective("EXT", slcMarkdown, targetLang)}`;
27194
27405
  }
27195
27406
  );
27196
27407
  if (rawExt && !rawExt.startsWith("\u26A0\uFE0F") && rawExt.length >= 200) {
27197
- await storage.saveArtifact(projectId, extRelPath, rawExt);
27408
+ await storage.saveArtifact(projectId, extRelPath, stampPlanHash(rawExt));
27198
27409
  await storage.updateArtifactState(projectId, lessonCode, "EXT", {
27199
27410
  state: "completed",
27200
27411
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),