@learnpack/learnpack 5.0.351 → 5.0.353

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 (70) hide show
  1. package/lib/commands/publish.js +6 -0
  2. package/lib/commands/serve.js +85 -259
  3. package/lib/models/creator.d.ts +6 -0
  4. package/lib/scripts/descriptionsGcsBackfill.d.ts +1 -0
  5. package/lib/scripts/descriptionsGcsBackfill.js +141 -0
  6. package/lib/scripts/descriptionsS3Backfill.d.ts +1 -0
  7. package/lib/scripts/descriptionsS3Backfill.js +157 -0
  8. package/lib/scripts/descriptionsSweep.d.ts +1 -0
  9. package/lib/scripts/descriptionsSweep.js +142 -0
  10. package/lib/utils/api.d.ts +7 -0
  11. package/lib/utils/api.js +8 -1
  12. package/lib/utils/creatorUtilities.js +2 -1
  13. package/lib/utils/descriptionHash.d.ts +66 -0
  14. package/lib/utils/descriptionHash.js +173 -0
  15. package/lib/utils/descriptions/gcsStorage.d.ts +16 -0
  16. package/lib/utils/descriptions/gcsStorage.js +60 -0
  17. package/lib/utils/descriptions/generateCourseDescriptions.d.ts +59 -0
  18. package/lib/utils/descriptions/generateCourseDescriptions.js +173 -0
  19. package/lib/utils/descriptions/mirrorDescriptions.d.ts +49 -0
  20. package/lib/utils/descriptions/mirrorDescriptions.js +109 -0
  21. package/lib/utils/descriptions/publishStage.d.ts +69 -0
  22. package/lib/utils/descriptions/publishStage.js +234 -0
  23. package/lib/utils/descriptions/resumePublication.d.ts +36 -0
  24. package/lib/utils/descriptions/resumePublication.js +113 -0
  25. package/lib/utils/descriptions/s3Storage.d.ts +30 -0
  26. package/lib/utils/descriptions/s3Storage.js +134 -0
  27. package/lib/utils/descriptions/workList.d.ts +75 -0
  28. package/lib/utils/descriptions/workList.js +177 -0
  29. package/lib/utils/gcsBucketName.d.ts +10 -0
  30. package/lib/utils/gcsBucketName.js +19 -0
  31. package/lib/utils/packageManifest.d.ts +18 -0
  32. package/lib/utils/packageManifest.js +81 -7
  33. package/lib/utils/publishEvents.d.ts +66 -0
  34. package/lib/utils/publishEvents.js +111 -0
  35. package/lib/utils/publishJournal.d.ts +119 -0
  36. package/lib/utils/publishJournal.js +275 -0
  37. package/lib/utils/rigoActions.d.ts +44 -0
  38. package/lib/utils/rigoActions.js +75 -1
  39. package/lib/utils/s3/packageManifestBackfill.d.ts +84 -0
  40. package/lib/utils/s3/packageManifestBackfill.js +487 -0
  41. package/lib/utils/syllabusSync.d.ts +71 -0
  42. package/lib/utils/syllabusSync.js +273 -0
  43. package/package.json +4 -1
  44. package/src/commands/publish.ts +7 -0
  45. package/src/commands/serve.ts +144 -335
  46. package/src/models/creator.ts +9 -0
  47. package/src/scripts/descriptionsGcsBackfill.ts +193 -0
  48. package/src/scripts/descriptionsS3Backfill.ts +208 -0
  49. package/src/scripts/descriptionsSweep.ts +185 -0
  50. package/src/ui/_app/app.css +1 -1
  51. package/src/ui/_app/app.js +143 -141
  52. package/src/ui/app.tar.gz +0 -0
  53. package/src/utils/api.ts +9 -0
  54. package/src/utils/creatorUtilities.ts +2 -1
  55. package/src/utils/descriptionHash.ts +196 -0
  56. package/src/utils/descriptions/gcsStorage.ts +67 -0
  57. package/src/utils/descriptions/generateCourseDescriptions.ts +301 -0
  58. package/src/utils/descriptions/mirrorDescriptions.ts +191 -0
  59. package/src/utils/descriptions/publishStage.ts +382 -0
  60. package/src/utils/descriptions/resumePublication.ts +200 -0
  61. package/src/utils/descriptions/s3Storage.ts +206 -0
  62. package/src/utils/descriptions/workList.ts +283 -0
  63. package/src/utils/export/README.md +0 -2
  64. package/src/utils/gcsBucketName.ts +19 -0
  65. package/src/utils/packageManifest.ts +101 -10
  66. package/src/utils/publishEvents.ts +181 -0
  67. package/src/utils/publishJournal.ts +383 -0
  68. package/src/utils/rigoActions.ts +130 -0
  69. package/src/utils/s3/packageManifestBackfill.ts +776 -0
  70. package/src/utils/syllabusSync.ts +390 -0
@@ -0,0 +1,119 @@
1
+ import { Bucket } from "@google-cloud/storage";
2
+ /**
3
+ * Publish journal — per-stage state of a publication.
4
+ *
5
+ * Publishing is a distributed transaction across three systems (S3 via Rigobot,
6
+ * breathecode assets, and our own buckets) plus an asynchronous description
7
+ * stage. Some of its outcomes are NOT derivable from content afterwards: in
8
+ * particular "was the manifest-updated event already emitted?" leaves no trace
9
+ * in S3 or GCS. The journal records what happened per stage so the sweep can
10
+ * resume an interrupted publication and honour the event contract (a publish
11
+ * that announced `descriptions: "queued"` must always be followed by exactly
12
+ * one `package_manifest_updated`).
13
+ *
14
+ * Design notes:
15
+ *
16
+ * - **Best effort, never a lock.** Every write swallows its errors: if the
17
+ * journal cannot be written the publication still proceeds. Losing the
18
+ * journal degrades us to the previous behaviour (no recovery, no diagnosis),
19
+ * which is acceptable; blocking a publication because a bookkeeping write
20
+ * failed is not.
21
+ * - **Flat key space.** Journals live in `publish-journal/{slug}__{id}.json`,
22
+ * a sibling prefix of `courses/`, so the sweep can list every unfinished
23
+ * publication with one prefix query and the concurrency guard can list a
24
+ * single course with another, without maintaining an index.
25
+ * - **Explicit cleanup.** A journal is deleted once every stage succeeded, and
26
+ * kept when any stage failed (visibility + retry). GCS lifecycle rules would
27
+ * be the natural alternative, but the service account only holds
28
+ * object-level permissions (`storage.buckets.*` is denied), so the cleanup
29
+ * has to happen in code.
30
+ */
31
+ export declare const JOURNAL_PREFIX = "publish-journal/";
32
+ /**
33
+ * Stages of a publication, in execution order.
34
+ *
35
+ * Only stages the orchestrator can observe on its own: writing the asset id
36
+ * back into Rigobot happens inside the asset sync call, so it is covered by
37
+ * `assetsSync` rather than being a stage that could never be marked.
38
+ */
39
+ export declare const STAGE_NAMES: readonly ["deploy", "assetsSync", "publishedEvent", "descriptions", "gcsMirror", "manifestEvent"];
40
+ export type StageName = typeof STAGE_NAMES[number];
41
+ export type StageStatus = "pending" | "running" | "done" | "failed";
42
+ export type Stage = {
43
+ status: StageStatus;
44
+ at?: string;
45
+ error?: string;
46
+ meta?: unknown;
47
+ };
48
+ export type PublishJournal = {
49
+ publishId: string;
50
+ courseSlug: string;
51
+ startedAt: string;
52
+ updatedAt: string;
53
+ /** Times the sweep has picked this journal up, to bound retries. */
54
+ attempts: number;
55
+ /** Set once retries are exhausted: kept for a human, ignored by the sweep. */
56
+ abandoned?: boolean;
57
+ stages: Record<StageName, Stage>;
58
+ };
59
+ export type JournalRef = {
60
+ courseSlug: string;
61
+ publishId: string;
62
+ key: string;
63
+ };
64
+ /**
65
+ * Minimal storage surface the journal needs, so callers can pass the real
66
+ * bucket adapter or an in-memory double. Object-level operations only.
67
+ */
68
+ export interface JournalStorage {
69
+ save(key: string, contents: string): Promise<void>;
70
+ read(key: string): Promise<string | null>;
71
+ remove(key: string): Promise<void>;
72
+ list(prefix: string): Promise<string[]>;
73
+ }
74
+ export declare function journalKey(courseSlug: string, publishId: string): string;
75
+ export declare function coursePrefix(courseSlug: string): string;
76
+ /** Inverse of `journalKey`; returns null for keys that don't match the shape. */
77
+ export declare function parseJournalKey(key: string): JournalRef | null;
78
+ /**
79
+ * Start a journal for a new publication. The generated `publishId` correlates
80
+ * the two publish events and is returned even if persisting failed.
81
+ */
82
+ export declare function createJournal(storage: JournalStorage, courseSlug: string, publishId?: string): Promise<PublishJournal>;
83
+ /** Update one stage and persist. Mutates and returns the same journal object. */
84
+ export declare function markStage(storage: JournalStorage, journal: PublishJournal, stage: StageName, status: StageStatus, extra?: {
85
+ error?: string;
86
+ meta?: unknown;
87
+ }): Promise<PublishJournal>;
88
+ /** Read a journal. Returns null when missing or unreadable. */
89
+ export declare function readJournal(storage: JournalStorage, courseSlug: string, publishId: string): Promise<PublishJournal | null>;
90
+ export declare function readJournalByKey(storage: JournalStorage, key: string): Promise<PublishJournal | null>;
91
+ /**
92
+ * Every journal still stored, i.e. every publication that did not finish
93
+ * cleanly. Returns an empty list on error (the sweep falls back to its deep
94
+ * scan rather than crashing).
95
+ */
96
+ export declare function listJournalRefs(storage: JournalStorage): Promise<JournalRef[]>;
97
+ /** Journals of a single course; backs the concurrent-publish guard. */
98
+ export declare function listJournalRefsForCourse(storage: JournalStorage, courseSlug: string): Promise<JournalRef[]>;
99
+ export declare function isStageTerminal(stage: Stage): boolean;
100
+ export declare function hasFailedStage(journal: PublishJournal): boolean;
101
+ /** True when no stage is left to run (all done, or done/failed mix). */
102
+ export declare function isJournalSettled(journal: PublishJournal): boolean;
103
+ /** Stages the sweep still has work to do on. */
104
+ export declare function pendingStages(journal: PublishJournal): StageName[];
105
+ /** A journal untouched for longer than `maxAgeMs` is presumed interrupted. */
106
+ export declare function isJournalStale(journal: PublishJournal, maxAgeMs: number, now?: number): boolean;
107
+ export declare function incrementAttempts(storage: JournalStorage, journal: PublishJournal): Promise<PublishJournal>;
108
+ /**
109
+ * Give up on a journal after too many attempts. It is kept, not deleted: a
110
+ * publication that never completed is exactly what someone needs to see.
111
+ */
112
+ export declare function abandonJournal(storage: JournalStorage, journal: PublishJournal): Promise<PublishJournal>;
113
+ /**
114
+ * Close a journal: delete it when every stage succeeded, keep it otherwise so
115
+ * the sweep can retry and a human can inspect it. Never throws.
116
+ */
117
+ export declare function finalizeJournal(storage: JournalStorage, journal: PublishJournal): Promise<"deleted" | "kept">;
118
+ /** GCS-backed adapter. Object-level operations only, by design. */
119
+ export declare function createGcsJournalStorage(bucket: Bucket): JournalStorage;
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.STAGE_NAMES = exports.JOURNAL_PREFIX = void 0;
4
+ exports.journalKey = journalKey;
5
+ exports.coursePrefix = coursePrefix;
6
+ exports.parseJournalKey = parseJournalKey;
7
+ exports.createJournal = createJournal;
8
+ exports.markStage = markStage;
9
+ exports.readJournal = readJournal;
10
+ exports.readJournalByKey = readJournalByKey;
11
+ exports.listJournalRefs = listJournalRefs;
12
+ exports.listJournalRefsForCourse = listJournalRefsForCourse;
13
+ exports.isStageTerminal = isStageTerminal;
14
+ exports.hasFailedStage = hasFailedStage;
15
+ exports.isJournalSettled = isJournalSettled;
16
+ exports.pendingStages = pendingStages;
17
+ exports.isJournalStale = isJournalStale;
18
+ exports.incrementAttempts = incrementAttempts;
19
+ exports.abandonJournal = abandonJournal;
20
+ exports.finalizeJournal = finalizeJournal;
21
+ exports.createGcsJournalStorage = createGcsJournalStorage;
22
+ const uuid_1 = require("uuid");
23
+ /**
24
+ * Publish journal — per-stage state of a publication.
25
+ *
26
+ * Publishing is a distributed transaction across three systems (S3 via Rigobot,
27
+ * breathecode assets, and our own buckets) plus an asynchronous description
28
+ * stage. Some of its outcomes are NOT derivable from content afterwards: in
29
+ * particular "was the manifest-updated event already emitted?" leaves no trace
30
+ * in S3 or GCS. The journal records what happened per stage so the sweep can
31
+ * resume an interrupted publication and honour the event contract (a publish
32
+ * that announced `descriptions: "queued"` must always be followed by exactly
33
+ * one `package_manifest_updated`).
34
+ *
35
+ * Design notes:
36
+ *
37
+ * - **Best effort, never a lock.** Every write swallows its errors: if the
38
+ * journal cannot be written the publication still proceeds. Losing the
39
+ * journal degrades us to the previous behaviour (no recovery, no diagnosis),
40
+ * which is acceptable; blocking a publication because a bookkeeping write
41
+ * failed is not.
42
+ * - **Flat key space.** Journals live in `publish-journal/{slug}__{id}.json`,
43
+ * a sibling prefix of `courses/`, so the sweep can list every unfinished
44
+ * publication with one prefix query and the concurrency guard can list a
45
+ * single course with another, without maintaining an index.
46
+ * - **Explicit cleanup.** A journal is deleted once every stage succeeded, and
47
+ * kept when any stage failed (visibility + retry). GCS lifecycle rules would
48
+ * be the natural alternative, but the service account only holds
49
+ * object-level permissions (`storage.buckets.*` is denied), so the cleanup
50
+ * has to happen in code.
51
+ */
52
+ exports.JOURNAL_PREFIX = "publish-journal/";
53
+ /**
54
+ * Stages of a publication, in execution order.
55
+ *
56
+ * Only stages the orchestrator can observe on its own: writing the asset id
57
+ * back into Rigobot happens inside the asset sync call, so it is covered by
58
+ * `assetsSync` rather than being a stage that could never be marked.
59
+ */
60
+ exports.STAGE_NAMES = [
61
+ "deploy",
62
+ "assetsSync",
63
+ "publishedEvent",
64
+ "descriptions",
65
+ "gcsMirror",
66
+ "manifestEvent",
67
+ ];
68
+ function journalKey(courseSlug, publishId) {
69
+ return `${exports.JOURNAL_PREFIX}${courseSlug}__${publishId}.json`;
70
+ }
71
+ function coursePrefix(courseSlug) {
72
+ return `${exports.JOURNAL_PREFIX}${courseSlug}__`;
73
+ }
74
+ /** Inverse of `journalKey`; returns null for keys that don't match the shape. */
75
+ function parseJournalKey(key) {
76
+ if (!key.startsWith(exports.JOURNAL_PREFIX) || !key.endsWith(".json")) {
77
+ return null;
78
+ }
79
+ const name = key.slice(exports.JOURNAL_PREFIX.length, -".json".length);
80
+ const separator = name.indexOf("__");
81
+ if (separator <= 0) {
82
+ return null;
83
+ }
84
+ const courseSlug = name.slice(0, separator);
85
+ const publishId = name.slice(separator + 2);
86
+ if (!courseSlug || !publishId) {
87
+ return null;
88
+ }
89
+ return { courseSlug, publishId, key };
90
+ }
91
+ function emptyStages() {
92
+ const stages = {};
93
+ for (const name of exports.STAGE_NAMES) {
94
+ stages[name] = { status: "pending" };
95
+ }
96
+ return stages;
97
+ }
98
+ /** Persist the journal. Never throws: bookkeeping must not break a publish. */
99
+ async function persist(storage, journal) {
100
+ try {
101
+ await storage.save(journalKey(journal.courseSlug, journal.publishId), JSON.stringify(journal, null, 2));
102
+ }
103
+ catch (error) {
104
+ console.warn(`[publish-journal] Could not persist journal for "${journal.courseSlug}":`, error.message);
105
+ }
106
+ }
107
+ /**
108
+ * Start a journal for a new publication. The generated `publishId` correlates
109
+ * the two publish events and is returned even if persisting failed.
110
+ */
111
+ async function createJournal(storage, courseSlug, publishId = (0, uuid_1.v4)()) {
112
+ const now = new Date().toISOString();
113
+ const journal = {
114
+ publishId,
115
+ courseSlug,
116
+ startedAt: now,
117
+ updatedAt: now,
118
+ attempts: 0,
119
+ stages: emptyStages(),
120
+ };
121
+ await persist(storage, journal);
122
+ return journal;
123
+ }
124
+ /** Update one stage and persist. Mutates and returns the same journal object. */
125
+ async function markStage(storage, journal, stage, status, extra = {}) {
126
+ const next = { status, at: new Date().toISOString() };
127
+ if (extra.error !== undefined) {
128
+ next.error = extra.error;
129
+ }
130
+ if (extra.meta !== undefined) {
131
+ next.meta = extra.meta;
132
+ }
133
+ journal.stages[stage] = next;
134
+ journal.updatedAt = next.at;
135
+ await persist(storage, journal);
136
+ return journal;
137
+ }
138
+ /** Read a journal. Returns null when missing or unreadable. */
139
+ async function readJournal(storage, courseSlug, publishId) {
140
+ return readJournalByKey(storage, journalKey(courseSlug, publishId));
141
+ }
142
+ async function readJournalByKey(storage, key) {
143
+ try {
144
+ const raw = await storage.read(key);
145
+ if (!raw) {
146
+ return null;
147
+ }
148
+ const parsed = JSON.parse(raw);
149
+ if (!parsed || !parsed.publishId || !parsed.stages) {
150
+ return null;
151
+ }
152
+ // Tolerate journals written before a stage was added to STAGE_NAMES.
153
+ for (const name of exports.STAGE_NAMES) {
154
+ if (!parsed.stages[name]) {
155
+ parsed.stages[name] = { status: "pending" };
156
+ }
157
+ }
158
+ return parsed;
159
+ }
160
+ catch (error) {
161
+ console.warn(`[publish-journal] Could not read journal "${key}":`, error.message);
162
+ return null;
163
+ }
164
+ }
165
+ /**
166
+ * Every journal still stored, i.e. every publication that did not finish
167
+ * cleanly. Returns an empty list on error (the sweep falls back to its deep
168
+ * scan rather than crashing).
169
+ */
170
+ async function listJournalRefs(storage) {
171
+ return listRefsByPrefix(storage, exports.JOURNAL_PREFIX);
172
+ }
173
+ /** Journals of a single course; backs the concurrent-publish guard. */
174
+ async function listJournalRefsForCourse(storage, courseSlug) {
175
+ return listRefsByPrefix(storage, coursePrefix(courseSlug));
176
+ }
177
+ async function listRefsByPrefix(storage, prefix) {
178
+ try {
179
+ const keys = await storage.list(prefix);
180
+ const refs = [];
181
+ for (const key of keys) {
182
+ const ref = parseJournalKey(key);
183
+ if (ref) {
184
+ refs.push(ref);
185
+ }
186
+ }
187
+ return refs;
188
+ }
189
+ catch (error) {
190
+ console.warn(`[publish-journal] Could not list journals "${prefix}":`, error.message);
191
+ return [];
192
+ }
193
+ }
194
+ function isStageTerminal(stage) {
195
+ return stage.status === "done" || stage.status === "failed";
196
+ }
197
+ function hasFailedStage(journal) {
198
+ return exports.STAGE_NAMES.some(name => journal.stages[name].status === "failed");
199
+ }
200
+ /** True when no stage is left to run (all done, or done/failed mix). */
201
+ function isJournalSettled(journal) {
202
+ return exports.STAGE_NAMES.every(name => isStageTerminal(journal.stages[name]));
203
+ }
204
+ /** Stages the sweep still has work to do on. */
205
+ function pendingStages(journal) {
206
+ return exports.STAGE_NAMES.filter(name => !isStageTerminal(journal.stages[name]));
207
+ }
208
+ /** A journal untouched for longer than `maxAgeMs` is presumed interrupted. */
209
+ function isJournalStale(journal, maxAgeMs, now = Date.now()) {
210
+ const updatedAt = Date.parse(journal.updatedAt);
211
+ if (Number.isNaN(updatedAt)) {
212
+ return true;
213
+ }
214
+ return now - updatedAt > maxAgeMs;
215
+ }
216
+ async function incrementAttempts(storage, journal) {
217
+ journal.attempts += 1;
218
+ journal.updatedAt = new Date().toISOString();
219
+ await persist(storage, journal);
220
+ return journal;
221
+ }
222
+ /**
223
+ * Give up on a journal after too many attempts. It is kept, not deleted: a
224
+ * publication that never completed is exactly what someone needs to see.
225
+ */
226
+ async function abandonJournal(storage, journal) {
227
+ journal.abandoned = true;
228
+ journal.updatedAt = new Date().toISOString();
229
+ await persist(storage, journal);
230
+ return journal;
231
+ }
232
+ /**
233
+ * Close a journal: delete it when every stage succeeded, keep it otherwise so
234
+ * the sweep can retry and a human can inspect it. Never throws.
235
+ */
236
+ async function finalizeJournal(storage, journal) {
237
+ if (!isJournalSettled(journal) || hasFailedStage(journal)) {
238
+ await persist(storage, journal);
239
+ return "kept";
240
+ }
241
+ try {
242
+ await storage.remove(journalKey(journal.courseSlug, journal.publishId));
243
+ return "deleted";
244
+ }
245
+ catch (error) {
246
+ console.warn(`[publish-journal] Could not delete journal for "${journal.courseSlug}":`, error.message);
247
+ return "kept";
248
+ }
249
+ }
250
+ /** GCS-backed adapter. Object-level operations only, by design. */
251
+ function createGcsJournalStorage(bucket) {
252
+ return {
253
+ async save(key, contents) {
254
+ await bucket.file(key).save(Buffer.from(contents, "utf8"), {
255
+ contentType: "application/json",
256
+ });
257
+ },
258
+ async read(key) {
259
+ try {
260
+ const [buf] = await bucket.file(key).download();
261
+ return buf.toString();
262
+ }
263
+ catch (_a) {
264
+ return null;
265
+ }
266
+ },
267
+ async remove(key) {
268
+ await bucket.file(key).delete();
269
+ },
270
+ async list(prefix) {
271
+ const [files] = await bucket.getFiles({ prefix });
272
+ return files.map(file => file.name);
273
+ },
274
+ };
275
+ }
@@ -99,6 +99,50 @@ type TInitialContentGeneratorInputs = {
99
99
  topic_description?: string;
100
100
  };
101
101
  export declare const initialContentGenerator: (token: string, inputs: TInitialContentGeneratorInputs, webhookUrl?: string, endpointSlug?: string) => Promise<any>;
102
+ export type TGenerateStepDescriptionsParams = {
103
+ /** README of the step, in `sourceLanguage`. Images are stripped before sending. */
104
+ readmeContent: string;
105
+ sourceLanguage: string;
106
+ /** Every language to produce; may or may not include `sourceLanguage`. */
107
+ outputLanguages: string[];
108
+ lessonTitle: string;
109
+ courseTitle?: string;
110
+ targetWordCount?: number;
111
+ promptVersion?: number;
112
+ };
113
+ export type TGenerateStepDescriptionsResult = {
114
+ /**
115
+ * Descriptions keyed by the `language_code` Rigobot answered with — building
116
+ * this record IS the reassociation, the response order is not contractual.
117
+ * A `null` value is the deliberate fallback for content too thin to describe.
118
+ */
119
+ descriptionsByLanguage: Record<string, string | null>;
120
+ /** Requested languages absent from the response (a protocol problem, not a
121
+ * "no description" answer: callers should retry these rather than storing a
122
+ * null that would look settled). */
123
+ missingLanguages: string[];
124
+ promptVersion: number;
125
+ /** Seconds the completion took; worth logging to watch the 30s Heroku edge. */
126
+ durationSeconds: number | null;
127
+ completionId: number | null;
128
+ };
129
+ /**
130
+ * Generate one short description per language for a single step.
131
+ *
132
+ * One request per step: the eye-catching saving is on the *languages* axis (one
133
+ * README in, N descriptions out, ~67% fewer input tokens for a typical course),
134
+ * while batching several steps together would save nothing and would cost cache
135
+ * granularity and partial results.
136
+ *
137
+ * Synchronous by design (`execute_async: false`): the caller owns the write, so
138
+ * every flow — the post-publish stage, the sweep and the backfill — persists
139
+ * its own results instead of relying on a callback. Callers throttle with a
140
+ * concurrency pool.
141
+ *
142
+ * All inputs travel as strings: Rigobot fills the template with plain string
143
+ * replacement and a non-string raises before reaching the LLM.
144
+ */
145
+ export declare const generateStepDescriptions: (token: string, params: TGenerateStepDescriptionsParams, endpointSlug?: string) => Promise<TGenerateStepDescriptionsResult | null>;
102
146
  type TAddInteractivityInputs = {
103
147
  components: string;
104
148
  prev_lesson: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateStepSlug = exports.generateCodeChallenge = exports.addInteractivity = exports.initialContentGenerator = exports.getLanguageCodes = exports.isPackageAuthor = exports.fillSidebarJSON = exports.generateCourseShortName = exports.isValidRigoToken = exports.translateCourseMetadata = exports.createStructuredPreviewReadme = exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
3
+ exports.generateStepSlug = exports.generateCodeChallenge = exports.addInteractivity = exports.generateStepDescriptions = exports.initialContentGenerator = exports.getLanguageCodes = exports.isPackageAuthor = exports.fillSidebarJSON = exports.generateCourseShortName = exports.isValidRigoToken = exports.translateCourseMetadata = exports.createStructuredPreviewReadme = exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
4
4
  exports.downloadImage = downloadImage;
5
5
  exports.createPreviewReadme = createPreviewReadme;
6
6
  exports.makeReadmeReadable = makeReadmeReadable;
@@ -10,6 +10,8 @@ const console_1 = require("../utils/console");
10
10
  const fs = require("fs");
11
11
  const path = require("path");
12
12
  const api_1 = require("./api");
13
+ const descriptionHash_1 = require("./descriptionHash");
14
+ const packageManifest_1 = require("./packageManifest");
13
15
  const createReadme = async (token, inputs, purpose, webhookUrl) => {
14
16
  try {
15
17
  const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/create-sequencial-readme/`, {
@@ -304,6 +306,78 @@ const initialContentGenerator = async (token, inputs, webhookUrl, endpointSlug =
304
306
  }
305
307
  };
306
308
  exports.initialContentGenerator = initialContentGenerator;
309
+ /**
310
+ * Generate one short description per language for a single step.
311
+ *
312
+ * One request per step: the eye-catching saving is on the *languages* axis (one
313
+ * README in, N descriptions out, ~67% fewer input tokens for a typical course),
314
+ * while batching several steps together would save nothing and would cost cache
315
+ * granularity and partial results.
316
+ *
317
+ * Synchronous by design (`execute_async: false`): the caller owns the write, so
318
+ * every flow — the post-publish stage, the sweep and the backfill — persists
319
+ * its own results instead of relying on a callback. Callers throttle with a
320
+ * concurrency pool.
321
+ *
322
+ * All inputs travel as strings: Rigobot fills the template with plain string
323
+ * replacement and a non-string raises before reaching the LLM.
324
+ */
325
+ const generateStepDescriptions = async (token, params, endpointSlug = "generate-step-descriptions") => {
326
+ var _a, _b, _c, _d, _e;
327
+ const promptVersion = (_a = params.promptVersion) !== null && _a !== void 0 ? _a : packageManifest_1.DESCRIPTION_PROMPT_VERSION;
328
+ const targetWordCount = (_b = params.targetWordCount) !== null && _b !== void 0 ? _b : packageManifest_1.DESCRIPTION_TARGET_WORD_COUNT;
329
+ const inputs = {
330
+ // Images never help a description and their alt text may be a whole
331
+ // image-generation prompt; markdown structure is kept, it helps the model.
332
+ readme_content: (0, descriptionHash_1.stripMarkdownImages)(params.readmeContent),
333
+ source_language: params.sourceLanguage,
334
+ output_languages: params.outputLanguages.join(","),
335
+ lesson_title: params.lessonTitle,
336
+ course_title: (_c = params.courseTitle) !== null && _c !== void 0 ? _c : "",
337
+ target_word_count: String(targetWordCount),
338
+ prompt_version: String(promptVersion),
339
+ };
340
+ try {
341
+ const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/${endpointSlug}/`, { inputs, execute_async: false }, {
342
+ headers: {
343
+ "Content-Type": "application/json",
344
+ Authorization: "Token " + token.trim(),
345
+ },
346
+ });
347
+ const data = response.data;
348
+ if (!data || data.status === "ERROR") {
349
+ console.error("Error in generateStepDescriptions:", (data === null || data === void 0 ? void 0 : data.status_text) || "completion returned ERROR");
350
+ return null;
351
+ }
352
+ const entries = Array.isArray((_d = data.parsed) === null || _d === void 0 ? void 0 : _d.descriptions) ?
353
+ data.parsed.descriptions :
354
+ [];
355
+ const descriptionsByLanguage = {};
356
+ for (const entry of entries) {
357
+ const code = entry === null || entry === void 0 ? void 0 : entry.language_code;
358
+ if (typeof code !== "string" || !code) {
359
+ continue;
360
+ }
361
+ descriptionsByLanguage[code] =
362
+ typeof entry.description === "string" ? entry.description : null;
363
+ }
364
+ const echoedVersion = Number.parseInt((_e = data.inputs) === null || _e === void 0 ? void 0 : _e.prompt_version, 10);
365
+ return {
366
+ descriptionsByLanguage,
367
+ missingLanguages: params.outputLanguages.filter(lang => !(lang in descriptionsByLanguage)),
368
+ promptVersion: Number.isFinite(echoedVersion) ?
369
+ echoedVersion :
370
+ promptVersion,
371
+ durationSeconds: typeof data.duration === "number" ? data.duration : null,
372
+ completionId: typeof data.id === "number" ? data.id : null,
373
+ };
374
+ }
375
+ catch (error) {
376
+ console.error("Error in generateStepDescriptions:", error);
377
+ return null;
378
+ }
379
+ };
380
+ exports.generateStepDescriptions = generateStepDescriptions;
307
381
  const addInteractivity = async (token, inputs, webhookUrl) => {
308
382
  try {
309
383
  const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/add-lesson-interactivy/`, {
@@ -0,0 +1,84 @@
1
+ import { Exercise } from "../configBuilder";
2
+ import { Syllabus } from "../../models/creator";
3
+ import { PackageManifest, ReadmeFrontmatter } from "../packageManifest";
4
+ export type PackageResultStatus = "created" | "updated" | "skipped" | "failed";
5
+ export type PackageResult = {
6
+ slug: string;
7
+ status: PackageResultStatus;
8
+ durationMs: number;
9
+ error?: string;
10
+ step?: string;
11
+ sources?: {
12
+ learnJson: boolean;
13
+ configJson: boolean;
14
+ sidebar: boolean;
15
+ syllabus: boolean;
16
+ readmes: number;
17
+ };
18
+ };
19
+ export type BackfillOptions = {
20
+ dryRun: boolean;
21
+ force: boolean;
22
+ skipExisting: boolean;
23
+ noInvalidate: boolean;
24
+ concurrency: number;
25
+ publishedAt: string | null;
26
+ resumeFrom?: string;
27
+ reportOut: string;
28
+ slug?: string;
29
+ bucket: string;
30
+ region: string;
31
+ cloudFrontDistributionId?: string;
32
+ };
33
+ export type BatchReport = {
34
+ total: number;
35
+ created: number;
36
+ updated: number;
37
+ skipped: number;
38
+ failed: number;
39
+ startedAt: string;
40
+ finishedAt: string;
41
+ options: BackfillOptions;
42
+ results: PackageResult[];
43
+ invalidation?: {
44
+ attempted: boolean;
45
+ skippedReason?: string;
46
+ id?: string;
47
+ paths?: string[];
48
+ error?: string;
49
+ };
50
+ };
51
+ export type AwsClient = {
52
+ send(command: unknown): Promise<unknown>;
53
+ };
54
+ type PackageSources = {
55
+ learnJson: Record<string, unknown>;
56
+ exercises: Exercise[];
57
+ sidebar: Record<string, Record<string, string>>;
58
+ syllabus: Syllabus | null;
59
+ readmeFrontmatters: Record<string, Record<string, ReadmeFrontmatter>>;
60
+ meta: PackageResult["sources"];
61
+ };
62
+ export declare function streamToString(body: unknown): Promise<string>;
63
+ export declare function withRetry<T>(fn: () => Promise<T>, attempts?: number, attempt?: number): Promise<T>;
64
+ export declare function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]>;
65
+ export declare function listPublishedSlugs(s3: AwsClient, bucket: string): Promise<string[]>;
66
+ export declare function fetchJsonObject<T>(s3: AwsClient, bucket: string, key: string): Promise<T | null>;
67
+ export declare function fetchTextObject(s3: AwsClient, bucket: string, key: string): Promise<string | null>;
68
+ export declare function listObjectKeys(s3: AwsClient, bucket: string, prefix: string): Promise<string[]>;
69
+ export declare function fetchPackageSources(s3: AwsClient, bucket: string, slug: string): Promise<PackageSources>;
70
+ export declare function buildManifestFromS3Sources(slug: string, sources: PackageSources, options: {
71
+ publishedAt: string | null;
72
+ generatedAt?: string;
73
+ }): PackageManifest;
74
+ export declare function processPackage(s3: AwsClient, bucket: string, slug: string, options: Pick<BackfillOptions, "dryRun" | "force" | "skipExisting" | "publishedAt">): Promise<PackageResult>;
75
+ export declare function invalidatePackageManifestPaths(cf: AwsClient, distributionId: string, paths: string[]): Promise<{
76
+ id: string;
77
+ }>;
78
+ export declare function runBatch(options: BackfillOptions, clients?: {
79
+ s3?: AwsClient;
80
+ cf?: AwsClient;
81
+ onProgress?: (index: number, total: number, result: PackageResult) => void;
82
+ }): Promise<BatchReport>;
83
+ export declare function getBatchExitCode(report: BatchReport): number;
84
+ export {};