@thanh01.pmt/curriculum-kit 1.0.13 → 1.0.15

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
@@ -14,7 +14,6 @@ var supabaseJs = require('@supabase/supabase-js');
14
14
  var url = require('url');
15
15
  var jsonrepair = require('jsonrepair');
16
16
  var rest = require('@octokit/rest');
17
- var workflow = require('workflow');
18
17
 
19
18
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
20
19
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -13526,11 +13525,11 @@ var DeterministicPipelineRunner = class {
13526
13525
  // src/services/prefillService.ts
13527
13526
  init_streamRunner();
13528
13527
  var DEFAULT_STREAM_IDLE_MS = 45e3;
13529
- var DEFAULT_STREAM_TOTAL_MS = 3e5;
13528
+ var DEFAULT_STREAM_TOTAL_MS = 42e4;
13530
13529
  var LAYER_TOTAL_BUDGET_MS = {
13531
- 1: 18e4,
13532
- 2: 36e4,
13533
- 3: 36e4
13530
+ 1: 42e4,
13531
+ 2: 42e4,
13532
+ 3: 42e4
13534
13533
  };
13535
13534
  function resolveStreamBudget(layer, opts) {
13536
13535
  const envIdle = Number(process.env.WIZARD_PREFILL_IDLE_TIMEOUT_MS);
@@ -13597,29 +13596,111 @@ function createStreamAbortSignal(budget) {
13597
13596
  }
13598
13597
  };
13599
13598
  }
13599
+ function closeTruncatedJson(candidate) {
13600
+ let inStr = false;
13601
+ let esc2 = false;
13602
+ const stack = [];
13603
+ for (let i = 0; i < candidate.length; i++) {
13604
+ const ch = candidate[i];
13605
+ if (esc2) {
13606
+ esc2 = false;
13607
+ continue;
13608
+ }
13609
+ if (inStr) {
13610
+ if (ch === "\\") esc2 = true;
13611
+ else if (ch === '"') inStr = false;
13612
+ continue;
13613
+ }
13614
+ if (ch === '"') inStr = true;
13615
+ else if (ch === "{" || ch === "[") stack.push(ch);
13616
+ else if (ch === "}" || ch === "]") stack.pop();
13617
+ }
13618
+ if (stack.length === 0 && !inStr) return null;
13619
+ let out = candidate;
13620
+ out = out.replace(/,\s*$/, "");
13621
+ if (inStr) {
13622
+ out = out.replace(/\\+$/, "");
13623
+ out += '"';
13624
+ }
13625
+ while (stack.length > 0) {
13626
+ out += stack.pop() === "{" ? "}" : "]";
13627
+ }
13628
+ return out;
13629
+ }
13630
+ function stripLlmJsonWrappers(rawText) {
13631
+ let cleaned = rawText.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
13632
+ const firstBrace = cleaned.indexOf("{");
13633
+ if (firstBrace > 0) {
13634
+ cleaned = cleaned.slice(firstBrace);
13635
+ }
13636
+ return cleaned.trim();
13637
+ }
13600
13638
  function safeParseJson(rawText) {
13601
13639
  if (!rawText) return null;
13602
- const cleaned = rawText.replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
13640
+ const cleaned = stripLlmJsonWrappers(rawText);
13641
+ if (!cleaned) return null;
13603
13642
  try {
13604
13643
  return JSON.parse(cleaned);
13605
13644
  } catch {
13606
- const firstBrace = cleaned.indexOf("{");
13645
+ }
13646
+ const firstBrace = cleaned.indexOf("{");
13647
+ if (firstBrace === -1) return null;
13648
+ const buildCandidates = () => {
13649
+ const list = [];
13650
+ const closed = closeTruncatedJson(cleaned.slice(firstBrace));
13651
+ if (closed) list.push(closed);
13607
13652
  const lastBrace = cleaned.lastIndexOf("}");
13608
- if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
13609
- const candidate = cleaned.slice(firstBrace, lastBrace + 1);
13610
- try {
13611
- return JSON.parse(candidate);
13612
- } catch {
13653
+ if (lastBrace > firstBrace) {
13654
+ list.push(cleaned.slice(firstBrace, lastBrace + 1));
13655
+ }
13656
+ return list;
13657
+ };
13658
+ for (const candidate of buildCandidates()) {
13659
+ try {
13660
+ return JSON.parse(candidate);
13661
+ } catch {
13662
+ }
13663
+ try {
13664
+ return JSON.parse(jsonrepair.jsonrepair(candidate));
13665
+ } catch {
13666
+ }
13667
+ }
13668
+ let depth = 0;
13669
+ let inStr = false;
13670
+ let esc2 = false;
13671
+ for (let i = firstBrace; i < cleaned.length; i++) {
13672
+ const ch = cleaned[i];
13673
+ if (esc2) {
13674
+ esc2 = false;
13675
+ continue;
13676
+ }
13677
+ if (inStr) {
13678
+ if (ch === "\\") esc2 = true;
13679
+ else if (ch === '"') inStr = false;
13680
+ continue;
13681
+ }
13682
+ if (ch === '"') inStr = true;
13683
+ else if (ch === "{") depth++;
13684
+ else if (ch === "}") {
13685
+ depth--;
13686
+ if (depth === 0) {
13687
+ const candidate = cleaned.slice(firstBrace, i + 1);
13613
13688
  try {
13614
- const repaired = jsonrepair.jsonrepair(candidate);
13615
- return JSON.parse(repaired);
13689
+ return JSON.parse(candidate);
13690
+ } catch {
13691
+ }
13692
+ try {
13693
+ return JSON.parse(jsonrepair.jsonrepair(candidate));
13616
13694
  } catch {
13617
- return null;
13618
13695
  }
13619
13696
  }
13620
13697
  }
13621
- return null;
13622
13698
  }
13699
+ try {
13700
+ return JSON.parse(jsonrepair.jsonrepair(cleaned));
13701
+ } catch {
13702
+ }
13703
+ return null;
13623
13704
  }
13624
13705
  async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onChunk, preferredModel, preferredProvider, budget) {
13625
13706
  const alibabaKey = apiKeys.alibaba || apiKeys.dashscope || process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY;
@@ -14672,1014 +14753,640 @@ template_contract: "artifact-template-v1"
14672
14753
  timestamp: dateStr
14673
14754
  };
14674
14755
  }
14675
- async function withRateLimitBackoff(options) {
14676
- let attempt = 1;
14677
- try {
14678
- const meta = workflow.getStepMetadata();
14679
- if (meta && typeof meta.attempt === "number") {
14680
- attempt = meta.attempt;
14681
- }
14682
- } catch {
14683
- }
14684
- try {
14685
- return await options.fn();
14686
- } catch (error) {
14687
- const errorMessage = error?.message || String(error);
14688
- const status = error?.status || error?.statusCode || error?.response?.status;
14689
- const isRateLimited = status === 429 || errorMessage.includes("429") || errorMessage.toLowerCase().includes("rate limit") || errorMessage.toLowerCase().includes("resource exhausted") || errorMessage.toLowerCase().includes("quota exceeded");
14690
- const isTransientError = status === 500 || status === 502 || status === 503 || status === 504 || errorMessage.includes("fetch failed") || errorMessage.includes("ECONNRESET") || errorMessage.includes("ETIMEDOUT") || errorMessage.includes("socket hang up");
14691
- if (isRateLimited || isTransientError) {
14692
- const retryAfterSeconds = Math.min(120, Math.pow(2, attempt) * 2);
14693
- const reason = isRateLimited ? "Rate limit exceeded (429)" : `Transient server error (${status || "network"})`;
14694
- throw new workflow.RetryableError(`[${options.stepName}] ${reason}. Retrying attempt ${attempt + 1}...`, {
14695
- retryAfter: `${retryAfterSeconds}s`
14756
+
14757
+ // src/index.ts
14758
+ init_errors();
14759
+
14760
+ // src/evaluators/deterministicStructuralLinter.ts
14761
+ var DeterministicStructuralLinter = class {
14762
+ /**
14763
+ * Validates structural invariants across a complete lesson bundle.
14764
+ */
14765
+ static lintBundle(input) {
14766
+ const { lesson, quiz, activity, slides, codeLab } = input;
14767
+ const findings = [];
14768
+ const strengths = [];
14769
+ let score = 100;
14770
+ const baseLessonId = lesson.lessonId;
14771
+ const baseLanguage = lesson.language;
14772
+ if (!lesson.title || lesson.title.trim().length === 0) {
14773
+ score -= 20;
14774
+ findings.push({
14775
+ id: "struct_missing_lesson_title",
14776
+ dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
14777
+ severity: "CRITICAL",
14778
+ title: "Missing Lesson Title",
14779
+ description: "Lesson plan has an empty or whitespace title.",
14780
+ remediationAdvice: "Provide a non-empty lesson title."
14696
14781
  });
14697
14782
  }
14698
- throw error;
14699
- }
14700
- }
14701
-
14702
- // src/workflow/steps/lessonSteps.ts
14703
- async function generateMasterLessonStep(input) {
14704
- "use step";
14705
- console.log(` \u23F3 [Step: Master Lesson] Generating 5E Master Lesson Plan for "${input.milestone.name}"...`);
14706
- const t0 = Date.now();
14707
- const res = await withRateLimitBackoff({
14708
- stepName: `generate-master-lesson-${input.milestone.id || "L01"}`,
14709
- fn: async () => {
14710
- return generateLessonMasterFlow({
14711
- milestone: input.milestone,
14712
- language: input.language,
14713
- topic: input.topic,
14714
- targetAudience: input.targetAudience,
14715
- contextContinuity: input.contextContinuity,
14716
- standardsContext: input.standardsContext,
14717
- modelOptions: input.modelOptions
14783
+ if (!lesson.learningObjectives || lesson.learningObjectives.length === 0) {
14784
+ score -= 25;
14785
+ findings.push({
14786
+ id: "struct_empty_learning_objectives",
14787
+ dimension: "CONSTRUCTIVE_ALIGNMENT",
14788
+ severity: "CRITICAL",
14789
+ title: "Empty Learning Objectives Array",
14790
+ description: "Lesson plan contains 0 learning objectives.",
14791
+ remediationAdvice: "Declare at least 2 structured Learning Objectives in the schema."
14718
14792
  });
14719
14793
  }
14720
- });
14721
- console.log(` \u2705 [Step: Master Lesson] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (Title: "${res.title}")`);
14722
- return res;
14723
- }
14724
- async function judgeMasterLessonStep(input) {
14725
- "use step";
14726
- console.log(` \u2696\uFE0F [Step: Quality Judge] Auditing Master Lesson pedagogical quality & language adherence...`);
14727
- const t0 = Date.now();
14728
- const res = await withRateLimitBackoff({
14729
- stepName: `judge-master-lesson-${input.lessonId}`,
14730
- fn: async () => {
14731
- return auditCurriculumQualityFlow({
14732
- targetArtifactType: "LESSON",
14733
- lessonId: input.lessonId,
14734
- expectedLanguage: input.language,
14735
- targetObjectives: input.targetObjectives,
14736
- generatedContentJson: JSON.stringify(input.lesson, null, 2),
14737
- standardStatements: input.standardStatements,
14738
- modelOptions: input.modelOptions
14794
+ if (!lesson.sections || lesson.sections.length === 0) {
14795
+ score -= 25;
14796
+ findings.push({
14797
+ id: "struct_empty_lesson_sections",
14798
+ dimension: "5E_INSTRUCTIONAL_FIDELITY",
14799
+ severity: "CRITICAL",
14800
+ title: "Empty Lesson Sections Array",
14801
+ description: "Lesson plan contains no instructional sections.",
14802
+ remediationAdvice: "Provide structured lesson flow sections."
14739
14803
  });
14740
14804
  }
14741
- });
14742
- console.log(` \u2705 [Step: Quality Judge] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (Verdict: ${res.overallVerdict}, Score: ${res.totalScore}/100)`);
14743
- return res;
14744
- }
14745
- async function repairMasterLessonStep(input) {
14746
- "use step";
14747
- console.log(` \u{1F6E0}\uFE0F [Step: Auto-Repair] Repairing Master Lesson based on Judge feedback...`);
14748
- const repairFeedback = `
14749
- PREVIOUS AUDIT VERDICT: ${input.auditReport.overallVerdict} (Score: ${input.auditReport.totalScore}/100)
14750
- Detected Issues:
14751
- ${input.auditReport.criteria.filter((c) => !c.passed).map((c) => `- [${c.name}] ${c.feedback}`).join("\n")}
14752
- Actionable Repairs:
14753
- ${input.auditReport.actionableRepairPrompts.map((p) => `* ${p}`).join("\n")}
14754
- Language Adherence Required: "${input.language}"
14755
- `.trim();
14756
- return withRateLimitBackoff({
14757
- stepName: `repair-master-lesson-${input.milestone.id || "L01"}`,
14758
- fn: async () => {
14759
- return generateLessonMasterFlow({
14760
- milestone: input.milestone,
14761
- language: input.language,
14762
- topic: input.topic,
14763
- targetAudience: input.targetAudience,
14764
- contextContinuity: repairFeedback,
14765
- standardsContext: input.standardsContext,
14766
- modelOptions: input.modelOptions
14805
+ if (quiz && quiz.quizId !== baseLessonId && !quiz.quizId.includes(baseLessonId)) {
14806
+ score -= 15;
14807
+ findings.push({
14808
+ id: "struct_quiz_id_mismatch",
14809
+ dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
14810
+ severity: "MAJOR",
14811
+ title: "Quiz ID Contract Mismatch",
14812
+ description: `Quiz ID "${quiz.quizId}" does not match Lesson ID "${baseLessonId}".`,
14813
+ remediationAdvice: `Sync Quiz quizId to "${baseLessonId}".`,
14814
+ affectedElement: quiz.quizId
14767
14815
  });
14768
14816
  }
14769
- });
14770
- }
14771
-
14772
- // src/workflow/steps/satelliteSteps.ts
14773
- async function generateActivityStep(options) {
14774
- "use step";
14775
- console.log(` \u23F3 [Step: Activity Lab] Generating hands-on ACT.md...`);
14776
- const t0 = Date.now();
14777
- const res = await withRateLimitBackoff({
14778
- stepName: `generate-act-${options.lesson.lessonId}`,
14779
- fn: async () => generateActivityFlow(options)
14780
- });
14781
- console.log(` \u2705 [Step: Activity Lab] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (Title: "${res.title}")`);
14782
- return res;
14783
- }
14784
- async function generateCodeLabStep(options) {
14785
- "use step";
14786
- console.log(` \u23F3 [Step: Code Lab] Generating starter & solution code LAB.md...`);
14787
- const t0 = Date.now();
14788
- const res = await withRateLimitBackoff({
14789
- stepName: `generate-lab-${options.lesson.lessonId}`,
14790
- fn: async () => generateCodeLabFlow(options)
14791
- });
14792
- console.log(` \u2705 [Step: Code Lab] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (Title: "${res.title}")`);
14793
- return res;
14794
- }
14795
- async function generateSelfLabStep(options) {
14796
- "use step";
14797
- console.log(` \u23F3 [Step: Self-Lab] Generating SELF_LAB.md with Progressive Hints...`);
14798
- const t0 = Date.now();
14799
- const res = await withRateLimitBackoff({
14800
- stepName: `generate-self-lab-${options.milestone.id || "L01"}`,
14801
- fn: async () => generateSelfLabFlow(options)
14802
- });
14803
- console.log(` \u2705 [Step: Self-Lab] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (Challenges: 3 tiers Bronze/Silver/Gold)`);
14804
- return res;
14805
- }
14806
- async function generateDiagnosticQuizStep(options) {
14807
- "use step";
14808
- console.log(` \u23F3 [Step: Diagnostic Quiz] Generating QUIZ.json Bloom Assessment...`);
14809
- const t0 = Date.now();
14810
- const res = await withRateLimitBackoff({
14811
- stepName: `generate-quiz-${options.milestone.id || "L01"}`,
14812
- fn: async () => generateDiagnosticQuizFlow(options)
14813
- });
14814
- console.log(` \u2705 [Step: Diagnostic Quiz] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (Questions: ${res.questions.length})`);
14815
- return res;
14816
- }
14817
- async function generateSlidesStep(options) {
14818
- "use step";
14819
- console.log(` \u23F3 [Step: Slides] Generating Marp presentation SLIDE.md...`);
14820
- const t0 = Date.now();
14821
- const res = await withRateLimitBackoff({
14822
- stepName: `generate-slides-${options.lesson.lessonId}`,
14823
- fn: async () => generateSlidesFlow(options)
14824
- });
14825
- console.log(` \u2705 [Step: Slides] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (Slides: ${res.slides.length})`);
14826
- return res;
14827
- }
14828
- async function generateHandoutStep(options) {
14829
- "use step";
14830
- console.log(` \u23F3 [Step: Handout] Generating 5-Second Rule HANDOUT.md...`);
14831
- const t0 = Date.now();
14832
- const res = await withRateLimitBackoff({
14833
- stepName: `generate-handout-${options.lesson.lessonId}`,
14834
- fn: async () => generateHandoutFlow(options)
14835
- });
14836
- console.log(` \u2705 [Step: Handout] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (Title: "${res.title}")`);
14837
- return res;
14838
- }
14839
- async function generateWorksheetStep(options) {
14840
- "use step";
14841
- return withRateLimitBackoff({
14842
- stepName: `generate-worksheet-${options.lesson.lessonId}`,
14843
- fn: async () => generateWorksheetFlow(options)
14844
- });
14845
- }
14846
- async function generateTeacherGuideStep(options) {
14847
- "use step";
14848
- return withRateLimitBackoff({
14849
- stepName: `generate-teacher-guide-${options.lesson.lessonId}`,
14850
- fn: async () => generateTeacherGuideFlow(options)
14851
- });
14852
- }
14853
- async function generateExtensionStep(options) {
14854
- "use step";
14855
- return withRateLimitBackoff({
14856
- stepName: `generate-extension-${options.lesson.lessonId}`,
14857
- fn: async () => generateExtensionFlow(options)
14858
- });
14859
- }
14860
-
14861
- // src/workflow/steps/satelliteJudgeStep.ts
14862
- async function judgeSatelliteStep(input) {
14863
- "use step";
14864
- const t0 = Date.now();
14865
- console.log(" \u{1F9D1}\u200D\u2696\uFE0F [Step: Satellite Judge] " + input.artifactType + " (" + input.artifactId + ")...");
14866
- return withRateLimitBackoff({
14867
- stepName: "judge-satellite-" + input.artifactType + "-" + input.artifactId,
14868
- fn: async () => {
14869
- return auditCurriculumQualityFlow({
14870
- targetArtifactType: input.artifactType,
14871
- lessonId: input.artifactId,
14872
- expectedLanguage: input.language,
14873
- targetObjectives: input.targetObjectives,
14874
- generatedContentJson: JSON.stringify(input.artifact),
14875
- standardStatements: input.standardStatements,
14876
- modelOptions: input.modelOptions
14817
+ if (activity && activity.lessonId !== baseLessonId) {
14818
+ score -= 15;
14819
+ findings.push({
14820
+ id: "struct_act_id_mismatch",
14821
+ dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
14822
+ severity: "MAJOR",
14823
+ title: "Activity Lesson ID Contract Mismatch",
14824
+ description: `Activity lessonId "${activity.lessonId}" does not match Lesson ID "${baseLessonId}".`,
14825
+ remediationAdvice: `Sync Activity lessonId to "${baseLessonId}".`,
14826
+ affectedElement: activity.lessonId
14877
14827
  });
14878
14828
  }
14879
- }).then((res) => {
14880
- console.log(" \u2705 [Satellite Judge] " + input.artifactType + " \u2192 " + res.overallVerdict + " (" + res.totalScore + "/100) in " + ((Date.now() - t0) / 1e3).toFixed(1) + "s");
14881
- return res;
14882
- });
14883
- }
14884
-
14885
- // src/workflow/steps/publishSteps.ts
14886
- async function saveMilestoneToWorkspaceStep(input) {
14887
- "use step";
14888
- const manager = new LocalWorkspaceManager(input.baseWorkspaceDir);
14889
- const savedFiles = [];
14890
- const lessonMd = serializeLessonToMarkdown(input.lesson);
14891
- const p1 = await manager.saveArtifact(input.jobId, input.milestoneSlug, "LESSON_5E.md", lessonMd, "LESSON");
14892
- savedFiles.push(p1);
14893
- if (input.activity) {
14894
- const actMd = serializeActivityToMarkdown(input.activity);
14895
- const p2 = await manager.saveArtifact(input.jobId, input.milestoneSlug, "ACT.md", actMd, "ACT");
14896
- savedFiles.push(p2);
14897
- }
14898
- if (input.codeLab) {
14899
- const labContent = serializeCodeLabToMarkdown(input.codeLab);
14900
- const p3 = await manager.saveArtifact(input.jobId, input.milestoneSlug, "LAB.md", labContent, "LAB");
14901
- savedFiles.push(p3);
14902
- }
14903
- if (input.selfLab) {
14904
- const selfLabMd = serializeSelfLabToMarkdown(input.selfLab);
14905
- const p4 = await manager.saveArtifact(input.jobId, input.milestoneSlug, "SELF_LAB.md", selfLabMd, "SELF_LAB");
14906
- savedFiles.push(p4);
14907
- }
14908
- if (input.quiz) {
14909
- const quizMd = serializeDiagnosticQuizToMarkdown(input.quiz);
14910
- const p5a = await manager.saveArtifact(input.jobId, input.milestoneSlug, "QUIZ.md", quizMd, "QUIZ_MD");
14911
- const p5b = await manager.saveArtifact(
14912
- input.jobId,
14913
- input.milestoneSlug,
14914
- "QUIZ.json",
14915
- JSON.stringify(input.quiz, null, 2),
14916
- "QUIZ_JSON"
14917
- );
14918
- savedFiles.push(p5a, p5b);
14919
- }
14920
- if (input.slides) {
14921
- const slidesContent = `---
14922
- marp: true
14923
- theme: default
14924
- paginate: true
14925
- ---
14926
-
14927
- # ${input.slides.title}
14928
-
14929
- ---
14930
-
14931
- ${input.slides.slides.map((s) => `## Slide ${s.slideNumber}: ${s.title}
14932
-
14933
- ${s.bulletPoints.map((b) => `- ${b}`).join("\n")}${s.codeSnippet ? `
14934
-
14935
- \`\`\`
14936
- ${s.codeSnippet}
14937
- \`\`\`` : ""}
14938
-
14939
- <!-- Presenter Notes: ${s.presenterNotes || ""} -->`).join("\n\n---\n\n")}
14940
- `.trim();
14941
- const p6 = await manager.saveArtifact(input.jobId, input.milestoneSlug, "SLIDE.md", slidesContent, "SLIDES");
14942
- savedFiles.push(p6);
14943
- }
14944
- if (input.handout) {
14945
- const handoutMd = serializeHandoutToMarkdown(input.handout);
14946
- const p7 = await manager.saveArtifact(input.jobId, input.milestoneSlug, "HANDOUT.md", handoutMd, "HANDOUT");
14947
- savedFiles.push(p7);
14948
- }
14949
- if (input.cheatSheetMarkdown) {
14950
- const p8 = await manager.saveArtifact(
14951
- input.jobId,
14952
- input.milestoneSlug,
14953
- "CHEAT_SHEET.md",
14954
- input.cheatSheetMarkdown,
14955
- "CHEAT_SHEET"
14956
- );
14957
- savedFiles.push(p8);
14958
- }
14959
- await manager.markMilestoneCompleted(input.jobId);
14960
- return {
14961
- milestoneSlug: input.milestoneSlug,
14962
- savedFiles
14963
- };
14964
- }
14965
- async function publishToGitStep(options) {
14966
- "use step";
14967
- return publishToGitHub(options);
14968
- }
14969
- async function publishToSupabaseStep(options) {
14970
- "use step";
14971
- return publishToSupabase(options);
14972
- }
14973
- var approvalPayloadSchema = zod.z.object({
14974
- approved: zod.z.boolean().describe("Whether the human reviewer approves the generated curriculum/lesson"),
14975
- reviewerName: zod.z.string().optional().describe("Name or ID of the reviewer"),
14976
- feedback: zod.z.string().optional().describe("Actionable feedback or required changes if rejected"),
14977
- timestamp: zod.z.string().optional().describe("ISO timestamp of the approval action")
14978
- });
14979
- var lessonApprovalHook = workflow.defineHook();
14980
- function approvalHookToken(projectId, lessonId, artifactType) {
14981
- return `approval:${projectId}:${lessonId}:${artifactType}`;
14982
- }
14983
-
14984
- // src/standards/standardsCoverageGate.ts
14985
- function resolveStatementRef(ref, packs) {
14986
- const [head, ...rest] = ref.split(":");
14987
- const candidatePacks = rest.length > 0 ? packs.filter((p) => p.manifest.id === head) : packs;
14988
- const statementId = rest.length > 0 ? rest.join(":") : ref;
14989
- for (const p of candidatePacks) {
14990
- if (p.statements.some((s) => s.id === statementId)) return { pack: p, statementId };
14991
- }
14992
- return null;
14993
- }
14994
- function evaluateStandardsCoverage(input) {
14995
- const rows = [];
14996
- const aoToLo = /* @__PURE__ */ new Map();
14997
- for (const ao of input.assessmentObjectives ?? []) aoToLo.set(ao.aoCode, ao.alignedLO);
14998
- const loToRefs = /* @__PURE__ */ new Map();
14999
- for (const lo of input.objectives) {
15000
- loToRefs.set(lo.code, new Set(lo.standardRefs ?? []));
15001
- }
15002
- const taughtLOs = /* @__PURE__ */ new Set();
15003
- for (const act of input.activities ?? []) for (const lo of act.lo) taughtLOs.add(lo);
15004
- const assessedLOs = /* @__PURE__ */ new Set();
15005
- for (const q of input.quizQuestions) {
15006
- if (q.alignedLO) assessedLOs.add(q.alignedLO);
15007
- if (q.alignedAO) {
15008
- const lo = aoToLo.get(q.alignedAO);
15009
- if (lo) assessedLOs.add(lo);
14829
+ if (slides && slides.lessonId !== baseLessonId) {
14830
+ score -= 15;
14831
+ findings.push({
14832
+ id: "struct_slides_id_mismatch",
14833
+ dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
14834
+ severity: "MAJOR",
14835
+ title: "Slide Deck Lesson ID Contract Mismatch",
14836
+ description: `Slide deck lessonId "${slides.lessonId}" does not match Lesson ID "${baseLessonId}".`,
14837
+ remediationAdvice: `Sync Slide deck lessonId to "${baseLessonId}".`,
14838
+ affectedElement: slides.lessonId
14839
+ });
15010
14840
  }
15011
- }
15012
- for (const pack of input.packs) {
15013
- const mappingByStatement = /* @__PURE__ */ new Map();
15014
- for (const m of pack.mappings ?? []) {
15015
- const prev = mappingByStatement.get(m.statementId);
15016
- if (!prev || prev !== "covers" && m.kind === "covers" || prev === "extends" && m.kind !== "extends") {
15017
- mappingByStatement.set(m.statementId, m.kind);
14841
+ const satellites = [
14842
+ { type: "QUIZ", lang: quiz?.language },
14843
+ { type: "ACT", lang: activity?.language },
14844
+ { type: "SLIDE", lang: slides?.language }
14845
+ ];
14846
+ for (const sat of satellites) {
14847
+ if (sat.lang && sat.lang.toLowerCase() !== baseLanguage.toLowerCase()) {
14848
+ score -= 25;
14849
+ findings.push({
14850
+ id: `struct_language_mismatch_${sat.type}`,
14851
+ dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
14852
+ severity: "CRITICAL",
14853
+ title: `Language Policy Inconsistency in ${sat.type}`,
14854
+ description: `Master Lesson is in "${baseLanguage}" but ${sat.type} is declared in "${sat.lang}".`,
14855
+ remediationAdvice: `Set ${sat.type} language to "${baseLanguage}".`
14856
+ });
15018
14857
  }
15019
14858
  }
15020
- for (const statement of pack.statements) {
15021
- const refFull = `${pack.manifest.id}:${statement.id}`;
15022
- const issues = [];
15023
- const los = input.objectives.filter((lo) => (lo.standardRefs ?? []).some((r) => r === statement.id || r === refFull)).map((lo) => lo.code);
15024
- const kind = mappingByStatement.get(statement.id);
15025
- const hasMapping = kind !== void 0;
15026
- const isComplianceRelevant = kind === "covers";
15027
- const hasActivity = los.some((lo) => taughtLOs.has(lo));
15028
- const hasAssessment = los.some((lo) => assessedLOs.has(lo));
15029
- let status;
15030
- if (!hasMapping) status = "UNMAPPED";
15031
- else if (!isComplianceRelevant) status = "PARTIAL";
15032
- else if (los.length > 0 && hasActivity && hasAssessment) status = "COVERED";
15033
- else status = "UNCOVERED";
15034
- if (status === "UNCOVERED") {
15035
- if (los.length === 0) issues.push("No learning objective anchors this statement (missing standardRefs).");
15036
- else {
15037
- if (!hasActivity) issues.push(`Anchored LO(s) ${los.join(", ")} have no activity in lesson flow.`);
15038
- if (!hasAssessment) issues.push(`Anchored LO(s) ${los.join(", ")} are not measured by any quiz question.`);
15039
- }
14859
+ if (quiz && quiz.questions) {
14860
+ for (let i = 0; i < quiz.questions.length; i++) {
14861
+ const q = quiz.questions[i];
14862
+ const qId = q.id || `Q${i + 1}`;
14863
+ const options = q.options || [];
14864
+ if (options.length < 4) {
14865
+ score -= 10;
14866
+ findings.push({
14867
+ id: `struct_quiz_option_count_${qId}`,
14868
+ dimension: "MISCONCEPTION_RIGOR",
14869
+ severity: "MAJOR",
14870
+ title: `Structural Option Count Error in ${qId}`,
14871
+ description: `Question ${qId} has ${options.length} options (standard schema requires 4).`,
14872
+ remediationAdvice: "Ensure each question has 4 options (A, B, C, D).",
14873
+ affectedElement: qId
14874
+ });
14875
+ }
14876
+ const correctCount = options.filter((o) => o.isCorrect).length;
14877
+ if (correctCount !== 1) {
14878
+ score -= 20;
14879
+ findings.push({
14880
+ id: `struct_quiz_key_count_${qId}`,
14881
+ dimension: "MISCONCEPTION_RIGOR",
14882
+ severity: "CRITICAL",
14883
+ title: `Key Assignment Error in ${qId}`,
14884
+ description: `Question ${qId} has ${correctCount} correct options (must be exactly 1).`,
14885
+ remediationAdvice: "Set `isCorrect: true` on exactly 1 option.",
14886
+ affectedElement: qId
14887
+ });
14888
+ }
15040
14889
  }
15041
- rows.push({
15042
- packId: pack.manifest.id,
15043
- statementId: statement.id,
15044
- statementText: Object.values(statement.texts)[0] ?? "",
15045
- status,
15046
- objectives: los,
15047
- hasActivity,
15048
- hasAssessment,
15049
- issues
15050
- });
15051
14890
  }
15052
- }
15053
- for (const lo of input.objectives) {
15054
- for (const r of lo.standardRefs ?? []) {
15055
- if (!resolveStatementRef(r, input.packs)) {
15056
- rows.push({
15057
- packId: "(unresolved)",
15058
- statementId: r,
15059
- statementText: "",
15060
- status: "UNCOVERED",
15061
- objectives: [lo.code],
15062
- hasActivity: false,
15063
- hasAssessment: false,
15064
- issues: [`standardRef "${r}" does not exist in any adopted pack \u2014 likely a hallucinated or stale ID`]
14891
+ if (activity?.materialsBOM?.hardware && activity.materialsBOM.hardware.length > 0) {
14892
+ const hwText = activity.materialsBOM.hardware.join(" ").toLowerCase();
14893
+ const hasLED = hwText.includes("led");
14894
+ const hasResistor = hwText.includes("\u0111i\u1EC7n tr\u1EDF") || hwText.includes("resistor") || hwText.includes("220") || hwText.includes("330") || hwText.includes("1k");
14895
+ if (hasLED && !hasResistor) {
14896
+ score -= 20;
14897
+ findings.push({
14898
+ id: "struct_hardware_unsafe_led_no_resistor",
14899
+ dimension: "TECHNICAL_AUTHENTICITY",
14900
+ severity: "CRITICAL",
14901
+ title: "Unsafe Circuit BOM: LED without Current-Limiting Resistor",
14902
+ description: "Hardware BOM includes LED without a 220\u03A9-1k\u03A9 resistor, causing circuit overload.",
14903
+ remediationAdvice: "Add a 220\u03A9 current-limiting resistor to the hardware materials list."
15065
14904
  });
15066
14905
  }
15067
14906
  }
15068
- }
15069
- const complianceRows = rows.filter((r) => r.status !== "UNMAPPED" && r.status !== "PARTIAL");
15070
- const covered = complianceRows.filter((r) => r.status === "COVERED").length;
15071
- const coveragePct = complianceRows.length === 0 ? 100 : Math.round(covered / complianceRows.length * 100);
15072
- const unresolvedCount = rows.filter((r) => r.packId === "(unresolved)").length;
15073
- const verdict = unresolvedCount > 0 || coveragePct < 60 ? "FAIL" : coveragePct < 100 ? "WARN" : "PASS";
15074
- const lines = [
15075
- "# Standards Coverage Report",
15076
- "",
15077
- `- Verdict: **${verdict}**`,
15078
- `- Compliance coverage: **${coveragePct}%** (${covered}/${complianceRows.length} "covers" statements fully taught + assessed)`,
15079
- `- Unresolved standardRefs: ${unresolvedCount}`,
15080
- "",
15081
- "| Pack | Statement | Status | LOs | Activity | Assessment |",
15082
- "|---|---|---|---|---|---|",
15083
- ...rows.map(
15084
- (r) => `|${r.packId}|${r.statementId}|${r.status}|${r.objectives.join(", ") || "\u2014"}|${r.hasActivity ? "\u2713" : "\u2717"}|${r.hasAssessment ? "\u2713" : "\u2717"}|`
15085
- )
15086
- ];
15087
- const issueLines = rows.filter((r) => r.issues.length > 0).flatMap((r) => r.issues.map((i) => `- [${r.packId}:${r.statementId}] ${i}`));
15088
- if (issueLines.length > 0) lines.push("", "## Issues", ...issueLines);
15089
- return {
15090
- verdict,
15091
- coveragePct,
15092
- rows,
15093
- summary: `Standards coverage ${coveragePct}% \u2014 verdict ${verdict}`,
15094
- rawMarkdownReport: lines.join("\n")
15095
- };
15096
- }
15097
- var StandardsRegistryAdapter = class {
15098
- client;
15099
- constructor(config = {}) {
15100
- if (config.client) {
15101
- this.client = config.client;
15102
- return;
15103
- }
15104
- const url = config.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || "";
15105
- const key = config.supabaseServiceRoleKey || process.env.SUPABASE_SERVICE_ROLE_KEY || config.supabaseAnonKey || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
15106
- if (!url || !key) {
15107
- throw new Error("StandardsRegistryAdapter: Supabase URL/key missing (pass client or env).");
15108
- }
15109
- this.client = supabaseJs.createClient(url, key);
15110
- }
15111
- // ─── Intake (write path — service role) ───────────────────────────────────
15112
- /** Persist a schema-validated pack as a new framework (status=draft). */
15113
- async importPack(pack, opts) {
15114
- const parsed = FrameworkPackSchema.safeParse(pack);
15115
- if (!parsed.success) {
15116
- throw new Error("importPack: pack failed schema validation: " + parsed.error.issues.map((i) => i.path.join(".") + ": " + i.message).join("; "));
15117
- }
15118
- const p = parsed.data;
15119
- const { data: fw, error: fwErr } = await this.client.from("standards_frameworks").insert({
15120
- pack_id: p.manifest.id,
15121
- content_version: p.manifest.contentVersion,
15122
- name: p.manifest.name,
15123
- spec_version: p.manifest.specVersion,
15124
- subject: p.manifest.subject,
15125
- languages: p.manifest.languages,
15126
- grade_model: p.manifest.gradeModel,
15127
- provenance: p.manifest.provenance,
15128
- trust: p.manifest.trust,
15129
- status: "draft",
15130
- organization_code: opts?.organizationCode ?? (p.manifest.trust === "verified" ? null : p.manifest.provenance.importedBy?.replace(/^org:/, "") || null),
15131
- original_file_path: opts?.originalFilePath ?? null,
15132
- created_by: opts?.createdBy ?? null
15133
- }).select("id").single();
15134
- if (fwErr) throw new Error("importPack: framework insert failed: " + fwErr.message);
15135
- const frameworkId = fw.id;
15136
- const statementRows = p.statements.map((s) => ({
15137
- framework_id: frameworkId,
15138
- statement_id: s.id,
15139
- parent_statement_id: s.parentId ?? null,
15140
- grade_min: s.gradeBand[0],
15141
- grade_max: s.gradeBand[1],
15142
- texts: s.texts,
15143
- classifications: s.classifications ?? [],
15144
- bloom_hint: s.bloomHint ?? null,
15145
- keywords: s.keywords ?? [],
15146
- source_ref: s.sourceRef ?? null,
15147
- provenance: s.provenance
15148
- }));
15149
- const { error: stErr, count: stCount } = await this.client.from("standards_statements").insert(statementRows, { count: "exact" });
15150
- if (stErr) throw new Error("importPack: statement insert failed: " + stErr.message);
15151
- let mappingCount = 0;
15152
- if (p.mappings && p.mappings.length > 0) {
15153
- const mappingRows = p.mappings.map((m) => ({
15154
- framework_id: frameworkId,
15155
- statement_id: m.statementId,
15156
- target_ref: m.targetRef,
15157
- kind: m.kind,
15158
- confidence: m.confidence,
15159
- provenance: m.provenance
15160
- }));
15161
- const { error: mpErr, count: mpCount } = await this.client.from("standards_mappings").insert(mappingRows, { count: "exact" });
15162
- if (mpErr) throw new Error("importPack: mapping insert failed: " + mpErr.message);
15163
- mappingCount = mpCount ?? mappingRows.length;
14907
+ score = Math.max(0, Math.min(100, score));
14908
+ const passed = score >= 80 && !findings.some((f) => f.severity === "CRITICAL");
14909
+ if (passed) {
14910
+ strengths.push("Deterministic structure, ID contracts, and schema invariants strictly verified.");
15164
14911
  }
15165
- return { frameworkDbId: frameworkId, statementsInserted: stCount ?? statementRows.length, mappingsInserted: mappingCount };
15166
- }
15167
- /** Activate a draft framework (immutable version is now live). */
15168
- async activatePack(frameworkDbId) {
15169
- const { error } = await this.client.from("standards_frameworks").update({ status: "active" }).eq("id", frameworkDbId).eq("status", "draft");
15170
- if (error) throw new Error("activatePack failed: " + error.message);
15171
- }
15172
- async deprecatePack(frameworkDbId) {
15173
- const { error } = await this.client.from("standards_frameworks").update({ status: "deprecated" }).eq("id", frameworkDbId);
15174
- if (error) throw new Error("deprecatePack failed: " + error.message);
15175
- }
15176
- async adoptPack(frameworkDbId, organizationCode, adoptedBy) {
15177
- const { error } = await this.client.from("standards_org_adoptions").upsert(
15178
- { framework_id: frameworkDbId, organization_code: organizationCode, adopted_by: adoptedBy ?? null },
15179
- { onConflict: "organization_code,framework_id" }
15180
- );
15181
- if (error) throw new Error("adoptPack failed: " + error.message);
14912
+ return {
14913
+ passed,
14914
+ structuralScore: score,
14915
+ findings,
14916
+ strengths
14917
+ };
15182
14918
  }
15183
- // ─── Runtime (read path — generation) ─────────────────────────────────────
14919
+ };
14920
+
14921
+ // src/evaluators/academicAuditor.ts
14922
+ var AcademicAuditor = class {
15184
14923
  /**
15185
- * Load all packs usable by an org: platform packs (verified, org IS NULL) +
15186
- * org packs + org adoptions. Hydrated into the same FrameworkPack shape the
15187
- * in-memory Phase-1 components consume (injector / coverage gate / judge).
14924
+ * Evaluates a complete lesson bundle.
14925
+ * Step 1 (Deterministic): Fast structural, contract, schema, and safety linting.
14926
+ * Step 2 (Semantic LLM-as-a-Judge): Deep pedagogical, cognitive (Bloom), and misconception analysis with live frontier LLM.
15188
14927
  */
15189
- async loadPacksForOrg(organizationCode) {
15190
- let query = this.client.from("standards_frameworks").select("*, standards_org_adoptions(organization_code)").eq("status", "active");
15191
- const { data: frameworks, error } = await query;
15192
- if (error) throw new Error("loadPacksForOrg: " + error.message);
15193
- const visible = (frameworks ?? []).filter((f) => {
15194
- const own = f.organization_code && organizationCode && f.organization_code === organizationCode;
15195
- const global = !f.organization_code;
15196
- const adopted = organizationCode && (f.standards_org_adoptions ?? []).some((a) => a.organization_code === organizationCode);
15197
- return global || own || adopted;
14928
+ static async auditLessonBundle(input) {
14929
+ const { lesson, quiz, activity, slides, codeLab, modelOptions, executeLLMJudge = true } = input;
14930
+ const structuralResult = DeterministicStructuralLinter.lintBundle({
14931
+ lesson,
14932
+ quiz,
14933
+ activity,
14934
+ slides,
14935
+ codeLab
15198
14936
  });
15199
- if (visible.length === 0) return [];
15200
- const ids = visible.map((f) => f.id);
15201
- const [{ data: statements, error: stErr }, { data: mappings, error: mpErr }, { data: overrides, error: ovErr }] = await Promise.all([
15202
- this.client.from("standards_statements").select("*").in("framework_id", ids),
15203
- this.client.from("standards_mappings").select("*").in("framework_id", ids),
15204
- this.client.from("standards_mapping_overrides").select("*").in("framework_id", ids)
15205
- ]);
15206
- if (stErr) throw new Error("loadPacksForOrg statements: " + stErr.message);
15207
- const overridesByFw = /* @__PURE__ */ new Map();
15208
- for (const o of overrides ?? []) {
15209
- const list = overridesByFw.get(o.framework_id) ?? [];
15210
- list.push(o);
15211
- overridesByFw.set(o.framework_id, list);
15212
- }
15213
- return visible.map((f) => {
15214
- const stmts = (statements ?? []).filter((s) => s.framework_id === f.id).map((s) => ({
15215
- id: s.statement_id,
15216
- parentId: s.parent_statement_id ?? void 0,
15217
- gradeBand: [s.grade_min, s.grade_max],
15218
- texts: s.texts,
15219
- classifications: s.classifications ?? [],
15220
- bloomHint: s.bloom_hint ?? void 0,
15221
- keywords: s.keywords ?? [],
15222
- sourceRef: s.source_ref ?? void 0,
15223
- provenance: s.provenance ?? { method: "imported" }
15224
- }));
15225
- const ov = overridesByFw.get(f.id) ?? [];
15226
- const retired = new Set(ov.filter((o) => o.kind === "retired").map((o) => o.statement_id + "|" + o.target_ref));
15227
- const embedded = (mappings ?? []).filter((m) => m.framework_id === f.id).map((m) => ({
15228
- statementId: m.statement_id,
15229
- targetRef: m.target_ref,
15230
- kind: m.kind,
15231
- confidence: Number(m.confidence),
15232
- provenance: m.provenance ?? { method: "imported" }
15233
- }));
15234
- const overlay = ov.filter((o) => o.kind !== "retired").map((o) => ({
15235
- statementId: o.statement_id,
15236
- targetRef: o.target_ref,
15237
- kind: o.kind,
15238
- confidence: Number(o.confidence),
15239
- provenance: { method: "human", reviewedBy: o.created_by ?? "overlay", note: o.note ?? void 0 }
15240
- }));
15241
- const bridge = /* @__PURE__ */ new Map();
15242
- for (const m of embedded) bridge.set(m.statementId + "|" + m.targetRef, m);
15243
- for (const m of overlay) bridge.set(m.statementId + "|" + m.targetRef, m);
15244
- const finalMappings = [...bridge.values()].filter((m) => !retired.has(m.statementId + "|" + m.targetRef));
15245
- const hydrated = FrameworkPackSchema.safeParse({
15246
- manifest: {
15247
- id: f.pack_id,
15248
- name: f.name,
15249
- specVersion: f.spec_version || "1.0",
15250
- contentVersion: f.content_version,
15251
- subject: f.subject,
15252
- languages: f.languages,
15253
- gradeModel: f.grade_model,
15254
- provenance: f.provenance,
15255
- trust: f.trust
15256
- },
15257
- statements: stmts,
15258
- mappings: finalMappings
15259
- });
15260
- if (!hydrated.success) {
15261
- throw new Error('loadPacksForOrg: hydrated pack "' + f.pack_id + '" failed schema: ' + hydrated.error.issues[0]?.message);
15262
- }
15263
- return { ...hydrated.data, dbId: f.id, status: f.status, organizationCode: f.organization_code ?? null };
15264
- });
15265
- }
15266
- };
15267
-
15268
- // src/workflow/workflows/milestoneWorkflow.ts
15269
- async function generateMilestoneWorkflow(input) {
15270
- "use workflow";
15271
- const language = input.language || "vi";
15272
- const milestoneId = input.milestone.id || input.milestone.concept_code || "L01";
15273
- const slugPrefix = /^\d+$/.test(milestoneId) ? `M${milestoneId.padStart(2, "0")}` : milestoneId;
15274
- const milestoneSlug = `${slugPrefix}_${(input.milestone.name || "milestone").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
15275
- const bundleTier = input.bundleTier || "minimum";
15276
- const gates = resolveGateSettings(input.gateSettings);
15277
- const techStack = input.techStack || input.milestone.tech_keywords || ["General Technology"];
15278
- console.log(" \u{1F6A6} [Gate] LESSON: " + gates.LESSON + " | ACT: " + gates.ACT + " | QUIZ: " + gates.QUIZ);
15279
- const targetObjectives = (input.milestone.learning_objectives || []).map((lo) => ({
15280
- code: lo.code,
15281
- description: lo.description || lo.name || "",
15282
- bloomLevel: lo.bloom_level || "understand"
15283
- }));
15284
- const packs = input.standardsPacks ?? [];
15285
- const { block: standardsContext, selected: selectedStatements } = buildStandardsContext({
15286
- packs,
15287
- gradeBand: input.milestone.grade_band ?? [6, 12],
15288
- topicText: [input.topic, input.milestone.name, input.milestone.description, ...input.milestone.tech_keywords || []].filter(Boolean).join(" "),
15289
- conceptCodes: (input.milestone.concept_code || "").split(",").map((c) => c.trim()).filter(Boolean),
15290
- language
15291
- });
15292
- const standardStatements = {};
15293
- for (const s of selectedStatements) {
15294
- standardStatements[s.packId + ":" + s.statement.id] = Object.values(s.statement.texts)[0] || "";
15295
- }
15296
- if (selectedStatements.length > 0) {
15297
- console.log(" \u{1F4D0} [Standards] Grounded with " + selectedStatements.length + " verbatim statement(s) from " + new Set(selectedStatements.map((s) => s.packId)).size + " pack(s)");
15298
- }
15299
- let lesson = await generateMasterLessonStep({
15300
- milestone: input.milestone,
15301
- language,
15302
- topic: input.topic,
15303
- targetAudience: input.targetAudience,
15304
- standardsContext: standardsContext || void 0,
15305
- modelOptions: input.modelOptions
15306
- });
15307
- let auditReport;
15308
- const lessonGate = gateModeFor(gates, "LESSON");
15309
- if (lessonGate === "LLM_JUDGE") {
15310
- auditReport = await judgeMasterLessonStep({
15311
- lessonId: milestoneId,
15312
- lesson,
15313
- language,
15314
- targetObjectives,
15315
- standardStatements: Object.keys(standardStatements).length > 0 ? standardStatements : void 0,
15316
- modelOptions: input.modelOptions
15317
- });
15318
- if (auditReport.overallVerdict === "FAIL" || auditReport.overallVerdict === "NEEDS_REVISION") {
15319
- lesson = await repairMasterLessonStep({
15320
- milestone: input.milestone,
15321
- language,
15322
- topic: input.topic,
15323
- targetAudience: input.targetAudience,
15324
- auditReport,
15325
- standardsContext: standardsContext || void 0,
15326
- modelOptions: input.modelOptions
15327
- });
15328
- }
15329
- } else if (lessonGate === "HITL") {
15330
- const hookToken = "gate:lesson:" + milestoneId;
15331
- console.log(" \u23F8 [Gate] LESSON awaiting HUMAN review (HITL) \u2014 hook " + hookToken);
15332
- const workflowPkg = await import('workflow');
15333
- const createHook = workflowPkg.createHook;
15334
- const hook = createHook({ token: hookToken });
15335
- const review = await hook;
15336
- console.log(" \u2705 [Gate] Human verdict: " + (review.approved ? "APPROVED" : "REJECTED") + (review.feedback ? " \u2014 " + review.feedback : ""));
15337
- if (!review.approved) {
15338
- const repairReport = {
15339
- overallVerdict: "NEEDS_REVISION",
15340
- totalScore: 0,
15341
- languageAdherencePassed: true,
15342
- criteria: [],
15343
- actionableRepairPrompts: review.feedback ? [review.feedback] : ["S\u1EEDa theo ph\u1EA3n h\u1ED3i c\u1EE7a ng\u01B0\u1EDDi duy\u1EC7t"]
15344
- };
15345
- lesson = await repairMasterLessonStep({
15346
- milestone: input.milestone,
15347
- language,
15348
- topic: input.topic,
15349
- targetAudience: input.targetAudience,
15350
- auditReport: repairReport,
15351
- standardsContext: standardsContext || void 0,
15352
- modelOptions: input.modelOptions
15353
- });
15354
- const hook2 = createHook({ token: hookToken + ":v2" });
15355
- const review2 = await hook2;
15356
- console.log(" \u2705 [Gate] Human verdict (v2): " + (review2.approved ? "APPROVED" : "REJECTED"));
15357
- if (!review2.approved) {
15358
- console.log(" \u{1F6D1} [Gate] LESSON rejected twice \u2014 satellites & save BLOCKED");
15359
- return {
15360
- milestoneId,
15361
- milestoneSlug,
15362
- status: "FAILED",
15363
- gateBlocked: { artifactType: "LESSON", reason: "HITL_REJECTED_V2", feedback: review2.feedback },
15364
- artifacts: void 0
15365
- };
15366
- }
15367
- }
15368
- }
15369
- const [activity, selfLab, quiz, slides, handout] = await Promise.all([
15370
- generateActivityStep({ lesson, language, modelOptions: input.modelOptions }),
15371
- generateSelfLabStep({ milestone: input.milestone, language, targetTechStack: techStack, modelOptions: input.modelOptions }),
15372
- generateDiagnosticQuizStep({ milestone: input.milestone, language, modelOptions: input.modelOptions }),
15373
- generateSlidesStep({ lesson, language, modelOptions: input.modelOptions }),
15374
- generateHandoutStep({ lesson, language, modelOptions: input.modelOptions })
15375
- ]);
15376
- const codeLab = await generateCodeLabStep({
15377
- lesson,
15378
- activity,
15379
- language,
15380
- targetTechStack: techStack,
15381
- modelOptions: input.modelOptions
15382
- });
15383
- const satelliteReports = {};
15384
- const satelliteCandidates = [
15385
- { type: "ACT", id: "ACT_" + milestoneId, artifact: activity },
15386
- { type: "QUIZ", id: "QUIZ_" + milestoneId, artifact: quiz },
15387
- { type: "SLIDE", id: "SLIDE_" + milestoneId, artifact: slides },
15388
- { type: "HANDOUT", id: "HANDOUT_" + milestoneId, artifact: handout },
15389
- { type: "CODE", id: "CODE_" + milestoneId, artifact: codeLab }
15390
- ];
15391
- const toJudge = satelliteCandidates.filter((c) => c.artifact && gateModeFor(gates, c.type) === "LLM_JUDGE");
15392
- if (toJudge.length > 0) {
15393
- const reports = await Promise.all(
15394
- toJudge.map(
15395
- (c) => judgeSatelliteStep({
15396
- artifactType: c.type,
15397
- artifactId: c.id,
15398
- artifact: c.artifact,
15399
- language,
15400
- targetObjectives,
15401
- standardStatements: Object.keys(standardStatements).length > 0 ? standardStatements : void 0,
15402
- modelOptions: input.modelOptions
15403
- }).catch((err) => {
15404
- console.warn(" \u26A0 [Gate] satellite judge failed for " + c.type + ": " + err.message);
15405
- return void 0;
15406
- })
15407
- )
15408
- );
15409
- for (let i = 0; i < toJudge.length; i++) {
15410
- const r = reports[i];
15411
- if (r) satelliteReports[toJudge[i].type] = r;
15412
- }
15413
- }
15414
- let worksheet;
15415
- let teacherGuide;
15416
- let extension;
15417
- if (bundleTier === "full") {
15418
- [worksheet, teacherGuide, extension] = await Promise.all([
15419
- generateWorksheetStep({ lesson, language, modelOptions: input.modelOptions }),
15420
- generateTeacherGuideStep({ lesson, activity, language, modelOptions: input.modelOptions }),
15421
- generateExtensionStep({ lesson, activity, language, modelOptions: input.modelOptions })
15422
- ]);
15423
- }
15424
- let standardsCoverage;
15425
- if (packs.length > 0) {
15426
- standardsCoverage = evaluateStandardsCoverage({
15427
- packs,
15428
- objectives: (lesson.learningObjectives || []).map((lo) => ({
15429
- code: lo.code,
15430
- standardRefs: lo.standardRefs,
15431
- conceptRefs: lo.conceptRefs
15432
- })),
15433
- // NOTE: ActivityLab has no structured LO linkage yet — activity coverage rows are derived
15434
- // from LO→quiz paths inside the gate. Wire ACT.loRefs in a future schema revision.
15435
- activities: [],
15436
- quizQuestions: (quiz?.questions || []).map((q) => ({ alignedLO: q.alignedLO, alignedAO: q.alignedAO })),
15437
- assessmentObjectives: quiz?.assessmentObjectives?.map((ao) => ({ aoCode: ao.aoCode, alignedLO: ao.alignedLO }))
15438
- });
15439
- console.log(" \u{1F4D0} [Standards Coverage] " + standardsCoverage.summary);
15440
- const enforce = input.enforceStandardsCoverage ?? true;
15441
- if (enforce && standardsCoverage.verdict === "FAIL") {
15442
- console.error(" \u26D4 [Standards Coverage] HARD BLOCK \u2014 " + standardsCoverage.summary);
15443
- console.error(standardsCoverage.rawMarkdownReport.split("\n").filter((l) => l.startsWith("- [")).slice(0, 10).join("\n"));
15444
- return {
15445
- milestoneId,
15446
- milestoneSlug,
15447
- status: "FAILED",
15448
- savedResult: { workspaceDir: "", files: [] },
15449
- judgeReport: auditReport,
15450
- standardsCoverage,
15451
- artifacts: { lesson, activity, codeLab, selfLab, quiz, slides, handout, worksheet, teacherGuide, extension }
15452
- };
15453
- }
15454
- }
15455
- const savedResult = await saveMilestoneToWorkspaceStep({
15456
- jobId: input.jobId,
15457
- milestoneSlug,
15458
- lesson,
15459
- activity,
15460
- codeLab,
15461
- selfLab,
15462
- quiz,
15463
- slides,
15464
- handout,
15465
- baseWorkspaceDir: input.baseWorkspaceDir
15466
- });
15467
- return {
15468
- milestoneId,
15469
- milestoneSlug,
15470
- status: "SUCCESS",
15471
- savedResult,
15472
- judgeReport: auditReport,
15473
- satelliteJudgeReports: Object.keys(satelliteReports).length > 0 ? satelliteReports : void 0,
15474
- gatesResolved: gates,
15475
- standardsCoverage,
15476
- artifacts: {
15477
- lesson,
15478
- activity,
15479
- codeLab,
15480
- selfLab,
15481
- quiz,
15482
- slides,
15483
- handout,
15484
- worksheet,
15485
- teacherGuide,
15486
- extension
15487
- }
15488
- };
15489
- }
15490
- async function generateRoadmapWorkflow(input) {
15491
- "use workflow";
15492
- const jobId = input.jobId || `job_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
15493
- const language = input.language || input.roadmap.target_language || "vi";
15494
- const topic = input.roadmap.topic || input.roadmap.goal || "General Curriculum";
15495
- const courseTitle = input.roadmap.title || `Curriculum: ${topic}`;
15496
- const batchSize = input.batchSize || 2;
15497
- const milestones = input.roadmap.milestones || [];
15498
- const workspaceManager = new LocalWorkspaceManager(input.baseWorkspaceDir);
15499
- const workspaceDir = await workspaceManager.initJobWorkspace(jobId, {
15500
- courseTitle,
15501
- topic,
15502
- language,
15503
- totalMilestones: milestones.length
15504
- });
15505
- const milestoneOutcomes = [];
15506
- for (let i = 0; i < milestones.length; i += batchSize) {
15507
- const batch = milestones.slice(i, i + batchSize);
15508
- const batchSettled = await Promise.allSettled(
15509
- batch.map(
15510
- (milestone) => generateMilestoneWorkflow({
15511
- jobId,
15512
- milestone,
15513
- language,
15514
- topic,
15515
- targetAudience: input.roadmap.target_audience,
15516
- techStack: input.roadmap.tech_stack || milestone.tech_keywords,
15517
- bundleTier: input.bundleTier,
15518
- gateSettings: input.gateSettings,
15519
- baseWorkspaceDir: input.baseWorkspaceDir,
15520
- modelOptions: input.modelOptions
15521
- })
15522
- )
15523
- );
15524
- for (const [idx, outcome] of batchSettled.entries()) {
15525
- const targetMilestone = batch[idx];
15526
- const mId = targetMilestone.id || targetMilestone.concept_code || `M${i + idx + 1}`;
15527
- if (outcome.status === "fulfilled") {
15528
- milestoneOutcomes.push({
15529
- milestoneId: mId,
15530
- status: "FULFILLED",
15531
- result: outcome.value
14937
+ const allFindings = [...structuralResult.findings];
14938
+ const strengths = [...structuralResult.strengths];
14939
+ let semanticScore = null;
14940
+ let semanticVerdict = "PASS";
14941
+ if (executeLLMJudge) {
14942
+ try {
14943
+ const judgeReport = await auditCurriculumQualityFlow({
14944
+ targetArtifactType: "LESSON_BUNDLE",
14945
+ lessonId: lesson.lessonId,
14946
+ expectedLanguage: lesson.language,
14947
+ targetObjectives: (lesson.learningObjectives || []).map((lo) => ({
14948
+ code: lo.code,
14949
+ description: lo.description,
14950
+ bloomLevel: lo.bloomLevel
14951
+ })),
14952
+ generatedContentJson: JSON.stringify({ lesson, quiz, activity, slides, codeLab }),
14953
+ modelOptions
15532
14954
  });
15533
- } else {
15534
- milestoneOutcomes.push({
15535
- milestoneId: mId,
15536
- status: "REJECTED",
15537
- error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)
14955
+ semanticScore = judgeReport.totalScore;
14956
+ semanticVerdict = judgeReport.overallVerdict;
14957
+ for (const criterion of judgeReport.criteria) {
14958
+ if (!criterion.passed) {
14959
+ allFindings.push({
14960
+ id: `llm_judge_${criterion.name.toLowerCase().replace(/\s+/g, "_")}`,
14961
+ dimension: "CONSTRUCTIVE_ALIGNMENT",
14962
+ severity: criterion.score < 50 ? "CRITICAL" : "MAJOR",
14963
+ title: `LLM-as-Judge Finding: ${criterion.name}`,
14964
+ description: criterion.feedback,
14965
+ remediationAdvice: judgeReport.actionableRepairPrompts.join("; ") || "Refine prompt context."
14966
+ });
14967
+ } else {
14968
+ strengths.push(`[LLM-Judge] ${criterion.name}: ${criterion.feedback}`);
14969
+ }
14970
+ }
14971
+ } catch (err) {
14972
+ semanticScore = null;
14973
+ semanticVerdict = "FAIL";
14974
+ allFindings.push({
14975
+ id: "llm_judge_connection_error",
14976
+ dimension: "CONSTRUCTIVE_ALIGNMENT",
14977
+ severity: "MINOR",
14978
+ title: "LLM-as-Judge Skipped / Fallback",
14979
+ description: `Semantic inference error: ${err.message || String(err)}`,
14980
+ remediationAdvice: "Check API credentials for LLM-as-Judge."
15538
14981
  });
15539
14982
  }
15540
14983
  }
15541
- if (i + batchSize < milestones.length) {
15542
- await workflow.sleep("2s");
14984
+ const criticalCount = allFindings.filter((f) => f.severity === "CRITICAL").length;
14985
+ const overallScore = executeLLMJudge && semanticScore !== null ? Math.round(structuralResult.structuralScore * 0.4 + semanticScore * 0.6) : structuralResult.structuralScore;
14986
+ const passed = structuralResult.passed && criticalCount === 0 && (executeLLMJudge ? semanticVerdict === "PASS" : true);
14987
+ let verdict = "REJECTED";
14988
+ if (overallScore >= 90 && criticalCount === 0) {
14989
+ verdict = "EXEMPLARY";
14990
+ } else if (overallScore >= 75 && criticalCount === 0) {
14991
+ verdict = "ACADEMICALLY_SOUND";
14992
+ } else if (overallScore >= 60) {
14993
+ verdict = "NEEDS_PEDAGOGICAL_REFINEMENT";
15543
14994
  }
14995
+ const summary = passed ? `\u2705 Lesson Bundle "${lesson.title}" (${lesson.lessonId}) passes academic & structural verification with score ${overallScore}/100 (${verdict}).` : `\u26A0\uFE0F Lesson Bundle "${lesson.title}" requires revision (${overallScore}/100 - ${verdict}). Found ${allFindings.length} finding(s) with ${criticalCount} critical blocker(s).`;
14996
+ const actionablePromptGuidance = allFindings.map(
14997
+ (f, idx) => `[${f.dimension}] ${idx + 1}. ${f.title}: ${f.remediationAdvice}`
14998
+ );
14999
+ const computeDimScore = (dimFindings2) => {
15000
+ const hasCritical = dimFindings2.some((f) => f.severity === "CRITICAL");
15001
+ const majorCount = dimFindings2.filter((f) => f.severity === "MAJOR").length;
15002
+ const minorCount = dimFindings2.filter((f) => f.severity === "MINOR").length;
15003
+ let dimScore = 100 - (hasCritical ? 40 : 0) - majorCount * 15 - minorCount * 5;
15004
+ dimScore = Math.max(0, Math.min(100, dimScore));
15005
+ return { score: dimScore, passed: dimScore >= 75 && !hasCritical };
15006
+ };
15007
+ const dimFindings = {
15008
+ constructiveAlignment: allFindings.filter((f) => f.dimension === "CONSTRUCTIVE_ALIGNMENT"),
15009
+ bloomProgression: allFindings.filter((f) => f.dimension === "BLOOM_PROGRESSION"),
15010
+ fiveEFidelity: allFindings.filter((f) => f.dimension === "5E_INSTRUCTIONAL_FIDELITY"),
15011
+ misconceptionRigor: allFindings.filter((f) => f.dimension === "MISCONCEPTION_RIGOR"),
15012
+ technicalAuthenticity: allFindings.filter((f) => f.dimension === "TECHNICAL_AUTHENTICITY")
15013
+ };
15014
+ const dimScores = Object.fromEntries(
15015
+ Object.entries(dimFindings).map(([k, v]) => [k, computeDimScore(v)])
15016
+ );
15017
+ return {
15018
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15019
+ targetId: lesson.lessonId,
15020
+ targetType: "BUNDLE",
15021
+ overallScore,
15022
+ passed,
15023
+ verdict,
15024
+ summary,
15025
+ dimensionScores: {
15026
+ constructiveAlignment: {
15027
+ score: dimScores.constructiveAlignment.score,
15028
+ weight: 0.2,
15029
+ passed: dimScores.constructiveAlignment.passed,
15030
+ strengths: dimScores.constructiveAlignment.passed ? ["Constructive alignment verified."] : [],
15031
+ findings: dimFindings.constructiveAlignment
15032
+ },
15033
+ bloomProgression: {
15034
+ score: dimScores.bloomProgression.score,
15035
+ weight: 0.2,
15036
+ passed: dimScores.bloomProgression.passed,
15037
+ strengths: dimScores.bloomProgression.passed ? ["Bloom taxonomy progression verified."] : [],
15038
+ findings: dimFindings.bloomProgression
15039
+ },
15040
+ fiveEFidelity: {
15041
+ score: dimScores.fiveEFidelity.score,
15042
+ weight: 0.15,
15043
+ passed: dimScores.fiveEFidelity.passed,
15044
+ strengths: dimScores.fiveEFidelity.passed ? ["5E instructional fidelity verified."] : [],
15045
+ findings: dimFindings.fiveEFidelity
15046
+ },
15047
+ misconceptionRigor: {
15048
+ score: dimScores.misconceptionRigor.score,
15049
+ weight: 0.2,
15050
+ passed: dimScores.misconceptionRigor.passed,
15051
+ strengths: dimScores.misconceptionRigor.passed ? ["Misconception rigor verified."] : [],
15052
+ findings: dimFindings.misconceptionRigor
15053
+ },
15054
+ technicalAuthenticity: {
15055
+ score: dimScores.technicalAuthenticity.score,
15056
+ weight: 0.15,
15057
+ passed: dimScores.technicalAuthenticity.passed,
15058
+ strengths: dimScores.technicalAuthenticity.passed ? ["Technical authenticity verified."] : [],
15059
+ findings: dimFindings.technicalAuthenticity
15060
+ },
15061
+ crossArtifactZeroDrift: {
15062
+ score: structuralResult.structuralScore,
15063
+ weight: 0.1,
15064
+ passed: structuralResult.passed,
15065
+ strengths: structuralResult.passed ? ["Cross-artifact zero drift verified."] : [],
15066
+ findings: allFindings.filter((f) => f.dimension === "CROSS_ARTIFACT_ZERO_DRIFT")
15067
+ }
15068
+ },
15069
+ criticalFindingsCount: criticalCount,
15070
+ allFindings,
15071
+ actionablePromptGuidance
15072
+ };
15544
15073
  }
15545
- const successfulMilestones = milestoneOutcomes.filter((m) => m.status === "FULFILLED").length;
15546
- const failedMilestones = milestoneOutcomes.filter((m) => m.status === "REJECTED").length;
15547
- let gitPublishResult = void 0;
15548
- let supabasePublishResult = void 0;
15549
- if (input.gitPublishOptions) {
15550
- gitPublishResult = await publishToGitStep({
15551
- jobId,
15552
- localJobDir: workspaceDir,
15553
- ...input.gitPublishOptions
15554
- });
15555
- }
15556
- if (input.supabasePublishOptions) {
15557
- supabasePublishResult = await publishToSupabaseStep({
15558
- jobId,
15559
- courseTitle,
15560
- topic,
15561
- language,
15562
- milestones: milestoneOutcomes.filter((m) => m.status === "FULFILLED" && m.result).map((m) => ({
15563
- milestoneId: m.milestoneId,
15564
- milestoneName: m.result.savedResult.milestoneSlug,
15565
- lessonSlug: m.result.savedResult.milestoneSlug
15566
- })),
15567
- ...input.supabasePublishOptions
15568
- });
15569
- }
15570
- return {
15571
- jobId,
15572
- courseTitle,
15573
- topic,
15574
- language,
15575
- totalMilestones: milestones.length,
15576
- successfulMilestones,
15577
- failedMilestones,
15578
- results: milestoneOutcomes,
15579
- gitPublishResult,
15580
- supabasePublishResult,
15581
- workspaceDir
15582
- };
15583
- }
15074
+ };
15584
15075
 
15585
- // src/workflow/workflows/singleArtifactWorkflow.ts
15586
- async function executeSingleArtifactStep(request) {
15587
- "use step";
15588
- const t0 = Date.now();
15589
- console.log(` \u23F3 [Step: Single Artifact] Generating ${request.artifactType.toUpperCase()} for "${request.topic.slice(0, 35)}"...`);
15590
- const res = await withRateLimitBackoff({
15591
- stepName: `generate-single-artifact-${request.artifactType}-${(request.topic || "topic").slice(0, 20)}`,
15592
- fn: async () => generateSingleArtifact(request)
15593
- });
15594
- console.log(` \u2705 [Step: Single Artifact] Done in ${((Date.now() - t0) / 1e3).toFixed(1)}s (File: "${res.filename}")`);
15595
- return res;
15596
- }
15597
- async function generateSingleArtifactWorkflow(request) {
15598
- "use workflow";
15599
- return executeSingleArtifactStep(request);
15600
- }
15601
-
15602
- // src/index.ts
15603
- init_errors();
15604
-
15605
- // src/evaluators/deterministicStructuralLinter.ts
15606
- var DeterministicStructuralLinter = class {
15076
+ // src/evaluators/bloomTaxonomyEvaluator.ts
15077
+ var BLOOM_ACTION_VERBS = {
15078
+ remember: [
15079
+ "list",
15080
+ "define",
15081
+ "recall",
15082
+ "state",
15083
+ "name",
15084
+ "identify",
15085
+ "label",
15086
+ "recognize",
15087
+ "li\u1EC7t k\xEA",
15088
+ "\u0111\u1ECBnh ngh\u0129a",
15089
+ "g\u1ECDi t\xEAn",
15090
+ "nh\u1EADn di\u1EC7n",
15091
+ "ch\u1EC9 ra",
15092
+ "nh\u1EAFc l\u1EA1i",
15093
+ "ghi nh\u1EDB"
15094
+ ],
15095
+ understand: [
15096
+ "explain",
15097
+ "describe",
15098
+ "summarize",
15099
+ "classify",
15100
+ "interpret",
15101
+ "predict",
15102
+ "trace",
15103
+ "paraphrase",
15104
+ "gi\u1EA3i th\xEDch",
15105
+ "m\xF4 t\u1EA3",
15106
+ "t\xF3m t\u1EAFt",
15107
+ "ph\xE2n lo\u1EA1i",
15108
+ "di\u1EC5n gi\u1EA3i",
15109
+ "d\u1EF1 \u0111o\xE1n",
15110
+ "l\u1EA7n theo",
15111
+ "hi\u1EC3u"
15112
+ ],
15113
+ apply: [
15114
+ "implement",
15115
+ "execute",
15116
+ "calculate",
15117
+ "solve",
15118
+ "construct",
15119
+ "debug",
15120
+ "modify",
15121
+ "build",
15122
+ "\xE1p d\u1EE5ng",
15123
+ "th\u1EF1c thi",
15124
+ "t\xEDnh to\xE1n",
15125
+ "gi\u1EA3i quy\u1EBFt",
15126
+ "x\xE2y d\u1EF1ng",
15127
+ "s\u1EEDa l\u1ED7i",
15128
+ "l\u1EAFp \u0111\u1EB7t",
15129
+ "vi\u1EBFt m\xE3",
15130
+ "l\u1EADp tr\xECnh"
15131
+ ],
15132
+ analyze: [
15133
+ "compare",
15134
+ "contrast",
15135
+ "decompose",
15136
+ "differentiate",
15137
+ "troubleshoot",
15138
+ "diagnose",
15139
+ "deconstruct",
15140
+ "so s\xE1nh",
15141
+ "\u0111\u1ED1i chi\u1EBFu",
15142
+ "ph\xE2n t\xEDch",
15143
+ "ph\xE2n r\xE3",
15144
+ "ch\u1EA9n \u0111o\xE1n",
15145
+ "t\xECm nguy\xEAn nh\xE2n g\u1ED1c",
15146
+ "b\xF3c t\xE1ch"
15147
+ ],
15148
+ evaluate: [
15149
+ "justify",
15150
+ "critique",
15151
+ "assess",
15152
+ "defend",
15153
+ "argue",
15154
+ "benchmark",
15155
+ "prioritize",
15156
+ "\u0111\xE1nh gi\xE1",
15157
+ "bi\u1EC7n minh",
15158
+ "ph\xEA ph\xE1n",
15159
+ "th\u1EA9m \u0111\u1ECBnh",
15160
+ "b\u1EA3o v\u1EC7 quan \u0111i\u1EC3m",
15161
+ "l\u1EF1a ch\u1ECDn t\u1ED1i \u01B0u"
15162
+ ],
15163
+ create: [
15164
+ "design",
15165
+ "synthesize",
15166
+ "architect",
15167
+ "formulate",
15168
+ "invent",
15169
+ "devise",
15170
+ "author",
15171
+ "thi\u1EBFt k\u1EBF",
15172
+ "t\u1ED5ng h\u1EE3p",
15173
+ "s\xE1ng t\u1EA1o",
15174
+ "ki\u1EBFn tr\xFAc",
15175
+ "ph\xE1t minh",
15176
+ "ho\xE0n thi\u1EC7n \u0111\u1ED3 \xE1n"
15177
+ ]
15178
+ };
15179
+ var RECALL_PATTERNS = [
15180
+ /^(?:chức năng chính của|định nghĩa của|cú pháp của|lệnh nào là|what is the definition of|which keyword|what does .* stand for)/i,
15181
+ /(?:là gì\?|được gọi là gì\?|có ý nghĩa gì\?)/i
15182
+ ];
15183
+ var BloomTaxonomyEvaluator = class {
15607
15184
  /**
15608
- * Validates structural invariants across a complete lesson bundle.
15185
+ * Audits a LessonPlan for Bloom taxonomy fidelity and cognitive scaffolding.
15609
15186
  */
15610
- static lintBundle(input) {
15611
- const { lesson, quiz, activity, slides, codeLab } = input;
15187
+ static evaluateLesson(lesson) {
15612
15188
  const findings = [];
15613
15189
  const strengths = [];
15614
15190
  let score = 100;
15615
- const baseLessonId = lesson.lessonId;
15616
- const baseLanguage = lesson.language;
15617
- if (!lesson.title || lesson.title.trim().length === 0) {
15618
- score -= 20;
15619
- findings.push({
15620
- id: "struct_missing_lesson_title",
15621
- dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15622
- severity: "CRITICAL",
15623
- title: "Missing Lesson Title",
15624
- description: "Lesson plan has an empty or whitespace title.",
15625
- remediationAdvice: "Provide a non-empty lesson title."
15626
- });
15191
+ const los = lesson.learningObjectives || [];
15192
+ if (los.length === 0) {
15193
+ return {
15194
+ score: 0,
15195
+ weight: 0.2,
15196
+ passed: false,
15197
+ strengths: [],
15198
+ findings: [
15199
+ {
15200
+ id: "bloom_missing_los",
15201
+ dimension: "BLOOM_PROGRESSION",
15202
+ severity: "CRITICAL",
15203
+ title: "Missing Learning Objectives",
15204
+ description: "The lesson plan contains zero declared Learning Objectives.",
15205
+ remediationAdvice: "Add 2-4 clearly articulated LOs with Bloom levels and observable success criteria."
15206
+ }
15207
+ ]
15208
+ };
15627
15209
  }
15628
- if (!lesson.learningObjectives || lesson.learningObjectives.length === 0) {
15629
- score -= 25;
15630
- findings.push({
15631
- id: "struct_empty_learning_objectives",
15632
- dimension: "CONSTRUCTIVE_ALIGNMENT",
15633
- severity: "CRITICAL",
15634
- title: "Empty Learning Objectives Array",
15635
- description: "Lesson plan contains 0 learning objectives.",
15636
- remediationAdvice: "Declare at least 2 structured Learning Objectives in the schema."
15637
- });
15210
+ for (const lo of los) {
15211
+ const declaredLevel = (lo.bloomLevel || "").toLowerCase();
15212
+ const text = `${lo.name || ""} ${lo.description || ""} ${lo.successCriteria || ""}`.toLowerCase();
15213
+ const isRecallPattern = RECALL_PATTERNS.some((p) => p.test(text));
15214
+ if (isRecallPattern && (declaredLevel === "apply" || declaredLevel === "analyze" || declaredLevel === "evaluate")) {
15215
+ score -= 15;
15216
+ findings.push({
15217
+ id: `bloom_inflation_${lo.code}`,
15218
+ dimension: "BLOOM_PROGRESSION",
15219
+ severity: "MAJOR",
15220
+ title: `Bloom Level Inflation in LO "${lo.code}"`,
15221
+ description: `LO is tagged as "${lo.bloomLevel}" but its phrasing reflects low-level Recall ("${lo.description}").`,
15222
+ remediationAdvice: `Rewrite the objective using authentic "${declaredLevel}" active verbs (e.g. build, debug, compare, diagnose) with an explicit application context.`,
15223
+ affectedElement: lo.code
15224
+ });
15225
+ }
15226
+ if (!lo.successCriteria || lo.successCriteria.trim().length < 15) {
15227
+ score -= 10;
15228
+ findings.push({
15229
+ id: `bloom_unmeasurable_criteria_${lo.code}`,
15230
+ dimension: "BLOOM_PROGRESSION",
15231
+ severity: "MINOR",
15232
+ title: `Vague Success Criteria in LO "${lo.code}"`,
15233
+ description: `Success criteria "${lo.successCriteria || ""}" is too brief to provide objective student evidence.`,
15234
+ remediationAdvice: 'Specify concrete, observable evidence (e.g. "LED blinks with 1s period without circuit shorting").',
15235
+ affectedElement: lo.code
15236
+ });
15237
+ } else {
15238
+ strengths.push(`LO "${lo.code}" defines concrete observable student evidence.`);
15239
+ }
15638
15240
  }
15639
- if (!lesson.sections || lesson.sections.length === 0) {
15640
- score -= 25;
15241
+ const diff = lesson.differentiation;
15242
+ if (diff) {
15243
+ if (!diff.bronzeTier || !diff.silverTier || !diff.goldTier) {
15244
+ score -= 15;
15245
+ findings.push({
15246
+ id: "bloom_incomplete_differentiation",
15247
+ dimension: "BLOOM_PROGRESSION",
15248
+ severity: "MAJOR",
15249
+ title: "Incomplete 3-Tier Scaffolding",
15250
+ description: "Differentiation must provide distinct Bronze (Foundation), Silver (Application), and Gold (Extension) challenges.",
15251
+ remediationAdvice: "Define all 3 tiers with increasing cognitive demands (Remember/Understand -> Apply -> Analyze/Create)."
15252
+ });
15253
+ } else {
15254
+ strengths.push("Complete 3-tier scaffolding (Bronze -> Silver -> Gold) present in Lesson Plan.");
15255
+ }
15256
+ } else {
15257
+ score -= 20;
15641
15258
  findings.push({
15642
- id: "struct_empty_lesson_sections",
15643
- dimension: "5E_INSTRUCTIONAL_FIDELITY",
15259
+ id: "bloom_missing_differentiation",
15260
+ dimension: "BLOOM_PROGRESSION",
15644
15261
  severity: "CRITICAL",
15645
- title: "Empty Lesson Sections Array",
15646
- description: "Lesson plan contains no instructional sections.",
15647
- remediationAdvice: "Provide structured lesson flow sections."
15262
+ title: "Missing Differentiation Scaffolding",
15263
+ description: "Lesson plan lacks differentiation tiers.",
15264
+ remediationAdvice: "Provide tiered practice instructions for varied learner paces."
15648
15265
  });
15649
15266
  }
15650
- if (quiz && quiz.quizId !== baseLessonId && !quiz.quizId.includes(baseLessonId)) {
15651
- score -= 15;
15267
+ score = Math.max(0, Math.min(100, score));
15268
+ return {
15269
+ score,
15270
+ weight: 0.2,
15271
+ passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
15272
+ strengths,
15273
+ findings
15274
+ };
15275
+ }
15276
+ /**
15277
+ * Audits a Diagnostic Quiz for true cognitive depth matching declared Bloom levels.
15278
+ */
15279
+ static evaluateQuiz(quiz) {
15280
+ const findings = [];
15281
+ const strengths = [];
15282
+ let score = 100;
15283
+ const questions = quiz.questions || [];
15284
+ if (questions.length < 3) {
15285
+ score -= 30;
15652
15286
  findings.push({
15653
- id: "struct_quiz_id_mismatch",
15654
- dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15287
+ id: "bloom_quiz_too_short",
15288
+ dimension: "BLOOM_PROGRESSION",
15655
15289
  severity: "MAJOR",
15656
- title: "Quiz ID Contract Mismatch",
15657
- description: `Quiz ID "${quiz.quizId}" does not match Lesson ID "${baseLessonId}".`,
15658
- remediationAdvice: `Sync Quiz quizId to "${baseLessonId}".`,
15659
- affectedElement: quiz.quizId
15290
+ title: "Insufficient Question Pool",
15291
+ description: `Quiz only contains ${questions.length} questions. Minimum diagnostic threshold is 3 questions.`,
15292
+ remediationAdvice: "Generate at least 3-5 diagnostic questions covering foundational through analytical depth."
15660
15293
  });
15661
15294
  }
15662
- if (activity && activity.lessonId !== baseLessonId) {
15663
- score -= 15;
15664
- findings.push({
15665
- id: "struct_act_id_mismatch",
15666
- dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15295
+ let understandCount = 0;
15296
+ let applyCount = 0;
15297
+ let analyzeCount = 0;
15298
+ for (let i = 0; i < questions.length; i++) {
15299
+ const q = questions[i];
15300
+ const qId = q.id || `Q${i + 1}`;
15301
+ const declaredBloom = (q.bloomLevel || "").toLowerCase();
15302
+ const stem = q.scenarioOrStem || "";
15303
+ if (declaredBloom === "understand") understandCount++;
15304
+ if (declaredBloom === "apply") applyCount++;
15305
+ if (declaredBloom === "analyze" || declaredBloom === "evaluate") analyzeCount++;
15306
+ const isRecall = RECALL_PATTERNS.some((p) => p.test(stem));
15307
+ if (isRecall && (declaredBloom === "apply" || declaredBloom === "analyze")) {
15308
+ score -= 15;
15309
+ findings.push({
15310
+ id: `bloom_quiz_misclassification_${qId}`,
15311
+ dimension: "BLOOM_PROGRESSION",
15312
+ severity: "MAJOR",
15313
+ title: `Cognitive Level Misclassification in ${qId}`,
15314
+ description: `Question stem "${stem.substring(0, 60)}..." is pure Recall/Definition, but is tagged as "${q.bloomLevel}".`,
15315
+ remediationAdvice: 'Either re-tag as "understand" or transform the stem into an authentic problem-solving scenario requiring code debugging or design decision.',
15316
+ affectedElement: qId
15317
+ });
15318
+ }
15319
+ if ((declaredBloom === "apply" || declaredBloom === "analyze") && !q.codeSnippet && !stem.includes("```") && !stem.includes("m\u1EA1ch")) {
15320
+ score -= 10;
15321
+ findings.push({
15322
+ id: `bloom_quiz_missing_code_context_${qId}`,
15323
+ dimension: "BLOOM_PROGRESSION",
15324
+ severity: "MINOR",
15325
+ title: `Missing Practical Context in High-Bloom Question ${qId}`,
15326
+ description: `Question ${qId} tagged as "${q.bloomLevel}" lacks concrete code or circuit context to evaluate hands-on execution.`,
15327
+ remediationAdvice: "Include an annotated code snippet or circuit state for students to trace, debug, or evaluate.",
15328
+ affectedElement: qId
15329
+ });
15330
+ }
15331
+ }
15332
+ if (questions.length > 0 && (understandCount > 0 || applyCount > 0)) {
15333
+ strengths.push(`Quiz features multi-level cognitive questions (Understand: ${understandCount}, Apply: ${applyCount}, Analyze: ${analyzeCount}).`);
15334
+ }
15335
+ score = Math.max(0, Math.min(100, score));
15336
+ return {
15337
+ score,
15338
+ weight: 0.2,
15339
+ passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
15340
+ strengths,
15341
+ findings
15342
+ };
15343
+ }
15344
+ };
15345
+
15346
+ // src/evaluators/crossArtifactDriftEvaluator.ts
15347
+ var CrossArtifactDriftEvaluator = class {
15348
+ /**
15349
+ * Audits consistency, metadata synchronization, and zero-drift across all artifacts in a lesson bundle.
15350
+ */
15351
+ static evaluateBundle(lesson, quiz, activity, slides) {
15352
+ const findings = [];
15353
+ const strengths = [];
15354
+ let score = 100;
15355
+ const baseLessonId = lesson.lessonId;
15356
+ const baseLanguage = lesson.language;
15357
+ if (quiz && quiz.quizId && quiz.quizId !== baseLessonId && !quiz.quizId.includes(baseLessonId)) {
15358
+ score -= 15;
15359
+ findings.push({
15360
+ id: "drift_quiz_id_mismatch",
15361
+ dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15667
15362
  severity: "MAJOR",
15668
- title: "Activity Lesson ID Contract Mismatch",
15669
- description: `Activity lessonId "${activity.lessonId}" does not match Lesson ID "${baseLessonId}".`,
15670
- remediationAdvice: `Sync Activity lessonId to "${baseLessonId}".`,
15363
+ title: "Quiz ID Mismatch with Master Lesson",
15364
+ description: `Quiz ID "${quiz.quizId}" diverges from master Lesson ID "${baseLessonId}".`,
15365
+ remediationAdvice: `Align Quiz ID to "${baseLessonId}".`,
15366
+ affectedElement: quiz.quizId
15367
+ });
15368
+ }
15369
+ if (activity && activity.lessonId && activity.lessonId !== baseLessonId) {
15370
+ score -= 15;
15371
+ findings.push({
15372
+ id: "drift_act_id_mismatch",
15373
+ dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15374
+ severity: "MAJOR",
15375
+ title: "Activity Lesson ID Mismatch",
15376
+ description: `Activity lessonId "${activity.lessonId}" diverges from master Lesson ID "${baseLessonId}".`,
15377
+ remediationAdvice: `Align Activity lessonId to "${baseLessonId}".`,
15671
15378
  affectedElement: activity.lessonId
15672
15379
  });
15673
15380
  }
15674
- if (slides && slides.lessonId !== baseLessonId) {
15381
+ if (slides && slides.lessonId && slides.lessonId !== baseLessonId) {
15675
15382
  score -= 15;
15676
15383
  findings.push({
15677
- id: "struct_slides_id_mismatch",
15384
+ id: "drift_slides_id_mismatch",
15678
15385
  dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15679
15386
  severity: "MAJOR",
15680
- title: "Slide Deck Lesson ID Contract Mismatch",
15681
- description: `Slide deck lessonId "${slides.lessonId}" does not match Lesson ID "${baseLessonId}".`,
15682
- remediationAdvice: `Sync Slide deck lessonId to "${baseLessonId}".`,
15387
+ title: "Slide Deck Lesson ID Mismatch",
15388
+ description: `Slide deck lessonId "${slides.lessonId}" diverges from master Lesson ID "${baseLessonId}".`,
15389
+ remediationAdvice: `Align Slide deck lessonId to "${baseLessonId}".`,
15683
15390
  affectedElement: slides.lessonId
15684
15391
  });
15685
15392
  }
@@ -15692,589 +15399,377 @@ var DeterministicStructuralLinter = class {
15692
15399
  if (sat.lang && sat.lang.toLowerCase() !== baseLanguage.toLowerCase()) {
15693
15400
  score -= 25;
15694
15401
  findings.push({
15695
- id: `struct_language_mismatch_${sat.type}`,
15402
+ id: `drift_language_mismatch_${sat.type}`,
15696
15403
  dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15697
15404
  severity: "CRITICAL",
15698
- title: `Language Policy Inconsistency in ${sat.type}`,
15405
+ title: `Language Contamination in ${sat.type}`,
15699
15406
  description: `Master Lesson is in "${baseLanguage}" but ${sat.type} is declared in "${sat.lang}".`,
15700
- remediationAdvice: `Set ${sat.type} language to "${baseLanguage}".`
15407
+ remediationAdvice: `Regenerate ${sat.type} strictly in "${baseLanguage}".`
15701
15408
  });
15702
15409
  }
15703
15410
  }
15704
- if (quiz && quiz.questions) {
15705
- for (let i = 0; i < quiz.questions.length; i++) {
15706
- const q = quiz.questions[i];
15707
- const qId = q.id || `Q${i + 1}`;
15708
- const options = q.options || [];
15709
- if (options.length < 4) {
15710
- score -= 10;
15711
- findings.push({
15712
- id: `struct_quiz_option_count_${qId}`,
15713
- dimension: "MISCONCEPTION_RIGOR",
15714
- severity: "MAJOR",
15715
- title: `Structural Option Count Error in ${qId}`,
15716
- description: `Question ${qId} has ${options.length} options (standard schema requires 4).`,
15717
- remediationAdvice: "Ensure each question has 4 options (A, B, C, D).",
15718
- affectedElement: qId
15719
- });
15720
- }
15721
- const correctCount = options.filter((o) => o.isCorrect).length;
15722
- if (correctCount !== 1) {
15411
+ const sections = lesson.sections || [];
15412
+ const totalSectionMins = sections.reduce((sum, s) => sum + (s.durationMinutes || 0), 0);
15413
+ if (totalSectionMins > 0 && (totalSectionMins < 40 || totalSectionMins > 180)) {
15414
+ score -= 10;
15415
+ findings.push({
15416
+ id: "drift_unrealistic_lesson_duration",
15417
+ dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15418
+ severity: "MINOR",
15419
+ title: `Unrealistic Lesson Flow Total Duration (${totalSectionMins} mins)`,
15420
+ description: `Sum of 5E section durations is ${totalSectionMins} minutes. Standard K-12/College lessons span 45-120 minutes.`,
15421
+ remediationAdvice: "Adjust individual section timings so total duration matches standard classroom blocks (e.g. 60m or 90m)."
15422
+ });
15423
+ } else if (totalSectionMins > 0) {
15424
+ strengths.push(`Lesson section timings sum up to a realistic classroom block (${totalSectionMins} mins).`);
15425
+ }
15426
+ if (score >= 90) {
15427
+ strengths.push("Zero drift verified: IDs, language policies, and pedagogical contracts are strictly aligned across all artifacts.");
15428
+ }
15429
+ score = Math.max(0, Math.min(100, score));
15430
+ return {
15431
+ score,
15432
+ weight: 0.1,
15433
+ passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
15434
+ strengths,
15435
+ findings
15436
+ };
15437
+ }
15438
+ };
15439
+
15440
+ // src/evaluators/constructiveAlignmentEvaluator.ts
15441
+ var ConstructiveAlignmentEvaluator = class {
15442
+ /**
15443
+ * Evaluates constructive alignment within a LessonPlan and across its satellite artifacts.
15444
+ */
15445
+ static evaluateLesson(lesson, quiz, activity) {
15446
+ const findings = [];
15447
+ const strengths = [];
15448
+ let score = 100;
15449
+ const los = lesson.learningObjectives || [];
15450
+ const sections = lesson.sections || [];
15451
+ const exitTicket = lesson.exitTicket;
15452
+ if (los.length === 0) {
15453
+ return {
15454
+ score: 0,
15455
+ weight: 0.2,
15456
+ passed: false,
15457
+ strengths: [],
15458
+ findings: [
15459
+ {
15460
+ id: "align_no_los",
15461
+ dimension: "CONSTRUCTIVE_ALIGNMENT",
15462
+ severity: "CRITICAL",
15463
+ title: "No Learning Objectives Defined",
15464
+ description: "Constructive alignment cannot be established without baseline LOs.",
15465
+ remediationAdvice: "Define at least 2 measurable Learning Objectives."
15466
+ }
15467
+ ]
15468
+ };
15469
+ }
15470
+ const combinedSectionText = sections.map((s) => `${s.title} ${s.teacherActions} ${s.studentActions} ${(s.analogies || []).join(" ")}`).join(" ").toLowerCase();
15471
+ for (const lo of los) {
15472
+ const loKeywords = (lo.name || lo.description || "").toLowerCase().split(/\s+/).filter((w) => w.length > 4);
15473
+ const hasCoverage = loKeywords.some((k) => combinedSectionText.includes(k));
15474
+ if (!hasCoverage && loKeywords.length > 0) {
15475
+ score -= 15;
15476
+ findings.push({
15477
+ id: `align_uncovered_lo_${lo.code}`,
15478
+ dimension: "CONSTRUCTIVE_ALIGNMENT",
15479
+ severity: "MAJOR",
15480
+ title: `Untaught Objective in Lesson Flow: "${lo.code}"`,
15481
+ description: `The LO "${lo.name || lo.code}" is declared in the objectives table but receives minimal coverage in the 5E lesson sections.`,
15482
+ remediationAdvice: `Add explicit Teacher Moves and Student Actions in the Explore/Explain sections addressing "${lo.name}".`,
15483
+ affectedElement: lo.code
15484
+ });
15485
+ }
15486
+ }
15487
+ if (!exitTicket || !exitTicket.questionStem || exitTicket.questionStem.trim().length < 15) {
15488
+ score -= 15;
15489
+ findings.push({
15490
+ id: "align_missing_exit_ticket",
15491
+ dimension: "CONSTRUCTIVE_ALIGNMENT",
15492
+ severity: "MAJOR",
15493
+ title: "Missing or Shallow Exit Ticket",
15494
+ description: "The lesson ends without a rigorous formative Exit Ticket to measure LO attainment before class dismissal.",
15495
+ remediationAdvice: "Include an Exit Ticket question matching the primary LO + a metacognitive reflection prompt."
15496
+ });
15497
+ } else {
15498
+ strengths.push("Formative Exit Ticket is properly anchored at the conclusion of the lesson.");
15499
+ }
15500
+ if (quiz) {
15501
+ const quizQuestions = quiz.questions || [];
15502
+ if (quizQuestions.length > 0) {
15503
+ const quizText = quizQuestions.map((q) => `${q.scenarioOrStem} ${(q.options || []).map((o) => o.text).join(" ")}`).join(" ").toLowerCase();
15504
+ const lessonTopicKeywords = (lesson.topic || lesson.title || "").toLowerCase().split(/\s+/).filter((w) => w.length > 3);
15505
+ const hasTopicMatch = lessonTopicKeywords.some((k) => quizText.includes(k));
15506
+ if (!hasTopicMatch && lessonTopicKeywords.length > 0) {
15723
15507
  score -= 20;
15724
15508
  findings.push({
15725
- id: `struct_quiz_key_count_${qId}`,
15726
- dimension: "MISCONCEPTION_RIGOR",
15509
+ id: "align_quiz_topic_divergence",
15510
+ dimension: "CONSTRUCTIVE_ALIGNMENT",
15727
15511
  severity: "CRITICAL",
15728
- title: `Key Assignment Error in ${qId}`,
15729
- description: `Question ${qId} has ${correctCount} correct options (must be exactly 1).`,
15730
- remediationAdvice: "Set `isCorrect: true` on exactly 1 option.",
15731
- affectedElement: qId
15512
+ title: "Quiz Topic Divergence from Lesson",
15513
+ description: `Quiz questions do not reflect the main topic "${lesson.topic}" of the master lesson.`,
15514
+ remediationAdvice: "Re-align quiz questions strictly to the core concepts taught in the lesson."
15732
15515
  });
15516
+ } else {
15517
+ strengths.push("Diagnostic Quiz questions tightly reflect the lesson topic and concepts.");
15733
15518
  }
15734
15519
  }
15735
15520
  }
15736
- if (activity?.materialsBOM?.hardware && activity.materialsBOM.hardware.length > 0) {
15737
- const hwText = activity.materialsBOM.hardware.join(" ").toLowerCase();
15738
- const hasLED = hwText.includes("led");
15739
- const hasResistor = hwText.includes("\u0111i\u1EC7n tr\u1EDF") || hwText.includes("resistor") || hwText.includes("220") || hwText.includes("330") || hwText.includes("1k");
15740
- if (hasLED && !hasResistor) {
15741
- score -= 20;
15521
+ if (activity) {
15522
+ const actObj = (activity.objective || "").toLowerCase();
15523
+ const lessonTitleLower = (lesson.title || "").toLowerCase();
15524
+ const hasActAlignment = los.some((lo) => actObj.includes((lo.name || "").toLowerCase()) || actObj.includes((lo.description || "").toLowerCase())) || actObj.includes(lessonTitleLower) || lessonTitleLower.split(/\s+/).some((w) => w.length > 4 && actObj.includes(w));
15525
+ if (!hasActAlignment && activity.objective) {
15526
+ score -= 15;
15742
15527
  findings.push({
15743
- id: "struct_hardware_unsafe_led_no_resistor",
15744
- dimension: "TECHNICAL_AUTHENTICITY",
15745
- severity: "CRITICAL",
15746
- title: "Unsafe Circuit BOM: LED without Current-Limiting Resistor",
15747
- description: "Hardware BOM includes LED without a 220\u03A9-1k\u03A9 resistor, causing circuit overload.",
15748
- remediationAdvice: "Add a 220\u03A9 current-limiting resistor to the hardware materials list."
15528
+ id: "align_act_objective_divergence",
15529
+ dimension: "CONSTRUCTIVE_ALIGNMENT",
15530
+ severity: "MAJOR",
15531
+ title: "Activity Objective Disconnected from Lesson Plan",
15532
+ description: `Activity objective "${activity.objective}" does not directly support the lesson LOs.`,
15533
+ remediationAdvice: "Ensure the Activity hands-on lab operationalizes the exact LOs specified in the Master Lesson."
15749
15534
  });
15535
+ } else {
15536
+ strengths.push("Activity lab objective directly operationalizes master lesson learning goals.");
15750
15537
  }
15751
15538
  }
15752
15539
  score = Math.max(0, Math.min(100, score));
15753
- const passed = score >= 80 && !findings.some((f) => f.severity === "CRITICAL");
15754
- if (passed) {
15755
- strengths.push("Deterministic structure, ID contracts, and schema invariants strictly verified.");
15756
- }
15757
15540
  return {
15758
- passed,
15759
- structuralScore: score,
15760
- findings,
15761
- strengths
15541
+ score,
15542
+ weight: 0.2,
15543
+ passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
15544
+ strengths,
15545
+ findings
15762
15546
  };
15763
15547
  }
15764
15548
  };
15765
15549
 
15766
- // src/evaluators/academicAuditor.ts
15767
- var AcademicAuditor = class {
15550
+ // src/evaluators/fiveEInstructionalEvaluator.ts
15551
+ var FIVE_E_STAGES = [
15552
+ { key: "engage", label: "Engage (Kh\u1EDFi \u0111\u1ED9ng / M\xF3c neo)", regex: /(?:engage|khởi động|hook|mở đầu|anchor)/i },
15553
+ { key: "explore", label: "Explore (Kh\xE1m ph\xE1 / Tr\u1EA3i nghi\u1EC7m)", regex: /(?:explore|khám phá|trải nghiệm|thử nghiệm)/i },
15554
+ { key: "explain", label: "Explain (Gi\u1EA3i th\xEDch / H\xECnh th\xE0nh ki\u1EBFn th\u1EE9c)", regex: /(?:explain|giải thích|khái niệm|kiến thức cốt lõi)/i },
15555
+ { key: "elaborate", label: "Elaborate (V\u1EADn d\u1EE5ng / M\u1EDF r\u1ED9ng)", regex: /(?:elaborate|vận dụng|luyện tập|thực hành|áp dụng)/i },
15556
+ { key: "evaluate", label: "Evaluate (\u0110\xE1nh gi\xE1 / T\u1ED5ng k\u1EBFt)", regex: /(?:evaluate|đánh giá|tổng kết|wrap-up|exit ticket)/i }
15557
+ ];
15558
+ var FiveEInstructionalEvaluator = class {
15768
15559
  /**
15769
- * Evaluates a complete lesson bundle.
15770
- * Step 1 (Deterministic): Fast structural, contract, schema, and safety linting.
15771
- * Step 2 (Semantic LLM-as-a-Judge): Deep pedagogical, cognitive (Bloom), and misconception analysis with live frontier LLM.
15560
+ * Evaluates the pedagogical structure of a Lesson Plan against the 5E Inquiry Model.
15772
15561
  */
15773
- static async auditLessonBundle(input) {
15774
- const { lesson, quiz, activity, slides, codeLab, modelOptions, executeLLMJudge = true } = input;
15775
- const structuralResult = DeterministicStructuralLinter.lintBundle({
15776
- lesson,
15777
- quiz,
15778
- activity,
15779
- slides,
15780
- codeLab
15781
- });
15782
- const allFindings = [...structuralResult.findings];
15783
- const strengths = [...structuralResult.strengths];
15784
- let semanticScore = null;
15785
- let semanticVerdict = "PASS";
15786
- if (executeLLMJudge) {
15787
- try {
15788
- const judgeReport = await auditCurriculumQualityFlow({
15789
- targetArtifactType: "LESSON_BUNDLE",
15790
- lessonId: lesson.lessonId,
15791
- expectedLanguage: lesson.language,
15792
- targetObjectives: (lesson.learningObjectives || []).map((lo) => ({
15793
- code: lo.code,
15794
- description: lo.description,
15795
- bloomLevel: lo.bloomLevel
15796
- })),
15797
- generatedContentJson: JSON.stringify({ lesson, quiz, activity, slides, codeLab }),
15798
- modelOptions
15799
- });
15800
- semanticScore = judgeReport.totalScore;
15801
- semanticVerdict = judgeReport.overallVerdict;
15802
- for (const criterion of judgeReport.criteria) {
15803
- if (!criterion.passed) {
15804
- allFindings.push({
15805
- id: `llm_judge_${criterion.name.toLowerCase().replace(/\s+/g, "_")}`,
15806
- dimension: "CONSTRUCTIVE_ALIGNMENT",
15807
- severity: criterion.score < 50 ? "CRITICAL" : "MAJOR",
15808
- title: `LLM-as-Judge Finding: ${criterion.name}`,
15809
- description: criterion.feedback,
15810
- remediationAdvice: judgeReport.actionableRepairPrompts.join("; ") || "Refine prompt context."
15811
- });
15812
- } else {
15813
- strengths.push(`[LLM-Judge] ${criterion.name}: ${criterion.feedback}`);
15814
- }
15562
+ static evaluateLesson(lesson) {
15563
+ const findings = [];
15564
+ const strengths = [];
15565
+ let score = 100;
15566
+ if (!lesson.hookScenario || lesson.hookScenario.trim().length < 30) {
15567
+ score -= 20;
15568
+ findings.push({
15569
+ id: "5e_weak_hook",
15570
+ dimension: "5E_INSTRUCTIONAL_FIDELITY",
15571
+ severity: "MAJOR",
15572
+ title: "Shallow or Missing Real-World Hook",
15573
+ description: "The lesson lacks a compelling, high-stakes real-world scenario to trigger inquiry.",
15574
+ remediationAdvice: "Frame the lesson around an authentic engineering or domain problem (e.g. server crash, sensor failure, clinical anomaly)."
15575
+ });
15576
+ } else {
15577
+ strengths.push("Engaging real-world hook scenario sets high-stakes context for inquiry.");
15578
+ }
15579
+ if (!lesson.hookQuestions || lesson.hookQuestions.length < 2) {
15580
+ score -= 10;
15581
+ findings.push({
15582
+ id: "5e_insufficient_hook_questions",
15583
+ dimension: "5E_INSTRUCTIONAL_FIDELITY",
15584
+ severity: "MINOR",
15585
+ title: "Insufficient Inquiry Questions in Hook",
15586
+ description: "Need at least 2 open-ended inquiry questions in the Engage phase to activate prior mental models.",
15587
+ remediationAdvice: "Add 2-3 provocative questions challenging common student assumptions."
15588
+ });
15589
+ }
15590
+ const sections = lesson.sections || [];
15591
+ const coveredStages = /* @__PURE__ */ new Set();
15592
+ for (const section of sections) {
15593
+ for (const stage of FIVE_E_STAGES) {
15594
+ if (stage.regex.test(section.title)) {
15595
+ coveredStages.add(stage.key);
15815
15596
  }
15816
- } catch (err) {
15817
- semanticScore = null;
15818
- semanticVerdict = "FAIL";
15819
- allFindings.push({
15820
- id: "llm_judge_connection_error",
15821
- dimension: "CONSTRUCTIVE_ALIGNMENT",
15822
- severity: "MINOR",
15823
- title: "LLM-as-Judge Skipped / Fallback",
15824
- description: `Semantic inference error: ${err.message || String(err)}`,
15825
- remediationAdvice: "Check API credentials for LLM-as-Judge."
15826
- });
15827
15597
  }
15828
15598
  }
15829
- const criticalCount = allFindings.filter((f) => f.severity === "CRITICAL").length;
15830
- const overallScore = executeLLMJudge && semanticScore !== null ? Math.round(structuralResult.structuralScore * 0.4 + semanticScore * 0.6) : structuralResult.structuralScore;
15831
- const passed = structuralResult.passed && criticalCount === 0 && (executeLLMJudge ? semanticVerdict === "PASS" : true);
15832
- let verdict = "REJECTED";
15833
- if (overallScore >= 90 && criticalCount === 0) {
15834
- verdict = "EXEMPLARY";
15835
- } else if (overallScore >= 75 && criticalCount === 0) {
15836
- verdict = "ACADEMICALLY_SOUND";
15837
- } else if (overallScore >= 60) {
15838
- verdict = "NEEDS_PEDAGOGICAL_REFINEMENT";
15599
+ if (lesson.hookScenario) coveredStages.add("engage");
15600
+ if (lesson.exitTicket) coveredStages.add("evaluate");
15601
+ const missingStages = FIVE_E_STAGES.filter((s) => !coveredStages.has(s.key));
15602
+ if (missingStages.length > 0) {
15603
+ score -= missingStages.length * 10;
15604
+ findings.push({
15605
+ id: "5e_missing_phases",
15606
+ dimension: "5E_INSTRUCTIONAL_FIDELITY",
15607
+ severity: missingStages.length > 2 ? "CRITICAL" : "MAJOR",
15608
+ title: `Incomplete 5E Instructional Cycle (Missing ${missingStages.length} phase(s))`,
15609
+ description: `Lesson plan does not clearly demarcate: ${missingStages.map((s) => s.label).join(", ")}.`,
15610
+ remediationAdvice: "Ensure the lesson explicitly walks through Engage -> Explore -> Explain -> Elaborate -> Evaluate."
15611
+ });
15612
+ } else {
15613
+ strengths.push("Full 5E Instructional Cycle (Engage, Explore, Explain, Elaborate, Evaluate) is completely covered.");
15839
15614
  }
15840
- const summary = passed ? `\u2705 Lesson Bundle "${lesson.title}" (${lesson.lessonId}) passes academic & structural verification with score ${overallScore}/100 (${verdict}).` : `\u26A0\uFE0F Lesson Bundle "${lesson.title}" requires revision (${overallScore}/100 - ${verdict}). Found ${allFindings.length} finding(s) with ${criticalCount} critical blocker(s).`;
15841
- const actionablePromptGuidance = allFindings.map(
15842
- (f, idx) => `[${f.dimension}] ${idx + 1}. ${f.title}: ${f.remediationAdvice}`
15843
- );
15844
- const computeDimScore = (dimFindings2) => {
15845
- const hasCritical = dimFindings2.some((f) => f.severity === "CRITICAL");
15846
- const majorCount = dimFindings2.filter((f) => f.severity === "MAJOR").length;
15847
- const minorCount = dimFindings2.filter((f) => f.severity === "MINOR").length;
15848
- let dimScore = 100 - (hasCritical ? 40 : 0) - majorCount * 15 - minorCount * 5;
15849
- dimScore = Math.max(0, Math.min(100, dimScore));
15850
- return { score: dimScore, passed: dimScore >= 75 && !hasCritical };
15851
- };
15852
- const dimFindings = {
15853
- constructiveAlignment: allFindings.filter((f) => f.dimension === "CONSTRUCTIVE_ALIGNMENT"),
15854
- bloomProgression: allFindings.filter((f) => f.dimension === "BLOOM_PROGRESSION"),
15855
- fiveEFidelity: allFindings.filter((f) => f.dimension === "5E_INSTRUCTIONAL_FIDELITY"),
15856
- misconceptionRigor: allFindings.filter((f) => f.dimension === "MISCONCEPTION_RIGOR"),
15857
- technicalAuthenticity: allFindings.filter((f) => f.dimension === "TECHNICAL_AUTHENTICITY")
15858
- };
15859
- const dimScores = Object.fromEntries(
15860
- Object.entries(dimFindings).map(([k, v]) => [k, computeDimScore(v)])
15861
- );
15615
+ let passiveSections = 0;
15616
+ for (let i = 0; i < sections.length; i++) {
15617
+ const s = sections[i];
15618
+ const studentAct = (s.studentActions || "").trim().toLowerCase();
15619
+ if (studentAct.includes("nghe gi\u1EA3ng") || studentAct.includes("ch\xE9p b\xE0i") || studentAct.includes("listen passively") || studentAct.length < 10) {
15620
+ passiveSections++;
15621
+ }
15622
+ }
15623
+ if (passiveSections > 0 && sections.length > 0) {
15624
+ score -= passiveSections * 8;
15625
+ findings.push({
15626
+ id: "5e_passive_student_roles",
15627
+ dimension: "5E_INSTRUCTIONAL_FIDELITY",
15628
+ severity: "MAJOR",
15629
+ title: "Passive Student Roles Detected in Lesson Sections",
15630
+ description: `${passiveSections} section(s) assign passive roles (listening/copying) to students rather than active inquiry, pair-discussion, or hands-on experimentation.`,
15631
+ remediationAdvice: "Transform student actions into active tasks (e.g. Think-Pair-Share, code tracing, hypothesis testing, live bug hunting)."
15632
+ });
15633
+ } else if (sections.length > 0) {
15634
+ strengths.push("Student roles emphasize active learning and hands-on participation throughout.");
15635
+ }
15636
+ score = Math.max(0, Math.min(100, score));
15862
15637
  return {
15863
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15864
- targetId: lesson.lessonId,
15865
- targetType: "BUNDLE",
15866
- overallScore,
15867
- passed,
15868
- verdict,
15869
- summary,
15870
- dimensionScores: {
15871
- constructiveAlignment: {
15872
- score: dimScores.constructiveAlignment.score,
15873
- weight: 0.2,
15874
- passed: dimScores.constructiveAlignment.passed,
15875
- strengths: dimScores.constructiveAlignment.passed ? ["Constructive alignment verified."] : [],
15876
- findings: dimFindings.constructiveAlignment
15877
- },
15878
- bloomProgression: {
15879
- score: dimScores.bloomProgression.score,
15880
- weight: 0.2,
15881
- passed: dimScores.bloomProgression.passed,
15882
- strengths: dimScores.bloomProgression.passed ? ["Bloom taxonomy progression verified."] : [],
15883
- findings: dimFindings.bloomProgression
15884
- },
15885
- fiveEFidelity: {
15886
- score: dimScores.fiveEFidelity.score,
15887
- weight: 0.15,
15888
- passed: dimScores.fiveEFidelity.passed,
15889
- strengths: dimScores.fiveEFidelity.passed ? ["5E instructional fidelity verified."] : [],
15890
- findings: dimFindings.fiveEFidelity
15891
- },
15892
- misconceptionRigor: {
15893
- score: dimScores.misconceptionRigor.score,
15894
- weight: 0.2,
15895
- passed: dimScores.misconceptionRigor.passed,
15896
- strengths: dimScores.misconceptionRigor.passed ? ["Misconception rigor verified."] : [],
15897
- findings: dimFindings.misconceptionRigor
15898
- },
15899
- technicalAuthenticity: {
15900
- score: dimScores.technicalAuthenticity.score,
15901
- weight: 0.15,
15902
- passed: dimScores.technicalAuthenticity.passed,
15903
- strengths: dimScores.technicalAuthenticity.passed ? ["Technical authenticity verified."] : [],
15904
- findings: dimFindings.technicalAuthenticity
15905
- },
15906
- crossArtifactZeroDrift: {
15907
- score: structuralResult.structuralScore,
15908
- weight: 0.1,
15909
- passed: structuralResult.passed,
15910
- strengths: structuralResult.passed ? ["Cross-artifact zero drift verified."] : [],
15911
- findings: allFindings.filter((f) => f.dimension === "CROSS_ARTIFACT_ZERO_DRIFT")
15912
- }
15913
- },
15914
- criticalFindingsCount: criticalCount,
15915
- allFindings,
15916
- actionablePromptGuidance
15638
+ score,
15639
+ weight: 0.15,
15640
+ passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
15641
+ strengths,
15642
+ findings
15917
15643
  };
15918
15644
  }
15919
15645
  };
15920
15646
 
15921
- // src/evaluators/bloomTaxonomyEvaluator.ts
15922
- var BLOOM_ACTION_VERBS = {
15923
- remember: [
15924
- "list",
15925
- "define",
15926
- "recall",
15927
- "state",
15928
- "name",
15929
- "identify",
15930
- "label",
15931
- "recognize",
15932
- "li\u1EC7t k\xEA",
15933
- "\u0111\u1ECBnh ngh\u0129a",
15934
- "g\u1ECDi t\xEAn",
15935
- "nh\u1EADn di\u1EC7n",
15936
- "ch\u1EC9 ra",
15937
- "nh\u1EAFc l\u1EA1i",
15938
- "ghi nh\u1EDB"
15939
- ],
15940
- understand: [
15941
- "explain",
15942
- "describe",
15943
- "summarize",
15944
- "classify",
15945
- "interpret",
15946
- "predict",
15947
- "trace",
15948
- "paraphrase",
15949
- "gi\u1EA3i th\xEDch",
15950
- "m\xF4 t\u1EA3",
15951
- "t\xF3m t\u1EAFt",
15952
- "ph\xE2n lo\u1EA1i",
15953
- "di\u1EC5n gi\u1EA3i",
15954
- "d\u1EF1 \u0111o\xE1n",
15955
- "l\u1EA7n theo",
15956
- "hi\u1EC3u"
15957
- ],
15958
- apply: [
15959
- "implement",
15960
- "execute",
15961
- "calculate",
15962
- "solve",
15963
- "construct",
15964
- "debug",
15965
- "modify",
15966
- "build",
15967
- "\xE1p d\u1EE5ng",
15968
- "th\u1EF1c thi",
15969
- "t\xEDnh to\xE1n",
15970
- "gi\u1EA3i quy\u1EBFt",
15971
- "x\xE2y d\u1EF1ng",
15972
- "s\u1EEDa l\u1ED7i",
15973
- "l\u1EAFp \u0111\u1EB7t",
15974
- "vi\u1EBFt m\xE3",
15975
- "l\u1EADp tr\xECnh"
15976
- ],
15977
- analyze: [
15978
- "compare",
15979
- "contrast",
15980
- "decompose",
15981
- "differentiate",
15982
- "troubleshoot",
15983
- "diagnose",
15984
- "deconstruct",
15985
- "so s\xE1nh",
15986
- "\u0111\u1ED1i chi\u1EBFu",
15987
- "ph\xE2n t\xEDch",
15988
- "ph\xE2n r\xE3",
15989
- "ch\u1EA9n \u0111o\xE1n",
15990
- "t\xECm nguy\xEAn nh\xE2n g\u1ED1c",
15991
- "b\xF3c t\xE1ch"
15992
- ],
15993
- evaluate: [
15994
- "justify",
15995
- "critique",
15996
- "assess",
15997
- "defend",
15998
- "argue",
15999
- "benchmark",
16000
- "prioritize",
16001
- "\u0111\xE1nh gi\xE1",
16002
- "bi\u1EC7n minh",
16003
- "ph\xEA ph\xE1n",
16004
- "th\u1EA9m \u0111\u1ECBnh",
16005
- "b\u1EA3o v\u1EC7 quan \u0111i\u1EC3m",
16006
- "l\u1EF1a ch\u1ECDn t\u1ED1i \u01B0u"
16007
- ],
16008
- create: [
16009
- "design",
16010
- "synthesize",
16011
- "architect",
16012
- "formulate",
16013
- "invent",
16014
- "devise",
16015
- "author",
16016
- "thi\u1EBFt k\u1EBF",
16017
- "t\u1ED5ng h\u1EE3p",
16018
- "s\xE1ng t\u1EA1o",
16019
- "ki\u1EBFn tr\xFAc",
16020
- "ph\xE1t minh",
16021
- "ho\xE0n thi\u1EC7n \u0111\u1ED3 \xE1n"
16022
- ]
16023
- };
16024
- var RECALL_PATTERNS = [
16025
- /^(?:chức năng chính của|định nghĩa của|cú pháp của|lệnh nào là|what is the definition of|which keyword|what does .* stand for)/i,
16026
- /(?:là gì\?|được gọi là gì\?|có ý nghĩa gì\?)/i
15647
+ // src/evaluators/codeHardwareFeasibilityEvaluator.ts
15648
+ var VALID_MERMAID_STARTERS = [
15649
+ "graph",
15650
+ "flowchart",
15651
+ "sequencediagram",
15652
+ "statediagram",
15653
+ "classdiagram",
15654
+ "erdiagram",
15655
+ "gantt",
15656
+ "gitgraph"
16027
15657
  ];
16028
- var BloomTaxonomyEvaluator = class {
15658
+ var CodeHardwareFeasibilityEvaluator = class {
16029
15659
  /**
16030
- * Audits a LessonPlan for Bloom taxonomy fidelity and cognitive scaffolding.
15660
+ * Audits technical accuracy, code syntax sanity, and hardware circuit safety.
16031
15661
  */
16032
- static evaluateLesson(lesson) {
15662
+ static evaluateTechnicalFeasibility(lesson, codeLab, activity) {
16033
15663
  const findings = [];
16034
15664
  const strengths = [];
16035
15665
  let score = 100;
16036
- const los = lesson.learningObjectives || [];
16037
- if (los.length === 0) {
16038
- return {
16039
- score: 0,
16040
- weight: 0.2,
16041
- passed: false,
16042
- strengths: [],
16043
- findings: [
16044
- {
16045
- id: "bloom_missing_los",
16046
- dimension: "BLOOM_PROGRESSION",
16047
- severity: "CRITICAL",
16048
- title: "Missing Learning Objectives",
16049
- description: "The lesson plan contains zero declared Learning Objectives.",
16050
- remediationAdvice: "Add 2-4 clearly articulated LOs with Bloom levels and observable success criteria."
16051
- }
16052
- ]
16053
- };
16054
- }
16055
- for (const lo of los) {
16056
- const declaredLevel = (lo.bloomLevel || "").toLowerCase();
16057
- const text = `${lo.name || ""} ${lo.description || ""} ${lo.successCriteria || ""}`.toLowerCase();
16058
- const isRecallPattern = RECALL_PATTERNS.some((p) => p.test(text));
16059
- if (isRecallPattern && (declaredLevel === "apply" || declaredLevel === "analyze" || declaredLevel === "evaluate")) {
16060
- score -= 15;
15666
+ if (lesson?.guidedPractice) {
15667
+ const { codeSnippet, codeLanguage, mermaidDiagram } = lesson.guidedPractice;
15668
+ if (!codeSnippet || codeSnippet.trim().length < 15) {
15669
+ score -= 20;
16061
15670
  findings.push({
16062
- id: `bloom_inflation_${lo.code}`,
16063
- dimension: "BLOOM_PROGRESSION",
15671
+ id: "tech_empty_guided_code",
15672
+ dimension: "TECHNICAL_AUTHENTICITY",
16064
15673
  severity: "MAJOR",
16065
- title: `Bloom Level Inflation in LO "${lo.code}"`,
16066
- description: `LO is tagged as "${lo.bloomLevel}" but its phrasing reflects low-level Recall ("${lo.description}").`,
16067
- remediationAdvice: `Rewrite the objective using authentic "${declaredLevel}" active verbs (e.g. build, debug, compare, diagnose) with an explicit application context.`,
16068
- affectedElement: lo.code
15674
+ title: "Empty or Trivial Guided Practice Code",
15675
+ description: "Guided practice lacks runnable code snippet or domain calculation template.",
15676
+ remediationAdvice: "Provide a complete, runnable code example with explanatory line-by-line comments."
16069
15677
  });
15678
+ } else {
15679
+ if (codeLanguage?.toLowerCase() === "arduino" || codeLanguage?.toLowerCase() === "cpp" || codeSnippet.includes("pinMode")) {
15680
+ if (codeSnippet.includes("digitalWrite") && !codeSnippet.includes("pinMode") && !codeSnippet.includes("setup()")) {
15681
+ score -= 15;
15682
+ findings.push({
15683
+ id: "tech_arduino_missing_pinmode",
15684
+ dimension: "TECHNICAL_AUTHENTICITY",
15685
+ severity: "MAJOR",
15686
+ title: "Missing pinMode() Configuration in Arduino Code",
15687
+ description: "Code calls `digitalWrite()` without initializing the pin with `pinMode(pin, OUTPUT)`.",
15688
+ remediationAdvice: "Ensure `setup()` configures pin direction before writing digital states."
15689
+ });
15690
+ }
15691
+ if (codeSnippet.includes("delay(0)") || codeSnippet.includes("delay(-")) {
15692
+ score -= 15;
15693
+ findings.push({
15694
+ id: "tech_arduino_invalid_delay",
15695
+ dimension: "TECHNICAL_AUTHENTICITY",
15696
+ severity: "MAJOR",
15697
+ title: "Invalid delay() Parameter",
15698
+ description: "delay() duration must be a positive integer in milliseconds.",
15699
+ remediationAdvice: "Use realistic delay timings (e.g. 500ms, 1000ms)."
15700
+ });
15701
+ }
15702
+ }
15703
+ strengths.push("Guided practice features runnable code snippet with clear syntax.");
16070
15704
  }
16071
- if (!lo.successCriteria || lo.successCriteria.trim().length < 15) {
16072
- score -= 10;
15705
+ if (mermaidDiagram) {
15706
+ const cleanDiagram = mermaidDiagram.trim().toLowerCase();
15707
+ const isValidStarter = VALID_MERMAID_STARTERS.some((starter) => cleanDiagram.startsWith(starter));
15708
+ if (!isValidStarter) {
15709
+ score -= 15;
15710
+ findings.push({
15711
+ id: "tech_invalid_mermaid_syntax",
15712
+ dimension: "TECHNICAL_AUTHENTICITY",
15713
+ severity: "MAJOR",
15714
+ title: "Invalid Mermaid Diagram Syntax",
15715
+ description: `Mermaid diagram does not start with a valid declaration (e.g. "flowchart TD", "graph TD", "sequenceDiagram"). Got: "${mermaidDiagram.substring(0, 30)}..."`,
15716
+ remediationAdvice: "Format Mermaid diagrams starting with `flowchart TD` or `sequenceDiagram`."
15717
+ });
15718
+ } else {
15719
+ strengths.push("Valid Mermaid architectural diagram included.");
15720
+ }
15721
+ }
15722
+ }
15723
+ if (codeLab) {
15724
+ const starter = codeLab.starterCode?.content || "";
15725
+ const solution = codeLab.solutionCode?.content || "";
15726
+ if (starter.length < 20 || solution.length < 20) {
15727
+ score -= 25;
16073
15728
  findings.push({
16074
- id: `bloom_unmeasurable_criteria_${lo.code}`,
16075
- dimension: "BLOOM_PROGRESSION",
16076
- severity: "MINOR",
16077
- title: `Vague Success Criteria in LO "${lo.code}"`,
16078
- description: `Success criteria "${lo.successCriteria || ""}" is too brief to provide objective student evidence.`,
16079
- remediationAdvice: 'Specify concrete, observable evidence (e.g. "LED blinks with 1s period without circuit shorting").',
16080
- affectedElement: lo.code
15729
+ id: "tech_codelab_incomplete_codes",
15730
+ dimension: "TECHNICAL_AUTHENTICITY",
15731
+ severity: "CRITICAL",
15732
+ title: "Incomplete CodeLab Starter / Solution Code",
15733
+ description: "CodeLab must provide both scaffolding starter code and complete reference solution code.",
15734
+ remediationAdvice: "Populate `starterCode` with TODO markers and `solutionCode` with tested implementation."
16081
15735
  });
16082
15736
  } else {
16083
- strengths.push(`LO "${lo.code}" defines concrete observable student evidence.`);
15737
+ strengths.push("CodeLab provides complete starter skeleton and working solution code.");
16084
15738
  }
16085
- }
16086
- const diff = lesson.differentiation;
16087
- if (diff) {
16088
- if (!diff.bronzeTier || !diff.silverTier || !diff.goldTier) {
15739
+ if (!codeLab.testCases || codeLab.testCases.length === 0) {
16089
15740
  score -= 15;
16090
15741
  findings.push({
16091
- id: "bloom_incomplete_differentiation",
16092
- dimension: "BLOOM_PROGRESSION",
15742
+ id: "tech_codelab_missing_testcases",
15743
+ dimension: "TECHNICAL_AUTHENTICITY",
16093
15744
  severity: "MAJOR",
16094
- title: "Incomplete 3-Tier Scaffolding",
16095
- description: "Differentiation must provide distinct Bronze (Foundation), Silver (Application), and Gold (Extension) challenges.",
16096
- remediationAdvice: "Define all 3 tiers with increasing cognitive demands (Remember/Understand -> Apply -> Analyze/Create)."
15745
+ title: "Missing Automated Verification Test Cases",
15746
+ description: "CodeLab lacks test cases for students to self-verify their implementations.",
15747
+ remediationAdvice: "Add at least 2 concrete test cases with input and expected output assertions."
16097
15748
  });
16098
- } else {
16099
- strengths.push("Complete 3-tier scaffolding (Bronze -> Silver -> Gold) present in Lesson Plan.");
16100
15749
  }
16101
- } else {
16102
- score -= 20;
16103
- findings.push({
16104
- id: "bloom_missing_differentiation",
16105
- dimension: "BLOOM_PROGRESSION",
16106
- severity: "CRITICAL",
16107
- title: "Missing Differentiation Scaffolding",
16108
- description: "Lesson plan lacks differentiation tiers.",
16109
- remediationAdvice: "Provide tiered practice instructions for varied learner paces."
16110
- });
16111
15750
  }
16112
- score = Math.max(0, Math.min(100, score));
16113
- return {
16114
- score,
16115
- weight: 0.2,
16116
- passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
16117
- strengths,
16118
- findings
16119
- };
16120
- }
16121
- /**
16122
- * Audits a Diagnostic Quiz for true cognitive depth matching declared Bloom levels.
16123
- */
16124
- static evaluateQuiz(quiz) {
16125
- const findings = [];
16126
- const strengths = [];
16127
- let score = 100;
16128
- const questions = quiz.questions || [];
16129
- if (questions.length < 3) {
16130
- score -= 30;
16131
- findings.push({
16132
- id: "bloom_quiz_too_short",
16133
- dimension: "BLOOM_PROGRESSION",
16134
- severity: "MAJOR",
16135
- title: "Insufficient Question Pool",
16136
- description: `Quiz only contains ${questions.length} questions. Minimum diagnostic threshold is 3 questions.`,
16137
- remediationAdvice: "Generate at least 3-5 diagnostic questions covering foundational through analytical depth."
16138
- });
16139
- }
16140
- let understandCount = 0;
16141
- let applyCount = 0;
16142
- let analyzeCount = 0;
16143
- for (let i = 0; i < questions.length; i++) {
16144
- const q = questions[i];
16145
- const qId = q.id || `Q${i + 1}`;
16146
- const declaredBloom = (q.bloomLevel || "").toLowerCase();
16147
- const stem = q.scenarioOrStem || "";
16148
- if (declaredBloom === "understand") understandCount++;
16149
- if (declaredBloom === "apply") applyCount++;
16150
- if (declaredBloom === "analyze" || declaredBloom === "evaluate") analyzeCount++;
16151
- const isRecall = RECALL_PATTERNS.some((p) => p.test(stem));
16152
- if (isRecall && (declaredBloom === "apply" || declaredBloom === "analyze")) {
16153
- score -= 15;
16154
- findings.push({
16155
- id: `bloom_quiz_misclassification_${qId}`,
16156
- dimension: "BLOOM_PROGRESSION",
16157
- severity: "MAJOR",
16158
- title: `Cognitive Level Misclassification in ${qId}`,
16159
- description: `Question stem "${stem.substring(0, 60)}..." is pure Recall/Definition, but is tagged as "${q.bloomLevel}".`,
16160
- remediationAdvice: 'Either re-tag as "understand" or transform the stem into an authentic problem-solving scenario requiring code debugging or design decision.',
16161
- affectedElement: qId
16162
- });
16163
- }
16164
- if ((declaredBloom === "apply" || declaredBloom === "analyze") && !q.codeSnippet && !stem.includes("```") && !stem.includes("m\u1EA1ch")) {
16165
- score -= 10;
16166
- findings.push({
16167
- id: `bloom_quiz_missing_code_context_${qId}`,
16168
- dimension: "BLOOM_PROGRESSION",
16169
- severity: "MINOR",
16170
- title: `Missing Practical Context in High-Bloom Question ${qId}`,
16171
- description: `Question ${qId} tagged as "${q.bloomLevel}" lacks concrete code or circuit context to evaluate hands-on execution.`,
16172
- remediationAdvice: "Include an annotated code snippet or circuit state for students to trace, debug, or evaluate.",
16173
- affectedElement: qId
16174
- });
16175
- }
16176
- }
16177
- if (questions.length > 0 && (understandCount > 0 || applyCount > 0)) {
16178
- strengths.push(`Quiz features multi-level cognitive questions (Understand: ${understandCount}, Apply: ${applyCount}, Analyze: ${analyzeCount}).`);
16179
- }
16180
- score = Math.max(0, Math.min(100, score));
16181
- return {
16182
- score,
16183
- weight: 0.2,
16184
- passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
16185
- strengths,
16186
- findings
16187
- };
16188
- }
16189
- };
16190
-
16191
- // src/evaluators/crossArtifactDriftEvaluator.ts
16192
- var CrossArtifactDriftEvaluator = class {
16193
- /**
16194
- * Audits consistency, metadata synchronization, and zero-drift across all artifacts in a lesson bundle.
16195
- */
16196
- static evaluateBundle(lesson, quiz, activity, slides) {
16197
- const findings = [];
16198
- const strengths = [];
16199
- let score = 100;
16200
- const baseLessonId = lesson.lessonId;
16201
- const baseLanguage = lesson.language;
16202
- if (quiz && quiz.quizId && quiz.quizId !== baseLessonId && !quiz.quizId.includes(baseLessonId)) {
16203
- score -= 15;
16204
- findings.push({
16205
- id: "drift_quiz_id_mismatch",
16206
- dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
16207
- severity: "MAJOR",
16208
- title: "Quiz ID Mismatch with Master Lesson",
16209
- description: `Quiz ID "${quiz.quizId}" diverges from master Lesson ID "${baseLessonId}".`,
16210
- remediationAdvice: `Align Quiz ID to "${baseLessonId}".`,
16211
- affectedElement: quiz.quizId
16212
- });
16213
- }
16214
- if (activity && activity.lessonId && activity.lessonId !== baseLessonId) {
16215
- score -= 15;
16216
- findings.push({
16217
- id: "drift_act_id_mismatch",
16218
- dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
16219
- severity: "MAJOR",
16220
- title: "Activity Lesson ID Mismatch",
16221
- description: `Activity lessonId "${activity.lessonId}" diverges from master Lesson ID "${baseLessonId}".`,
16222
- remediationAdvice: `Align Activity lessonId to "${baseLessonId}".`,
16223
- affectedElement: activity.lessonId
16224
- });
16225
- }
16226
- if (slides && slides.lessonId && slides.lessonId !== baseLessonId) {
16227
- score -= 15;
16228
- findings.push({
16229
- id: "drift_slides_id_mismatch",
16230
- dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
16231
- severity: "MAJOR",
16232
- title: "Slide Deck Lesson ID Mismatch",
16233
- description: `Slide deck lessonId "${slides.lessonId}" diverges from master Lesson ID "${baseLessonId}".`,
16234
- remediationAdvice: `Align Slide deck lessonId to "${baseLessonId}".`,
16235
- affectedElement: slides.lessonId
16236
- });
16237
- }
16238
- const satellites = [
16239
- { type: "QUIZ", lang: quiz?.language },
16240
- { type: "ACT", lang: activity?.language },
16241
- { type: "SLIDE", lang: slides?.language }
16242
- ];
16243
- for (const sat of satellites) {
16244
- if (sat.lang && sat.lang.toLowerCase() !== baseLanguage.toLowerCase()) {
16245
- score -= 25;
15751
+ if (activity?.materialsBOM?.hardware && activity.materialsBOM.hardware.length > 0) {
15752
+ const hwText = activity.materialsBOM.hardware.join(" ").toLowerCase();
15753
+ const hasLED = hwText.includes("led");
15754
+ const hasResistor = hwText.includes("\u0111i\u1EC7n tr\u1EDF") || hwText.includes("resistor") || hwText.includes("220") || hwText.includes("330") || hwText.includes("1k");
15755
+ if (hasLED && !hasResistor) {
15756
+ score -= 20;
16246
15757
  findings.push({
16247
- id: `drift_language_mismatch_${sat.type}`,
16248
- dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
15758
+ id: "tech_hardware_unsafe_led_no_resistor",
15759
+ dimension: "TECHNICAL_AUTHENTICITY",
16249
15760
  severity: "CRITICAL",
16250
- title: `Language Contamination in ${sat.type}`,
16251
- description: `Master Lesson is in "${baseLanguage}" but ${sat.type} is declared in "${sat.lang}".`,
16252
- remediationAdvice: `Regenerate ${sat.type} strictly in "${baseLanguage}".`
15761
+ title: "Dangerous Hardware Circuit: LED without Current-Limiting Resistor",
15762
+ description: "Activity specifies an LED on breadboard/microcontroller without a 220\u03A9-1k\u03A9 current-limiting resistor, which causes electrical overload and burnout.",
15763
+ remediationAdvice: "Add a 220\u03A9 or 330\u03A9 current-limiting resistor to the hardware BOM."
16253
15764
  });
15765
+ } else if (hasLED && hasResistor) {
15766
+ strengths.push("Hardware BOM safely pairs LED with current-limiting resistor protection.");
16254
15767
  }
16255
15768
  }
16256
- const sections = lesson.sections || [];
16257
- const totalSectionMins = sections.reduce((sum, s) => sum + (s.durationMinutes || 0), 0);
16258
- if (totalSectionMins > 0 && (totalSectionMins < 40 || totalSectionMins > 180)) {
16259
- score -= 10;
16260
- findings.push({
16261
- id: "drift_unrealistic_lesson_duration",
16262
- dimension: "CROSS_ARTIFACT_ZERO_DRIFT",
16263
- severity: "MINOR",
16264
- title: `Unrealistic Lesson Flow Total Duration (${totalSectionMins} mins)`,
16265
- description: `Sum of 5E section durations is ${totalSectionMins} minutes. Standard K-12/College lessons span 45-120 minutes.`,
16266
- remediationAdvice: "Adjust individual section timings so total duration matches standard classroom blocks (e.g. 60m or 90m)."
16267
- });
16268
- } else if (totalSectionMins > 0) {
16269
- strengths.push(`Lesson section timings sum up to a realistic classroom block (${totalSectionMins} mins).`);
16270
- }
16271
- if (score >= 90) {
16272
- strengths.push("Zero drift verified: IDs, language policies, and pedagogical contracts are strictly aligned across all artifacts.");
16273
- }
16274
15769
  score = Math.max(0, Math.min(100, score));
16275
15770
  return {
16276
15771
  score,
16277
- weight: 0.1,
15772
+ weight: 0.15,
16278
15773
  passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
16279
15774
  strengths,
16280
15775
  findings
@@ -16282,19 +15777,22 @@ var CrossArtifactDriftEvaluator = class {
16282
15777
  }
16283
15778
  };
16284
15779
 
16285
- // src/evaluators/constructiveAlignmentEvaluator.ts
16286
- var ConstructiveAlignmentEvaluator = class {
15780
+ // src/evaluators/misconceptionEvaluator.ts
15781
+ var LAZY_DISTRACTOR_PATTERNS = [
15782
+ /^(?:tất cả các đáp án trên đều (?:đúng|sai)|cả a,?\s*b,?\s*c đều (?:đúng|sai)|all of the above|none of the above)[\.\?!]?$/i,
15783
+ /^(?:không có đáp án nào đúng|không có chức năng|không làm gì cả|không ảnh hưởng)[\.\?!]?$/i,
15784
+ /^(?:đáp án khác|other)[\.\?!]?$/i
15785
+ ];
15786
+ var MisconceptionEvaluator = class {
16287
15787
  /**
16288
- * Evaluates constructive alignment within a LessonPlan and across its satellite artifacts.
15788
+ * Audits the psychometric and pedagogical rigor of diagnostic questions and their distractors.
16289
15789
  */
16290
- static evaluateLesson(lesson, quiz, activity) {
15790
+ static evaluateQuiz(quiz) {
16291
15791
  const findings = [];
16292
15792
  const strengths = [];
16293
15793
  let score = 100;
16294
- const los = lesson.learningObjectives || [];
16295
- const sections = lesson.sections || [];
16296
- const exitTicket = lesson.exitTicket;
16297
- if (los.length === 0) {
15794
+ const questions = quiz.questions || [];
15795
+ if (questions.length === 0) {
16298
15796
  return {
16299
15797
  score: 0,
16300
15798
  weight: 0.2,
@@ -16302,85 +15800,84 @@ var ConstructiveAlignmentEvaluator = class {
16302
15800
  strengths: [],
16303
15801
  findings: [
16304
15802
  {
16305
- id: "align_no_los",
16306
- dimension: "CONSTRUCTIVE_ALIGNMENT",
15803
+ id: "misconception_no_questions",
15804
+ dimension: "MISCONCEPTION_RIGOR",
16307
15805
  severity: "CRITICAL",
16308
- title: "No Learning Objectives Defined",
16309
- description: "Constructive alignment cannot be established without baseline LOs.",
16310
- remediationAdvice: "Define at least 2 measurable Learning Objectives."
15806
+ title: "Empty Question Bank",
15807
+ description: "No questions provided in quiz artifact.",
15808
+ remediationAdvice: "Generate diagnostic questions with deliberate misconception traps."
16311
15809
  }
16312
15810
  ]
16313
15811
  };
16314
15812
  }
16315
- const combinedSectionText = sections.map((s) => `${s.title} ${s.teacherActions} ${s.studentActions} ${(s.analogies || []).join(" ")}`).join(" ").toLowerCase();
16316
- for (const lo of los) {
16317
- const loKeywords = (lo.name || lo.description || "").toLowerCase().split(/\s+/).filter((w) => w.length > 4);
16318
- const hasCoverage = loKeywords.some((k) => combinedSectionText.includes(k));
16319
- if (!hasCoverage && loKeywords.length > 0) {
15813
+ let questionsWithFullExplanations = 0;
15814
+ for (let i = 0; i < questions.length; i++) {
15815
+ const q = questions[i];
15816
+ const qId = q.id || `Q${i + 1}`;
15817
+ const options = q.options || [];
15818
+ if (options.length < 4) {
16320
15819
  score -= 15;
16321
15820
  findings.push({
16322
- id: `align_uncovered_lo_${lo.code}`,
16323
- dimension: "CONSTRUCTIVE_ALIGNMENT",
15821
+ id: `misconception_few_options_${qId}`,
15822
+ dimension: "MISCONCEPTION_RIGOR",
16324
15823
  severity: "MAJOR",
16325
- title: `Untaught Objective in Lesson Flow: "${lo.code}"`,
16326
- description: `The LO "${lo.name || lo.code}" is declared in the objectives table but receives minimal coverage in the 5E lesson sections.`,
16327
- remediationAdvice: `Add explicit Teacher Moves and Student Actions in the Explore/Explain sections addressing "${lo.name}".`,
16328
- affectedElement: lo.code
15824
+ title: `Insufficient Distractors in ${qId}`,
15825
+ description: `Question ${qId} has only ${options.length} options. Standard diagnostic rigor requires 4 plausible choices (1 key + 3 diagnostic distractors).`,
15826
+ remediationAdvice: "Provide 4 full options (A, B, C, D) representing distinct cognitive states.",
15827
+ affectedElement: qId
16329
15828
  });
16330
15829
  }
16331
- }
16332
- if (!exitTicket || !exitTicket.questionStem || exitTicket.questionStem.trim().length < 15) {
16333
- score -= 15;
16334
- findings.push({
16335
- id: "align_missing_exit_ticket",
16336
- dimension: "CONSTRUCTIVE_ALIGNMENT",
16337
- severity: "MAJOR",
16338
- title: "Missing or Shallow Exit Ticket",
16339
- description: "The lesson ends without a rigorous formative Exit Ticket to measure LO attainment before class dismissal.",
16340
- remediationAdvice: "Include an Exit Ticket question matching the primary LO + a metacognitive reflection prompt."
16341
- });
16342
- } else {
16343
- strengths.push("Formative Exit Ticket is properly anchored at the conclusion of the lesson.");
16344
- }
16345
- if (quiz) {
16346
- const quizQuestions = quiz.questions || [];
16347
- if (quizQuestions.length > 0) {
16348
- const quizText = quizQuestions.map((q) => `${q.scenarioOrStem} ${(q.options || []).map((o) => o.text).join(" ")}`).join(" ").toLowerCase();
16349
- const lessonTopicKeywords = (lesson.topic || lesson.title || "").toLowerCase().split(/\s+/).filter((w) => w.length > 3);
16350
- const hasTopicMatch = lessonTopicKeywords.some((k) => quizText.includes(k));
16351
- if (!hasTopicMatch && lessonTopicKeywords.length > 0) {
16352
- score -= 20;
16353
- findings.push({
16354
- id: "align_quiz_topic_divergence",
16355
- dimension: "CONSTRUCTIVE_ALIGNMENT",
16356
- severity: "CRITICAL",
16357
- title: "Quiz Topic Divergence from Lesson",
16358
- description: `Quiz questions do not reflect the main topic "${lesson.topic}" of the master lesson.`,
16359
- remediationAdvice: "Re-align quiz questions strictly to the core concepts taught in the lesson."
16360
- });
16361
- } else {
16362
- strengths.push("Diagnostic Quiz questions tightly reflect the lesson topic and concepts.");
16363
- }
16364
- }
16365
- }
16366
- if (activity) {
16367
- const actObj = (activity.objective || "").toLowerCase();
16368
- const lessonTitleLower = (lesson.title || "").toLowerCase();
16369
- const hasActAlignment = los.some((lo) => actObj.includes((lo.name || "").toLowerCase()) || actObj.includes((lo.description || "").toLowerCase())) || actObj.includes(lessonTitleLower) || lessonTitleLower.split(/\s+/).some((w) => w.length > 4 && actObj.includes(w));
16370
- if (!hasActAlignment && activity.objective) {
16371
- score -= 15;
15830
+ const correctCount = options.filter((o) => o.isCorrect).length;
15831
+ if (correctCount !== 1) {
15832
+ score -= 25;
16372
15833
  findings.push({
16373
- id: "align_act_objective_divergence",
16374
- dimension: "CONSTRUCTIVE_ALIGNMENT",
16375
- severity: "MAJOR",
16376
- title: "Activity Objective Disconnected from Lesson Plan",
16377
- description: `Activity objective "${activity.objective}" does not directly support the lesson LOs.`,
16378
- remediationAdvice: "Ensure the Activity hands-on lab operationalizes the exact LOs specified in the Master Lesson."
15834
+ id: `misconception_invalid_correct_count_${qId}`,
15835
+ dimension: "MISCONCEPTION_RIGOR",
15836
+ severity: "CRITICAL",
15837
+ title: `Key Assignment Error in ${qId}`,
15838
+ description: `Question ${qId} has ${correctCount} correct options (must have exactly 1 true answer).`,
15839
+ remediationAdvice: "Set `isCorrect: true` on exactly one option and `isCorrect: false` on all distractors.",
15840
+ affectedElement: qId
15841
+ });
15842
+ }
15843
+ let missingExplanation = false;
15844
+ for (const opt of options) {
15845
+ const text = (opt.text || "").trim();
15846
+ const explanation = (opt.explanation || "").trim();
15847
+ if (LAZY_DISTRACTOR_PATTERNS.some((p) => p.test(text))) {
15848
+ score -= 10;
15849
+ findings.push({
15850
+ id: `misconception_lazy_distractor_${qId}_${opt.id}`,
15851
+ dimension: "MISCONCEPTION_RIGOR",
15852
+ severity: "MAJOR",
15853
+ title: `Low-Utility Distractor in ${qId} (${opt.id})`,
15854
+ description: `Option "${text}" is a generic/throwaway distractor ("All/None of the above" or "No effect") that does not diagnose student cognitive models.`,
15855
+ remediationAdvice: "Replace with an authentic student misconception (e.g. inverted logic, missing pullup, off-by-one boundary, unit confusion).",
15856
+ affectedElement: `${qId}.${opt.id}`
15857
+ });
15858
+ }
15859
+ if (!explanation || explanation.length < 20) {
15860
+ missingExplanation = true;
15861
+ }
15862
+ }
15863
+ if (missingExplanation) {
15864
+ score -= 10;
15865
+ findings.push({
15866
+ id: `misconception_shallow_explanation_${qId}`,
15867
+ dimension: "MISCONCEPTION_RIGOR",
15868
+ severity: "MAJOR",
15869
+ title: `Shallow Distractor Explanations in ${qId}`,
15870
+ description: `Question ${qId} lacks detailed pedagogical explanations for all options. Students and teachers cannot diagnose root causes without option-level rationale.`,
15871
+ remediationAdvice: "For EVERY option (A, B, C, D), explicitly write why the choice is correct or what mental misconception it represents.",
15872
+ affectedElement: qId
16379
15873
  });
16380
15874
  } else {
16381
- strengths.push("Activity lab objective directly operationalizes master lesson learning goals.");
15875
+ questionsWithFullExplanations++;
16382
15876
  }
16383
15877
  }
15878
+ if (questionsWithFullExplanations === questions.length && questions.length >= 3) {
15879
+ strengths.push("All quiz questions feature rigorous 4-option breakdown with comprehensive misconception analysis.");
15880
+ }
16384
15881
  score = Math.max(0, Math.min(100, score));
16385
15882
  return {
16386
15883
  score,
@@ -16392,345 +15889,287 @@ var ConstructiveAlignmentEvaluator = class {
16392
15889
  }
16393
15890
  };
16394
15891
 
16395
- // src/evaluators/fiveEInstructionalEvaluator.ts
16396
- var FIVE_E_STAGES = [
16397
- { key: "engage", label: "Engage (Kh\u1EDFi \u0111\u1ED9ng / M\xF3c neo)", regex: /(?:engage|khởi động|hook|mở đầu|anchor)/i },
16398
- { key: "explore", label: "Explore (Kh\xE1m ph\xE1 / Tr\u1EA3i nghi\u1EC7m)", regex: /(?:explore|khám phá|trải nghiệm|thử nghiệm)/i },
16399
- { key: "explain", label: "Explain (Gi\u1EA3i th\xEDch / H\xECnh th\xE0nh ki\u1EBFn th\u1EE9c)", regex: /(?:explain|giải thích|khái niệm|kiến thức cốt lõi)/i },
16400
- { key: "elaborate", label: "Elaborate (V\u1EADn d\u1EE5ng / M\u1EDF r\u1ED9ng)", regex: /(?:elaborate|vận dụng|luyện tập|thực hành|áp dụng)/i },
16401
- { key: "evaluate", label: "Evaluate (\u0110\xE1nh gi\xE1 / T\u1ED5ng k\u1EBFt)", regex: /(?:evaluate|đánh giá|tổng kết|wrap-up|exit ticket)/i }
16402
- ];
16403
- var FiveEInstructionalEvaluator = class {
16404
- /**
16405
- * Evaluates the pedagogical structure of a Lesson Plan against the 5E Inquiry Model.
16406
- */
16407
- static evaluateLesson(lesson) {
16408
- const findings = [];
16409
- const strengths = [];
16410
- let score = 100;
16411
- if (!lesson.hookScenario || lesson.hookScenario.trim().length < 30) {
16412
- score -= 20;
16413
- findings.push({
16414
- id: "5e_weak_hook",
16415
- dimension: "5E_INSTRUCTIONAL_FIDELITY",
16416
- severity: "MAJOR",
16417
- title: "Shallow or Missing Real-World Hook",
16418
- description: "The lesson lacks a compelling, high-stakes real-world scenario to trigger inquiry.",
16419
- remediationAdvice: "Frame the lesson around an authentic engineering or domain problem (e.g. server crash, sensor failure, clinical anomaly)."
16420
- });
16421
- } else {
16422
- strengths.push("Engaging real-world hook scenario sets high-stakes context for inquiry.");
15892
+ // src/standards/standardsCoverageGate.ts
15893
+ function resolveStatementRef(ref, packs) {
15894
+ const [head, ...rest] = ref.split(":");
15895
+ const candidatePacks = rest.length > 0 ? packs.filter((p) => p.manifest.id === head) : packs;
15896
+ const statementId = rest.length > 0 ? rest.join(":") : ref;
15897
+ for (const p of candidatePacks) {
15898
+ if (p.statements.some((s) => s.id === statementId)) return { pack: p, statementId };
15899
+ }
15900
+ return null;
15901
+ }
15902
+ function evaluateStandardsCoverage(input) {
15903
+ const rows = [];
15904
+ const aoToLo = /* @__PURE__ */ new Map();
15905
+ for (const ao of input.assessmentObjectives ?? []) aoToLo.set(ao.aoCode, ao.alignedLO);
15906
+ const loToRefs = /* @__PURE__ */ new Map();
15907
+ for (const lo of input.objectives) {
15908
+ loToRefs.set(lo.code, new Set(lo.standardRefs ?? []));
15909
+ }
15910
+ const taughtLOs = /* @__PURE__ */ new Set();
15911
+ for (const act of input.activities ?? []) for (const lo of act.lo) taughtLOs.add(lo);
15912
+ const assessedLOs = /* @__PURE__ */ new Set();
15913
+ for (const q of input.quizQuestions) {
15914
+ if (q.alignedLO) assessedLOs.add(q.alignedLO);
15915
+ if (q.alignedAO) {
15916
+ const lo = aoToLo.get(q.alignedAO);
15917
+ if (lo) assessedLOs.add(lo);
16423
15918
  }
16424
- if (!lesson.hookQuestions || lesson.hookQuestions.length < 2) {
16425
- score -= 10;
16426
- findings.push({
16427
- id: "5e_insufficient_hook_questions",
16428
- dimension: "5E_INSTRUCTIONAL_FIDELITY",
16429
- severity: "MINOR",
16430
- title: "Insufficient Inquiry Questions in Hook",
16431
- description: "Need at least 2 open-ended inquiry questions in the Engage phase to activate prior mental models.",
16432
- remediationAdvice: "Add 2-3 provocative questions challenging common student assumptions."
16433
- });
15919
+ }
15920
+ for (const pack of input.packs) {
15921
+ const mappingByStatement = /* @__PURE__ */ new Map();
15922
+ for (const m of pack.mappings ?? []) {
15923
+ const prev = mappingByStatement.get(m.statementId);
15924
+ if (!prev || prev !== "covers" && m.kind === "covers" || prev === "extends" && m.kind !== "extends") {
15925
+ mappingByStatement.set(m.statementId, m.kind);
15926
+ }
16434
15927
  }
16435
- const sections = lesson.sections || [];
16436
- const coveredStages = /* @__PURE__ */ new Set();
16437
- for (const section of sections) {
16438
- for (const stage of FIVE_E_STAGES) {
16439
- if (stage.regex.test(section.title)) {
16440
- coveredStages.add(stage.key);
15928
+ for (const statement of pack.statements) {
15929
+ const refFull = `${pack.manifest.id}:${statement.id}`;
15930
+ const issues = [];
15931
+ const los = input.objectives.filter((lo) => (lo.standardRefs ?? []).some((r) => r === statement.id || r === refFull)).map((lo) => lo.code);
15932
+ const kind = mappingByStatement.get(statement.id);
15933
+ const hasMapping = kind !== void 0;
15934
+ const isComplianceRelevant = kind === "covers";
15935
+ const hasActivity = los.some((lo) => taughtLOs.has(lo));
15936
+ const hasAssessment = los.some((lo) => assessedLOs.has(lo));
15937
+ let status;
15938
+ if (!hasMapping) status = "UNMAPPED";
15939
+ else if (!isComplianceRelevant) status = "PARTIAL";
15940
+ else if (los.length > 0 && hasActivity && hasAssessment) status = "COVERED";
15941
+ else status = "UNCOVERED";
15942
+ if (status === "UNCOVERED") {
15943
+ if (los.length === 0) issues.push("No learning objective anchors this statement (missing standardRefs).");
15944
+ else {
15945
+ if (!hasActivity) issues.push(`Anchored LO(s) ${los.join(", ")} have no activity in lesson flow.`);
15946
+ if (!hasAssessment) issues.push(`Anchored LO(s) ${los.join(", ")} are not measured by any quiz question.`);
16441
15947
  }
16442
15948
  }
16443
- }
16444
- if (lesson.hookScenario) coveredStages.add("engage");
16445
- if (lesson.exitTicket) coveredStages.add("evaluate");
16446
- const missingStages = FIVE_E_STAGES.filter((s) => !coveredStages.has(s.key));
16447
- if (missingStages.length > 0) {
16448
- score -= missingStages.length * 10;
16449
- findings.push({
16450
- id: "5e_missing_phases",
16451
- dimension: "5E_INSTRUCTIONAL_FIDELITY",
16452
- severity: missingStages.length > 2 ? "CRITICAL" : "MAJOR",
16453
- title: `Incomplete 5E Instructional Cycle (Missing ${missingStages.length} phase(s))`,
16454
- description: `Lesson plan does not clearly demarcate: ${missingStages.map((s) => s.label).join(", ")}.`,
16455
- remediationAdvice: "Ensure the lesson explicitly walks through Engage -> Explore -> Explain -> Elaborate -> Evaluate."
15949
+ rows.push({
15950
+ packId: pack.manifest.id,
15951
+ statementId: statement.id,
15952
+ statementText: Object.values(statement.texts)[0] ?? "",
15953
+ status,
15954
+ objectives: los,
15955
+ hasActivity,
15956
+ hasAssessment,
15957
+ issues
16456
15958
  });
16457
- } else {
16458
- strengths.push("Full 5E Instructional Cycle (Engage, Explore, Explain, Elaborate, Evaluate) is completely covered.");
16459
15959
  }
16460
- let passiveSections = 0;
16461
- for (let i = 0; i < sections.length; i++) {
16462
- const s = sections[i];
16463
- const studentAct = (s.studentActions || "").trim().toLowerCase();
16464
- if (studentAct.includes("nghe gi\u1EA3ng") || studentAct.includes("ch\xE9p b\xE0i") || studentAct.includes("listen passively") || studentAct.length < 10) {
16465
- passiveSections++;
15960
+ }
15961
+ for (const lo of input.objectives) {
15962
+ for (const r of lo.standardRefs ?? []) {
15963
+ if (!resolveStatementRef(r, input.packs)) {
15964
+ rows.push({
15965
+ packId: "(unresolved)",
15966
+ statementId: r,
15967
+ statementText: "",
15968
+ status: "UNCOVERED",
15969
+ objectives: [lo.code],
15970
+ hasActivity: false,
15971
+ hasAssessment: false,
15972
+ issues: [`standardRef "${r}" does not exist in any adopted pack \u2014 likely a hallucinated or stale ID`]
15973
+ });
16466
15974
  }
16467
15975
  }
16468
- if (passiveSections > 0 && sections.length > 0) {
16469
- score -= passiveSections * 8;
16470
- findings.push({
16471
- id: "5e_passive_student_roles",
16472
- dimension: "5E_INSTRUCTIONAL_FIDELITY",
16473
- severity: "MAJOR",
16474
- title: "Passive Student Roles Detected in Lesson Sections",
16475
- description: `${passiveSections} section(s) assign passive roles (listening/copying) to students rather than active inquiry, pair-discussion, or hands-on experimentation.`,
16476
- remediationAdvice: "Transform student actions into active tasks (e.g. Think-Pair-Share, code tracing, hypothesis testing, live bug hunting)."
16477
- });
16478
- } else if (sections.length > 0) {
16479
- strengths.push("Student roles emphasize active learning and hands-on participation throughout.");
15976
+ }
15977
+ const complianceRows = rows.filter((r) => r.status !== "UNMAPPED" && r.status !== "PARTIAL");
15978
+ const covered = complianceRows.filter((r) => r.status === "COVERED").length;
15979
+ const coveragePct = complianceRows.length === 0 ? 100 : Math.round(covered / complianceRows.length * 100);
15980
+ const unresolvedCount = rows.filter((r) => r.packId === "(unresolved)").length;
15981
+ const verdict = unresolvedCount > 0 || coveragePct < 60 ? "FAIL" : coveragePct < 100 ? "WARN" : "PASS";
15982
+ const lines = [
15983
+ "# Standards Coverage Report",
15984
+ "",
15985
+ `- Verdict: **${verdict}**`,
15986
+ `- Compliance coverage: **${coveragePct}%** (${covered}/${complianceRows.length} "covers" statements fully taught + assessed)`,
15987
+ `- Unresolved standardRefs: ${unresolvedCount}`,
15988
+ "",
15989
+ "| Pack | Statement | Status | LOs | Activity | Assessment |",
15990
+ "|---|---|---|---|---|---|",
15991
+ ...rows.map(
15992
+ (r) => `|${r.packId}|${r.statementId}|${r.status}|${r.objectives.join(", ") || "\u2014"}|${r.hasActivity ? "\u2713" : "\u2717"}|${r.hasAssessment ? "\u2713" : "\u2717"}|`
15993
+ )
15994
+ ];
15995
+ const issueLines = rows.filter((r) => r.issues.length > 0).flatMap((r) => r.issues.map((i) => `- [${r.packId}:${r.statementId}] ${i}`));
15996
+ if (issueLines.length > 0) lines.push("", "## Issues", ...issueLines);
15997
+ return {
15998
+ verdict,
15999
+ coveragePct,
16000
+ rows,
16001
+ summary: `Standards coverage ${coveragePct}% \u2014 verdict ${verdict}`,
16002
+ rawMarkdownReport: lines.join("\n")
16003
+ };
16004
+ }
16005
+ var StandardsRegistryAdapter = class {
16006
+ client;
16007
+ constructor(config = {}) {
16008
+ if (config.client) {
16009
+ this.client = config.client;
16010
+ return;
16480
16011
  }
16481
- score = Math.max(0, Math.min(100, score));
16482
- return {
16483
- score,
16484
- weight: 0.15,
16485
- passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
16486
- strengths,
16487
- findings
16488
- };
16012
+ const url = config.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || "";
16013
+ const key = config.supabaseServiceRoleKey || process.env.SUPABASE_SERVICE_ROLE_KEY || config.supabaseAnonKey || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
16014
+ if (!url || !key) {
16015
+ throw new Error("StandardsRegistryAdapter: Supabase URL/key missing (pass client or env).");
16016
+ }
16017
+ this.client = supabaseJs.createClient(url, key);
16489
16018
  }
16490
- };
16491
-
16492
- // src/evaluators/codeHardwareFeasibilityEvaluator.ts
16493
- var VALID_MERMAID_STARTERS = [
16494
- "graph",
16495
- "flowchart",
16496
- "sequencediagram",
16497
- "statediagram",
16498
- "classdiagram",
16499
- "erdiagram",
16500
- "gantt",
16501
- "gitgraph"
16502
- ];
16503
- var CodeHardwareFeasibilityEvaluator = class {
16504
- /**
16505
- * Audits technical accuracy, code syntax sanity, and hardware circuit safety.
16506
- */
16507
- static evaluateTechnicalFeasibility(lesson, codeLab, activity) {
16508
- const findings = [];
16509
- const strengths = [];
16510
- let score = 100;
16511
- if (lesson?.guidedPractice) {
16512
- const { codeSnippet, codeLanguage, mermaidDiagram } = lesson.guidedPractice;
16513
- if (!codeSnippet || codeSnippet.trim().length < 15) {
16514
- score -= 20;
16515
- findings.push({
16516
- id: "tech_empty_guided_code",
16517
- dimension: "TECHNICAL_AUTHENTICITY",
16518
- severity: "MAJOR",
16519
- title: "Empty or Trivial Guided Practice Code",
16520
- description: "Guided practice lacks runnable code snippet or domain calculation template.",
16521
- remediationAdvice: "Provide a complete, runnable code example with explanatory line-by-line comments."
16522
- });
16523
- } else {
16524
- if (codeLanguage?.toLowerCase() === "arduino" || codeLanguage?.toLowerCase() === "cpp" || codeSnippet.includes("pinMode")) {
16525
- if (codeSnippet.includes("digitalWrite") && !codeSnippet.includes("pinMode") && !codeSnippet.includes("setup()")) {
16526
- score -= 15;
16527
- findings.push({
16528
- id: "tech_arduino_missing_pinmode",
16529
- dimension: "TECHNICAL_AUTHENTICITY",
16530
- severity: "MAJOR",
16531
- title: "Missing pinMode() Configuration in Arduino Code",
16532
- description: "Code calls `digitalWrite()` without initializing the pin with `pinMode(pin, OUTPUT)`.",
16533
- remediationAdvice: "Ensure `setup()` configures pin direction before writing digital states."
16534
- });
16535
- }
16536
- if (codeSnippet.includes("delay(0)") || codeSnippet.includes("delay(-")) {
16537
- score -= 15;
16538
- findings.push({
16539
- id: "tech_arduino_invalid_delay",
16540
- dimension: "TECHNICAL_AUTHENTICITY",
16541
- severity: "MAJOR",
16542
- title: "Invalid delay() Parameter",
16543
- description: "delay() duration must be a positive integer in milliseconds.",
16544
- remediationAdvice: "Use realistic delay timings (e.g. 500ms, 1000ms)."
16545
- });
16546
- }
16547
- }
16548
- strengths.push("Guided practice features runnable code snippet with clear syntax.");
16549
- }
16550
- if (mermaidDiagram) {
16551
- const cleanDiagram = mermaidDiagram.trim().toLowerCase();
16552
- const isValidStarter = VALID_MERMAID_STARTERS.some((starter) => cleanDiagram.startsWith(starter));
16553
- if (!isValidStarter) {
16554
- score -= 15;
16555
- findings.push({
16556
- id: "tech_invalid_mermaid_syntax",
16557
- dimension: "TECHNICAL_AUTHENTICITY",
16558
- severity: "MAJOR",
16559
- title: "Invalid Mermaid Diagram Syntax",
16560
- description: `Mermaid diagram does not start with a valid declaration (e.g. "flowchart TD", "graph TD", "sequenceDiagram"). Got: "${mermaidDiagram.substring(0, 30)}..."`,
16561
- remediationAdvice: "Format Mermaid diagrams starting with `flowchart TD` or `sequenceDiagram`."
16562
- });
16563
- } else {
16564
- strengths.push("Valid Mermaid architectural diagram included.");
16565
- }
16566
- }
16567
- }
16568
- if (codeLab) {
16569
- const starter = codeLab.starterCode?.content || "";
16570
- const solution = codeLab.solutionCode?.content || "";
16571
- if (starter.length < 20 || solution.length < 20) {
16572
- score -= 25;
16573
- findings.push({
16574
- id: "tech_codelab_incomplete_codes",
16575
- dimension: "TECHNICAL_AUTHENTICITY",
16576
- severity: "CRITICAL",
16577
- title: "Incomplete CodeLab Starter / Solution Code",
16578
- description: "CodeLab must provide both scaffolding starter code and complete reference solution code.",
16579
- remediationAdvice: "Populate `starterCode` with TODO markers and `solutionCode` with tested implementation."
16580
- });
16581
- } else {
16582
- strengths.push("CodeLab provides complete starter skeleton and working solution code.");
16583
- }
16584
- if (!codeLab.testCases || codeLab.testCases.length === 0) {
16585
- score -= 15;
16586
- findings.push({
16587
- id: "tech_codelab_missing_testcases",
16588
- dimension: "TECHNICAL_AUTHENTICITY",
16589
- severity: "MAJOR",
16590
- title: "Missing Automated Verification Test Cases",
16591
- description: "CodeLab lacks test cases for students to self-verify their implementations.",
16592
- remediationAdvice: "Add at least 2 concrete test cases with input and expected output assertions."
16593
- });
16594
- }
16019
+ // ─── Intake (write path — service role) ───────────────────────────────────
16020
+ /** Persist a schema-validated pack as a new framework (status=draft). */
16021
+ async importPack(pack, opts) {
16022
+ const parsed = FrameworkPackSchema.safeParse(pack);
16023
+ if (!parsed.success) {
16024
+ throw new Error("importPack: pack failed schema validation: " + parsed.error.issues.map((i) => i.path.join(".") + ": " + i.message).join("; "));
16595
16025
  }
16596
- if (activity?.materialsBOM?.hardware && activity.materialsBOM.hardware.length > 0) {
16597
- const hwText = activity.materialsBOM.hardware.join(" ").toLowerCase();
16598
- const hasLED = hwText.includes("led");
16599
- const hasResistor = hwText.includes("\u0111i\u1EC7n tr\u1EDF") || hwText.includes("resistor") || hwText.includes("220") || hwText.includes("330") || hwText.includes("1k");
16600
- if (hasLED && !hasResistor) {
16601
- score -= 20;
16602
- findings.push({
16603
- id: "tech_hardware_unsafe_led_no_resistor",
16604
- dimension: "TECHNICAL_AUTHENTICITY",
16605
- severity: "CRITICAL",
16606
- title: "Dangerous Hardware Circuit: LED without Current-Limiting Resistor",
16607
- description: "Activity specifies an LED on breadboard/microcontroller without a 220\u03A9-1k\u03A9 current-limiting resistor, which causes electrical overload and burnout.",
16608
- remediationAdvice: "Add a 220\u03A9 or 330\u03A9 current-limiting resistor to the hardware BOM."
16609
- });
16610
- } else if (hasLED && hasResistor) {
16611
- strengths.push("Hardware BOM safely pairs LED with current-limiting resistor protection.");
16612
- }
16026
+ const p = parsed.data;
16027
+ const { data: fw, error: fwErr } = await this.client.from("standards_frameworks").insert({
16028
+ pack_id: p.manifest.id,
16029
+ content_version: p.manifest.contentVersion,
16030
+ name: p.manifest.name,
16031
+ spec_version: p.manifest.specVersion,
16032
+ subject: p.manifest.subject,
16033
+ languages: p.manifest.languages,
16034
+ grade_model: p.manifest.gradeModel,
16035
+ provenance: p.manifest.provenance,
16036
+ trust: p.manifest.trust,
16037
+ status: "draft",
16038
+ organization_code: opts?.organizationCode ?? (p.manifest.trust === "verified" ? null : p.manifest.provenance.importedBy?.replace(/^org:/, "") || null),
16039
+ original_file_path: opts?.originalFilePath ?? null,
16040
+ created_by: opts?.createdBy ?? null
16041
+ }).select("id").single();
16042
+ if (fwErr) throw new Error("importPack: framework insert failed: " + fwErr.message);
16043
+ const frameworkId = fw.id;
16044
+ const statementRows = p.statements.map((s) => ({
16045
+ framework_id: frameworkId,
16046
+ statement_id: s.id,
16047
+ parent_statement_id: s.parentId ?? null,
16048
+ grade_min: s.gradeBand[0],
16049
+ grade_max: s.gradeBand[1],
16050
+ texts: s.texts,
16051
+ classifications: s.classifications ?? [],
16052
+ bloom_hint: s.bloomHint ?? null,
16053
+ keywords: s.keywords ?? [],
16054
+ source_ref: s.sourceRef ?? null,
16055
+ provenance: s.provenance
16056
+ }));
16057
+ const { error: stErr, count: stCount } = await this.client.from("standards_statements").insert(statementRows, { count: "exact" });
16058
+ if (stErr) throw new Error("importPack: statement insert failed: " + stErr.message);
16059
+ let mappingCount = 0;
16060
+ if (p.mappings && p.mappings.length > 0) {
16061
+ const mappingRows = p.mappings.map((m) => ({
16062
+ framework_id: frameworkId,
16063
+ statement_id: m.statementId,
16064
+ target_ref: m.targetRef,
16065
+ kind: m.kind,
16066
+ confidence: m.confidence,
16067
+ provenance: m.provenance
16068
+ }));
16069
+ const { error: mpErr, count: mpCount } = await this.client.from("standards_mappings").insert(mappingRows, { count: "exact" });
16070
+ if (mpErr) throw new Error("importPack: mapping insert failed: " + mpErr.message);
16071
+ mappingCount = mpCount ?? mappingRows.length;
16613
16072
  }
16614
- score = Math.max(0, Math.min(100, score));
16615
- return {
16616
- score,
16617
- weight: 0.15,
16618
- passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
16619
- strengths,
16620
- findings
16621
- };
16073
+ return { frameworkDbId: frameworkId, statementsInserted: stCount ?? statementRows.length, mappingsInserted: mappingCount };
16622
16074
  }
16623
- };
16624
-
16625
- // src/evaluators/misconceptionEvaluator.ts
16626
- var LAZY_DISTRACTOR_PATTERNS = [
16627
- /^(?:tất cả các đáp án trên đều (?:đúng|sai)|cả a,?\s*b,?\s*c đều (?:đúng|sai)|all of the above|none of the above)[\.\?!]?$/i,
16628
- /^(?:không có đáp án nào đúng|không có chức năng|không làm gì cả|không ảnh hưởng)[\.\?!]?$/i,
16629
- /^(?:đáp án khác|other)[\.\?!]?$/i
16630
- ];
16631
- var MisconceptionEvaluator = class {
16075
+ /** Activate a draft framework (immutable version is now live). */
16076
+ async activatePack(frameworkDbId) {
16077
+ const { error } = await this.client.from("standards_frameworks").update({ status: "active" }).eq("id", frameworkDbId).eq("status", "draft");
16078
+ if (error) throw new Error("activatePack failed: " + error.message);
16079
+ }
16080
+ async deprecatePack(frameworkDbId) {
16081
+ const { error } = await this.client.from("standards_frameworks").update({ status: "deprecated" }).eq("id", frameworkDbId);
16082
+ if (error) throw new Error("deprecatePack failed: " + error.message);
16083
+ }
16084
+ async adoptPack(frameworkDbId, organizationCode, adoptedBy) {
16085
+ const { error } = await this.client.from("standards_org_adoptions").upsert(
16086
+ { framework_id: frameworkDbId, organization_code: organizationCode, adopted_by: adoptedBy ?? null },
16087
+ { onConflict: "organization_code,framework_id" }
16088
+ );
16089
+ if (error) throw new Error("adoptPack failed: " + error.message);
16090
+ }
16091
+ // ─── Runtime (read path — generation) ─────────────────────────────────────
16632
16092
  /**
16633
- * Audits the psychometric and pedagogical rigor of diagnostic questions and their distractors.
16093
+ * Load all packs usable by an org: platform packs (verified, org IS NULL) +
16094
+ * org packs + org adoptions. Hydrated into the same FrameworkPack shape the
16095
+ * in-memory Phase-1 components consume (injector / coverage gate / judge).
16634
16096
  */
16635
- static evaluateQuiz(quiz) {
16636
- const findings = [];
16637
- const strengths = [];
16638
- let score = 100;
16639
- const questions = quiz.questions || [];
16640
- if (questions.length === 0) {
16641
- return {
16642
- score: 0,
16643
- weight: 0.2,
16644
- passed: false,
16645
- strengths: [],
16646
- findings: [
16647
- {
16648
- id: "misconception_no_questions",
16649
- dimension: "MISCONCEPTION_RIGOR",
16650
- severity: "CRITICAL",
16651
- title: "Empty Question Bank",
16652
- description: "No questions provided in quiz artifact.",
16653
- remediationAdvice: "Generate diagnostic questions with deliberate misconception traps."
16654
- }
16655
- ]
16656
- };
16097
+ async loadPacksForOrg(organizationCode) {
16098
+ let query = this.client.from("standards_frameworks").select("*, standards_org_adoptions(organization_code)").eq("status", "active");
16099
+ const { data: frameworks, error } = await query;
16100
+ if (error) throw new Error("loadPacksForOrg: " + error.message);
16101
+ const visible = (frameworks ?? []).filter((f) => {
16102
+ const own = f.organization_code && organizationCode && f.organization_code === organizationCode;
16103
+ const global = !f.organization_code;
16104
+ const adopted = organizationCode && (f.standards_org_adoptions ?? []).some((a) => a.organization_code === organizationCode);
16105
+ return global || own || adopted;
16106
+ });
16107
+ if (visible.length === 0) return [];
16108
+ const ids = visible.map((f) => f.id);
16109
+ const [{ data: statements, error: stErr }, { data: mappings, error: mpErr }, { data: overrides, error: ovErr }] = await Promise.all([
16110
+ this.client.from("standards_statements").select("*").in("framework_id", ids),
16111
+ this.client.from("standards_mappings").select("*").in("framework_id", ids),
16112
+ this.client.from("standards_mapping_overrides").select("*").in("framework_id", ids)
16113
+ ]);
16114
+ if (stErr) throw new Error("loadPacksForOrg statements: " + stErr.message);
16115
+ const overridesByFw = /* @__PURE__ */ new Map();
16116
+ for (const o of overrides ?? []) {
16117
+ const list = overridesByFw.get(o.framework_id) ?? [];
16118
+ list.push(o);
16119
+ overridesByFw.set(o.framework_id, list);
16657
16120
  }
16658
- let questionsWithFullExplanations = 0;
16659
- for (let i = 0; i < questions.length; i++) {
16660
- const q = questions[i];
16661
- const qId = q.id || `Q${i + 1}`;
16662
- const options = q.options || [];
16663
- if (options.length < 4) {
16664
- score -= 15;
16665
- findings.push({
16666
- id: `misconception_few_options_${qId}`,
16667
- dimension: "MISCONCEPTION_RIGOR",
16668
- severity: "MAJOR",
16669
- title: `Insufficient Distractors in ${qId}`,
16670
- description: `Question ${qId} has only ${options.length} options. Standard diagnostic rigor requires 4 plausible choices (1 key + 3 diagnostic distractors).`,
16671
- remediationAdvice: "Provide 4 full options (A, B, C, D) representing distinct cognitive states.",
16672
- affectedElement: qId
16673
- });
16674
- }
16675
- const correctCount = options.filter((o) => o.isCorrect).length;
16676
- if (correctCount !== 1) {
16677
- score -= 25;
16678
- findings.push({
16679
- id: `misconception_invalid_correct_count_${qId}`,
16680
- dimension: "MISCONCEPTION_RIGOR",
16681
- severity: "CRITICAL",
16682
- title: `Key Assignment Error in ${qId}`,
16683
- description: `Question ${qId} has ${correctCount} correct options (must have exactly 1 true answer).`,
16684
- remediationAdvice: "Set `isCorrect: true` on exactly one option and `isCorrect: false` on all distractors.",
16685
- affectedElement: qId
16686
- });
16687
- }
16688
- let missingExplanation = false;
16689
- for (const opt of options) {
16690
- const text = (opt.text || "").trim();
16691
- const explanation = (opt.explanation || "").trim();
16692
- if (LAZY_DISTRACTOR_PATTERNS.some((p) => p.test(text))) {
16693
- score -= 10;
16694
- findings.push({
16695
- id: `misconception_lazy_distractor_${qId}_${opt.id}`,
16696
- dimension: "MISCONCEPTION_RIGOR",
16697
- severity: "MAJOR",
16698
- title: `Low-Utility Distractor in ${qId} (${opt.id})`,
16699
- description: `Option "${text}" is a generic/throwaway distractor ("All/None of the above" or "No effect") that does not diagnose student cognitive models.`,
16700
- remediationAdvice: "Replace with an authentic student misconception (e.g. inverted logic, missing pullup, off-by-one boundary, unit confusion).",
16701
- affectedElement: `${qId}.${opt.id}`
16702
- });
16703
- }
16704
- if (!explanation || explanation.length < 20) {
16705
- missingExplanation = true;
16706
- }
16707
- }
16708
- if (missingExplanation) {
16709
- score -= 10;
16710
- findings.push({
16711
- id: `misconception_shallow_explanation_${qId}`,
16712
- dimension: "MISCONCEPTION_RIGOR",
16713
- severity: "MAJOR",
16714
- title: `Shallow Distractor Explanations in ${qId}`,
16715
- description: `Question ${qId} lacks detailed pedagogical explanations for all options. Students and teachers cannot diagnose root causes without option-level rationale.`,
16716
- remediationAdvice: "For EVERY option (A, B, C, D), explicitly write why the choice is correct or what mental misconception it represents.",
16717
- affectedElement: qId
16718
- });
16719
- } else {
16720
- questionsWithFullExplanations++;
16121
+ return visible.map((f) => {
16122
+ const stmts = (statements ?? []).filter((s) => s.framework_id === f.id).map((s) => ({
16123
+ id: s.statement_id,
16124
+ parentId: s.parent_statement_id ?? void 0,
16125
+ gradeBand: [s.grade_min, s.grade_max],
16126
+ texts: s.texts,
16127
+ classifications: s.classifications ?? [],
16128
+ bloomHint: s.bloom_hint ?? void 0,
16129
+ keywords: s.keywords ?? [],
16130
+ sourceRef: s.source_ref ?? void 0,
16131
+ provenance: s.provenance ?? { method: "imported" }
16132
+ }));
16133
+ const ov = overridesByFw.get(f.id) ?? [];
16134
+ const retired = new Set(ov.filter((o) => o.kind === "retired").map((o) => o.statement_id + "|" + o.target_ref));
16135
+ const embedded = (mappings ?? []).filter((m) => m.framework_id === f.id).map((m) => ({
16136
+ statementId: m.statement_id,
16137
+ targetRef: m.target_ref,
16138
+ kind: m.kind,
16139
+ confidence: Number(m.confidence),
16140
+ provenance: m.provenance ?? { method: "imported" }
16141
+ }));
16142
+ const overlay = ov.filter((o) => o.kind !== "retired").map((o) => ({
16143
+ statementId: o.statement_id,
16144
+ targetRef: o.target_ref,
16145
+ kind: o.kind,
16146
+ confidence: Number(o.confidence),
16147
+ provenance: { method: "human", reviewedBy: o.created_by ?? "overlay", note: o.note ?? void 0 }
16148
+ }));
16149
+ const bridge = /* @__PURE__ */ new Map();
16150
+ for (const m of embedded) bridge.set(m.statementId + "|" + m.targetRef, m);
16151
+ for (const m of overlay) bridge.set(m.statementId + "|" + m.targetRef, m);
16152
+ const finalMappings = [...bridge.values()].filter((m) => !retired.has(m.statementId + "|" + m.targetRef));
16153
+ const hydrated = FrameworkPackSchema.safeParse({
16154
+ manifest: {
16155
+ id: f.pack_id,
16156
+ name: f.name,
16157
+ specVersion: f.spec_version || "1.0",
16158
+ contentVersion: f.content_version,
16159
+ subject: f.subject,
16160
+ languages: f.languages,
16161
+ gradeModel: f.grade_model,
16162
+ provenance: f.provenance,
16163
+ trust: f.trust
16164
+ },
16165
+ statements: stmts,
16166
+ mappings: finalMappings
16167
+ });
16168
+ if (!hydrated.success) {
16169
+ throw new Error('loadPacksForOrg: hydrated pack "' + f.pack_id + '" failed schema: ' + hydrated.error.issues[0]?.message);
16721
16170
  }
16722
- }
16723
- if (questionsWithFullExplanations === questions.length && questions.length >= 3) {
16724
- strengths.push("All quiz questions feature rigorous 4-option breakdown with comprehensive misconception analysis.");
16725
- }
16726
- score = Math.max(0, Math.min(100, score));
16727
- return {
16728
- score,
16729
- weight: 0.2,
16730
- passed: score >= 75 && !findings.some((f) => f.severity === "CRITICAL"),
16731
- strengths,
16732
- findings
16733
- };
16171
+ return { ...hydrated.data, dbId: f.id, status: f.status, organizationCode: f.organization_code ?? null };
16172
+ });
16734
16173
  }
16735
16174
  };
16736
16175
 
@@ -17408,8 +16847,6 @@ exports.WorksheetSchema = WorksheetSchema;
17408
16847
  exports.activityTools = activityTools;
17409
16848
  exports.analystTools = analystTools;
17410
16849
  exports.analyzeProjectCreationIntent = analyzeProjectCreationIntent;
17411
- exports.approvalHookToken = approvalHookToken;
17412
- exports.approvalPayloadSchema = approvalPayloadSchema;
17413
16850
  exports.assertAcyclic = assertAcyclic;
17414
16851
  exports.assessorTools = assessorTools;
17415
16852
  exports.auditCurriculumQualityFlow = auditCurriculumQualityFlow;
@@ -17435,6 +16872,7 @@ exports.buildStandardsContext = buildStandardsContext;
17435
16872
  exports.buildStandardsContextBlock = buildStandardsContextBlock;
17436
16873
  exports.buildTeacherGuidePrompt = buildTeacherGuidePrompt;
17437
16874
  exports.buildWorksheetPrompt = buildWorksheetPrompt;
16875
+ exports.closeTruncatedJson = closeTruncatedJson;
17438
16876
  exports.computeContentHash = computeContentHash;
17439
16877
  exports.computePackingSpec = computePackingSpec;
17440
16878
  exports.conductPreliminaryResearch = conductPreliminaryResearch;
@@ -17450,7 +16888,6 @@ exports.ensureExpositionForLesson = ensureExpositionForLesson;
17450
16888
  exports.ensureKnowledgeExposition = ensureKnowledgeExposition;
17451
16889
  exports.evaluateStandardsCoverage = evaluateStandardsCoverage;
17452
16890
  exports.executeCurriculumCommand = executeCurriculumCommand;
17453
- exports.executeSingleArtifactStep = executeSingleArtifactStep;
17454
16891
  exports.exportAllProjectQuizzes = exportAllProjectQuizzes;
17455
16892
  exports.expositionCacheKey = expositionCacheKey;
17456
16893
  exports.extractScopeSequenceRows = extractScopeSequenceRows;
@@ -17461,37 +16898,24 @@ exports.formatQuizzesToCsv = formatQuizzesToCsv;
17461
16898
  exports.fulfillMediaLedger = fulfillMediaLedger;
17462
16899
  exports.gateModeFor = gateModeFor;
17463
16900
  exports.generateActivityFlow = generateActivityFlow;
17464
- exports.generateActivityStep = generateActivityStep;
17465
16901
  exports.generateCodeLabFlow = generateCodeLabFlow;
17466
- exports.generateCodeLabStep = generateCodeLabStep;
17467
16902
  exports.generateDiagnosticQuizFlow = generateDiagnosticQuizFlow;
17468
- exports.generateDiagnosticQuizStep = generateDiagnosticQuizStep;
17469
16903
  exports.generateEducationalImage = generateEducationalImage;
17470
16904
  exports.generateExtensionFlow = generateExtensionFlow;
17471
- exports.generateExtensionStep = generateExtensionStep;
17472
16905
  exports.generateHandoutFlow = generateHandoutFlow;
17473
- exports.generateHandoutStep = generateHandoutStep;
17474
16906
  exports.generateLessonMasterFlow = generateLessonMasterFlow;
17475
- exports.generateMasterLessonStep = generateMasterLessonStep;
17476
16907
  exports.generateMilestoneCurriculumBundle = generateMilestoneCurriculumBundle;
17477
- exports.generateMilestoneWorkflow = generateMilestoneWorkflow;
17478
16908
  exports.generatePhase1SotArtifacts = generatePhase1SotArtifacts;
17479
16909
  exports.generatePhase2SotArtifacts = generatePhase2SotArtifacts;
17480
16910
  exports.generateProjectInstruction = generateProjectInstruction;
17481
16911
  exports.generateProjectInstructionFlow = generateProjectInstructionFlow;
17482
16912
  exports.generateRoadmapCurriculum = generateRoadmapCurriculum;
17483
- exports.generateRoadmapWorkflow = generateRoadmapWorkflow;
17484
16913
  exports.generateSelfLabFlow = generateSelfLabFlow;
17485
- exports.generateSelfLabStep = generateSelfLabStep;
17486
16914
  exports.generateSelfPacedBundle = generateSelfPacedBundle;
17487
16915
  exports.generateSingleArtifact = generateSingleArtifact;
17488
- exports.generateSingleArtifactWorkflow = generateSingleArtifactWorkflow;
17489
16916
  exports.generateSlidesFlow = generateSlidesFlow;
17490
- exports.generateSlidesStep = generateSlidesStep;
17491
16917
  exports.generateTeacherGuideFlow = generateTeacherGuideFlow;
17492
- exports.generateTeacherGuideStep = generateTeacherGuideStep;
17493
16918
  exports.generateWorksheetFlow = generateWorksheetFlow;
17494
- exports.generateWorksheetStep = generateWorksheetStep;
17495
16919
  exports.getAIModel = getAIModel;
17496
16920
  exports.getArtifactMetadata = getArtifactMetadata;
17497
16921
  exports.getDesignatedFallbackChain = getDesignatedFallbackChain;
@@ -17512,9 +16936,6 @@ exports.isExpositionFresh = isExpositionFresh;
17512
16936
  exports.isModelAllowed = isModelAllowed;
17513
16937
  exports.isProviderEnabled = isProviderEnabled;
17514
16938
  exports.isTranslationDue = isTranslationDue;
17515
- exports.judgeMasterLessonStep = judgeMasterLessonStep;
17516
- exports.judgeSatelliteStep = judgeSatelliteStep;
17517
- exports.lessonApprovalHook = lessonApprovalHook;
17518
16939
  exports.lintAndSanitizeArtifact = lintAndSanitizeArtifact;
17519
16940
  exports.lintCurriculumFramework = lintCurriculumFramework;
17520
16941
  exports.lintFrameworkPack = lintFrameworkPack;
@@ -17528,13 +16949,10 @@ exports.parseRoadmapJsonToProjectPayload = parseRoadmapJsonToProjectPayload;
17528
16949
  exports.produceBatchLessons = produceBatchLessons;
17529
16950
  exports.produceSingleLesson = produceSingleLesson;
17530
16951
  exports.publishToGitHub = publishToGitHub;
17531
- exports.publishToGitStep = publishToGitStep;
17532
16952
  exports.publishToSupabase = publishToSupabase;
17533
- exports.publishToSupabaseStep = publishToSupabaseStep;
17534
16953
  exports.rankGenCandidates = rankGenCandidates;
17535
16954
  exports.renderFrameworkFromPlan = renderFrameworkFromPlan;
17536
16955
  exports.renderMediaPlaceholder = renderMediaPlaceholder;
17537
- exports.repairMasterLessonStep = repairMasterLessonStep;
17538
16956
  exports.researcherTools = researcherTools;
17539
16957
  exports.resolveGateSettings = resolveGateSettings;
17540
16958
  exports.resolveStandardsPacks = resolveStandardsPacks;
@@ -17543,7 +16961,6 @@ exports.resolveTranslationTargets = resolveTranslationTargets;
17543
16961
  exports.reviewerTools = reviewerTools;
17544
16962
  exports.runCurriculumAIInference = runCurriculumAIInference;
17545
16963
  exports.safeParseJson = safeParseJson;
17546
- exports.saveMilestoneToWorkspaceStep = saveMilestoneToWorkspaceStep;
17547
16964
  exports.searchEducationalImages = searchEducationalImages;
17548
16965
  exports.searchEducationalVideos = searchEducationalVideos;
17549
16966
  exports.selectStatementsForLesson = selectStatementsForLesson;
@@ -17562,6 +16979,7 @@ exports.streamLLMWithFallback = streamLLMWithFallback;
17562
16979
  exports.streamLayerPrefillWithFallback = streamLayerPrefillWithFallback;
17563
16980
  exports.streamLessonMasterFlow = streamLessonMasterFlow;
17564
16981
  exports.streamSelfLabFlow = streamSelfLabFlow;
16982
+ exports.stripLlmJsonWrappers = stripLlmJsonWrappers;
17565
16983
  exports.techSmeTools = techSmeTools;
17566
16984
  exports.topoSort = topoSort;
17567
16985
  exports.uploadAssetToBucket = uploadAssetToBucket;
@@ -17570,6 +16988,5 @@ exports.validateFrameworkPack = validateFrameworkPack;
17570
16988
  exports.validateMarkdownTables = validateMarkdownTables;
17571
16989
  exports.validateMermaidSyntax = validateMermaidSyntax;
17572
16990
  exports.withAutoRepair = withAutoRepair;
17573
- exports.withRateLimitBackoff = withRateLimitBackoff;
17574
16991
  //# sourceMappingURL=index.cjs.map
17575
16992
  //# sourceMappingURL=index.cjs.map