@thanh01.pmt/curriculum-kit 1.0.0

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.
Files changed (68) hide show
  1. package/README.md +22 -0
  2. package/dist/ai/index.cjs +4980 -0
  3. package/dist/ai/index.cjs.map +1 -0
  4. package/dist/ai/index.d.cts +1287 -0
  5. package/dist/ai/index.d.ts +1287 -0
  6. package/dist/ai/index.mjs +4924 -0
  7. package/dist/ai/index.mjs.map +1 -0
  8. package/dist/index.cjs +15308 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.cts +747 -0
  11. package/dist/index.d.ts +747 -0
  12. package/dist/index.mjs +14965 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/media/index.cjs +1238 -0
  15. package/dist/media/index.cjs.map +1 -0
  16. package/dist/media/index.d.cts +254 -0
  17. package/dist/media/index.d.ts +254 -0
  18. package/dist/media/index.mjs +1221 -0
  19. package/dist/media/index.mjs.map +1 -0
  20. package/dist/milestoneBundleGenerator-CbXhs1CP.d.ts +62 -0
  21. package/dist/milestoneBundleGenerator-DrBtHzBO.d.cts +62 -0
  22. package/dist/pipeline/index.cjs +4894 -0
  23. package/dist/pipeline/index.cjs.map +1 -0
  24. package/dist/pipeline/index.d.cts +161 -0
  25. package/dist/pipeline/index.d.ts +161 -0
  26. package/dist/pipeline/index.mjs +4869 -0
  27. package/dist/pipeline/index.mjs.map +1 -0
  28. package/dist/provider-factory-DH3udYlN.d.cts +25 -0
  29. package/dist/provider-factory-DH3udYlN.d.ts +25 -0
  30. package/dist/publishers/index.cjs +575 -0
  31. package/dist/publishers/index.cjs.map +1 -0
  32. package/dist/publishers/index.d.cts +85 -0
  33. package/dist/publishers/index.d.ts +85 -0
  34. package/dist/publishers/index.mjs +561 -0
  35. package/dist/publishers/index.mjs.map +1 -0
  36. package/dist/schemas/index.cjs +1640 -0
  37. package/dist/schemas/index.cjs.map +1 -0
  38. package/dist/schemas/index.d.cts +14194 -0
  39. package/dist/schemas/index.d.ts +14194 -0
  40. package/dist/schemas/index.mjs +1517 -0
  41. package/dist/schemas/index.mjs.map +1 -0
  42. package/dist/standards/index.cjs +910 -0
  43. package/dist/standards/index.cjs.map +1 -0
  44. package/dist/standards/index.d.cts +130 -0
  45. package/dist/standards/index.d.ts +130 -0
  46. package/dist/standards/index.mjs +890 -0
  47. package/dist/standards/index.mjs.map +1 -0
  48. package/dist/standardsCoverageGate-BWAkXY76.d.cts +767 -0
  49. package/dist/standardsCoverageGate-BWAkXY76.d.ts +767 -0
  50. package/dist/storage/index.cjs +595 -0
  51. package/dist/storage/index.cjs.map +1 -0
  52. package/dist/storage/index.d.cts +77 -0
  53. package/dist/storage/index.d.ts +77 -0
  54. package/dist/storage/index.mjs +583 -0
  55. package/dist/storage/index.mjs.map +1 -0
  56. package/dist/streamRunner-C7j5LKon.d.cts +60 -0
  57. package/dist/streamRunner-C7j5LKon.d.ts +60 -0
  58. package/dist/supabasePublisher-C627qXFT.d.cts +63 -0
  59. package/dist/supabasePublisher-C627qXFT.d.ts +63 -0
  60. package/dist/types-BUJGYiep.d.cts +89 -0
  61. package/dist/types-BUJGYiep.d.ts +89 -0
  62. package/dist/workflow/index.cjs +5266 -0
  63. package/dist/workflow/index.cjs.map +1 -0
  64. package/dist/workflow/index.d.cts +295 -0
  65. package/dist/workflow/index.d.ts +295 -0
  66. package/dist/workflow/index.mjs +5216 -0
  67. package/dist/workflow/index.mjs.map +1 -0
  68. package/package.json +105 -0
@@ -0,0 +1,583 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { createClient } from '@supabase/supabase-js';
5
+
6
+ // src/storage/fileSystemCurriculumAdapter.ts
7
+ function computeContentHash(content) {
8
+ return "sha256:" + crypto.createHash("sha256").update(content.trim(), "utf-8").digest("hex");
9
+ }
10
+ var STANDARD_SOT_FILES = [
11
+ "PROJECT_BRIEF.md",
12
+ "LEARNER_PROFILE.md",
13
+ "CURRICULUM_FRAMEWORK.md",
14
+ "PROJECT_STATUS.md",
15
+ "CONTENT_STYLE_GUIDE.md",
16
+ "ART_DIRECTION.md",
17
+ "REFERENCE_PACK.md"
18
+ ];
19
+ var FileSystemCurriculumAdapter = class {
20
+ baseDir;
21
+ constructor(options = {}) {
22
+ this.baseDir = options.baseDir || path.resolve(process.cwd(), "output", "projects");
23
+ }
24
+ getProjectDir(projectId) {
25
+ const raw = (projectId || "").replace("proj_", "");
26
+ const directPath = path.join(this.baseDir, raw);
27
+ if (fs.existsSync(directPath)) return directPath;
28
+ const fullIdPath = path.join(this.baseDir, projectId);
29
+ if (fs.existsSync(fullIdPath)) return fullIdPath;
30
+ return directPath;
31
+ }
32
+ /** Ensures the resolved path stays within the project directory (prevents path traversal). */
33
+ assertInsideProject(projectDir, targetPath) {
34
+ const resolvedProject = path.resolve(projectDir);
35
+ const resolvedTarget = path.resolve(targetPath);
36
+ if (!resolvedTarget.startsWith(resolvedProject + path.sep) && resolvedTarget !== resolvedProject) {
37
+ throw new Error(`[FileSystemCurriculumAdapter] Path traversal blocked: ${targetPath} is outside project directory ${projectDir}`);
38
+ }
39
+ }
40
+ async projectExists(projectId) {
41
+ return fs.existsSync(this.getProjectDir(projectId));
42
+ }
43
+ async readSotDocument(projectId, filename) {
44
+ const projectDir = this.getProjectDir(projectId);
45
+ const possiblePaths = [
46
+ path.join(projectDir, "_sot", filename),
47
+ path.join(projectDir, filename)
48
+ ];
49
+ for (const p of possiblePaths) {
50
+ if (fs.existsSync(p) && fs.statSync(p).isFile()) {
51
+ return fs.readFileSync(p, "utf-8");
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ async listSotDocuments(projectId) {
57
+ const projectDir = this.getProjectDir(projectId);
58
+ const sotDir = path.join(projectDir, "_sot");
59
+ return STANDARD_SOT_FILES.map((filename) => {
60
+ const p = fs.existsSync(path.join(sotDir, filename)) ? path.join(sotDir, filename) : fs.existsSync(path.join(projectDir, filename)) ? path.join(projectDir, filename) : null;
61
+ if (p && fs.existsSync(p)) {
62
+ const stats = fs.statSync(p);
63
+ return {
64
+ name: filename.replace(".md", "").replace(/_/g, " "),
65
+ filename,
66
+ exists: true,
67
+ sizeBytes: stats.size
68
+ };
69
+ }
70
+ return {
71
+ name: filename.replace(".md", "").replace(/_/g, " "),
72
+ filename,
73
+ exists: false,
74
+ sizeBytes: 0
75
+ };
76
+ });
77
+ }
78
+ async readArtifact(projectId, relPathOrIdentifier) {
79
+ const projectDir = this.getProjectDir(projectId);
80
+ const directCandidate = path.join(projectDir, relPathOrIdentifier);
81
+ this.assertInsideProject(projectDir, directCandidate);
82
+ const possiblePaths = [
83
+ directCandidate,
84
+ path.join(projectDir, "_sot", relPathOrIdentifier),
85
+ path.join(projectDir, "lessons", relPathOrIdentifier),
86
+ path.join(projectDir, "_sot", path.basename(relPathOrIdentifier)),
87
+ path.join(projectDir, "lessons", path.basename(relPathOrIdentifier))
88
+ ];
89
+ for (const p of possiblePaths) {
90
+ if (fs.existsSync(p) && fs.statSync(p).isFile()) {
91
+ return fs.readFileSync(p, "utf-8");
92
+ }
93
+ }
94
+ if (relPathOrIdentifier.includes(".md") || relPathOrIdentifier.includes("/")) {
95
+ return null;
96
+ }
97
+ const lessonMatch = relPathOrIdentifier.match(/U\d+_M\d+_L\d+/i);
98
+ if (lessonMatch) {
99
+ const lessonId = lessonMatch[0].toUpperCase();
100
+ const lessonDir = path.join(projectDir, "lessons", lessonId);
101
+ if (fs.existsSync(lessonDir) && fs.statSync(lessonDir).isDirectory()) {
102
+ const lower = relPathOrIdentifier.toLowerCase();
103
+ let artifactPrefix = "LESSON";
104
+ if (lower.includes("act") || lower.includes("lab") || lower.includes("th\u1EF1c h\xE0nh")) {
105
+ artifactPrefix = "ACT";
106
+ } else if (lower.includes("quiz") || lower.includes("tr\u1EAFc nghi\u1EC7m") || lower.includes("\u0111\xE1nh gi\xE1")) {
107
+ artifactPrefix = "QUIZ";
108
+ } else if (lower.includes("slide") || lower.includes("b\xE0i gi\u1EA3ng") || lower.includes("tr\xECnh chi\u1EBFu")) {
109
+ artifactPrefix = "SLIDE";
110
+ } else if (lower.includes("guide") || lower.includes("h\u01B0\u1EDBng d\u1EABn")) {
111
+ artifactPrefix = "GUIDE";
112
+ } else if (lower.includes("wks") || lower.includes("worksheet") || lower.includes("phi\u1EBFu b\xE0i t\u1EADp")) {
113
+ artifactPrefix = "WKS";
114
+ } else if (lower.includes("handout") || lower.includes("t\xE0i li\u1EC7u")) {
115
+ artifactPrefix = "HANDOUT";
116
+ } else if (lower.includes("code")) {
117
+ artifactPrefix = "CODE";
118
+ } else if (lower.includes("ext") || lower.includes("extension")) {
119
+ artifactPrefix = "EXT";
120
+ }
121
+ const candidateFile = path.join(lessonDir, `${artifactPrefix}_${lessonId}.md`);
122
+ if (fs.existsSync(candidateFile)) {
123
+ return fs.readFileSync(candidateFile, "utf-8");
124
+ }
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+ async saveArtifact(projectId, relPath, content) {
130
+ const projectDir = this.getProjectDir(projectId);
131
+ const targetPath = path.join(projectDir, relPath);
132
+ this.assertInsideProject(projectDir, targetPath);
133
+ const targetDir = path.dirname(targetPath);
134
+ if (!fs.existsSync(targetDir)) {
135
+ fs.mkdirSync(targetDir, { recursive: true });
136
+ }
137
+ if (fs.existsSync(targetPath)) {
138
+ try {
139
+ const oldContent = fs.readFileSync(targetPath, "utf-8");
140
+ if (oldContent !== content) {
141
+ const historyDir = path.join(projectDir, ".history", relPath);
142
+ fs.mkdirSync(historyDir, { recursive: true });
143
+ fs.writeFileSync(path.join(historyDir, `${Date.now()}.md`), oldContent, "utf-8");
144
+ const versions = fs.readdirSync(historyDir).filter((f) => f.endsWith(".md")).sort();
145
+ while (versions.length > 10) {
146
+ fs.unlinkSync(path.join(historyDir, versions.shift()));
147
+ }
148
+ }
149
+ } catch (histErr) {
150
+ console.warn("[FileSystemCurriculumAdapter] version history snapshot failed:", histErr?.message || histErr);
151
+ }
152
+ }
153
+ fs.writeFileSync(targetPath, content, "utf-8");
154
+ const filename = path.basename(relPath);
155
+ const match = filename.match(/(LESSON|ACT|QUIZ|SLIDE|GUIDE|HANDOUT|WKS|EXT)_(U\d+_M\d+_L\d+)\.md/i);
156
+ if (match) {
157
+ const lessonId = match[2].toUpperCase();
158
+ const lessonsDir = path.join(projectDir, "lessons", lessonId);
159
+ if (!fs.existsSync(lessonsDir)) {
160
+ fs.mkdirSync(lessonsDir, { recursive: true });
161
+ }
162
+ fs.writeFileSync(path.join(lessonsDir, filename), content, "utf-8");
163
+ const legacyContentDir = path.join(projectDir, "_content", lessonId);
164
+ if (fs.existsSync(legacyContentDir)) {
165
+ fs.writeFileSync(path.join(legacyContentDir, filename), content, "utf-8");
166
+ }
167
+ }
168
+ }
169
+ async listLessons(projectId) {
170
+ const lessonsDir = path.join(this.getProjectDir(projectId), "lessons");
171
+ if (!fs.existsSync(lessonsDir)) return [];
172
+ const lessonFolders = fs.readdirSync(lessonsDir).filter((f) => !f.startsWith("."));
173
+ return lessonFolders.map((folder) => {
174
+ const lDir = path.join(lessonsDir, folder);
175
+ const isDir = fs.statSync(lDir).isDirectory();
176
+ if (!isDir) {
177
+ return {
178
+ lessonId: folder,
179
+ artifactsCount: 0,
180
+ isComplete: false,
181
+ artifacts: []
182
+ };
183
+ }
184
+ const artifacts = fs.readdirSync(lDir).filter((f) => !f.startsWith("."));
185
+ return {
186
+ lessonId: folder,
187
+ artifactsCount: artifacts.length,
188
+ isComplete: artifacts.length >= 4,
189
+ artifacts
190
+ };
191
+ });
192
+ }
193
+ async getProjectStatus(projectId) {
194
+ const sotList = await this.listSotDocuments(projectId);
195
+ const lessons = await this.listLessons(projectId);
196
+ const completedCount = lessons.filter((l) => l.isComplete).length;
197
+ const totalLessons = lessons.length || 4;
198
+ const progressPercent = Math.round(completedCount / totalLessons * 100);
199
+ const sotTable = sotList.map((doc) => `| \`${doc.filename}\` | ${doc.exists ? "\u2705 S\u1EB5n s\xE0ng" : "\u274C Ch\u01B0a c\xF3"} |`).join("\n");
200
+ const lessonTable = lessons.length > 0 ? lessons.map((l) => `| **${l.lessonId}** | ${l.artifactsCount}/4 Artifacts | ${l.isComplete ? "\u2705 Ho\xE0n t\u1EA5t" : "\u23F3 \u0110ang t\u1EA1o sinh"} | ${l.artifacts.join(", ") || "-"} |`).join("\n") : "| Ch\u01B0a c\xF3 b\xE0i h\u1ECDc n\xE0o tr\xEAn \u0111\u0129a c\u1EE9ng | 0/4 | \u23F3 Ch\u1EDD ch\u1EA1y | - |";
201
+ const rawMarkdownReport = `## \u{1F4CA} B\xC1O C\xC1O TR\u1EA0NG TH\xC1I D\u1EF0 \xC1N: \`${projectId.toUpperCase()}\`
202
+
203
+ ### 1. N\u1EC1n t\u1EA3ng T\xE0i li\u1EC7u (SOT Readiness)
204
+ | T\xE0i li\u1EC7u SOT | Tr\u1EA1ng th\xE1i |
205
+ |---|:---:|
206
+ ${sotTable}
207
+
208
+ ### 2. Danh m\u1EE5c B\xE0i h\u1ECDc & H\u1ECDc li\u1EC7u (Lessons & Artifacts Matrix)
209
+ | M\xE3 b\xE0i h\u1ECDc | S\u1ED1 l\u01B0\u1EE3ng Artifact | Tr\u1EA1ng th\xE1i | Danh s\xE1ch Artifact \u0111\xE3 t\u1EA1o |
210
+ |---|:---:|:---:|---|
211
+ ${lessonTable}
212
+
213
+ **Ti\u1EBFn \u0111\u1ED9 t\u1ED5ng th\u1EC3:** ${completedCount}/${totalLessons} b\xE0i h\u1ECDc (${progressPercent}%)
214
+
215
+ ### 3. \u0110\u1EC1 xu\u1EA5t H\xE0nh \u0111\u1ED9ng Ti\u1EBFp theo:
216
+ - S\u1EED d\u1EE5ng l\u1EC7nh \`/create-lesson <id>\` \u0111\u1EC3 t\u1EA1o sinh c\xE1c b\xE0i h\u1ECDc ti\u1EBFp theo trong pipeline.
217
+ - S\u1EED d\u1EE5ng l\u1EC7nh \`/audit-quality\` \u0111\u1EC3 ch\u1EA1y ki\u1EC3m th\u1EED ti\xEAu chu\u1EA9n Bloom v\xE0 Alignment Matrix.`;
218
+ return {
219
+ projectId,
220
+ sotReadiness: sotList,
221
+ lessons,
222
+ completedCount,
223
+ totalLessons,
224
+ progressPercent,
225
+ rawMarkdownReport
226
+ };
227
+ }
228
+ async getPipelineState(projectId) {
229
+ const projectDir = this.getProjectDir(projectId);
230
+ const stateFile = path.join(projectDir, "_pipeline", "state.json");
231
+ if (fs.existsSync(stateFile)) {
232
+ try {
233
+ const raw = fs.readFileSync(stateFile, "utf-8");
234
+ const parsed = JSON.parse(raw);
235
+ if (parsed && typeof parsed === "object" && parsed.tasks) {
236
+ return parsed;
237
+ }
238
+ } catch (err) {
239
+ console.warn(`[FileSystemCurriculumAdapter] Error reading pipeline state for ${projectId}:`, err);
240
+ }
241
+ }
242
+ return {
243
+ projectId,
244
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
245
+ tasks: {}
246
+ };
247
+ }
248
+ async savePipelineState(projectId, state) {
249
+ const projectDir = this.getProjectDir(projectId);
250
+ const pipelineDir = path.join(projectDir, "_pipeline");
251
+ if (!fs.existsSync(pipelineDir)) {
252
+ fs.mkdirSync(pipelineDir, { recursive: true });
253
+ }
254
+ const stateFile = path.join(pipelineDir, "state.json");
255
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
256
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), "utf-8");
257
+ }
258
+ async updateArtifactState(projectId, taskId, artifactType, update) {
259
+ const current = await this.getPipelineState(projectId);
260
+ if (!current.tasks[taskId]) {
261
+ current.tasks[taskId] = {};
262
+ }
263
+ const existingInfo = current.tasks[taskId][artifactType] || {
264
+ state: "pending"
265
+ };
266
+ current.tasks[taskId][artifactType] = {
267
+ ...existingInfo,
268
+ ...update
269
+ };
270
+ await this.savePipelineState(projectId, current);
271
+ return current;
272
+ }
273
+ };
274
+ var SupabaseCurriculumAdapter = class {
275
+ client;
276
+ bucketName;
277
+ constructor(config = {}) {
278
+ this.bucketName = config.bucketName || process.env.SUPABASE_STORAGE_BUCKET || "course-materials";
279
+ if (config.client) {
280
+ this.client = config.client;
281
+ } else {
282
+ const url = config.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || "";
283
+ const key = config.supabaseServiceRoleKey || process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
284
+ if (!url || !key) {
285
+ throw new Error("Supabase URL or Key is missing in SupabaseCurriculumAdapter");
286
+ }
287
+ this.client = createClient(url, key);
288
+ }
289
+ }
290
+ async projectExists(projectId) {
291
+ try {
292
+ const { data, error } = await this.client.storage.from(this.bucketName).list(projectId, { limit: 5 });
293
+ return !error && !!data && data.length > 0;
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+ async readSotDocument(projectId, filename) {
299
+ const paths = [
300
+ `${projectId}/_sot/${filename}`,
301
+ `${projectId}/${filename}`,
302
+ `${projectId}/_designer/${filename}`,
303
+ `${projectId}/_analyst/${filename}`
304
+ ];
305
+ for (const p of paths) {
306
+ try {
307
+ const { data, error } = await this.client.storage.from(this.bucketName).download(p);
308
+ if (!error && data) {
309
+ return await data.text();
310
+ }
311
+ } catch {
312
+ }
313
+ }
314
+ return null;
315
+ }
316
+ async listSotDocuments(projectId) {
317
+ const results = [];
318
+ for (const filename of STANDARD_SOT_FILES) {
319
+ const content = await this.readSotDocument(projectId, filename);
320
+ results.push({
321
+ name: filename.replace(".md", "").replace(/_/g, " "),
322
+ filename,
323
+ exists: !!content,
324
+ content: content || void 0,
325
+ sizeBytes: content ? content.length : 0
326
+ });
327
+ }
328
+ return results;
329
+ }
330
+ async readArtifact(projectId, relPathOrIdentifier) {
331
+ const cleanPath = relPathOrIdentifier.replace(/^\//, "");
332
+ const possiblePaths = [
333
+ `${projectId}/${cleanPath}`,
334
+ `${projectId}/_sot/${cleanPath}`,
335
+ `${projectId}/_content/${cleanPath}`,
336
+ `${projectId}/lessons/${cleanPath}`,
337
+ `${projectId}/_sot/${cleanPath.split("/").pop()}`,
338
+ `${projectId}/lessons/${cleanPath.split("/").pop()}`
339
+ ];
340
+ for (const p of possiblePaths) {
341
+ try {
342
+ const { data, error } = await this.client.storage.from(this.bucketName).download(p);
343
+ if (!error && data) {
344
+ return await data.text();
345
+ }
346
+ } catch {
347
+ }
348
+ }
349
+ const lessonMatch = relPathOrIdentifier.match(/U\d+_M\d+_L\d+/i);
350
+ if (lessonMatch) {
351
+ const lessonId = lessonMatch[0].toUpperCase();
352
+ const unitCode = lessonId.split("_")[0] || "U01";
353
+ const lower = relPathOrIdentifier.toLowerCase();
354
+ let artifactPrefix = "LESSON";
355
+ if (lower.includes("act") || lower.includes("lab") || lower.includes("th\u1EF1c h\xE0nh")) {
356
+ artifactPrefix = "ACT";
357
+ } else if (lower.includes("quiz") || lower.includes("tr\u1EAFc nghi\u1EC7m") || lower.includes("\u0111\xE1nh gi\xE1")) {
358
+ artifactPrefix = "QUIZ";
359
+ } else if (lower.includes("slide") || lower.includes("b\xE0i gi\u1EA3ng") || lower.includes("tr\xECnh chi\u1EBFu")) {
360
+ artifactPrefix = "SLIDE";
361
+ } else if (lower.includes("guide") || lower.includes("h\u01B0\u1EDBng d\u1EABn")) {
362
+ artifactPrefix = "GUIDE";
363
+ }
364
+ const candidatePaths = [
365
+ `${projectId}/_content/${unitCode}/${artifactPrefix}_${lessonId}.md`,
366
+ `${projectId}/lessons/${lessonId}/${artifactPrefix}_${lessonId}.md`,
367
+ `${projectId}/${artifactPrefix}_${lessonId}.md`
368
+ ];
369
+ for (const p of candidatePaths) {
370
+ try {
371
+ const { data, error } = await this.client.storage.from(this.bucketName).download(p);
372
+ if (!error && data) {
373
+ return await data.text();
374
+ }
375
+ } catch {
376
+ }
377
+ }
378
+ }
379
+ return null;
380
+ }
381
+ async saveArtifact(projectId, relPath, content) {
382
+ const fullPath = `${projectId}/${relPath.replace(/^\//, "")}`;
383
+ const { error } = await this.client.storage.from(this.bucketName).upload(fullPath, Buffer.from(content, "utf-8"), {
384
+ contentType: "text/markdown",
385
+ upsert: true
386
+ });
387
+ if (error) {
388
+ throw new Error(`Failed to upload artifact to Supabase Storage: ${error.message}`);
389
+ }
390
+ try {
391
+ await this.client.from("curriculum_artifacts").upsert({
392
+ project_id: projectId,
393
+ rel_path: relPath,
394
+ content,
395
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
396
+ }, { onConflict: "project_id,rel_path" });
397
+ } catch {
398
+ }
399
+ }
400
+ async listLessons(projectId) {
401
+ const summaries = [];
402
+ const processedLessons = /* @__PURE__ */ new Set();
403
+ try {
404
+ const { data: unitDirs } = await this.client.storage.from(this.bucketName).list(`${projectId}/_content`);
405
+ if (unitDirs) {
406
+ for (const u of unitDirs) {
407
+ const { data: files } = await this.client.storage.from(this.bucketName).list(`${projectId}/_content/${u.name}`);
408
+ if (files) {
409
+ const lessonMap = {};
410
+ files.forEach((f) => {
411
+ const m = f.name.match(/U\d+_M\d+_L\d+/i);
412
+ if (m) {
413
+ const lid = m[0].toUpperCase();
414
+ lessonMap[lid] = lessonMap[lid] || [];
415
+ lessonMap[lid].push(f.name);
416
+ }
417
+ });
418
+ for (const [lid, arts] of Object.entries(lessonMap)) {
419
+ if (!processedLessons.has(lid)) {
420
+ processedLessons.add(lid);
421
+ summaries.push({
422
+ lessonId: lid,
423
+ artifactsCount: arts.length,
424
+ isComplete: arts.length >= 4,
425
+ artifacts: arts
426
+ });
427
+ }
428
+ }
429
+ }
430
+ }
431
+ }
432
+ const { data: lessonDirs } = await this.client.storage.from(this.bucketName).list(`${projectId}/lessons`);
433
+ if (lessonDirs) {
434
+ for (const dir of lessonDirs) {
435
+ if (!processedLessons.has(dir.name)) {
436
+ const { data: files } = await this.client.storage.from(this.bucketName).list(`${projectId}/lessons/${dir.name}`);
437
+ const artifacts = (files || []).map((f) => f.name).filter((n) => !n.startsWith("."));
438
+ processedLessons.add(dir.name);
439
+ summaries.push({
440
+ lessonId: dir.name,
441
+ artifactsCount: artifacts.length,
442
+ isComplete: artifacts.length >= 4,
443
+ artifacts
444
+ });
445
+ }
446
+ }
447
+ }
448
+ } catch {
449
+ }
450
+ return summaries;
451
+ }
452
+ async getProjectStatus(projectId) {
453
+ const sotList = await this.listSotDocuments(projectId);
454
+ const lessons = await this.listLessons(projectId);
455
+ const completedCount = lessons.filter((l) => l.isComplete).length;
456
+ const totalLessons = lessons.length || 4;
457
+ const progressPercent = Math.round(completedCount / totalLessons * 100);
458
+ const sotTable = sotList.map((doc) => `| \`${doc.filename}\` | ${doc.exists ? "\u2705 S\u1EB5n s\xE0ng" : "\u274C Ch\u01B0a c\xF3"} |`).join("\n");
459
+ const lessonTable = lessons.length > 0 ? lessons.map((l) => `| **${l.lessonId}** | ${l.artifactsCount}/4 Artifacts | ${l.isComplete ? "\u2705 Ho\xE0n t\u1EA5t" : "\u23F3 \u0110ang t\u1EA1o sinh"} | ${l.artifacts.join(", ") || "-"} |`).join("\n") : "| Ch\u01B0a c\xF3 b\xE0i h\u1ECDc n\xE0o tr\xEAn cloud storage | 0/4 | \u23F3 Ch\u1EDD ch\u1EA1y | - |";
460
+ const rawMarkdownReport = `## \u{1F4CA} B\xC1O C\xC1O TR\u1EA0NG TH\xC1I D\u1EF0 \xC1N (CLOUD): \`${projectId.toUpperCase()}\`
461
+
462
+ ### 1. N\u1EC1n t\u1EA3ng T\xE0i li\u1EC7u (SOT Readiness)
463
+ | T\xE0i li\u1EC7u SOT | Tr\u1EA1ng th\xE1i |
464
+ |---|:---:|
465
+ ${sotTable}
466
+
467
+ ### 2. Danh m\u1EE5c B\xE0i h\u1ECDc & H\u1ECDc li\u1EC7u (Lessons & Artifacts Matrix)
468
+ | M\xE3 b\xE0i h\u1ECDc | S\u1ED1 l\u01B0\u1EE3ng Artifact | Tr\u1EA1ng th\xE1i | Danh s\xE1ch Artifact \u0111\xE3 t\u1EA1o |
469
+ |---|:---:|:---:|---|
470
+ ${lessonTable}
471
+
472
+ **Ti\u1EBFn \u0111\u1ED9 t\u1ED5ng th\u1EC3:** ${completedCount}/${totalLessons} b\xE0i h\u1ECDc (${progressPercent}%)`;
473
+ return {
474
+ projectId,
475
+ sotReadiness: sotList,
476
+ lessons,
477
+ completedCount,
478
+ totalLessons,
479
+ progressPercent,
480
+ rawMarkdownReport
481
+ };
482
+ }
483
+ /**
484
+ * Uploads a complete milestone bundle (Markdown files) to Supabase Storage
485
+ */
486
+ async uploadMilestoneBundle(projectSlug, bundle) {
487
+ const basePath = `${projectSlug}/${bundle.milestoneId}`;
488
+ const uploads = [
489
+ {
490
+ path: `${basePath}/LESSON.md`,
491
+ content: bundle.lessonMarkdown,
492
+ type: "text/markdown"
493
+ },
494
+ {
495
+ path: `${basePath}/ACT.md`,
496
+ content: bundle.activityMarkdown,
497
+ type: "text/markdown"
498
+ },
499
+ {
500
+ path: `${basePath}/HANDOUT.md`,
501
+ content: bundle.handoutMarkdown,
502
+ type: "text/markdown"
503
+ }
504
+ ];
505
+ const results = {};
506
+ for (const item of uploads) {
507
+ const { data, error } = await this.client.storage.from(this.bucketName).upload(item.path, Buffer.from(item.content, "utf-8"), {
508
+ contentType: item.type,
509
+ upsert: true
510
+ });
511
+ if (!error && data) {
512
+ const { data: publicUrlData } = this.client.storage.from(this.bucketName).getPublicUrl(item.path);
513
+ results[item.path] = publicUrlData.publicUrl;
514
+ }
515
+ }
516
+ return {
517
+ lessonUrl: results[`${basePath}/LESSON.md`],
518
+ activityUrl: results[`${basePath}/ACT.md`],
519
+ handoutUrl: results[`${basePath}/HANDOUT.md`]
520
+ };
521
+ }
522
+ async getPipelineState(projectId) {
523
+ const p = `${projectId}/_pipeline/state.json`;
524
+ try {
525
+ const { data, error } = await this.client.storage.from(this.bucketName).download(p);
526
+ if (!error && data) {
527
+ const text = await data.text();
528
+ return JSON.parse(text);
529
+ }
530
+ } catch {
531
+ }
532
+ return {
533
+ projectId,
534
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
535
+ tasks: {}
536
+ };
537
+ }
538
+ async savePipelineState(projectId, state) {
539
+ const p = `${projectId}/_pipeline/state.json`;
540
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
541
+ await this.client.storage.from(this.bucketName).upload(p, Buffer.from(JSON.stringify(state, null, 2), "utf-8"), {
542
+ contentType: "application/json",
543
+ upsert: true
544
+ });
545
+ }
546
+ async updateArtifactState(projectId, taskId, artifactType, update) {
547
+ const current = await this.getPipelineState(projectId);
548
+ if (!current.tasks[taskId]) {
549
+ current.tasks[taskId] = {};
550
+ }
551
+ current.tasks[taskId][artifactType] = {
552
+ ...current.tasks[taskId][artifactType] || { state: "pending" },
553
+ ...update
554
+ };
555
+ await this.savePipelineState(projectId, current);
556
+ return current;
557
+ }
558
+ };
559
+
560
+ // src/storage/factory.ts
561
+ function createCurriculumStorage(options = {}) {
562
+ const provider = options.provider || process.env.CURRICULUM_STORAGE_PROVIDER || "auto";
563
+ if (provider === "supabase") {
564
+ return new SupabaseCurriculumAdapter(options.supabaseConfig);
565
+ }
566
+ if (provider === "auto") {
567
+ const hasSupabase = Boolean(
568
+ (process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL) && (process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) && process.env.STORAGE_USE_SUPABASE === "true"
569
+ );
570
+ if (hasSupabase) {
571
+ try {
572
+ return new SupabaseCurriculumAdapter(options.supabaseConfig);
573
+ } catch (e) {
574
+ console.warn("\u26A0\uFE0F Falling back to FileSystemCurriculumAdapter due to Supabase initialization error:", e);
575
+ }
576
+ }
577
+ }
578
+ return new FileSystemCurriculumAdapter(options.fileSystemOptions);
579
+ }
580
+
581
+ export { FileSystemCurriculumAdapter, STANDARD_SOT_FILES, SupabaseCurriculumAdapter, computeContentHash, createCurriculumStorage };
582
+ //# sourceMappingURL=index.mjs.map
583
+ //# sourceMappingURL=index.mjs.map