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