@thanh01.pmt/curriculum-kit 1.0.12 → 1.0.14

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