@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.cjs CHANGED
@@ -11519,6 +11519,21 @@ function normalizeHeading(str) {
11519
11519
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
11520
11520
  }
11521
11521
  function findCanonicalFuzzy(norm) {
11522
+ 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")) {
11523
+ return "Technical Overview & Architecture Blueprint";
11524
+ }
11525
+ if (norm.includes("pinout") || norm.includes("phan cung") || norm.includes("hardware") || norm.includes("wiring") || norm.includes("ket noi")) {
11526
+ return "Hardware Pinout & Wiring Configuration Matrix";
11527
+ }
11528
+ if (norm.includes("pedagog") || norm.includes("phuong phap") || norm.includes("day hoc") || norm.includes("su pham")) {
11529
+ return "Core Pedagogical Concept Anchor & Real-World Domain Bridge";
11530
+ }
11531
+ if (norm.includes("standard") || norm.includes("tieu chuan") || norm.includes("csta") || norm.includes("cs2023") || norm.includes("chuan academic")) {
11532
+ return "Standards Alignment";
11533
+ }
11534
+ if (norm.includes("roadmap") || norm.includes("lo trinh") || norm.includes("milestone") || norm.includes("giai doan")) {
11535
+ return void 0;
11536
+ }
11522
11537
  if (norm.includes("symbol") || norm.includes("identifier ledger") || norm.includes("dinh danh")) {
11523
11538
  return "Symbol & Identifier Ledger";
11524
11539
  }
@@ -11609,6 +11624,137 @@ function extractSymbolLedger(lessonMarkdown) {
11609
11624
  return result;
11610
11625
  }
11611
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
+
11612
11758
  // src/services/knowledgeExpositionService.ts
11613
11759
  var EXPOSITION_REL = (lessonCode) => "_sot/expositions/" + lessonCode + ".md";
11614
11760
  var ExpositionApprovalError = class extends Error {
@@ -11655,7 +11801,7 @@ function buildSystemPrompt(targetLanguage = "vi", techStack, hardwarePlatform) {
11655
11801
  langDirective
11656
11802
  ].join("\n");
11657
11803
  }
11658
- function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMarkdown) {
11804
+ function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMarkdown, refPack) {
11659
11805
  const depthLines = session.depth_assignments.map((d) => "- " + d.node_id + " [" + d.depth.toUpperCase() + "]: " + DEPTH_RULES[d.depth]).join("\n");
11660
11806
  const termLines = glossary.map((g) => "- " + g.term + (g.definition ? " \u2014 " + g.definition : "") + (g.example ? " (example: " + g.example + ")" : "")).join("\n");
11661
11807
  const langCode = resolveTargetLanguageCode(targetLanguage);
@@ -11668,6 +11814,28 @@ function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMark
11668
11814
  const hQuestions = headings["Self-Check Questions"] || (langCode === "vi" ? "C\xE2u h\u1ECFi T\u1EF1 ki\u1EC3m tra" : "Self-Check Questions");
11669
11815
  const skeleton = `## ${hScope} / ## ${hTerms} / ## ${hNarratives} / ## ${hExamples} / ## ${hMistakes} / ## ${hQuestions}`;
11670
11816
  const headingDirective = buildHeadingDirective("KNOWLEDGE_EXPOSITION", slcMarkdown, targetLanguage);
11817
+ let refPackBlock = "";
11818
+ if (refPack && refPack.trim()) {
11819
+ const refExcerpt = buildSectionAwareExcerpt(refPack, {
11820
+ priorities: [
11821
+ "Technical Overview & Architecture Blueprint",
11822
+ "Hardware Pinout & Wiring Configuration Matrix",
11823
+ "Standards Alignment"
11824
+ ],
11825
+ budget: 2500
11826
+ }).excerpt;
11827
+ if (refExcerpt) {
11828
+ refPackBlock = [
11829
+ "",
11830
+ "[GROUND TRUTH \u2014 BINDING, from REFERENCE_PACK.md]:",
11831
+ "Tool versions, APIs, and platform facts below are CANONICAL. Do NOT",
11832
+ "contradict them; do NOT substitute versions from memory. If a fact you",
11833
+ "need is not stated here, stay generic rather than inventing specifics.",
11834
+ "",
11835
+ refExcerpt
11836
+ ].join("\n");
11837
+ }
11838
+ }
11671
11839
  return [
11672
11840
  "Session: " + session.id + " \u2014 " + session.title,
11673
11841
  "Objective: " + session.prose_objective,
@@ -11680,6 +11848,7 @@ function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMark
11680
11848
  "",
11681
11849
  "Glossary terms (definitions are canonical):",
11682
11850
  termLines || "(none provided \u2014 write definitions and mark them for glossary sync)",
11851
+ refPackBlock,
11683
11852
  "",
11684
11853
  "Section skeleton to fill (replace ONLY the {{placeholders}}):",
11685
11854
  skeleton,
@@ -11688,7 +11857,7 @@ function buildUserPrompt(session, plan, glossary, targetLanguage = "vi", slcMark
11688
11857
  ].filter(Boolean).join("\n");
11689
11858
  }
11690
11859
  async function ensureKnowledgeExposition(options) {
11691
- const { projectId, lessonCode, plan, glossary, llmFn, storage, techStack, hardwarePlatform } = options;
11860
+ const { projectId, lessonCode, plan, glossary, llmFn, storage, techStack, hardwarePlatform, refPack } = options;
11692
11861
  const session = plan.sessions.find((s) => s.id === lessonCode);
11693
11862
  if (!session) throw new Error("Session " + lessonCode + " not found in plan " + plan.plan_id);
11694
11863
  const existing = await storage.readArtifact(projectId, EXPOSITION_REL(lessonCode));
@@ -11702,9 +11871,16 @@ async function ensureKnowledgeExposition(options) {
11702
11871
  throw new ExpositionApprovalError("Approval hash " + plan.approval.plan_hash + " does not match current plan hash " + plan.plan_hash + " \u2014 re-approve after plan changes");
11703
11872
  }
11704
11873
  const targetLanguage = options.targetLanguage || plan.translation?.target_language || "vi";
11874
+ const originalSystemPrompt = buildSystemPrompt(targetLanguage, techStack, hardwarePlatform);
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 : "");
11705
11881
  const content = (await llmFn(
11706
- buildSystemPrompt(targetLanguage, techStack, hardwarePlatform),
11707
- buildUserPrompt(session, plan, glossary, targetLanguage, options.slcMarkdown)
11882
+ originalSystemPrompt,
11883
+ originalUserPrompt
11708
11884
  )).trim();
11709
11885
  if (content.length < 200) {
11710
11886
  throw new Error("EXPOSITION too short for " + lessonCode + " (" + content.length + " chars) \u2014 refusing to save");
@@ -11735,11 +11911,30 @@ async function ensureKnowledgeExposition(options) {
11735
11911
  const parsed = JSON.parse(match[0]);
11736
11912
  return { verdict: parsed.verdict ?? "NEEDS_REVISION", score: parsed.score ?? 0, critique: parsed.critique ?? "" };
11737
11913
  };
11738
- 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) };
11739
11925
  if (verdict.verdict !== "APPROVED") {
11740
- const repairPrompt = finalContent + "\n\nJUDGE CRITIQUE (fix ALL issues, output the COMPLETE corrected document):\n" + verdict.critique;
11741
- finalContent = (await llmFn(buildSystemPrompt(), repairPrompt)).trim();
11742
- verdict = await judgeOnce(finalContent);
11926
+ const repairPrompt = [
11927
+ originalUserPrompt,
11928
+ "",
11929
+ "--- YOUR PREVIOUS DRAFT (REJECTED, fix ALL issues below) ---",
11930
+ finalContent,
11931
+ "",
11932
+ "--- CRITIQUE (fix ALL issues, output the COMPLETE corrected document) ---",
11933
+ verdict.critique
11934
+ ].join("\n");
11935
+ finalContent = (await llmFn(originalSystemPrompt, repairPrompt)).trim();
11936
+ det = runDeterministic(finalContent);
11937
+ verdict = det.ok ? await judgeOnce(finalContent) : { verdict: "NEEDS_REVISION", score: 0, critique: validatorRepairPrompt(det.issues) };
11743
11938
  }
11744
11939
  if (verdict.verdict !== "APPROVED") {
11745
11940
  throw new Error("EXPOSITION failed judge for " + lessonCode + " (verdict " + verdict.verdict + ", score " + verdict.score + "): " + verdict.critique);
@@ -11779,12 +11974,27 @@ async function ensureExpositionForLesson(storage, projectId, lessonCode, options
11779
11974
  try {
11780
11975
  const lpRaw = await storage.readSotDocument(projectId, "LEARNER_PROFILE.md");
11781
11976
  if (lpRaw) {
11782
- const hwMatch = lpRaw.match(/(?:Student Equipment|Hardware|Platform|Thiết bị)[:\s*`]+([^\n\r]+)/i);
11783
- if (hwMatch && !hw) hw = hwMatch[1].trim();
11977
+ const hwPatterns = [
11978
+ /(?:Student Equipment|Hardware|Platform|Thiết bị|Thiết bị học tập)\s*(?:\(|:|\*)*\s*([^\n\r]+?)(?:\s*\*\*|[.).]?\s*$)/i,
11979
+ /(?:máy|device|machine)\s+([^,.;\n]{4,60}(?:M\d|Intel|PC|computer|mini)[^,.;\n]{0,30})/i
11980
+ ];
11981
+ for (const p of hwPatterns) {
11982
+ const m = lpRaw.match(p);
11983
+ if (m && !hw) {
11984
+ hw = m[1].trim();
11985
+ break;
11986
+ }
11987
+ }
11784
11988
  }
11785
11989
  } catch {
11786
11990
  }
11787
11991
  }
11992
+ let refPackContent;
11993
+ try {
11994
+ refPackContent = await storage.readSotDocument(projectId, "REFERENCE_PACK.md") || void 0;
11995
+ } catch {
11996
+ refPackContent = void 0;
11997
+ }
11788
11998
  const { runCurriculumAIInference: runCurriculumAIInference2 } = await Promise.resolve().then(() => (init_streamRunner(), streamRunner_exports));
11789
11999
  const targetLang = options.targetLanguage || plan.translation?.target_language || "vi";
11790
12000
  const result = await ensureKnowledgeExposition({
@@ -11796,6 +12006,7 @@ async function ensureExpositionForLesson(storage, projectId, lessonCode, options
11796
12006
  slcMarkdown: options.slcMarkdown,
11797
12007
  techStack: tech,
11798
12008
  hardwarePlatform: hw,
12009
+ refPack: refPackContent,
11799
12010
  llmFn: async (systemPrompt, userPrompt) => {
11800
12011
  const runnerOpts = options.customInference ? { customInference: options.customInference } : {};
11801
12012
  const out = await runCurriculumAIInference2(
@@ -26094,11 +26305,16 @@ async function produceSingleLesson(storage, projectId, lessonId, options = {}) {
26094
26305
  }).excerpt : "";
26095
26306
  let effectiveRefPack = refPack ? buildSectionAwareExcerpt(refPack, {
26096
26307
  priorities: [
26308
+ // Toolchain/version matrix FIRST — the most-frequently-hallucinated
26309
+ // facts are tool versions ("Xcode 15" vs ground truth "Xcode 16").
26310
+ // The bilingual fuzzy matcher in contextBuilder resolves these keys
26311
+ // even when the generated RefPack headings are Vietnamese.
26097
26312
  "Technical Overview & Architecture Blueprint",
26098
26313
  "Hardware Pinout & Wiring Configuration Matrix",
26314
+ "Standards Alignment",
26099
26315
  "Core Pedagogical Concept Anchor & Real-World Domain Bridge"
26100
26316
  ],
26101
- budget: 2500
26317
+ budget: 3500
26102
26318
  }).excerpt : "";
26103
26319
  const baseContextPrefix = `PROJECT & ACADEMIC CONTEXT:
26104
26320
  - Project ID: ${projectId}
@@ -26147,6 +26363,10 @@ ${expositionContext}`);
26147
26363
  parts.push(`### REFERENCE PACK GROUND TRUTH
26148
26364
  ${effectiveRefPack}`);
26149
26365
  }
26366
+ const productSpec = buildProductSpecBlock({ refPack, briefing: options.briefing });
26367
+ if (productSpec) {
26368
+ parts.push(productSpec);
26369
+ }
26150
26370
  return parts.length > 0 ? `
26151
26371
 
26152
26372
  [GROUND TRUTH]:
@@ -26305,6 +26525,23 @@ ${yamlBlock.trim()}
26305
26525
  `);
26306
26526
  };
26307
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
+ }
26308
26545
  const judgeObjectives = parseLessonObjectives(lessonContent, concept, bloomLevel);
26309
26546
  const judgeExpositionExcerpt = expositionContext ? buildExpositionExcerpt(expositionContext, { budget: 3e3, sectionLanguageContract: slcMarkdown, artifactType: "KNOWLEDGE_EXPOSITION" }).excerpt : void 0;
26310
26547
  const judgeResult = await LLMJudgeEngine.evaluateArtifactWithLLM({
@@ -26560,6 +26797,23 @@ ${currentContent}` }],
26560
26797
  ${lessonExcerpt}${symbolLedgerBlock}`;
26561
26798
  const judgeSat = (sat, content) => {
26562
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
+ }
26563
26817
  return judgeSatelliteArtifact({
26564
26818
  storage,
26565
26819
  projectId,