@thanh01.pmt/curriculum-kit 1.4.18 → 1.4.20

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.
@@ -18878,6 +18878,268 @@ ${lines.join("\n")}
18878
18878
  - For example: if canonical section is "Computational Thinking Focus", your heading must be "## [REQUIRED] ${headings["Computational Thinking Focus"] || "Computational Thinking Focus"}".`;
18879
18879
  }
18880
18880
 
18881
+ // src/services/curriculumHorizon.ts
18882
+ init_errors();
18883
+ var DETAILED_BRIDGE_WINDOW = 2;
18884
+ var BOUNDARY_PEEK_WINDOW = 2;
18885
+ function parseAllSessions(plan, frameworkMarkdown) {
18886
+ if (plan) {
18887
+ const rawSessions = Array.isArray(plan.sessions) ? plan.sessions : Array.isArray(plan) ? plan : [];
18888
+ if (rawSessions.length > 0) {
18889
+ return rawSessions.map((s, idx) => ({
18890
+ id: s.id || `L${String(idx + 1).padStart(2, "0")}`,
18891
+ order: typeof s.order === "number" ? s.order : idx + 1,
18892
+ title: s.title || "",
18893
+ prose_objective: s.prose_objective || s.objective || "",
18894
+ new_keywords: Array.isArray(s.new_keywords) ? s.new_keywords : Array.isArray(s.keywords) ? s.keywords : [],
18895
+ prerequisite_keywords: Array.isArray(s.prerequisite_keywords) ? s.prerequisite_keywords : [],
18896
+ depth_assignments: Array.isArray(s.depth_assignments) ? s.depth_assignments : []
18897
+ }));
18898
+ }
18899
+ }
18900
+ if (frameworkMarkdown && typeof frameworkMarkdown === "string") {
18901
+ const lines = frameworkMarkdown.split("\n");
18902
+ let headerCols = [];
18903
+ const sessions = [];
18904
+ for (const line of lines) {
18905
+ const trimmed = line.trim();
18906
+ if (!trimmed.startsWith("|")) continue;
18907
+ const cols = trimmed.split("|").slice(1, -1).map((c) => c.trim());
18908
+ const lower = cols.map((c) => c.toLowerCase());
18909
+ if (lower.some((c) => c.includes("lesson code") || c === "m\xE3 b\xE0i" || c.includes("m\xE3 b\xE0i h\u1ECDc"))) {
18910
+ headerCols = cols;
18911
+ continue;
18912
+ }
18913
+ if (/^[-: |]+$/.test(trimmed.slice(1, -1))) continue;
18914
+ if (cols.length < 3) continue;
18915
+ let lessonCode = "";
18916
+ let title = "";
18917
+ let objective = "";
18918
+ let concept = "";
18919
+ let keywordsStr = "";
18920
+ if (headerCols.length > 0) {
18921
+ const col = (name) => {
18922
+ const idx = headerCols.findIndex((h) => h.toLowerCase().includes(name));
18923
+ return idx >= 0 ? cols[idx] || "" : "";
18924
+ };
18925
+ lessonCode = (col("lesson code") || col("m\xE3 b\xE0i") || cols[1] || "").replace(/\*\*/g, "").trim();
18926
+ title = (col("title") || col("t\xEAn") || cols[2] || "").replace(/\*\*/g, "").trim();
18927
+ objective = col("learning objective") || col("objective") || col("m\u1EE5c ti\xEAu") || "";
18928
+ concept = col("key concept") || col("concept") || col("kh\xE1i ni\u1EC7m") || "";
18929
+ keywordsStr = col("keywords") || col("t\u1EEB kh\xF3a") || "";
18930
+ } else {
18931
+ lessonCode = (cols[1] || "").replace(/\*\*/g, "").trim();
18932
+ title = (cols[2] || "").replace(/\*\*/g, "").trim();
18933
+ objective = cols[5] || "";
18934
+ concept = cols[4] || "";
18935
+ }
18936
+ if (lessonCode && /^[A-Za-z0-9_\-]+$/.test(lessonCode)) {
18937
+ const keywords = keywordsStr ? keywordsStr.split(/[,;\n]/).map((k) => k.trim().replace(/^`|`$/g, "")).filter(Boolean) : concept ? concept.split(/[,;\n]/).map((k) => k.trim().replace(/^`|`$/g, "")).filter(Boolean) : [];
18938
+ sessions.push({
18939
+ id: lessonCode,
18940
+ order: sessions.length + 1,
18941
+ title: title || lessonCode,
18942
+ prose_objective: objective,
18943
+ new_keywords: keywords
18944
+ });
18945
+ }
18946
+ }
18947
+ if (sessions.length > 0) {
18948
+ return sessions;
18949
+ }
18950
+ }
18951
+ return [];
18952
+ }
18953
+ async function extractCurriculumHorizon(opts) {
18954
+ const { plan, frameworkMarkdown, targetLessonId, storage, projectId } = opts;
18955
+ const sessions = parseAllSessions(plan, frameworkMarkdown);
18956
+ if (sessions.length === 0) {
18957
+ throw new CurriculumError({
18958
+ errorCode: "ERR_LESSON_NOT_IN_SOT",
18959
+ lessonId: targetLessonId,
18960
+ message: `[CurriculumHorizon] No sessions could be parsed from CURRICULUM_PLAN.json or CURRICULUM_FRAMEWORK.md. Fail fast \u2014 cannot construct curriculum horizon without SOT.`,
18961
+ suggestedAction: "Ensure CURRICULUM_PLAN.json or CURRICULUM_FRAMEWORK.md is generated and contains valid sessions.",
18962
+ retryable: false
18963
+ });
18964
+ }
18965
+ const targetCodePattern = new RegExp("^" + targetLessonId.replace(/_/g, "[_-]") + "$", "i");
18966
+ const targetIndex = sessions.findIndex(
18967
+ (s) => s.id === targetLessonId || targetCodePattern.test(s.id)
18968
+ );
18969
+ if (targetIndex < 0) {
18970
+ throw new CurriculumError({
18971
+ errorCode: "ERR_LESSON_NOT_IN_SOT",
18972
+ lessonId: targetLessonId,
18973
+ message: `[CurriculumHorizon] Lesson "${targetLessonId}" was not found among the ${sessions.length} planned sessions. Fail fast \u2014 refusing to generate unanchored lesson.`,
18974
+ suggestedAction: `Check lesson id against planned sessions: [${sessions.map((s) => s.id).join(", ")}]`,
18975
+ retryable: false
18976
+ });
18977
+ }
18978
+ const currentSession = sessions[targetIndex];
18979
+ const compactEnd = Math.max(0, targetIndex - DETAILED_BRIDGE_WINDOW);
18980
+ const compactSessions = sessions.slice(0, compactEnd);
18981
+ const masteredKeywords = Array.from(
18982
+ new Set(
18983
+ compactSessions.flatMap((s) => s.new_keywords || []).map((k) => k.trim()).filter(Boolean)
18984
+ )
18985
+ );
18986
+ const masteredConcepts = Array.from(
18987
+ new Set(
18988
+ compactSessions.map((s) => s.title || s.prose_objective || "").map((t) => t.trim()).filter(Boolean)
18989
+ )
18990
+ );
18991
+ const bridgeStart = Math.max(0, targetIndex - DETAILED_BRIDGE_WINDOW);
18992
+ const bridgeSessions = sessions.slice(bridgeStart, targetIndex);
18993
+ const detailedBridge = [];
18994
+ for (const session of bridgeSessions) {
18995
+ const bridgeIndex = sessions.indexOf(session) + 1;
18996
+ const bridge = {
18997
+ lessonId: session.id,
18998
+ lessonIndex: bridgeIndex,
18999
+ title: session.title,
19000
+ proseObjective: session.prose_objective,
19001
+ keywords: session.new_keywords || []
19002
+ };
19003
+ if (storage && projectId) {
19004
+ try {
19005
+ const candidates = [
19006
+ `_content/${session.id.replace(/_L\d+$/, "")}/LESSON_${session.id}.md`,
19007
+ `_content/LESSON_${session.id}.md`,
19008
+ `LESSON_${session.id}.md`
19009
+ ];
19010
+ let lessonMd = "";
19011
+ for (const c of candidates) {
19012
+ try {
19013
+ const raw = await storage.readArtifact(projectId, c);
19014
+ if (raw) {
19015
+ lessonMd = raw;
19016
+ break;
19017
+ }
19018
+ } catch {
19019
+ }
19020
+ }
19021
+ if (lessonMd) {
19022
+ const ledger = extractSymbolLedger(lessonMd);
19023
+ if (ledger.primarySymbol || ledger.entryFileName || ledger.keySymbols.length > 0) {
19024
+ bridge.symbolLedger = {
19025
+ primaryStructOrClass: ledger.primarySymbol,
19026
+ mainEntryFile: ledger.entryFileName,
19027
+ keyVariables: ledger.keySymbols
19028
+ };
19029
+ }
19030
+ }
19031
+ } catch {
19032
+ }
19033
+ }
19034
+ detailedBridge.push(bridge);
19035
+ }
19036
+ const peekStart = targetIndex + 1;
19037
+ const peekEnd = Math.min(sessions.length, peekStart + BOUNDARY_PEEK_WINDOW);
19038
+ const boundaryPeek = sessions.slice(peekStart, peekEnd).map((s, idx) => ({
19039
+ lessonId: s.id,
19040
+ lessonIndex: peekStart + idx + 1,
19041
+ title: s.title,
19042
+ keywords: s.new_keywords || []
19043
+ }));
19044
+ return {
19045
+ targetLessonId,
19046
+ targetLessonIndex: targetIndex + 1,
19047
+ totalLessons: sessions.length,
19048
+ targetTitle: currentSession.title,
19049
+ targetKeywords: currentSession.new_keywords || [],
19050
+ compactMasterySet: {
19051
+ masteredKeywords,
19052
+ masteredConcepts
19053
+ },
19054
+ detailedBridge,
19055
+ boundaryPeek
19056
+ };
19057
+ }
19058
+ function renderHorizonPromptBlock(horizon) {
19059
+ const {
19060
+ targetLessonId,
19061
+ targetLessonIndex,
19062
+ totalLessons,
19063
+ targetTitle,
19064
+ targetKeywords,
19065
+ compactMasterySet,
19066
+ detailedBridge,
19067
+ boundaryPeek
19068
+ } = horizon;
19069
+ const lines = [
19070
+ `# \u{1F9ED} CURRICULUM HORIZON & SCAFFOLDING MATRIX (Lesson ${targetLessonIndex}/${totalLessons}: "${targetTitle}" [${targetLessonId}])`
19071
+ ];
19072
+ if (compactMasterySet.masteredKeywords.length > 0 || compactMasterySet.masteredConcepts.length > 0) {
19073
+ const rawKeywords = compactMasterySet.masteredKeywords;
19074
+ const displayedKeywords = rawKeywords.length > 20 ? rawKeywords.slice(-20) : rawKeywords;
19075
+ const vocab = displayedKeywords.map((k) => `\`${k}\``).join(", ");
19076
+ const moreSuffix = rawKeywords.length > 20 ? ` *(+${rawKeywords.length - 20} earlier terms)*` : "";
19077
+ const concepts = compactMasterySet.masteredConcepts.length > 0 ? `
19078
+ - **Prior Concept Foundations:** ${compactMasterySet.masteredConcepts.slice(-3).join("; ")}` : "";
19079
+ lines.push(
19080
+ `
19081
+ ## 1. \u{1F393} MASTERED VOCABULARY (Prior Lessons 1..${Math.max(1, targetLessonIndex - 3)} \u2014 Compact Set):`,
19082
+ `- **Mastered Terms & Syntax:** ${vocab || "(Core fundamentals)"}${moreSuffix}${concepts}`
19083
+ );
19084
+ } else if (targetLessonIndex === 1) {
19085
+ lines.push(
19086
+ `
19087
+ ## 1. \u{1F393} PRIOR KNOWLEDGE FRONTIER:`,
19088
+ `- **Entry Point:** Inaugural lesson (Lesson 1/${totalLessons}). Students have no prior course vocabulary. All concepts must be introduced from baseline.`
19089
+ );
19090
+ }
19091
+ if (detailedBridge.length > 0) {
19092
+ lines.push(`
19093
+ ## 2. \u{1F309} IMMEDIATE PREDECESSOR CONTEXT (Bridge Lessons):`);
19094
+ for (const bridge of detailedBridge) {
19095
+ lines.push(`### Lesson ${bridge.lessonIndex} (${bridge.lessonId}): ${bridge.title}`);
19096
+ if (bridge.proseObjective) {
19097
+ const obj = bridge.proseObjective.length > 120 ? bridge.proseObjective.slice(0, 117) + "..." : bridge.proseObjective;
19098
+ lines.push(`- **Objective:** ${obj}`);
19099
+ }
19100
+ if (bridge.keywords.length > 0) {
19101
+ lines.push(`- **Keywords:** ${bridge.keywords.map((k) => `\`${k}\``).join(", ")}`);
19102
+ }
19103
+ if (bridge.symbolLedger) {
19104
+ const parts = [];
19105
+ if (bridge.symbolLedger.primaryStructOrClass) {
19106
+ parts.push(`Primary Struct: \`${bridge.symbolLedger.primaryStructOrClass}\``);
19107
+ }
19108
+ if (bridge.symbolLedger.mainEntryFile) {
19109
+ parts.push(`Entry File: \`${bridge.symbolLedger.mainEntryFile}\``);
19110
+ }
19111
+ if (bridge.symbolLedger.keyVariables?.length) {
19112
+ parts.push(`Symbols: ${bridge.symbolLedger.keyVariables.map((v) => `\`${v}\``).join(", ")}`);
19113
+ }
19114
+ if (parts.length > 0) {
19115
+ lines.push(`- **Code Symbol Ledger:** ${parts.join(" | ")}`);
19116
+ }
19117
+ }
19118
+ }
19119
+ lines.push(
19120
+ `\u{1F449} INSTRUCTION: Seamlessly connect the opening Hook and Guided Practice by referencing the student's recent work from Lesson ${detailedBridge[detailedBridge.length - 1]?.lessonIndex}.`
19121
+ );
19122
+ }
19123
+ const currentKeywordsStr = targetKeywords.length > 0 ? targetKeywords.map((k) => `\`${k}\``).join(", ") : "`Current Lesson Concepts`";
19124
+ lines.push(
19125
+ `
19126
+ ## 3. \u{1F3AF} ALLOWED DESIGN SPACE (Positive-Only Allow List):`,
19127
+ `- **Student Toolkit:** Mastered Vocabulary + Immediate Bridge Keywords + Current Lesson Scope (${currentKeywordsStr}).`,
19128
+ `- **MANDATE:** ALL extension challenges (EXT), hands-on activities (ACT), and assessment questions (QUIZ) MUST be 100% solvable using ONLY items within this toolkit. Do NOT require unintroduced APIs!`
19129
+ );
19130
+ if (boundaryPeek.length > 0) {
19131
+ const nextLesson = boundaryPeek[0];
19132
+ const nextKeywords = nextLesson.keywords.length > 0 ? nextLesson.keywords.map((k) => `\`${k}\``).join(", ") : "upcoming features";
19133
+ lines.push(
19134
+ `
19135
+ ## 4. \u{1F6A7} BOUNDARY (Next Lesson Peek):`,
19136
+ `- **Upcoming Lesson ${nextLesson.lessonIndex} ("${nextLesson.title}"):** Introduces ${nextKeywords}.`,
19137
+ `- **BOUNDARY MANDATE:** These concepts are NOT yet available to the student. Do not introduce or require these future concepts.`
19138
+ );
19139
+ }
19140
+ return lines.join("\n");
19141
+ }
19142
+
18881
19143
  // src/services/contextBuilder.ts
18882
19144
  var DEFAULT_LESSON_PRIORITIES = [
18883
19145
  "Symbol & Identifier Ledger",
@@ -19438,6 +19700,20 @@ async function generateSingleArtifact(req) {
19438
19700
  - Entry Source File: \`${symbolLedger.entryFileName || symbolLedger.primarySymbol + ".swift"}\`
19439
19701
  - Key Identifiers to inherit: ${symbolLedger.keySymbols.map((s) => `\`${s}\``).join(", ")}
19440
19702
  - INVARIANT: You MUST use these exact identifier names in all code examples, exercises, and starter templates. Do NOT invent conflicting struct or class names.` : "";
19703
+ let horizon = contextSot.horizon || null;
19704
+ if (!horizon && (contextSot.plan || contextSot.framework) && lessonId) {
19705
+ try {
19706
+ horizon = await extractCurriculumHorizon({
19707
+ plan: contextSot.plan,
19708
+ frameworkMarkdown: contextSot.framework,
19709
+ targetLessonId: lessonId
19710
+ });
19711
+ } catch {
19712
+ }
19713
+ }
19714
+ const horizonPrompt = horizon ? `
19715
+
19716
+ ${renderHorizonPromptBlock(horizon)}` : "";
19441
19717
  const systemPrompt = `You are ${meta.persona}, an expert Curriculum & Pedagogical Specialist in the Curriculum OS Multi-Agent Team.
19442
19718
  Your task is to author a canonical, publication-grade learning artifact of type "${artifactType.toUpperCase()}".
19443
19719
 
@@ -19451,7 +19727,7 @@ ${headingDirective}
19451
19727
  6. DOMAIN & TECH STACK GUARDRAILS:
19452
19728
  ${domainGuardrail}
19453
19729
  ${symbolLedgerPrompt}
19454
- 8. OUTPUT INTEGRITY: Output clean Markdown starting directly with YAML frontmatter. Do not wrap entire response in triple backticks.${artifactSpecializedPrompt}${mediaPolicy ? `
19730
+ 8. OUTPUT INTEGRITY: Output clean Markdown starting directly with YAML frontmatter. Do not wrap entire response in triple backticks.${artifactSpecializedPrompt}${horizonPrompt}${mediaPolicy ? `
19455
19731
 
19456
19732
  IMAGE PLANNING (media ledger) \u2014 QUOTA GEN: t\u1ED1i \u0111a ${mediaPolicy.maxGenImagesPerArtifact} \u1EA3nh AI cho artifact n\xE0y (\u1EA3nh search kho kh\xF4ng gi\u1EDBi h\u1EA1n).
19457
19733
  Khi n\u1ED9i dung c\u1EA7n minh h\u1ECDa (diagram quy tr\xECnh, s\u01A1 \u0111\u1ED3 kh\xE1i ni\u1EC7m, step-by-step visual), ch\xE8n placeholder \u0111\xFAng format [IMAGE: slug] (slug ch\u1EEF th\u01B0\u1EDDng-g\u1EA1ch ngang, unique trong b\xE0i, vd img-for-loop-diagram) T\u1EA0I \u0110\xDANG CH\u1ED6 c\u1EA7n \u1EA3nh, tr\xEAn d\xF2ng ri\xEAng. KH\xD4NG t\u1EF1 sinh \u1EA3nh, KH\xD4NG d\xF9ng markdown image \u2014 placeholder s\u1EBD \u0111\u01B0\u1EE3c thay b\u1EB1ng \u1EA3nh th\u1EADt sau khi Media Curator duy\u1EC7t. Ch\u1EC9 \u0111\u1EB7t cho \u1EA3nh TH\u1EF0C S\u1EF0 c\u1EA7n thi\u1EBFt.` : ""}`;
@@ -19932,13 +20208,9 @@ ${includeUnitTests ? "- Unit Testing: Provide automated assertions or test runne
19932
20208
  - Exact quantities calculated for ${studentCount} students.
19933
20209
  - Component specifications, estimated unit cost, and affordable alternatives.`;
19934
20210
  case "ext": {
19935
- const lessonIdxMatch = (req.lessonId || "").match(/L0*(\d+)/i);
19936
- const lessonIdx = lessonIdxMatch ? parseInt(lessonIdxMatch[1], 10) : 1;
19937
- const isEarlyLesson = lessonIdx <= 4;
19938
- const zpdCeilingRule = isEarlyLesson ? `- \u{1F6D1} ZPD COMPLEXITY CEILING (FOUNDATIONAL LESSON #${lessonIdx}):
19939
- \u2022 SCOPE LOCK: This is an early foundational lesson. The extension challenge MUST focus on creative design variations, parameterization, or edge cases of the current lesson concepts ONLY.
19940
- \u2022 STRICTLY FORBIDDEN: NEVER introduce advanced mechanisms from future lessons (No complex event buses, No remote network/API calls, No persistent databases, No async concurrency, No out-of-scope hardware/sensors).` : `- \u{1F680} ZPD ADVANCED CHALLENGE (LESSON #${lessonIdx}):
19941
- \u2022 Focus on architectural edge cases, algorithmic optimization, and synthesis of learned concepts up to Lesson #${lessonIdx}.`;
20211
+ const zpdCeilingRule = `- \u{1F3AF} CURRICULUM HORIZON ALLOWED SPACE & BOUNDARY MANDATE:
20212
+ \u2022 SCOPE LOCK: All extension challenges MUST be 100% solvable using ONLY items from the student's Mastered Vocabulary and Current Lesson Scope.
20213
+ \u2022 STRICTLY FORBIDDEN: NEVER introduce unintroduced APIs or mechanisms from upcoming lessons (No complex event buses, No remote network/API calls, No persistent databases, No async concurrency, No out-of-scope hardware/sensors unless explicitly part of the allowed toolkit).`;
19942
20214
  return `
19943
20215
  ### \u{1F680} EXTENSION CHALLENGE ARCHITECTURE INVARIANTS:
19944
20216
  ${zpdCeilingRule}