@thanh01.pmt/curriculum-kit 1.4.21 → 1.4.23

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
@@ -655,6 +669,15 @@ interface GenerateExpositionOptions {
655
669
  slcMarkdown?: Record<string, Record<string, string>> | string | null;
656
670
  techStack?: string;
657
671
  hardwarePlatform?: string;
672
+ /** REFERENCE_PACK.md raw content — technical ground truth (versions, APIs).
673
+ * 5-Whys RC-vacuum: without it the KX model regresses to pretrained facts. */
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
+ };
658
681
  }
659
682
  interface ExpositionResult {
660
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
@@ -655,6 +669,15 @@ interface GenerateExpositionOptions {
655
669
  slcMarkdown?: Record<string, Record<string, string>> | string | null;
656
670
  techStack?: string;
657
671
  hardwarePlatform?: string;
672
+ /** REFERENCE_PACK.md raw content — technical ground truth (versions, APIs).
673
+ * 5-Whys RC-vacuum: without it the KX model regresses to pretrained facts. */
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
+ };
658
681
  }
659
682
  interface ExpositionResult {
660
683
  relPath: string;
package/dist/index.mjs CHANGED
@@ -11507,6 +11507,21 @@ function normalizeHeading(str) {
11507
11507
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
11508
11508
  }
11509
11509
  function findCanonicalFuzzy(norm) {
11510
+ if (norm.includes("toolchain") || norm.includes("moi truong phat trien") || norm.includes("cong cu") || norm.includes("version") || norm.includes("phan mem") || norm.includes("development environment") || norm.includes("technical overview") || norm.includes("kien truc")) {
11511
+ return "Technical Overview & Architecture Blueprint";
11512
+ }
11513
+ if (norm.includes("pinout") || norm.includes("phan cung") || norm.includes("hardware") || norm.includes("wiring") || norm.includes("ket noi")) {
11514
+ return "Hardware Pinout & Wiring Configuration Matrix";
11515
+ }
11516
+ if (norm.includes("pedagog") || norm.includes("phuong phap") || norm.includes("day hoc") || norm.includes("su pham")) {
11517
+ return "Core Pedagogical Concept Anchor & Real-World Domain Bridge";
11518
+ }
11519
+ if (norm.includes("standard") || norm.includes("tieu chuan") || norm.includes("csta") || norm.includes("cs2023") || norm.includes("chuan academic")) {
11520
+ return "Standards Alignment";
11521
+ }
11522
+ if (norm.includes("roadmap") || norm.includes("lo trinh") || norm.includes("milestone") || norm.includes("giai doan")) {
11523
+ return void 0;
11524
+ }
11510
11525
  if (norm.includes("symbol") || norm.includes("identifier ledger") || norm.includes("dinh danh")) {
11511
11526
  return "Symbol & Identifier Ledger";
11512
11527
  }
@@ -11597,6 +11612,137 @@ function extractSymbolLedger(lessonMarkdown) {
11597
11612
  return result;
11598
11613
  }
11599
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
+
11600
11746
  // src/services/knowledgeExpositionService.ts
11601
11747
  var EXPOSITION_REL = (lessonCode) => "_sot/expositions/" + lessonCode + ".md";
11602
11748
  var ExpositionApprovalError = class extends Error {
@@ -11643,7 +11789,7 @@ function buildSystemPrompt(targetLanguage = "vi", techStack, hardwarePlatform) {
11643
11789
  langDirective
11644
11790
  ].join("\n");
11645
11791
  }
11646
- function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMarkdown) {
11792
+ function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMarkdown, refPack) {
11647
11793
  const depthLines = session.depth_assignments.map((d) => "- " + d.node_id + " [" + d.depth.toUpperCase() + "]: " + DEPTH_RULES[d.depth]).join("\n");
11648
11794
  const termLines = glossary.map((g) => "- " + g.term + (g.definition ? " \u2014 " + g.definition : "") + (g.example ? " (example: " + g.example + ")" : "")).join("\n");
11649
11795
  const langCode = resolveTargetLanguageCode(targetLanguage);
@@ -11656,6 +11802,28 @@ function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMark
11656
11802
  const hQuestions = headings["Self-Check Questions"] || (langCode === "vi" ? "C\xE2u h\u1ECFi T\u1EF1 ki\u1EC3m tra" : "Self-Check Questions");
11657
11803
  const skeleton = `## ${hScope} / ## ${hTerms} / ## ${hNarratives} / ## ${hExamples} / ## ${hMistakes} / ## ${hQuestions}`;
11658
11804
  const headingDirective = buildHeadingDirective("KNOWLEDGE_EXPOSITION", slcMarkdown, targetLanguage);
11805
+ let refPackBlock = "";
11806
+ if (refPack && refPack.trim()) {
11807
+ const refExcerpt = buildSectionAwareExcerpt(refPack, {
11808
+ priorities: [
11809
+ "Technical Overview & Architecture Blueprint",
11810
+ "Hardware Pinout & Wiring Configuration Matrix",
11811
+ "Standards Alignment"
11812
+ ],
11813
+ budget: 2500
11814
+ }).excerpt;
11815
+ if (refExcerpt) {
11816
+ refPackBlock = [
11817
+ "",
11818
+ "[GROUND TRUTH \u2014 BINDING, from REFERENCE_PACK.md]:",
11819
+ "Tool versions, APIs, and platform facts below are CANONICAL. Do NOT",
11820
+ "contradict them; do NOT substitute versions from memory. If a fact you",
11821
+ "need is not stated here, stay generic rather than inventing specifics.",
11822
+ "",
11823
+ refExcerpt
11824
+ ].join("\n");
11825
+ }
11826
+ }
11659
11827
  return [
11660
11828
  "Session: " + session.id + " \u2014 " + session.title,
11661
11829
  "Objective: " + session.prose_objective,
@@ -11668,6 +11836,7 @@ function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMark
11668
11836
  "",
11669
11837
  "Glossary terms (definitions are canonical):",
11670
11838
  termLines || "(none provided \u2014 write definitions and mark them for glossary sync)",
11839
+ refPackBlock,
11671
11840
  "",
11672
11841
  "Section skeleton to fill (replace ONLY the {{placeholders}}):",
11673
11842
  skeleton,
@@ -11676,7 +11845,7 @@ function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMark
11676
11845
  ].filter(Boolean).join("\n");
11677
11846
  }
11678
11847
  async function ensureKnowledgeExposition(options) {
11679
- const { projectId, lessonCode, plan, glossary, llmFn, storage, techStack, hardwarePlatform } = options;
11848
+ const { projectId, lessonCode, plan, glossary, llmFn, storage, techStack, hardwarePlatform, refPack } = options;
11680
11849
  const session = plan.sessions.find((s) => s.id === lessonCode);
11681
11850
  if (!session) throw new Error("Session " + lessonCode + " not found in plan " + plan.plan_id);
11682
11851
  const existing = await storage.readArtifact(projectId, EXPOSITION_REL(lessonCode));
@@ -11690,9 +11859,16 @@ async function ensureKnowledgeExposition(options) {
11690
11859
  throw new ExpositionApprovalError("Approval hash " + plan.approval.plan_hash + " does not match current plan hash " + plan.plan_hash + " \u2014 re-approve after plan changes");
11691
11860
  }
11692
11861
  const targetLanguage = options.targetLanguage || plan.translation?.target_language || "vi";
11862
+ const originalSystemPrompt = buildSystemPrompt(targetLanguage, techStack, hardwarePlatform);
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 : "");
11693
11869
  const content = (await llmFn(
11694
- buildSystemPrompt(targetLanguage, techStack, hardwarePlatform),
11695
- buildUserPrompt(session, plan, glossary, targetLanguage, options.slcMarkdown)
11870
+ originalSystemPrompt,
11871
+ originalUserPrompt
11696
11872
  )).trim();
11697
11873
  if (content.length < 200) {
11698
11874
  throw new Error("EXPOSITION too short for " + lessonCode + " (" + content.length + " chars) \u2014 refusing to save");
@@ -11723,11 +11899,30 @@ async function ensureKnowledgeExposition(options) {
11723
11899
  const parsed = JSON.parse(match[0]);
11724
11900
  return { verdict: parsed.verdict ?? "NEEDS_REVISION", score: parsed.score ?? 0, critique: parsed.critique ?? "" };
11725
11901
  };
11726
- 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) };
11727
11913
  if (verdict.verdict !== "APPROVED") {
11728
- const repairPrompt = finalContent + "\n\nJUDGE CRITIQUE (fix ALL issues, output the COMPLETE corrected document):\n" + verdict.critique;
11729
- finalContent = (await llmFn(buildSystemPrompt(), repairPrompt)).trim();
11730
- verdict = await judgeOnce(finalContent);
11914
+ const repairPrompt = [
11915
+ originalUserPrompt,
11916
+ "",
11917
+ "--- YOUR PREVIOUS DRAFT (REJECTED, fix ALL issues below) ---",
11918
+ finalContent,
11919
+ "",
11920
+ "--- CRITIQUE (fix ALL issues, output the COMPLETE corrected document) ---",
11921
+ verdict.critique
11922
+ ].join("\n");
11923
+ finalContent = (await llmFn(originalSystemPrompt, repairPrompt)).trim();
11924
+ det = runDeterministic(finalContent);
11925
+ verdict = det.ok ? await judgeOnce(finalContent) : { verdict: "NEEDS_REVISION", score: 0, critique: validatorRepairPrompt(det.issues) };
11731
11926
  }
11732
11927
  if (verdict.verdict !== "APPROVED") {
11733
11928
  throw new Error("EXPOSITION failed judge for " + lessonCode + " (verdict " + verdict.verdict + ", score " + verdict.score + "): " + verdict.critique);
@@ -11767,12 +11962,27 @@ async function ensureExpositionForLesson(storage, projectId, lessonCode, options
11767
11962
  try {
11768
11963
  const lpRaw = await storage.readSotDocument(projectId, "LEARNER_PROFILE.md");
11769
11964
  if (lpRaw) {
11770
- const hwMatch = lpRaw.match(/(?:Student Equipment|Hardware|Platform|Thiết bị)[:\s*`]+([^\n\r]+)/i);
11771
- if (hwMatch && !hw) hw = hwMatch[1].trim();
11965
+ const hwPatterns = [
11966
+ /(?:Student Equipment|Hardware|Platform|Thiết bị|Thiết bị học tập)\s*(?:\(|:|\*)*\s*([^\n\r]+?)(?:\s*\*\*|[.).]?\s*$)/i,
11967
+ /(?:máy|device|machine)\s+([^,.;\n]{4,60}(?:M\d|Intel|PC|computer|mini)[^,.;\n]{0,30})/i
11968
+ ];
11969
+ for (const p of hwPatterns) {
11970
+ const m = lpRaw.match(p);
11971
+ if (m && !hw) {
11972
+ hw = m[1].trim();
11973
+ break;
11974
+ }
11975
+ }
11772
11976
  }
11773
11977
  } catch {
11774
11978
  }
11775
11979
  }
11980
+ let refPackContent;
11981
+ try {
11982
+ refPackContent = await storage.readSotDocument(projectId, "REFERENCE_PACK.md") || void 0;
11983
+ } catch {
11984
+ refPackContent = void 0;
11985
+ }
11776
11986
  const { runCurriculumAIInference: runCurriculumAIInference2 } = await Promise.resolve().then(() => (init_streamRunner(), streamRunner_exports));
11777
11987
  const targetLang = options.targetLanguage || plan.translation?.target_language || "vi";
11778
11988
  const result = await ensureKnowledgeExposition({
@@ -11784,6 +11994,7 @@ async function ensureExpositionForLesson(storage, projectId, lessonCode, options
11784
11994
  slcMarkdown: options.slcMarkdown,
11785
11995
  techStack: tech,
11786
11996
  hardwarePlatform: hw,
11997
+ refPack: refPackContent,
11787
11998
  llmFn: async (systemPrompt, userPrompt) => {
11788
11999
  const runnerOpts = options.customInference ? { customInference: options.customInference } : {};
11789
12000
  const out = await runCurriculumAIInference2(
@@ -26082,11 +26293,16 @@ async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
26082
26293
  }).excerpt : "";
26083
26294
  let effectiveRefPack = refPack ? buildSectionAwareExcerpt(refPack, {
26084
26295
  priorities: [
26296
+ // Toolchain/version matrix FIRST — the most-frequently-hallucinated
26297
+ // facts are tool versions ("Xcode 15" vs ground truth "Xcode 16").
26298
+ // The bilingual fuzzy matcher in contextBuilder resolves these keys
26299
+ // even when the generated RefPack headings are Vietnamese.
26085
26300
  "Technical Overview & Architecture Blueprint",
26086
26301
  "Hardware Pinout & Wiring Configuration Matrix",
26302
+ "Standards Alignment",
26087
26303
  "Core Pedagogical Concept Anchor & Real-World Domain Bridge"
26088
26304
  ],
26089
- budget: 2500
26305
+ budget: 3500
26090
26306
  }).excerpt : "";
26091
26307
  const baseContextPrefix = `PROJECT & ACADEMIC CONTEXT:
26092
26308
  - Project ID: ${projectId}
@@ -26135,6 +26351,10 @@ ${expositionContext}`);
26135
26351
  parts.push(`### REFERENCE PACK GROUND TRUTH
26136
26352
  ${effectiveRefPack}`);
26137
26353
  }
26354
+ const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26355
+ if (productSpec) {
26356
+ parts.push(productSpec);
26357
+ }
26138
26358
  return parts.length > 0 ? `
26139
26359
 
26140
26360
  [GROUND TRUTH]:
@@ -26293,6 +26513,23 @@ ${yamlBlock.trim()}
26293
26513
  `);
26294
26514
  };
26295
26515
  var injectYamlReviewMetadata = injectYamlReviewMetadata2;
26516
+ const lessonDet = validateArtifactDeterministic({ content: lessonContent, refPack });
26517
+ if (!lessonDet.ok) {
26518
+ const detCritique = validatorRepairPrompt(lessonDet.issues);
26519
+ onProgress?.("@reviewer", `\u26D4 LESSON deterministic validator FAIL: ${detCritique.slice(0, 200)}`);
26520
+ await storage.updateArtifactState(projectId, lessonCode, "LESSON", {
26521
+ state: "rejected",
26522
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26523
+ contentHash: computeContentHash(lessonContent),
26524
+ review: {
26525
+ decision: "NEEDS_REVISION",
26526
+ reviewedBy: "@heuristic-linter",
26527
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
26528
+ score: 0,
26529
+ critique: detCritique
26530
+ }
26531
+ });
26532
+ }
26296
26533
  const judgeObjectives = parseLessonObjectives(lessonContent, concept, bloomLevel);
26297
26534
  const judgeExpositionExcerpt = expositionContext ? buildExpositionExcerpt(expositionContext, { budget: 3e3, sectionLanguageContract: slcMarkdown, artifactType: "KNOWLEDGE_EXPOSITION" }).excerpt : void 0;
26298
26535
  const judgeResult = await LLMJudgeEngine.evaluateArtifactWithLLM({
@@ -26548,6 +26785,23 @@ ${currentContent}` }],
26548
26785
  ${lessonExcerpt}${symbolLedgerBlock}`;
26549
26786
  const judgeSat = (sat, content) => {
26550
26787
  if (gateModeFor(gates, sat) !== "LLM_JUDGE") return Promise.resolve();
26788
+ const det = validateArtifactDeterministic({ content, refPack });
26789
+ if (!det.ok) {
26790
+ const critique = validatorRepairPrompt(det.issues);
26791
+ onProgress?.("@reviewer", `\u26D4 ${sat} deterministic validator FAIL: ${critique.slice(0, 200)}`);
26792
+ return storage.updateArtifactState(projectId, lessonCode, sat, {
26793
+ state: "rejected",
26794
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26795
+ contentHash: computeContentHash(content),
26796
+ review: {
26797
+ decision: "NEEDS_REVISION",
26798
+ reviewedBy: "@heuristic-linter",
26799
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
26800
+ score: 0,
26801
+ critique
26802
+ }
26803
+ });
26804
+ }
26551
26805
  return judgeSatelliteArtifact({
26552
26806
  storage,
26553
26807
  projectId,