@thanh01.pmt/curriculum-kit 1.0.6 → 1.0.7

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.
@@ -7,7 +7,7 @@ var google = require('@ai-sdk/google');
7
7
  var openai = require('@ai-sdk/openai');
8
8
  var deepseek = require('@ai-sdk/deepseek');
9
9
  var zod = require('zod');
10
- var workflowPkg = require('workflow');
10
+ var workflow = require('workflow');
11
11
  var fs2 = require('fs/promises');
12
12
  var rest = require('@octokit/rest');
13
13
  var supabaseJs = require('@supabase/supabase-js');
@@ -17,27 +17,8 @@ require('crypto');
17
17
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
18
18
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
19
19
 
20
- function _interopNamespace(e) {
21
- if (e && e.__esModule) return e;
22
- var n = Object.create(null);
23
- if (e) {
24
- Object.keys(e).forEach(function (k) {
25
- if (k !== 'default') {
26
- var d = Object.getOwnPropertyDescriptor(e, k);
27
- Object.defineProperty(n, k, d.get ? d : {
28
- enumerable: true,
29
- get: function () { return e[k]; }
30
- });
31
- }
32
- });
33
- }
34
- n.default = e;
35
- return Object.freeze(n);
36
- }
37
-
38
20
  var fs__default = /*#__PURE__*/_interopDefault(fs);
39
21
  var path4__default = /*#__PURE__*/_interopDefault(path4);
40
- var workflowPkg__namespace = /*#__PURE__*/_interopNamespace(workflowPkg);
41
22
  var fs2__default = /*#__PURE__*/_interopDefault(fs2);
42
23
 
43
24
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -3885,7 +3866,7 @@ Respond with a valid JSON object matching the schema.`
3885
3866
  async function withRateLimitBackoff(options) {
3886
3867
  let attempt = 1;
3887
3868
  try {
3888
- const meta = workflowPkg.getStepMetadata();
3869
+ const meta = workflow.getStepMetadata();
3889
3870
  if (meta && typeof meta.attempt === "number") {
3890
3871
  attempt = meta.attempt;
3891
3872
  }
@@ -3901,7 +3882,7 @@ async function withRateLimitBackoff(options) {
3901
3882
  if (isRateLimited || isTransientError) {
3902
3883
  const retryAfterSeconds = Math.min(120, Math.pow(2, attempt) * 2);
3903
3884
  const reason = isRateLimited ? "Rate limit exceeded (429)" : `Transient server error (${status || "network"})`;
3904
- throw new workflowPkg.RetryableError(`[${options.stepName}] ${reason}. Retrying attempt ${attempt + 1}...`, {
3885
+ throw new workflow.RetryableError(`[${options.stepName}] ${reason}. Retrying attempt ${attempt + 1}...`, {
3905
3886
  retryAfter: `${retryAfterSeconds}s`
3906
3887
  });
3907
3888
  }
@@ -4975,14 +4956,14 @@ async function publishToSupabaseStep(options) {
4975
4956
 
4976
4957
  // src/workflow/gateSettings.ts
4977
4958
  var DEFAULT_GATE_SETTINGS = Object.freeze({
4978
- LEARNER_PROFILE: "HITL",
4979
- PROJECT_BRIEF: "HITL",
4980
- REFERENCE_PACK: "HITL",
4981
- CURRICULUM_FRAMEWORK: "HITL",
4982
- KNOWLEDGE_EXPOSITION: "LLM_JUDGE",
4983
- STYLE_GUIDE: "HITL",
4984
- ART_DIRECTION: "HITL",
4985
- LESSON: "LLM_JUDGE",
4959
+ LEARNER_PROFILE: "OFF",
4960
+ PROJECT_BRIEF: "OFF",
4961
+ REFERENCE_PACK: "OFF",
4962
+ CURRICULUM_FRAMEWORK: "OFF",
4963
+ KNOWLEDGE_EXPOSITION: "OFF",
4964
+ STYLE_GUIDE: "OFF",
4965
+ ART_DIRECTION: "OFF",
4966
+ LESSON: "OFF",
4986
4967
  ACT: "OFF",
4987
4968
  QUIZ: "OFF",
4988
4969
  SLIDE: "OFF",
@@ -4990,7 +4971,7 @@ var DEFAULT_GATE_SETTINGS = Object.freeze({
4990
4971
  WKS: "OFF",
4991
4972
  HANDOUT: "OFF",
4992
4973
  EXT: "OFF",
4993
- CODE: "LLM_JUDGE",
4974
+ CODE: "OFF",
4994
4975
  MEDIA_SCRIPT: "OFF",
4995
4976
  EXIT_TICKET: "OFF"
4996
4977
  });
@@ -5051,10 +5032,10 @@ var approvalPayloadSchema = zod.z.object({
5051
5032
  feedback: zod.z.string().optional().describe("Actionable feedback or required changes if rejected"),
5052
5033
  timestamp: zod.z.string().optional().describe("ISO timestamp of the approval action")
5053
5034
  });
5054
- var hookFactory = workflowPkg__namespace.defineHook || workflowPkg__namespace.createHook || ((opts) => opts);
5055
- var lessonApprovalHook = hookFactory({
5056
- schema: approvalPayloadSchema
5057
- });
5035
+ var lessonApprovalHook = workflow.defineHook();
5036
+ function approvalHookToken(projectId, lessonId, artifactType) {
5037
+ return `approval:${projectId}:${lessonId}:${artifactType}`;
5038
+ }
5058
5039
  var BloomHintSchema = zod.z.object({
5059
5040
  process: zod.z.enum(["Remember", "Understand", "Apply", "Analyze", "Evaluate", "Create"]).describe("Cognitive process axis"),
5060
5041
  knowledge: zod.z.enum(["FACTUAL", "CONCEPTUAL", "PROCEDURAL", "METACOGNITIVE"]).optional().describe("Knowledge dimension axis (optional \u2014 most source standards do not tag it)")
@@ -5657,9 +5638,9 @@ async function generateMilestoneWorkflow(input) {
5657
5638
  } else if (lessonGate === "HITL") {
5658
5639
  const hookToken = "gate:lesson:" + milestoneId;
5659
5640
  console.log(" \u23F8 [Gate] LESSON awaiting HUMAN review (HITL) \u2014 hook " + hookToken);
5660
- const workflowPkg2 = await import('workflow');
5661
- const createHook2 = workflowPkg2.createHook;
5662
- const hook = createHook2({ token: hookToken });
5641
+ const workflowPkg = await import('workflow');
5642
+ const createHook = workflowPkg.createHook;
5643
+ const hook = createHook({ token: hookToken });
5663
5644
  const review = await hook;
5664
5645
  console.log(" \u2705 [Gate] Human verdict: " + (review.approved ? "APPROVED" : "REJECTED") + (review.feedback ? " \u2014 " + review.feedback : ""));
5665
5646
  if (!review.approved) {
@@ -5679,7 +5660,7 @@ async function generateMilestoneWorkflow(input) {
5679
5660
  standardsContext: standardsContext || void 0,
5680
5661
  modelOptions: input.modelOptions
5681
5662
  });
5682
- const hook2 = createHook2({ token: hookToken + ":v2" });
5663
+ const hook2 = createHook({ token: hookToken + ":v2" });
5683
5664
  const review2 = await hook2;
5684
5665
  console.log(" \u2705 [Gate] Human verdict (v2): " + (review2.approved ? "APPROVED" : "REJECTED"));
5685
5666
  if (!review2.approved) {
@@ -5867,7 +5848,7 @@ async function generateRoadmapWorkflow(input) {
5867
5848
  }
5868
5849
  }
5869
5850
  if (i + batchSize < milestones.length) {
5870
- await workflowPkg.sleep("2s");
5851
+ await workflow.sleep("2s");
5871
5852
  }
5872
5853
  }
5873
5854
  const successfulMilestones = milestoneOutcomes.filter((m) => m.status === "FULFILLED").length;
@@ -5915,6 +5896,271 @@ init_streamRunner();
5915
5896
 
5916
5897
  // src/services/lessonProductionService.ts
5917
5898
  init_streamRunner();
5899
+
5900
+ // src/ai/llmJudgeEngine.ts
5901
+ var LLMJudgeEngine = class {
5902
+ /**
5903
+ * Executes genuine LLM-as-Judge evaluation using the audit flow.
5904
+ */
5905
+ static async evaluateArtifactWithLLM(input) {
5906
+ try {
5907
+ const report = await auditCurriculumQualityFlow({
5908
+ targetArtifactType: input.targetArtifactType,
5909
+ lessonId: input.lessonId,
5910
+ expectedLanguage: input.expectedLanguage,
5911
+ targetObjectives: input.targetObjectives,
5912
+ generatedContentJson: input.generatedContentJson,
5913
+ standardStatements: input.standardStatements,
5914
+ modelOptions: input.modelOptions
5915
+ });
5916
+ const passed = report.overallVerdict === "PASS" && report.languageAdherencePassed;
5917
+ const failedCriteria = report.criteria.filter((c) => !c.passed);
5918
+ const passedCriteria = report.criteria.filter((c) => c.passed);
5919
+ return {
5920
+ decision: report.overallVerdict === "PASS" ? "APPROVED" : report.overallVerdict === "NEEDS_REVISION" ? "NEEDS_REVISION" : "REJECTED",
5921
+ score: report.totalScore,
5922
+ passed,
5923
+ evaluatedBy: "@agent-as-judge",
5924
+ critique: passed ? `H\u1ECDc li\u1EC7u ${input.targetArtifactType} cho b\xE0i "${input.lessonId}" \u0111\u1EA1t \u0111i\u1EC3m th\u1EA9m \u0111\u1ECBnh ${report.totalScore}/100. \u0110\u1EE7 \u0111i\u1EC1u ki\u1EC7n ph\xEA duy\u1EC7t.` : `H\u1ECDc li\u1EC7u ${input.targetArtifactType} ch\u01B0a \u0111\u1EA1t y\xEAu c\u1EA7u: ${failedCriteria.map((c) => c.feedback).join("; ")}`,
5925
+ strengths: passedCriteria.map((c) => `${c.name}: ${c.feedback}`),
5926
+ weaknesses: failedCriteria.map((c) => `${c.name}: ${c.feedback}`),
5927
+ autoRepairInstructions: report.actionableRepairPrompts.length > 0 ? report.actionableRepairPrompts.join("\n") : void 0
5928
+ };
5929
+ } catch (err) {
5930
+ return {
5931
+ decision: "NEEDS_REVISION",
5932
+ score: 50,
5933
+ passed: false,
5934
+ evaluatedBy: "@agent-as-judge",
5935
+ critique: `L\u1ED7i trong qu\xE1 tr\xECnh th\u1EA9m \u0111\u1ECBnh LLM: ${err.message || String(err)}`,
5936
+ strengths: [],
5937
+ weaknesses: ["Kh\xF4ng th\u1EC3 ho\xE0n t\u1EA5t th\u1EA9m \u0111\u1ECBnh t\u1EF1 \u0111\u1ED9ng do s\u1EF1 c\u1ED1 k\u1EBFt n\u1ED1i m\xF4 h\xECnh."],
5938
+ autoRepairInstructions: "Ch\u1EA1y l\u1EA1i quy tr\xECnh th\u1EA9m \u0111\u1ECBnh ho\u1EB7c ki\u1EC3m tra c\u1EA5u h\xECnh API keys."
5939
+ };
5940
+ }
5941
+ }
5942
+ /**
5943
+ * Evaluates Gate 1 (SOT Foundation: LEARNER_PROFILE, PROJECT_BRIEF, REFERENCE_PACK)
5944
+ * Calibrated heuristic pre-check.
5945
+ */
5946
+ static evaluateGate1(learnerProfileMarkdown, referencePackMarkdown) {
5947
+ if (!learnerProfileMarkdown?.trim() || !referencePackMarkdown?.trim()) {
5948
+ return {
5949
+ decision: "REJECTED",
5950
+ score: 0,
5951
+ passed: false,
5952
+ evaluatedBy: "@heuristic-linter",
5953
+ critique: "T\u1EC7p SOT r\u1ED7ng ho\u1EB7c kh\xF4ng t\u1ED3n t\u1EA1i.",
5954
+ strengths: [],
5955
+ weaknesses: ["Thi\u1EBFu n\u1ED9i dung t\u1EC7p SOT c\u01A1 s\u1EDF."],
5956
+ autoRepairInstructions: "Kh\u1EDFi t\u1EA1o v\xE0 bi\xEAn so\u1EA1n \u0111\u1EA7y \u0111\u1EE7 LEARNER_PROFILE.md v\xE0 REFERENCE_PACK.md."
5957
+ };
5958
+ }
5959
+ const hasAgeGroup = /(?:độ tuổi|lứa tuổi|age|grade|thcs|tiểu học|k-12|sinh viên|người đi làm)/i.test(learnerProfileMarkdown);
5960
+ const hasPriorKnowledge = /(?:kiến thức nền|nền tảng|prior knowledge|tiên quyết|prerequisite)/i.test(learnerProfileMarkdown);
5961
+ const hasGroundTruth = /(?:reference|csta|apple|ieee|cs2023|nguồn tham khảo|tài liệu chuẩn|rfc|w3c)/i.test(referencePackMarkdown);
5962
+ const hasClientObjectives = /(?:client objectives|mục tiêu|output|chuẩn đầu ra)/i.test(learnerProfileMarkdown);
5963
+ let score = 30;
5964
+ const strengths = [];
5965
+ const weaknesses = [];
5966
+ if (hasAgeGroup) {
5967
+ score += 20;
5968
+ strengths.push("H\u1ED3 s\u01A1 ng\u01B0\u1EDDi h\u1ECDc x\xE1c \u0111\u1ECBnh r\xF5 \u0111\u1ED1i t\u01B0\u1EE3ng v\xE0 l\u1EE9a tu\u1ED5i m\u1EE5c ti\xEAu.");
5969
+ } else {
5970
+ weaknesses.push("Thi\u1EBFu \u0111\u1ECBnh ngh\u0129a r\xF5 r\xE0ng v\u1EC1 \u0111\u1ED1i t\u01B0\u1EE3ng/\u0111\u1ED9 tu\u1ED5i h\u1ECDc vi\xEAn.");
5971
+ }
5972
+ if (hasPriorKnowledge) {
5973
+ score += 20;
5974
+ strengths.push("C\xF3 ph\xE2n t\xEDch ki\u1EBFn th\u1EE9c n\u1EC1n t\u1EA3ng v\xE0 r\xE0o c\u1EA3n nh\u1EADn th\u1EE9c.");
5975
+ } else {
5976
+ weaknesses.push("Ch\u01B0a l\xE0m r\xF5 ki\u1EBFn th\u1EE9c ti\xEAn quy\u1EBFt (Prerequisites).");
5977
+ }
5978
+ if (hasGroundTruth) {
5979
+ score += 20;
5980
+ strengths.push("REFERENCE_PACK tr\xEDch d\u1EABn t\xE0i li\u1EC7u k\u1EF9 thu\u1EADt chu\u1EA9n m\u1EF1c.");
5981
+ } else {
5982
+ weaknesses.push("T\u1EC7p REFERENCE_PACK thi\u1EBFu danh m\u1EE5c t\xE0i li\u1EC7u tham chi\u1EBFu chu\u1EA9n.");
5983
+ }
5984
+ if (hasClientObjectives) {
5985
+ score += 10;
5986
+ strengths.push("C\xF3 x\xE1c \u0111\u1ECBnh r\xF5 m\u1EE5c ti\xEAu \u0111\u1EA7u ra v\xE0 k\u1EF3 v\u1ECDng kh\xE1ch h\xE0ng.");
5987
+ }
5988
+ const passed = score >= 80;
5989
+ return {
5990
+ decision: passed ? "APPROVED" : score >= 60 ? "NEEDS_REVISION" : "REJECTED",
5991
+ score,
5992
+ passed,
5993
+ evaluatedBy: "@heuristic-linter",
5994
+ critique: passed ? "B\u1ED9 t\xE0i li\u1EC7u n\u1EC1n t\u1EA3ng SOT \u0111\u1EA1t chu\u1EA9n c\u1EA5u tr\xFAc. Cho ph\xE9p chuy\u1EC3n sang Phase 2 (Thi\u1EBFt k\u1EBF Khung ch\u01B0\u01A1ng tr\xECnh)." : "T\xE0i li\u1EC7u SOT c\u1EA7n b\u1ED5 sung th\xEAm c\xE1c tr\u01B0\u1EDDng th\xF4ng tin chu\u1EA9n tr\u01B0\u1EDBc khi ph\xE2n r\xE3 b\xE0i h\u1ECDc.",
5995
+ strengths,
5996
+ weaknesses,
5997
+ autoRepairInstructions: passed ? void 0 : "B\u1ED5 sung \u0111\u1EA7y \u0111\u1EE7 th\xF4ng tin l\u1EE9a tu\u1ED5i, ki\u1EBFn th\u1EE9c ti\xEAn quy\u1EBFt v\xE0 \xEDt nh\u1EA5t 2 ngu\u1ED3n t\xE0i li\u1EC7u k\u1EF9 thu\u1EADt chu\u1EA9n v\xE0o REFERENCE_PACK.md."
5998
+ };
5999
+ }
6000
+ /**
6001
+ * Evaluates Gate 2 (Curriculum Framework: Units, Modules, Lessons, Bloom Progression)
6002
+ */
6003
+ static evaluateGate2(frameworkMarkdown, targetUnitCount = 4) {
6004
+ if (!frameworkMarkdown?.trim()) {
6005
+ return {
6006
+ decision: "REJECTED",
6007
+ score: 0,
6008
+ passed: false,
6009
+ evaluatedBy: "@heuristic-linter",
6010
+ critique: "T\u1EC7p CURRICULUM_FRAMEWORK.md r\u1ED7ng.",
6011
+ strengths: [],
6012
+ weaknesses: ["Thi\u1EBFu n\u1ED9i dung khung ch\u01B0\u01A1ng tr\xECnh."]
6013
+ };
6014
+ }
6015
+ const unitMatches = frameworkMarkdown.match(/(?:##\s+(?:Unit\s+\d+|U\d{2})|\|\s*U\d{2}\s*:|\|\s*Unit\s+\d+)/gi) || [];
6016
+ const hasBloom = /(?:bloom|nhận biết|thông hiểu|vận dụng|phân tích|đánh giá|sáng tạo|remember|understand|apply|analyze|evaluate|create)/i.test(frameworkMarkdown);
6017
+ const has5E = /(?:5e|engage|explore|explain|elaborate|evaluate|pbl|edp|hybrid)/i.test(frameworkMarkdown);
6018
+ const hasScopeSequence = /(?:scope & sequence|lộ trình|tiến trình|bài học)/i.test(frameworkMarkdown);
6019
+ let score = 30;
6020
+ const strengths = [];
6021
+ const weaknesses = [];
6022
+ if (unitMatches.length >= targetUnitCount) {
6023
+ score += 25;
6024
+ strengths.push(`Ph\xE2n r\xE3 \u0111\u1EE7 ${unitMatches.length} Units theo \u0111\xFAng y\xEAu c\u1EA7u c\u1EA5u tr\xFAc.`);
6025
+ } else {
6026
+ score += Math.min(20, unitMatches.length * 5);
6027
+ weaknesses.push(`S\u1ED1 l\u01B0\u1EE3ng Unit (${unitMatches.length}) ch\u01B0a \u0111\u1EA1t m\u1EE5c ti\xEAu ${targetUnitCount} Units.`);
6028
+ }
6029
+ if (hasBloom) {
6030
+ score += 20;
6031
+ strengths.push("C\xF3 ph\xE2n b\u1ED5 thang \u0111o Bloom r\xF5 r\xE0ng theo ti\u1EBFn tr\xECnh nh\u1EADn th\u1EE9c.");
6032
+ } else {
6033
+ weaknesses.push("Thi\u1EBFu \xE1nh x\u1EA1 m\u1EE9c \u0111\u1ED9 Bloom trong c\xE1c b\xE0i h\u1ECDc.");
6034
+ }
6035
+ if (has5E) {
6036
+ score += 15;
6037
+ strengths.push("C\u1EA5u tr\xFAc b\xE0i h\u1ECDc \u0111\u1ECBnh h\u01B0\u1EDBng chu\u1EA9n m\xF4 h\xECnh s\u01B0 ph\u1EA1m (5E/PBL/EDP).");
6038
+ }
6039
+ if (hasScopeSequence) {
6040
+ score += 10;
6041
+ strengths.push("C\xF3 b\u1EA3ng Scope & Sequence chi ti\u1EBFt.");
6042
+ }
6043
+ const passed = score >= 80;
6044
+ return {
6045
+ decision: passed ? "APPROVED" : score >= 60 ? "NEEDS_REVISION" : "REJECTED",
6046
+ score,
6047
+ passed,
6048
+ evaluatedBy: "@heuristic-linter",
6049
+ critique: passed ? "Khung ch\u01B0\u01A1ng tr\xECnh \u0111\u1EA1t chu\u1EA9n ki\u1EBFn tr\xFAc ph\xE2n r\xE3. Cho ph\xE9p ti\u1EBFn h\xE0nh s\u1EA3n xu\u1EA5t h\u1ECDc li\u1EC7u." : "C\u1EA7n b\u1ED5 sung chi ti\u1EBFt c\u1EA5u tr\xFAc b\xE0i h\u1ECDc, b\u1EA3ng ph\xE2n r\xE3 v\xE0 m\u1EE9c \u0111\u1ED9 Bloom t\u01B0\u01A1ng \u1EE9ng.",
6050
+ strengths,
6051
+ weaknesses
6052
+ };
6053
+ }
6054
+ /**
6055
+ * Evaluates a produced Lesson Artifact (LESSON, ACT, QUIZ, SLIDE) with heuristic structure checks
6056
+ */
6057
+ static evaluateArtifact(artifactType, content, lessonTitle) {
6058
+ if (!content?.trim() || content.trim().length < 50) {
6059
+ return {
6060
+ decision: "REJECTED",
6061
+ score: 0,
6062
+ passed: false,
6063
+ evaluatedBy: "@heuristic-linter",
6064
+ critique: `N\u1ED9i dung ${artifactType} cho b\xE0i "${lessonTitle}" qu\xE1 ng\u1EAFn ho\u1EB7c r\u1ED7ng.`,
6065
+ strengths: [],
6066
+ weaknesses: ["H\u1ECDc li\u1EC7u kh\xF4ng c\xF3 n\u1ED9i dung th\u1EF1c t\u1EBF."]
6067
+ };
6068
+ }
6069
+ let score = 40;
6070
+ const strengths = [];
6071
+ const weaknesses = [];
6072
+ switch (artifactType.toUpperCase()) {
6073
+ case "LESSON": {
6074
+ const phases = [
6075
+ { name: "Engage", regex: /(?:Engage|Khởi động|Warm-up|Hook|Mở đầu)/i },
6076
+ { name: "Explore", regex: /(?:Explore|Khám phá|Demo|Thử nghiệm)/i },
6077
+ { name: "Explain", regex: /(?:Explain|Giải thích|Core Concepts|Lý thuyết)/i },
6078
+ { name: "Elaborate", regex: /(?:Elaborate|Vận dụng|Guided Practice|Independent Practice|Challenge|Thực hành)/i },
6079
+ { name: "Evaluate", regex: /(?:Evaluate|Đánh giá|Wrap-up|Exit Ticket|Tổng kết)/i }
6080
+ ];
6081
+ let phaseCount = 0;
6082
+ for (const p of phases) {
6083
+ if (p.regex.test(content)) phaseCount++;
6084
+ }
6085
+ score += phaseCount * 10;
6086
+ if (phaseCount >= 4) {
6087
+ strengths.push(`Gi\xE1o \xE1n bao ph\u1EE7 ${phaseCount}/5 pha c\u1EE7a ti\u1EBFn tr\xECnh 5E.`);
6088
+ } else {
6089
+ weaknesses.push(`Gi\xE1o \xE1n ch\u1EC9 c\xF3 ${phaseCount}/5 pha 5E.`);
6090
+ }
6091
+ if (content.length > 500) score += 10;
6092
+ break;
6093
+ }
6094
+ case "ACT": {
6095
+ const hasSteps = /(?:bước|step\s+\d+|hướng dẫn)/i.test(content);
6096
+ const hasCode = /```[\s\S]*?```/.test(content);
6097
+ const hasObjectives = /(?:mục tiêu|objective|yêu cầu)/i.test(content);
6098
+ if (hasSteps) {
6099
+ score += 20;
6100
+ strengths.push("B\xE0i th\u1EF1c h\xE0nh c\xF3 quy tr\xECnh t\u1EEBng b\u01B0\u1EDBc r\xF5 r\xE0ng.");
6101
+ }
6102
+ if (hasCode) {
6103
+ score += 20;
6104
+ strengths.push("C\xF3 m\xE3 ngu\u1ED3n m\u1EABu/l\u1EC7nh th\u1EF1c thi minh h\u1ECDa.");
6105
+ }
6106
+ if (hasObjectives) {
6107
+ score += 10;
6108
+ strengths.push("C\xF3 x\xE1c \u0111\u1ECBnh m\u1EE5c ti\xEAu th\u1EF1c h\xE0nh.");
6109
+ }
6110
+ break;
6111
+ }
6112
+ case "QUIZ": {
6113
+ const hasQuestions = /(?:question\s*\d+|câu\s*\d+|\?)/i.test(content);
6114
+ const hasOptions = /(?:[A-D]\.|\"options\")/i.test(content);
6115
+ const hasExplanation = /(?:explanation|giải thích|vì sao|đáp án)/i.test(content);
6116
+ if (hasQuestions) {
6117
+ score += 20;
6118
+ strengths.push("C\xF3 c\xE2u h\u1ECFi ch\u1EA9n \u0111o\xE1n.");
6119
+ }
6120
+ if (hasOptions) {
6121
+ score += 20;
6122
+ strengths.push("\u0110\u1EA7y \u0111\u1EE7 c\xE1c l\u1EF1a ch\u1ECDn \u0111\xE1p \xE1n.");
6123
+ }
6124
+ if (hasExplanation) {
6125
+ score += 15;
6126
+ strengths.push("C\xF3 ph\u1EA7n gi\u1EA3i th\xEDch \u0111\xE1p \xE1n chi ti\u1EBFt.");
6127
+ }
6128
+ break;
6129
+ }
6130
+ case "SLIDE": {
6131
+ const hasMarp = /(?:marp:\s*true|---\s*\n)/i.test(content);
6132
+ const slideCount = (content.match(/---\s*\n/g) || []).length;
6133
+ if (hasMarp) {
6134
+ score += 25;
6135
+ strengths.push("\u0110\u1ECBnh d\u1EA1ng t\u01B0\u01A1ng th\xEDch Marp Presentation.");
6136
+ }
6137
+ if (slideCount >= 5) {
6138
+ score += 25;
6139
+ strengths.push(`B\u1ED9 slide c\xF3 dung l\u01B0\u1EE3ng h\u1EE3p l\xFD (${slideCount + 1} slides).`);
6140
+ } else {
6141
+ weaknesses.push(`S\u1ED1 l\u01B0\u1EE3ng slide (${slideCount + 1}) c\xF2n qu\xE1 \xEDt.`);
6142
+ }
6143
+ break;
6144
+ }
6145
+ default: {
6146
+ if (content.length > 200) score += 40;
6147
+ break;
6148
+ }
6149
+ }
6150
+ const passed = score >= 80;
6151
+ return {
6152
+ decision: passed ? "APPROVED" : score >= 60 ? "NEEDS_REVISION" : "REJECTED",
6153
+ score: Math.min(100, score),
6154
+ passed,
6155
+ evaluatedBy: "@heuristic-linter",
6156
+ critique: passed ? `H\u1ECDc li\u1EC7u ${artifactType} cho b\xE0i "${lessonTitle}" \u0111\u1EA1t ki\u1EC3m tra c\u1EA5u tr\xFAc (${score}/100).` : `H\u1ECDc li\u1EC7u ${artifactType} c\u1EA7n ho\xE0n thi\u1EC7n th\xEAm c\u1EA5u tr\xFAc \u0111\u1EC3 \u0111\u1EA1t chu\u1EA9n s\u01B0 ph\u1EA1m (${score}/100).`,
6157
+ strengths,
6158
+ weaknesses
6159
+ };
6160
+ }
6161
+ };
6162
+
6163
+ // src/services/lessonProductionService.ts
5918
6164
  init_errors();
5919
6165
 
5920
6166
  // src/services/lessonProductionService.ts
@@ -6014,6 +6260,8 @@ async function generateSingleArtifact(req) {
6014
6260
  extraContext,
6015
6261
  config,
6016
6262
  contextSot = {},
6263
+ mediaPolicy,
6264
+ researchContext,
6017
6265
  options = {},
6018
6266
  onChunk
6019
6267
  } = req;
@@ -6051,7 +6299,10 @@ ${langRule}
6051
6299
  3. PEDAGOGICAL GROUNDING: Strictly align with the ${pedagogy.toUpperCase()} model, Bloom's Revised Taxonomy, and IEEE/ACM CS2023 guidelines.
6052
6300
  4. SCAFFOLDING LEVEL: "${scaffoldingLevel.toUpperCase()}".${scaffoldingRule}
6053
6301
  5. TARGET AUDIENCE: ${gradeLevel ? `Grade ${gradeLevel}, ` : ""}${targetAge} on ${hwStr}.${deviceRule}${classDynamicsRule}${extraContextRule}
6054
- 6. OUTPUT INTEGRITY: Output clean Markdown starting directly with YAML frontmatter. Do not wrap entire response in triple backticks.${artifactSpecializedPrompt}`;
6302
+ 6. OUTPUT INTEGRITY: Output clean Markdown starting directly with YAML frontmatter. Do not wrap entire response in triple backticks.${artifactSpecializedPrompt}${mediaPolicy ? `
6303
+
6304
+ IMAGE PLANNING (media ledger) \u2014 QUOTA GEN: t\u1ED1i \u0111a ${mediaPolicy.maxGenImagesPerArtifact} \u1EA3nh AI cho artifact n\xE0y (\u1EA3nh search kho kh\xF4ng gi\u1EDBi h\u1EA1n).
6305
+ Khi n\u1ED9i dung c\u1EA7n minh h\u1ECDa (diagram quy tr\xECnh, s\u01A1 \u0111\u1ED3 kh\xE1i ni\u1EC7m, step-by-step visual), ch\xE8n placeholder \u0111\xFAng format [IMAGE: slug] (slug ch\u1EEF th\u01B0\u1EDDng-g\u1EA1ch ngang, unique trong b\xE0i, vd img-for-loop-diagram) T\u1EA0I \u0110\xDANG CH\u1ED6 c\u1EA7n \u1EA3nh, tr\xEAn d\xF2ng ri\xEAng. KH\xD4NG t\u1EF1 sinh \u1EA3nh, KH\xD4NG d\xF9ng markdown image \u2014 placeholder s\u1EBD \u0111\u01B0\u1EE3c thay b\u1EB1ng \u1EA3nh th\u1EADt sau khi Media Curator duy\u1EC7t. Ch\u1EC9 \u0111\u1EB7t cho \u1EA3nh TH\u1EF0C S\u1EF0 c\u1EA7n thi\u1EBFt.` : ""}`;
6055
6306
  const userPrompt = `### CONTEXT GROUND TRUTH:
6056
6307
  - Topic / Concept Domain: "${topic}"
6057
6308
  - Lesson ID: "${lessonId || "ON_DEMAND"}"
@@ -6066,6 +6317,9 @@ ${selectedDevices ? `- Device Mode: "${selectedDevices}"
6066
6317
  ` : ""}${extraContext ? `- User Specific Guidance & Requirements: "${extraContext}"
6067
6318
  ` : ""}${config ? `- Full Task Config: ${JSON.stringify(config)}
6068
6319
  ` : ""}
6320
+ ${researchContext ? `### RESEARCH GROUND TRUTH (fact-checked context \u2014 \u01B0u ti\xEAn d\xF9ng, KH\xD4NG b\u1ECBa s\u1ED1 li\u1EC7u):
6321
+ ${researchContext.slice(0, 4e3)}
6322
+ ` : ""}
6069
6323
  ${contextSot.framework ? `### SOT CURRICULUM FRAMEWORK:
6070
6324
  ${contextSot.framework.slice(0, 1500)}
6071
6325
  ` : ""}
@@ -6106,13 +6360,171 @@ Generate the complete, thorough, publication-grade "${artifactType.toUpperCase()
6106
6360
  }
6107
6361
  );
6108
6362
  const { thought, content } = extractThoughtAndContent(rawResult);
6109
- return {
6110
- artifactType,
6111
- filename: meta.filename,
6112
- content: content || accumulatedContent,
6113
- persona: meta.persona,
6114
- thought: thought || accumulatedThought
6363
+ const finalContent = content || accumulatedContent;
6364
+ const finalThought = thought || accumulatedThought;
6365
+ const gates = resolveGateSettings(req.gateSettings);
6366
+ const kitGateType = {
6367
+ lesson: "LESSON",
6368
+ slide: "SLIDE",
6369
+ act: "ACT",
6370
+ quiz: "QUIZ",
6371
+ guide: "GUIDE",
6372
+ handout: "HANDOUT",
6373
+ worksheet: "WKS",
6374
+ ext: "EXT",
6375
+ codelab: "CODE",
6376
+ exit_ticket: "EXIT_TICKET"
6377
+ }[artifactType];
6378
+ const gateMode = kitGateType ? gateModeFor(gates, kitGateType) : "OFF";
6379
+ if (gateMode === "OFF") {
6380
+ return { artifactType, filename: meta.filename, content: finalContent, persona: meta.persona, thought: finalThought };
6381
+ }
6382
+ const injectReviewMeta = (doc, metaMap) => {
6383
+ const fm = doc.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
6384
+ if (!fm) {
6385
+ return `---
6386
+ ${Object.entries(metaMap).map(([k, v]) => `${k}: "${v}"`).join("\n")}
6387
+ ---
6388
+
6389
+ ${doc}`;
6390
+ }
6391
+ let yaml = fm[1] ?? "";
6392
+ for (const [k, v] of Object.entries(metaMap)) {
6393
+ const re = new RegExp(`^${k}:.*$`, "m");
6394
+ if (re.test(yaml)) yaml = yaml.replace(re, `${k}: "${v}"`);
6395
+ else yaml += `
6396
+ ${k}: "${v}"`;
6397
+ }
6398
+ return doc.replace(fm[0], `---
6399
+ ${yaml.trim()}
6400
+ ---
6401
+ `);
6115
6402
  };
6403
+ if (gateMode === "HITL") {
6404
+ return {
6405
+ artifactType,
6406
+ filename: meta.filename,
6407
+ content: injectReviewMeta(finalContent, { review_status: "PENDING_REVIEW", review_decision: "PENDING" }),
6408
+ persona: meta.persona,
6409
+ thought: finalThought,
6410
+ pausedForReview: true,
6411
+ gateStatus: "awaiting_approval"
6412
+ };
6413
+ }
6414
+ const judgeObjectives = [{ code: "A1", description: `Artifact fulfills its pedagogical contract for topic "${topic}" with complete required sections`, bloomLevel: "Analyze" }];
6415
+ try {
6416
+ const verdict = await LLMJudgeEngine.evaluateArtifactWithLLM({
6417
+ targetArtifactType: artifactType.toUpperCase(),
6418
+ lessonId: lessonId || "ON_DEMAND",
6419
+ expectedLanguage: language.toLowerCase().includes("viet") ? "vietnamese" : "english",
6420
+ targetObjectives: judgeObjectives,
6421
+ generatedContentJson: JSON.stringify({ content: finalContent, artifactType, lessonId: lessonId || "ON_DEMAND", title: displayTitle })
6422
+ });
6423
+ const passed = Boolean(verdict.passed) && verdict.score >= 85;
6424
+ if (passed) {
6425
+ return {
6426
+ artifactType,
6427
+ filename: meta.filename,
6428
+ content: injectReviewMeta(finalContent, {
6429
+ review_status: "APPROVED",
6430
+ reviewed_by: "@reviewer (LLM-as-Judge)",
6431
+ review_decision: "APPROVED"
6432
+ }),
6433
+ persona: meta.persona,
6434
+ thought: finalThought,
6435
+ judgeScore: verdict.score,
6436
+ judgeCritique: verdict.critique
6437
+ };
6438
+ }
6439
+ const repairFeedback = [verdict.autoRepairInstructions, ...verdict.weaknesses || []].filter(Boolean).join("\n");
6440
+ const repairedRaw = await streamCurriculumAIInference(
6441
+ [
6442
+ { role: "user", content: `You previously authored \`${meta.filename}\` and the academic Judge REJECTED it with the critique below.
6443
+ Rewrite the COMPLETE corrected document: fix every listed issue, keep the mandatory structure, frontmatter and all approved sections. Output 100% clean Markdown only.
6444
+
6445
+ JUDGE CRITIQUE:
6446
+ ${repairFeedback}
6447
+
6448
+ CURRENT DOCUMENT:
6449
+ ${finalContent}` }
6450
+ ],
6451
+ userPrompt,
6452
+ { ...options, systemPersona: systemPrompt },
6453
+ (chunk, type) => {
6454
+ if (onChunk) onChunk(chunk, type === "thought" ? "thought" : "content");
6455
+ }
6456
+ );
6457
+ const repairedExtract = extractThoughtAndContent(repairedRaw);
6458
+ const repaired = repairedExtract.content || repairedRaw;
6459
+ if (repaired && repaired.trim().length > 100) {
6460
+ const reVerdict = await LLMJudgeEngine.evaluateArtifactWithLLM({
6461
+ targetArtifactType: artifactType.toUpperCase(),
6462
+ lessonId: lessonId || "ON_DEMAND",
6463
+ expectedLanguage: language.toLowerCase().includes("viet") ? "vietnamese" : "english",
6464
+ targetObjectives: judgeObjectives,
6465
+ generatedContentJson: JSON.stringify({ content: repaired, artifactType, lessonId: lessonId || "ON_DEMAND", title: displayTitle })
6466
+ });
6467
+ if (reVerdict.passed && reVerdict.score >= 85) {
6468
+ return {
6469
+ artifactType,
6470
+ filename: meta.filename,
6471
+ content: injectReviewMeta(repaired, {
6472
+ review_status: "APPROVED",
6473
+ reviewed_by: "@reviewer (LLM-as-Judge)",
6474
+ review_decision: "APPROVED"
6475
+ }),
6476
+ persona: meta.persona,
6477
+ thought: repairedExtract.thought || "",
6478
+ judgeScore: reVerdict.score,
6479
+ judgeCritique: reVerdict.critique
6480
+ };
6481
+ }
6482
+ return {
6483
+ artifactType,
6484
+ filename: meta.filename,
6485
+ content: injectReviewMeta(repaired, {
6486
+ review_status: "NEEDS_REVISION",
6487
+ reviewed_by: "@reviewer (LLM-as-Judge)",
6488
+ review_decision: "NEEDS_REVISION"
6489
+ }),
6490
+ persona: meta.persona,
6491
+ thought: repairedExtract.thought || "",
6492
+ pausedForReview: true,
6493
+ gateStatus: "judge_needs_revision",
6494
+ judgeScore: reVerdict.score,
6495
+ judgeCritique: reVerdict.critique
6496
+ };
6497
+ }
6498
+ return {
6499
+ artifactType,
6500
+ filename: meta.filename,
6501
+ content: injectReviewMeta(finalContent, {
6502
+ review_status: "NEEDS_REVISION",
6503
+ reviewed_by: "@reviewer (LLM-as-Judge)",
6504
+ review_decision: "NEEDS_REVISION"
6505
+ }),
6506
+ persona: meta.persona,
6507
+ thought: finalThought,
6508
+ pausedForReview: true,
6509
+ gateStatus: "judge_needs_revision",
6510
+ judgeScore: verdict.score,
6511
+ judgeCritique: verdict.critique
6512
+ };
6513
+ } catch (judgeErr) {
6514
+ return {
6515
+ artifactType,
6516
+ filename: meta.filename,
6517
+ content: injectReviewMeta(finalContent, {
6518
+ review_status: "NEEDS_REVISION",
6519
+ review_decision: "NEEDS_REVISION",
6520
+ critique: `Judge unavailable (fail-closed): ${String(judgeErr?.message || judgeErr).slice(0, 300)}`
6521
+ }),
6522
+ persona: meta.persona,
6523
+ thought: finalThought,
6524
+ pausedForReview: true,
6525
+ gateStatus: "judge_error"
6526
+ };
6527
+ }
6116
6528
  } catch (error) {
6117
6529
  throw createAiInferenceError({
6118
6530
  errorCode: "ERR_AI_ALL_PROVIDERS_FAILED",
@@ -6387,6 +6799,7 @@ async function generateSingleArtifactWorkflow(request) {
6387
6799
  }
6388
6800
 
6389
6801
  exports.DEFAULT_GATE_SETTINGS = DEFAULT_GATE_SETTINGS;
6802
+ exports.approvalHookToken = approvalHookToken;
6390
6803
  exports.approvalPayloadSchema = approvalPayloadSchema;
6391
6804
  exports.executeSingleArtifactStep = executeSingleArtifactStep;
6392
6805
  exports.gateModeFor = gateModeFor;