@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.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);
@@ -26216,6 +26363,10 @@ ${expositionContext}`);
26216
26363
  parts.push(`### REFERENCE PACK GROUND TRUTH
26217
26364
  ${effectiveRefPack}`);
26218
26365
  }
26366
+ const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26367
+ if (productSpec) {
26368
+ parts.push(productSpec);
26369
+ }
26219
26370
  return parts.length > 0 ? `
26220
26371
 
26221
26372
  [GROUND TRUTH]:
@@ -26374,6 +26525,23 @@ ${yamlBlock.trim()}
26374
26525
  `);
26375
26526
  };
26376
26527
  var injectYamlReviewMetadata = injectYamlReviewMetadata2;
26528
+ const lessonDet = validateArtifactDeterministic({ content: lessonContent, refPack });
26529
+ if (!lessonDet.ok) {
26530
+ const detCritique = validatorRepairPrompt(lessonDet.issues);
26531
+ onProgress?.("@reviewer", `\u26D4 LESSON deterministic validator FAIL: ${detCritique.slice(0, 200)}`);
26532
+ await storage.updateArtifactState(projectId, lessonCode, "LESSON", {
26533
+ state: "rejected",
26534
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26535
+ contentHash: computeContentHash(lessonContent),
26536
+ review: {
26537
+ decision: "NEEDS_REVISION",
26538
+ reviewedBy: "@heuristic-linter",
26539
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
26540
+ score: 0,
26541
+ critique: detCritique
26542
+ }
26543
+ });
26544
+ }
26377
26545
  const judgeObjectives = parseLessonObjectives(lessonContent, concept, bloomLevel);
26378
26546
  const judgeExpositionExcerpt = expositionContext ? buildExpositionExcerpt(expositionContext, { budget: 3e3, sectionLanguageContract: slcMarkdown, artifactType: "KNOWLEDGE_EXPOSITION" }).excerpt : void 0;
26379
26547
  const judgeResult = await LLMJudgeEngine.evaluateArtifactWithLLM({
@@ -26629,6 +26797,23 @@ ${currentContent}` }],
26629
26797
  ${lessonExcerpt}${symbolLedgerBlock}`;
26630
26798
  const judgeSat = (sat, content) => {
26631
26799
  if (gateModeFor(gates, sat) !== "LLM_JUDGE") return Promise.resolve();
26800
+ const det = validateArtifactDeterministic({ content, refPack });
26801
+ if (!det.ok) {
26802
+ const critique = validatorRepairPrompt(det.issues);
26803
+ onProgress?.("@reviewer", `\u26D4 ${sat} deterministic validator FAIL: ${critique.slice(0, 200)}`);
26804
+ return storage.updateArtifactState(projectId, lessonCode, sat, {
26805
+ state: "rejected",
26806
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26807
+ contentHash: computeContentHash(content),
26808
+ review: {
26809
+ decision: "NEEDS_REVISION",
26810
+ reviewedBy: "@heuristic-linter",
26811
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
26812
+ score: 0,
26813
+ critique
26814
+ }
26815
+ });
26816
+ }
26632
26817
  return judgeSatelliteArtifact({
26633
26818
  storage,
26634
26819
  projectId,