@thanh01.pmt/curriculum-kit 1.4.22 → 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
@@ -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);
@@ -26204,6 +26351,10 @@ ${expositionContext}`);
26204
26351
  parts.push(`### REFERENCE PACK GROUND TRUTH
26205
26352
  ${effectiveRefPack}`);
26206
26353
  }
26354
+ const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26355
+ if (productSpec) {
26356
+ parts.push(productSpec);
26357
+ }
26207
26358
  return parts.length > 0 ? `
26208
26359
 
26209
26360
  [GROUND TRUTH]:
@@ -26362,6 +26513,23 @@ ${yamlBlock.trim()}
26362
26513
  `);
26363
26514
  };
26364
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
+ }
26365
26533
  const judgeObjectives = parseLessonObjectives(lessonContent, concept, bloomLevel);
26366
26534
  const judgeExpositionExcerpt = expositionContext ? buildExpositionExcerpt(expositionContext, { budget: 3e3, sectionLanguageContract: slcMarkdown, artifactType: "KNOWLEDGE_EXPOSITION" }).excerpt : void 0;
26367
26535
  const judgeResult = await LLMJudgeEngine.evaluateArtifactWithLLM({
@@ -26617,6 +26785,23 @@ ${currentContent}` }],
26617
26785
  ${lessonExcerpt}${symbolLedgerBlock}`;
26618
26786
  const judgeSat = (sat, content) => {
26619
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
+ }
26620
26805
  return judgeSatelliteArtifact({
26621
26806
  storage,
26622
26807
  projectId,