@thanh01.pmt/curriculum-kit 1.4.8 → 1.4.10
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 +94 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.mjs +94 -29
- package/dist/index.mjs.map +1 -1
- package/dist/storage/index.cjs +37 -8
- package/dist/storage/index.cjs.map +1 -1
- package/dist/storage/index.mjs +37 -8
- package/dist/storage/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -8417,13 +8417,16 @@ function computeContentHash(content) {
|
|
|
8417
8417
|
return "sha256:" + crypto__default.default.createHash("sha256").update(content.trim(), "utf-8").digest("hex");
|
|
8418
8418
|
}
|
|
8419
8419
|
var STANDARD_SOT_FILES = [
|
|
8420
|
-
"PROJECT_BRIEF.md",
|
|
8421
8420
|
"LEARNER_PROFILE.md",
|
|
8421
|
+
"PROJECT_BRIEF.md",
|
|
8422
|
+
"REFERENCE_PACK.md",
|
|
8423
|
+
"PROJECT_GRAPH.json",
|
|
8424
|
+
"HYBRID_GRAPH.json",
|
|
8425
|
+
"SECTION_LANGUAGE_CONTRACT.md",
|
|
8422
8426
|
"CURRICULUM_FRAMEWORK.md",
|
|
8423
|
-
"PROJECT_STATUS.md",
|
|
8424
8427
|
"CONTENT_STYLE_GUIDE.md",
|
|
8425
8428
|
"ART_DIRECTION.md",
|
|
8426
|
-
"
|
|
8429
|
+
"ALIGNMENT_MATRIX.md"
|
|
8427
8430
|
];
|
|
8428
8431
|
var FileSystemCurriculumAdapter = class {
|
|
8429
8432
|
baseDir;
|
|
@@ -8465,19 +8468,33 @@ var FileSystemCurriculumAdapter = class {
|
|
|
8465
8468
|
async listSotDocuments(projectId) {
|
|
8466
8469
|
const projectDir = this.getProjectDir(projectId);
|
|
8467
8470
|
const sotDir = path3__default.default.join(projectDir, "_sot");
|
|
8468
|
-
|
|
8471
|
+
const discoveredFiles = new Set(STANDARD_SOT_FILES);
|
|
8472
|
+
if (fs2__default.default.existsSync(sotDir)) {
|
|
8473
|
+
try {
|
|
8474
|
+
const diskFiles = fs2__default.default.readdirSync(sotDir);
|
|
8475
|
+
for (const f of diskFiles) {
|
|
8476
|
+
if (f.startsWith(".") || f.endsWith(".review.json")) continue;
|
|
8477
|
+
const fullPath = path3__default.default.join(sotDir, f);
|
|
8478
|
+
if (fs2__default.default.statSync(fullPath).isFile()) {
|
|
8479
|
+
discoveredFiles.add(f);
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
} catch {
|
|
8483
|
+
}
|
|
8484
|
+
}
|
|
8485
|
+
return Array.from(discoveredFiles).map((filename) => {
|
|
8469
8486
|
const p = fs2__default.default.existsSync(path3__default.default.join(sotDir, filename)) ? path3__default.default.join(sotDir, filename) : fs2__default.default.existsSync(path3__default.default.join(projectDir, filename)) ? path3__default.default.join(projectDir, filename) : null;
|
|
8470
8487
|
if (p && fs2__default.default.existsSync(p)) {
|
|
8471
8488
|
const stats = fs2__default.default.statSync(p);
|
|
8472
8489
|
return {
|
|
8473
|
-
name: filename.replace(
|
|
8490
|
+
name: filename.replace(/\.(md|json)$/i, "").replace(/_/g, " "),
|
|
8474
8491
|
filename,
|
|
8475
8492
|
exists: true,
|
|
8476
8493
|
sizeBytes: stats.size
|
|
8477
8494
|
};
|
|
8478
8495
|
}
|
|
8479
8496
|
return {
|
|
8480
|
-
name: filename.replace(
|
|
8497
|
+
name: filename.replace(/\.(md|json)$/i, "").replace(/_/g, " "),
|
|
8481
8498
|
filename,
|
|
8482
8499
|
exists: false,
|
|
8483
8500
|
sizeBytes: 0
|
|
@@ -8723,11 +8740,23 @@ var SupabaseCurriculumAdapter = class {
|
|
|
8723
8740
|
return null;
|
|
8724
8741
|
}
|
|
8725
8742
|
async listSotDocuments(projectId) {
|
|
8743
|
+
const discoveredFiles = new Set(STANDARD_SOT_FILES);
|
|
8744
|
+
try {
|
|
8745
|
+
const { data, error } = await this.client.storage.from(this.bucketName).list(`${projectId}/_sot`);
|
|
8746
|
+
if (!error && data) {
|
|
8747
|
+
for (const item of data) {
|
|
8748
|
+
if (item.name && !item.name.startsWith(".") && !item.name.endsWith(".review.json")) {
|
|
8749
|
+
discoveredFiles.add(item.name);
|
|
8750
|
+
}
|
|
8751
|
+
}
|
|
8752
|
+
}
|
|
8753
|
+
} catch {
|
|
8754
|
+
}
|
|
8726
8755
|
const results = [];
|
|
8727
|
-
for (const filename of
|
|
8756
|
+
for (const filename of discoveredFiles) {
|
|
8728
8757
|
const content = await this.readSotDocument(projectId, filename);
|
|
8729
8758
|
results.push({
|
|
8730
|
-
name: filename.replace(
|
|
8759
|
+
name: filename.replace(/\.(md|json)$/i, "").replace(/_/g, " "),
|
|
8731
8760
|
filename,
|
|
8732
8761
|
exists: !!content,
|
|
8733
8762
|
content: content || void 0,
|
|
@@ -9104,10 +9133,12 @@ async function auditQualityReport(storage, projectId) {
|
|
|
9104
9133
|
computedScore += Math.round(bloomPassCount / total * 20);
|
|
9105
9134
|
computedScore += Math.round(Math.min(syntaxExecutableCount, total) / total * 20);
|
|
9106
9135
|
computedScore += Math.round(scopeCompletenessCount / total * 20);
|
|
9136
|
+
const sotCompletedCount = status.sotReadiness.filter((s) => s.exists).length;
|
|
9137
|
+
const sotTotalCount = status.sotReadiness.length;
|
|
9107
9138
|
if (sotReady) {
|
|
9108
|
-
details.push(
|
|
9139
|
+
details.push(`\u2705 N\u1EC1n t\u1EA3ng SOT (${sotCompletedCount}/${sotTotalCount} t\xE0i li\u1EC7u) s\u1EB5n s\xE0ng.`);
|
|
9109
9140
|
} else {
|
|
9110
|
-
details.push(
|
|
9141
|
+
details.push(`\u26A0\uFE0F C\u1EA7n ho\xE0n thi\u1EC7n \u0111\u1EA7y \u0111\u1EE7 t\xE0i li\u1EC7u SOT c\u01A1 s\u1EDF (${sotCompletedCount}/${sotTotalCount}).`);
|
|
9111
9142
|
}
|
|
9112
9143
|
details.push(`\u{1F3AF} Ph\u1EA1m vi h\u1ECDc li\u1EC7u theo SOT (Artifact Scope): [${artifactScope.join(", ")}].`);
|
|
9113
9144
|
if (pedagogy5EPass) {
|
|
@@ -9123,7 +9154,7 @@ async function auditQualityReport(storage, projectId) {
|
|
|
9123
9154
|
details.push(`\u2705 \u0110\u1ED9 ph\u1EE7 \u0111\u1EA7y \u0111\u1EE7 c\xE1c h\u1ECDc li\u1EC7u trong Artifact Scope: ${scopeCompletenessCount}/${total} b\xE0i h\u1ECDc.`);
|
|
9124
9155
|
const rawMarkdownReport = `## \u{1F6E1}\uFE0F B\xC1O C\xC1O KI\u1EC2M \u0110\u1ECANH CH\u1EA4T L\u01AF\u1EE2NG (QUALITY AUDIT): \`${projectId.toUpperCase()}\`
|
|
9125
9156
|
- **\u0110i\u1EC3m th\u1EA9m \u0111\u1ECBnh ch\u1EA5t l\u01B0\u1EE3ng:** **${computedScore}/100**
|
|
9126
|
-
- **Tr\u1EA1ng th\xE1i SOT:** ${sotReady ?
|
|
9157
|
+
- **Tr\u1EA1ng th\xE1i SOT:** ${sotReady ? `\u2705 ${sotCompletedCount}/${sotTotalCount} File SOT ho\xE0n t\u1EA5t` : `\u26A0\uFE0F C\u1EA7n b\u1ED5 sung t\xE0i li\u1EC7u SOT thi\u1EBFu (${sotCompletedCount}/${sotTotalCount})`}.
|
|
9127
9158
|
- **Ph\u1EA1m vi h\u1ECDc li\u1EC7u (SOT Artifact Scope):** \`${artifactScope.join(", ")}\`
|
|
9128
9159
|
- **C\u1EA5u tr\xFAc S\u01B0 ph\u1EA1m (5E / EDP):** ${pedagogy5ECount}/${total} b\xE0i h\u1ECDc \u0111\u1EA1t chu\u1EA9n ph\xE2n pha.
|
|
9129
9160
|
- **Thang \u0111o nh\u1EADn th\u1EE9c Bloom:** ${bloomPassCount}/${total} b\xE0i h\u1ECDc \u0111\u1EA1t ma tr\u1EADn m\u1EE5c ti\xEAu.
|
|
@@ -9225,7 +9256,9 @@ function extractScopeSequenceRows(framework) {
|
|
|
9225
9256
|
const trimmed = line.trim();
|
|
9226
9257
|
if (!trimmed.startsWith("|")) continue;
|
|
9227
9258
|
const lower = trimmed.toLowerCase();
|
|
9228
|
-
|
|
9259
|
+
const hasCodeCol = lower.includes("lesson code") || lower.includes("m\xE3 b\xE0i") || lower.includes("m\xE3 b\xE0i h\u1ECDc");
|
|
9260
|
+
const hasObjCol = lower.includes("learning objective") || lower.includes("m\u1EE5c ti\xEAu") || lower.includes("m\u1EE5c ti\xEAu h\u1ECDc t\u1EADp");
|
|
9261
|
+
if (hasCodeCol && hasObjCol) {
|
|
9229
9262
|
headerFound = true;
|
|
9230
9263
|
continue;
|
|
9231
9264
|
}
|
|
@@ -27269,13 +27302,13 @@ var DeterministicPipelineRunner = class {
|
|
|
27269
27302
|
|
|
27270
27303
|
// src/services/prefillService.ts
|
|
27271
27304
|
init_streamRunner();
|
|
27272
|
-
var DEFAULT_STREAM_IDLE_MS =
|
|
27305
|
+
var DEFAULT_STREAM_IDLE_MS = 18e4;
|
|
27273
27306
|
var DEFAULT_STREAM_TOTAL_MS = 48e4;
|
|
27274
27307
|
var LAYER_IDLE_BUDGET_MS = {
|
|
27275
|
-
1:
|
|
27276
|
-
2:
|
|
27308
|
+
1: 18e4,
|
|
27309
|
+
2: 18e4,
|
|
27277
27310
|
3: 18e4
|
|
27278
|
-
//
|
|
27311
|
+
// 3 minutes idle protection across all layers for free-tier reasoning models
|
|
27279
27312
|
};
|
|
27280
27313
|
var LAYER_TOTAL_BUDGET_MS = {
|
|
27281
27314
|
1: 48e4,
|
|
@@ -27336,10 +27369,20 @@ function extractStreamChunk(part) {
|
|
|
27336
27369
|
}
|
|
27337
27370
|
return {};
|
|
27338
27371
|
}
|
|
27339
|
-
function createStreamAbortSignal(budget) {
|
|
27372
|
+
function createStreamAbortSignal(budget, parentSignal) {
|
|
27340
27373
|
const controller = new AbortController();
|
|
27341
27374
|
let idleTimer = null;
|
|
27342
27375
|
let totalTimer = null;
|
|
27376
|
+
const onParentAbort = () => {
|
|
27377
|
+
controller.abort(parentSignal?.reason || new Error("Aborted by parent signal"));
|
|
27378
|
+
};
|
|
27379
|
+
if (parentSignal) {
|
|
27380
|
+
if (parentSignal.aborted) {
|
|
27381
|
+
controller.abort(parentSignal.reason || new Error("Parent signal already aborted"));
|
|
27382
|
+
} else {
|
|
27383
|
+
parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
27384
|
+
}
|
|
27385
|
+
}
|
|
27343
27386
|
const armIdle = () => {
|
|
27344
27387
|
if (idleTimer) clearTimeout(idleTimer);
|
|
27345
27388
|
if (budget.idleMs > 0) {
|
|
@@ -27366,6 +27409,9 @@ function createStreamAbortSignal(budget) {
|
|
|
27366
27409
|
dispose: () => {
|
|
27367
27410
|
if (idleTimer) clearTimeout(idleTimer);
|
|
27368
27411
|
if (totalTimer) clearTimeout(totalTimer);
|
|
27412
|
+
if (parentSignal) {
|
|
27413
|
+
parentSignal.removeEventListener("abort", onParentAbort);
|
|
27414
|
+
}
|
|
27369
27415
|
idleTimer = null;
|
|
27370
27416
|
totalTimer = null;
|
|
27371
27417
|
}
|
|
@@ -27477,7 +27523,7 @@ function safeParseJson(rawText) {
|
|
|
27477
27523
|
}
|
|
27478
27524
|
return null;
|
|
27479
27525
|
}
|
|
27480
|
-
async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onChunk, preferredModel, preferredProvider, budget) {
|
|
27526
|
+
async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onChunk, preferredModel, preferredProvider, budget, signal) {
|
|
27481
27527
|
const alibabaKey = apiKeys.alibaba || apiKeys.dashscope || process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY;
|
|
27482
27528
|
const nvidiaKey = apiKeys.nvidia || process.env.NVIDIA_API_KEY;
|
|
27483
27529
|
const deepseekKey = apiKeys.deepseek || process.env.DEEPSEEK_API_KEY;
|
|
@@ -27509,7 +27555,6 @@ async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onC
|
|
|
27509
27555
|
const nemotronModels = [
|
|
27510
27556
|
"nvidia/nemotron-3-ultra-550b-a55b",
|
|
27511
27557
|
"nvidia/nemotron-3.5-lightning-30b-a3b",
|
|
27512
|
-
"nvidia/nemotron-3-super-120b-a12b",
|
|
27513
27558
|
"nvidia/llama-3.1-nemotron-70b-instruct"
|
|
27514
27559
|
].filter((m) => isModelAllowed(m));
|
|
27515
27560
|
for (const m of nemotronModels) {
|
|
@@ -27560,8 +27605,12 @@ THINKING DIRECTIVE: Do ALL your reasoning internally BEFORE emitting output. Do
|
|
|
27560
27605
|
totalMs: budget?.totalMs ?? DEFAULT_STREAM_TOTAL_MS
|
|
27561
27606
|
};
|
|
27562
27607
|
for (const candidate of candidates) {
|
|
27608
|
+
if (signal?.aborted) {
|
|
27609
|
+
console.log(`[curriculum-kit:VercelAI] AbortSignal already aborted, cancelling candidate loop.`);
|
|
27610
|
+
break;
|
|
27611
|
+
}
|
|
27563
27612
|
const t0 = Date.now();
|
|
27564
|
-
const abort = createStreamAbortSignal(resolvedBudget);
|
|
27613
|
+
const abort = createStreamAbortSignal(resolvedBudget, signal);
|
|
27565
27614
|
try {
|
|
27566
27615
|
const modelInstance = getAIModel({
|
|
27567
27616
|
provider: candidate.provider,
|
|
@@ -27593,6 +27642,13 @@ THINKING DIRECTIVE: Do ALL your reasoning internally BEFORE emitting output. Do
|
|
|
27593
27642
|
}
|
|
27594
27643
|
} catch (e) {
|
|
27595
27644
|
console.warn(`[curriculum-kit:VercelAI] Candidate ${candidate.provider} (${candidate.model}) failed in ${Date.now() - t0}ms:`, e?.message || e);
|
|
27645
|
+
const isClientAborted = Boolean(
|
|
27646
|
+
signal?.aborted || e?.name === "AbortError" && signal?.aborted || e?.message && (e.message.includes("Controller is already closed") || e.message.includes("The operation was aborted") || e.message.includes("Aborted by parent signal"))
|
|
27647
|
+
);
|
|
27648
|
+
if (isClientAborted) {
|
|
27649
|
+
console.log(`[curriculum-kit:VercelAI] Client aborted or stream closed, halting candidate fallback loop.`);
|
|
27650
|
+
break;
|
|
27651
|
+
}
|
|
27596
27652
|
} finally {
|
|
27597
27653
|
abort.dispose();
|
|
27598
27654
|
}
|
|
@@ -27644,7 +27700,8 @@ Return concise JSON matching:
|
|
|
27644
27700
|
totalMs: options.timeoutMs,
|
|
27645
27701
|
model: options.model,
|
|
27646
27702
|
provider: options.provider
|
|
27647
|
-
})
|
|
27703
|
+
}),
|
|
27704
|
+
options.signal
|
|
27648
27705
|
);
|
|
27649
27706
|
let parsedResearch = {
|
|
27650
27707
|
groundTruthSummary: `Verified ecosystem standards for ${projectName}`,
|
|
@@ -27854,7 +27911,8 @@ REQUIREMENT: Synthesize ALL parameters above \u2014 every single line in FULL AC
|
|
|
27854
27911
|
totalMs: options.timeoutMs,
|
|
27855
27912
|
model: options.model,
|
|
27856
27913
|
provider: options.provider
|
|
27857
|
-
})
|
|
27914
|
+
}),
|
|
27915
|
+
options.signal
|
|
27858
27916
|
);
|
|
27859
27917
|
const parsed = safeParseJson(rawContent);
|
|
27860
27918
|
return {
|
|
@@ -27870,6 +27928,7 @@ var pad22 = (n) => String(n).padStart(2, "0");
|
|
|
27870
27928
|
var esc = (s) => s.replace(/\|/g, "/").replace(/\n/g, " ").trim();
|
|
27871
27929
|
var BLOOM_OF_DEPTH = { ulo: "Understand", cio: "Apply", sio: "Create" };
|
|
27872
27930
|
function renderFrameworkFromPlan(plan, meta) {
|
|
27931
|
+
const isVi = meta.targetLanguage === "vi";
|
|
27873
27932
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
27874
27933
|
const totalSessions = plan.sessions.length;
|
|
27875
27934
|
const totalMinutes = plan.sessions.reduce(
|
|
@@ -27887,10 +27946,16 @@ function renderFrameworkFromPlan(plan, meta) {
|
|
|
27887
27946
|
const depthTops = [...new Set(s.depth_assignments.map((d) => d.depth))].map((d) => BLOOM_OF_DEPTH[d] ?? "Apply");
|
|
27888
27947
|
const bloom = depthTops[depthTops.length - 1] ?? "Apply";
|
|
27889
27948
|
const keyConcepts = s.new_keywords.length > 0 ? s.new_keywords.join(", ") : s.depth_assignments.map((d) => d.node_id).join(", ");
|
|
27890
|
-
const prereq = s.prerequisite_decisions.length > 0 ? s.prerequisite_decisions.filter((p) => p.decision === "taught_in_prior_lesson").length + " prior-taught, " + s.prerequisite_decisions.filter((p) => p.decision === "recap_in_lesson").length + " recap" : "None";
|
|
27949
|
+
const prereq = s.prerequisite_decisions.length > 0 ? s.prerequisite_decisions.filter((p) => p.decision === "taught_in_prior_lesson").length + " prior-taught, " + s.prerequisite_decisions.filter((p) => p.decision === "recap_in_lesson").length + " recap" : isVi ? "Kh\xF4ng" : "None";
|
|
27891
27950
|
const duration = s.knowledge_minutes + s.practice_minutes + s.overhead_minutes;
|
|
27892
27951
|
return "| " + pad22(i + 1) + " | " + s.id + " | " + esc(s.title) + " | " + s.unit_id + " | " + s.unit_id + "_M01 | " + esc(s.prose_objective) + " | " + esc(keyConcepts) + " | " + esc(s.exit_evidence[0] ?? "") + " | " + esc(prereq) + " | " + bloom + " | " + duration + " |";
|
|
27893
27952
|
}).join("\n");
|
|
27953
|
+
const courseOverviewHeader = isVi ? "## [REQUIRED] T\u1ED5ng quan Kh\xF3a h\u1ECDc" : "## [REQUIRED] Course Overview";
|
|
27954
|
+
const globalObjectivesHeader = isVi ? "## [REQUIRED] M\u1EE5c ti\xEAu H\u1ECDc t\u1EADp To\xE0n di\u1EC7n" : "## [REQUIRED] Global Learning Objectives";
|
|
27955
|
+
const structuralHierarchyHeader = isVi ? "## [REQUIRED] C\u1EA5u tr\xFAc Kh\xF3a h\u1ECDc (Unit & Module)" : "## [REQUIRED] Structural Hierarchy (Unit & Module)";
|
|
27956
|
+
const scopeSequenceHeader = isVi ? "## [REQUIRED] Ph\u1EA1m vi & Tr\xECnh t\u1EF1 Chi ti\u1EBFt (Scope & Sequence)" : "## [REQUIRED] Scope & Sequence (Detailed Roadmap)";
|
|
27957
|
+
const standardFormatHeader = isVi ? "## [REQUIRED] C\u1EA5u tr\xFAc B\xE0i h\u1ECDc Chu\u1EA9n" : "## [REQUIRED] Standard Lesson Format";
|
|
27958
|
+
const scopeTableHeader = isVi ? "| # | Lesson Code | Title | Unit | Module | Learning Objective (H\u1ECDc sinh l\xE0m \u0111\u01B0\u1EE3c g\xEC...) | Key Concepts (H\u1ECDc g\xEC) | Hands-on Deliverable (S\u1EA3n ph\u1EA9m bu\u1ED5i h\u1ECDc) | Prerequisites | Bloom | Duration (mins) |" : "| # | Lesson Code | Title | Unit | Module | Learning Objective (Students will be able to...) | Key Concepts (Hoc gi) | Hands-on Deliverable (Lam duoc gi) | Prerequisites | Bloom | Duration (mins) |";
|
|
27894
27959
|
return [
|
|
27895
27960
|
"---",
|
|
27896
27961
|
'id: "CURRICULUM-FRAMEWORK"',
|
|
@@ -27912,7 +27977,7 @@ function renderFrameworkFromPlan(plan, meta) {
|
|
|
27912
27977
|
"",
|
|
27913
27978
|
"_Projection of CURRICULUM_PLAN (hash " + plan.plan_hash + "). Approving this framework approves the per-session SCOPE recorded in the plan._",
|
|
27914
27979
|
"",
|
|
27915
|
-
|
|
27980
|
+
courseOverviewHeader,
|
|
27916
27981
|
"- **Official Course Name:** " + esc(meta.courseName),
|
|
27917
27982
|
meta.shortDescription ? "- **Short Description:** " + esc(meta.shortDescription) : "",
|
|
27918
27983
|
"- **Total Units:** " + plan.units.length,
|
|
@@ -27920,20 +27985,20 @@ function renderFrameworkFromPlan(plan, meta) {
|
|
|
27920
27985
|
"- **Total Duration:** ~" + totalHours + " hours (" + totalSessions + " sessions x " + plan.constraints.session_duration_minutes + " min).",
|
|
27921
27986
|
"- **Entry Level:** " + plan.constraints.entry_level + " (age band " + plan.constraints.age_band[0] + "-" + plan.constraints.age_band[1] + ").",
|
|
27922
27987
|
"",
|
|
27923
|
-
|
|
27988
|
+
globalObjectivesHeader,
|
|
27924
27989
|
...plan.course.objectives.map((o, i) => i + 1 + ". **[" + o.bloom + "]:** " + esc(o.statement)),
|
|
27925
27990
|
"",
|
|
27926
|
-
|
|
27991
|
+
structuralHierarchyHeader,
|
|
27927
27992
|
"| Unit | Module | Lessons | Module Objective | Lesson Codes |",
|
|
27928
27993
|
"|---|---|:---:|---|---|",
|
|
27929
27994
|
hierarchyRows || "| (none) | | | | |",
|
|
27930
27995
|
"",
|
|
27931
|
-
|
|
27932
|
-
|
|
27996
|
+
scopeSequenceHeader,
|
|
27997
|
+
scopeTableHeader,
|
|
27933
27998
|
"|:---:|:---:|---|---|---|---|---|---|---|:---:|:---:|",
|
|
27934
27999
|
scopeRows || "| 01 | U01_M01_L01 | (empty plan) | U01 | U01_M01 | - | - | - | None | Understand | " + plan.constraints.session_duration_minutes + " |",
|
|
27935
28000
|
"",
|
|
27936
|
-
|
|
28001
|
+
standardFormatHeader,
|
|
27937
28002
|
"- **Session duration:** " + plan.constraints.session_duration_minutes + " minutes (overhead " + String(plan.sessions[0]?.overhead_minutes ?? 0) + " min).",
|
|
27938
28003
|
"- **Knowledge/practice split per session:** see plan JSON (knowledge_minutes / practice_minutes).",
|
|
27939
28004
|
"- **Pedagogical flow:** 5E (Engage / Explore / Explain / Elaborate / Evaluate) unless policy overrides.",
|