@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,59 @@
1
+ import { Syllabus } from "../../models/creator";
2
+ import { PackageManifest } from "../packageManifest";
3
+ import { TGenerateStepDescriptionsParams, TGenerateStepDescriptionsResult } from "../rigoActions";
4
+ import { SyllabusSyncStorage } from "../syllabusSync";
5
+ import { CourseExercise, CourseReadmes } from "./workList";
6
+ /**
7
+ * Generate and persist the step descriptions of one published course.
8
+ *
9
+ * This is the single implementation behind every flow that produces
10
+ * descriptions: the background stage that runs after publishing, the sweep that
11
+ * recovers interrupted publications, and the one-off backfill over the
12
+ * historical catalogue. They differ only in what triggers them.
13
+ *
14
+ * Storage is a port so the service can be driven against the published bucket
15
+ * (the real case) or an in-memory double (tests), and so the manifest
16
+ * re-projection stays where the storage-specific knowledge lives.
17
+ */
18
+ export declare const DEFAULT_DESCRIPTIONS_CONCURRENCY = 5;
19
+ export interface CourseDescriptionsStorage {
20
+ readSyllabus(courseSlug: string): Promise<Syllabus | null>;
21
+ writeSyllabus(courseSlug: string, syllabus: Syllabus): Promise<void>;
22
+ listExercises(courseSlug: string): Promise<CourseExercise[]>;
23
+ readReadme(courseSlug: string, exerciseSlug: string, fileName: string): Promise<string | null>;
24
+ /** Re-generate `package-manifest.json` from the syllabus and invalidate the CDN. */
25
+ reprojectManifest?(courseSlug: string): Promise<void>;
26
+ /** Read the stored manifest, to report the updated one in the event. */
27
+ readManifest?(courseSlug: string): Promise<PackageManifest | null>;
28
+ /** Enables the provisional additive syllabus reconciliation. */
29
+ syllabusSyncStorage?: SyllabusSyncStorage;
30
+ }
31
+ export type GenerateCourseDescriptionsOptions = {
32
+ token: string;
33
+ dryRun?: boolean;
34
+ force?: boolean;
35
+ concurrency?: number;
36
+ targetWordCount?: number;
37
+ promptVersion?: number;
38
+ /** Additive syllabus reconciliation before computing the work list. */
39
+ reconcile?: boolean;
40
+ /** Injection point for tests. */
41
+ generate?: (token: string, params: TGenerateStepDescriptionsParams) => Promise<TGenerateStepDescriptionsResult | null>;
42
+ };
43
+ export type GenerateCourseDescriptionsResult = {
44
+ /** `skipped` means there was nothing to do, not that something went wrong. */
45
+ status: "skipped" | "processed" | "failed";
46
+ steps: number;
47
+ /** (step, language) pairs written. */
48
+ generated: number;
49
+ /** Steps whose completion failed outright. */
50
+ failed: number;
51
+ /** (step, language) pairs the model never answered; retried later. */
52
+ missing: number;
53
+ errors: string[];
54
+ /** Completion durations, to watch how close we run to the request limits. */
55
+ durationsSeconds: number[];
56
+ };
57
+ /** Read every README of the course, keyed by exercise slug and language. */
58
+ export declare function readCourseReadmes(storage: CourseDescriptionsStorage, courseSlug: string, exercises: CourseExercise[], concurrency: number): Promise<CourseReadmes>;
59
+ export declare function generateCourseDescriptions(storage: CourseDescriptionsStorage, courseSlug: string, options: GenerateCourseDescriptionsOptions): Promise<GenerateCourseDescriptionsResult>;
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_DESCRIPTIONS_CONCURRENCY = void 0;
4
+ exports.readCourseReadmes = readCourseReadmes;
5
+ exports.generateCourseDescriptions = generateCourseDescriptions;
6
+ const rigoActions_1 = require("../rigoActions");
7
+ const packageManifestBackfill_1 = require("../s3/packageManifestBackfill");
8
+ const syllabusSync_1 = require("../syllabusSync");
9
+ const workList_1 = require("./workList");
10
+ /**
11
+ * Generate and persist the step descriptions of one published course.
12
+ *
13
+ * This is the single implementation behind every flow that produces
14
+ * descriptions: the background stage that runs after publishing, the sweep that
15
+ * recovers interrupted publications, and the one-off backfill over the
16
+ * historical catalogue. They differ only in what triggers them.
17
+ *
18
+ * Storage is a port so the service can be driven against the published bucket
19
+ * (the real case) or an in-memory double (tests), and so the manifest
20
+ * re-projection stays where the storage-specific knowledge lives.
21
+ */
22
+ exports.DEFAULT_DESCRIPTIONS_CONCURRENCY = 5;
23
+ function emptyResult(status) {
24
+ return {
25
+ status,
26
+ steps: 0,
27
+ generated: 0,
28
+ failed: 0,
29
+ missing: 0,
30
+ errors: [],
31
+ durationsSeconds: [],
32
+ };
33
+ }
34
+ /** Read every README of the course, keyed by exercise slug and language. */
35
+ async function readCourseReadmes(storage, courseSlug, exercises, concurrency) {
36
+ const readmes = {};
37
+ const targets = [];
38
+ for (const exercise of exercises) {
39
+ for (const [lang, file] of Object.entries(exercise.translations || {})) {
40
+ targets.push({ exerciseSlug: exercise.slug, lang, file });
41
+ }
42
+ }
43
+ const contents = await (0, packageManifestBackfill_1.mapWithConcurrency)(targets, concurrency, target => storage.readReadme(courseSlug, target.exerciseSlug, target.file));
44
+ for (const [index, target] of targets.entries()) {
45
+ const content = contents[index];
46
+ if (typeof content !== "string") {
47
+ continue;
48
+ }
49
+ if (!readmes[target.exerciseSlug]) {
50
+ readmes[target.exerciseSlug] = {};
51
+ }
52
+ readmes[target.exerciseSlug][target.lang] = content;
53
+ }
54
+ return readmes;
55
+ }
56
+ /** Ensure the translation slot exists before writing a description into it. */
57
+ function ensureSlot(lesson, lang) {
58
+ if (!lesson.translations) {
59
+ lesson.translations = {};
60
+ }
61
+ if (!lesson.translations[lang]) {
62
+ lesson.translations[lang] = { completionId: 0, startedAt: Date.now() };
63
+ }
64
+ return lesson.translations[lang];
65
+ }
66
+ function applyStepResult(step, result) {
67
+ let generated = 0;
68
+ for (const language of step.languages) {
69
+ if (!(language.lang in result.descriptionsByLanguage)) {
70
+ // Never answered: leave the slot untouched so a later run retries it.
71
+ // Writing a null plus a fingerprint here would look settled forever.
72
+ continue;
73
+ }
74
+ const slot = ensureSlot(step.lesson, language.lang);
75
+ if (slot.descriptionSource === "human") {
76
+ continue;
77
+ }
78
+ const description = result.descriptionsByLanguage[language.lang];
79
+ slot.description = description;
80
+ slot.descriptionStatus = description ? "generated" : "none";
81
+ slot.descriptionSource = "auto";
82
+ slot.descriptionPromptVersion = result.promptVersion;
83
+ slot.sourceContentHash = language.fingerprint.sha256;
84
+ slot.sourceSimHash = language.fingerprint.simhash;
85
+ generated += 1;
86
+ }
87
+ return { generated, missing: result.missingLanguages.length };
88
+ }
89
+ async function generateCourseDescriptions(storage, courseSlug, options) {
90
+ var _a, _b;
91
+ const concurrency = (_a = options.concurrency) !== null && _a !== void 0 ? _a : exports.DEFAULT_DESCRIPTIONS_CONCURRENCY;
92
+ const generate = (_b = options.generate) !== null && _b !== void 0 ? _b : rigoActions_1.generateStepDescriptions;
93
+ const syllabus = await storage.readSyllabus(courseSlug);
94
+ if (!syllabus) {
95
+ return emptyResult("skipped");
96
+ }
97
+ const exercises = await storage.listExercises(courseSlug);
98
+ if (exercises.length === 0) {
99
+ return emptyResult("skipped");
100
+ }
101
+ if (options.reconcile && storage.syllabusSyncStorage) {
102
+ try {
103
+ await (0, syllabusSync_1.synchronizeSyllabusWithBucket)(storage.syllabusSyncStorage, courseSlug, syllabus, { prune: false });
104
+ }
105
+ catch (error) {
106
+ // Provisional patch: never let reconciliation block generation.
107
+ console.error(`[descriptions] Reconciliation failed for "${courseSlug}":`, error.message);
108
+ }
109
+ }
110
+ const readmes = await readCourseReadmes(storage, courseSlug, exercises, concurrency);
111
+ const work = (0, workList_1.buildDescriptionWorkList)({
112
+ syllabus,
113
+ exercises,
114
+ readmes,
115
+ promptVersion: options.promptVersion,
116
+ force: options.force,
117
+ });
118
+ if (work.length === 0) {
119
+ return emptyResult("skipped");
120
+ }
121
+ const result = emptyResult("processed");
122
+ result.steps = work.length;
123
+ if (options.dryRun) {
124
+ result.generated = (0, workList_1.countWorkItems)(work);
125
+ return result;
126
+ }
127
+ // One completion per step, several in flight: a slow step never stalls the
128
+ // course and Rigobot is never hit with an unbounded burst.
129
+ const outcomes = await (0, packageManifestBackfill_1.mapWithConcurrency)(work, concurrency, async (step) => {
130
+ var _a;
131
+ return ({
132
+ step,
133
+ generated: await generate(options.token, {
134
+ readmeContent: step.baseContent,
135
+ sourceLanguage: step.baseLanguage,
136
+ outputLanguages: step.languages.map(language => language.lang),
137
+ lessonTitle: step.lesson.title,
138
+ courseTitle: (_a = syllabus.courseInfo) === null || _a === void 0 ? void 0 : _a.title,
139
+ targetWordCount: options.targetWordCount,
140
+ promptVersion: options.promptVersion,
141
+ }),
142
+ });
143
+ });
144
+ for (const outcome of outcomes) {
145
+ if (!outcome.generated) {
146
+ result.failed += 1;
147
+ result.errors.push(`generation failed for "${outcome.step.exerciseSlug}"`);
148
+ continue;
149
+ }
150
+ if (outcome.generated.durationSeconds !== null) {
151
+ result.durationsSeconds.push(outcome.generated.durationSeconds);
152
+ }
153
+ const applied = applyStepResult(outcome.step, outcome.generated);
154
+ result.generated += applied.generated;
155
+ result.missing += applied.missing;
156
+ }
157
+ if (result.generated === 0 && result.failed > 0) {
158
+ result.status = "failed";
159
+ return result;
160
+ }
161
+ await storage.writeSyllabus(courseSlug, syllabus);
162
+ if (storage.reprojectManifest) {
163
+ try {
164
+ await storage.reprojectManifest(courseSlug);
165
+ }
166
+ catch (error) {
167
+ // The syllabus (the source of truth) is already saved; a failed
168
+ // projection is recoverable and must not lose the generated text.
169
+ result.errors.push(`manifest projection failed: ${error.message}`);
170
+ }
171
+ }
172
+ return result;
173
+ }
@@ -0,0 +1,49 @@
1
+ import { Syllabus } from "../../models/creator";
2
+ import { CourseDescriptionsStorage } from "./generateCourseDescriptions";
3
+ import { CourseExercise, CourseReadmes } from "./workList";
4
+ /**
5
+ * Copy descriptions from the published syllabus back into the draft one.
6
+ *
7
+ * Descriptions are generated against the published package because it is an
8
+ * immutable snapshot. The draft in GCS, meanwhile, keeps moving: between the
9
+ * publication and the moment the descriptions land, steps may have been added,
10
+ * removed, renamed, reordered or edited.
11
+ *
12
+ * The guard against that drift is the content hash, not a timestamp or a
13
+ * position: a description is copied only when the draft README hashes to the
14
+ * very content the description was generated from. Anything that moved simply
15
+ * misses and waits for the next publication — a stale description is never
16
+ * written onto content it does not describe.
17
+ *
18
+ * Without this mirror the draft would never hold a description, and every
19
+ * publication would regenerate the whole course from scratch: the freshness
20
+ * check reads the fingerprints stored in the syllabus that travels inside the
21
+ * zip.
22
+ */
23
+ export type MirrorResult = {
24
+ /** (step, language) pairs copied into the draft. */
25
+ copied: number;
26
+ /** Pairs with no reusable description: content moved on, or none published. */
27
+ missed: number;
28
+ /** Pairs the draft already had up to date. */
29
+ fresh: number;
30
+ };
31
+ export type MirrorOptions = {
32
+ promptVersion?: number;
33
+ dryRun?: boolean;
34
+ concurrency?: number;
35
+ };
36
+ /**
37
+ * Pure core: writes into `targetSyllabus` in place. Callers own the I/O, which
38
+ * keeps the drift rules testable without a bucket.
39
+ */
40
+ export declare function mirrorDescriptionsIntoSyllabus(input: {
41
+ sourceSyllabus: Syllabus | null;
42
+ targetSyllabus: Syllabus;
43
+ exercises: CourseExercise[];
44
+ /** READMEs of the TARGET (draft), which is what must match the hash. */
45
+ readmes: CourseReadmes;
46
+ promptVersion?: number;
47
+ }): MirrorResult;
48
+ /** Read the draft, mirror into it, and save it when something changed. */
49
+ export declare function mirrorDescriptionsToDraft(storage: CourseDescriptionsStorage, courseSlug: string, sourceSyllabus: Syllabus | null, options?: MirrorOptions): Promise<MirrorResult>;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mirrorDescriptionsIntoSyllabus = mirrorDescriptionsIntoSyllabus;
4
+ exports.mirrorDescriptionsToDraft = mirrorDescriptionsToDraft;
5
+ const descriptionHash_1 = require("../descriptionHash");
6
+ const packageManifest_1 = require("../packageManifest");
7
+ const generateCourseDescriptions_1 = require("./generateCourseDescriptions");
8
+ const workList_1 = require("./workList");
9
+ function emptyMirrorResult() {
10
+ return { copied: 0, missed: 0, fresh: 0 };
11
+ }
12
+ /**
13
+ * Pure core: writes into `targetSyllabus` in place. Callers own the I/O, which
14
+ * keeps the drift rules testable without a bucket.
15
+ */
16
+ function mirrorDescriptionsIntoSyllabus(input) {
17
+ var _a, _b, _c, _d, _e, _f, _g;
18
+ const result = emptyMirrorResult();
19
+ if (!input.sourceSyllabus) {
20
+ return result;
21
+ }
22
+ const promptVersion = (_a = input.promptVersion) !== null && _a !== void 0 ? _a : packageManifest_1.DESCRIPTION_PROMPT_VERSION;
23
+ for (const exercise of input.exercises) {
24
+ const targetLesson = (0, workList_1.findSyllabusLesson)(input.targetSyllabus, exercise.slug);
25
+ if (!targetLesson) {
26
+ // The step is not in the draft (removed, or renamed after publishing).
27
+ continue;
28
+ }
29
+ const sourceLesson = (0, workList_1.findSyllabusLesson)(input.sourceSyllabus, exercise.slug);
30
+ const exerciseReadmes = input.readmes[exercise.slug] || {};
31
+ for (const lang of Object.keys(exercise.translations || {})) {
32
+ const content = exerciseReadmes[lang];
33
+ if (typeof content !== "string") {
34
+ continue;
35
+ }
36
+ const fingerprint = (0, descriptionHash_1.fingerprintReadme)(content);
37
+ const targetSlot = (_b = targetLesson.translations) === null || _b === void 0 ? void 0 : _b[lang];
38
+ if ((targetSlot === null || targetSlot === void 0 ? void 0 : targetSlot.descriptionSource) === "human") {
39
+ result.fresh += 1;
40
+ continue;
41
+ }
42
+ if ((targetSlot === null || targetSlot === void 0 ? void 0 : targetSlot.sourceContentHash) === fingerprint.sha256 &&
43
+ ((_c = targetSlot === null || targetSlot === void 0 ? void 0 : targetSlot.descriptionPromptVersion) !== null && _c !== void 0 ? _c : 0) >= promptVersion) {
44
+ result.fresh += 1;
45
+ continue;
46
+ }
47
+ const sourceSlot = (_d = sourceLesson === null || sourceLesson === void 0 ? void 0 : sourceLesson.translations) === null || _d === void 0 ? void 0 : _d[lang];
48
+ // The hash guard: only reuse a description generated from exactly this
49
+ // content. Note a published `description: null` is reusable too — it is a
50
+ // real answer ("not enough content to describe"), not a missing one.
51
+ const reusable = sourceSlot !== undefined &&
52
+ sourceSlot.sourceContentHash === fingerprint.sha256 &&
53
+ ((_e = sourceSlot.descriptionPromptVersion) !== null && _e !== void 0 ? _e : 0) >= promptVersion &&
54
+ sourceSlot.descriptionStatus !== "error";
55
+ if (!reusable) {
56
+ result.missed += 1;
57
+ continue;
58
+ }
59
+ if (!targetLesson.translations) {
60
+ targetLesson.translations = {};
61
+ }
62
+ if (!targetLesson.translations[lang]) {
63
+ targetLesson.translations[lang] = {
64
+ completionId: 0,
65
+ startedAt: Date.now(),
66
+ };
67
+ }
68
+ const destination = targetLesson.translations[lang];
69
+ destination.description = (_f = sourceSlot.description) !== null && _f !== void 0 ? _f : null;
70
+ destination.descriptionStatus = sourceSlot.description ?
71
+ "generated" :
72
+ "none";
73
+ destination.descriptionSource = "auto";
74
+ destination.descriptionPromptVersion =
75
+ (_g = sourceSlot.descriptionPromptVersion) !== null && _g !== void 0 ? _g : promptVersion;
76
+ destination.sourceContentHash = fingerprint.sha256;
77
+ destination.sourceSimHash = fingerprint.simhash;
78
+ result.copied += 1;
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ /** Read the draft, mirror into it, and save it when something changed. */
84
+ async function mirrorDescriptionsToDraft(storage, courseSlug, sourceSyllabus, options = {}) {
85
+ var _a;
86
+ if (!sourceSyllabus) {
87
+ return emptyMirrorResult();
88
+ }
89
+ const targetSyllabus = await storage.readSyllabus(courseSlug);
90
+ if (!targetSyllabus) {
91
+ return emptyMirrorResult();
92
+ }
93
+ const exercises = await storage.listExercises(courseSlug);
94
+ if (exercises.length === 0) {
95
+ return emptyMirrorResult();
96
+ }
97
+ const readmes = await (0, generateCourseDescriptions_1.readCourseReadmes)(storage, courseSlug, exercises, (_a = options.concurrency) !== null && _a !== void 0 ? _a : generateCourseDescriptions_1.DEFAULT_DESCRIPTIONS_CONCURRENCY);
98
+ const result = mirrorDescriptionsIntoSyllabus({
99
+ sourceSyllabus,
100
+ targetSyllabus,
101
+ exercises,
102
+ readmes,
103
+ promptVersion: options.promptVersion,
104
+ });
105
+ if (result.copied > 0 && !options.dryRun) {
106
+ await storage.writeSyllabus(courseSlug, targetSyllabus);
107
+ }
108
+ return result;
109
+ }
@@ -0,0 +1,69 @@
1
+ import { Bucket } from "@google-cloud/storage";
2
+ import { JournalStorage, PublishJournal } from "../publishJournal";
3
+ import { CourseDescriptionsStorage, GenerateCourseDescriptionsOptions, GenerateCourseDescriptionsResult } from "./generateCourseDescriptions";
4
+ import { MirrorResult } from "./mirrorDescriptions";
5
+ import { PublishEventContext, sendPublishEvent } from "../publishEvents";
6
+ import { CourseExercise } from "./workList";
7
+ /**
8
+ * Stage B of a publication: generate the descriptions the new content needs.
9
+ *
10
+ * It runs in the background, after the HTTP response, because a single
11
+ * completion takes ~15s and a course may need several — far past any request
12
+ * limit. The publication itself is already finished and durable by then; this
13
+ * stage only enriches the published package.
14
+ */
15
+ /**
16
+ * Whether the freshly built package needs any description, decided from the
17
+ * build directory that is still on disk after zipping.
18
+ *
19
+ * Reading from disk (instead of re-reading GCS or S3) is free and, at this
20
+ * exact moment, all three hold the same content: the answer is what the publish
21
+ * response promises to breathecode, so it must be cheap enough to compute
22
+ * before responding.
23
+ */
24
+ /** Reads a path relative to the build root; null when it does not exist. */
25
+ export type BuildFileReader = (relativePath: string) => string | null;
26
+ export declare function createBuildFileReader(buildRoot: string): BuildFileReader;
27
+ export declare function countPendingDescriptions(buildRoot: string, exercises: CourseExercise[], readFile?: BuildFileReader): number;
28
+ export type PublishDescriptionsStageResult = {
29
+ generation: GenerateCourseDescriptionsResult | null;
30
+ mirror: MirrorResult | null;
31
+ /** False when generation failed outright; drives the event status. */
32
+ succeeded: boolean;
33
+ };
34
+ /**
35
+ * Generate against the published package, then mirror what still matches back
36
+ * into the draft. Stages are recorded as they go so an interrupted run can be
37
+ * resumed by the sweep.
38
+ */
39
+ export declare function runPublishDescriptionsStage(params: {
40
+ courseSlug: string;
41
+ rigobotToken: string;
42
+ gcsBucket?: Bucket;
43
+ journal: PublishJournal;
44
+ journalStorage: JournalStorage;
45
+ /** Injection points for tests; production resolves them from env/bucket. */
46
+ publishedStorage?: CourseDescriptionsStorage;
47
+ draftStorage?: CourseDescriptionsStorage;
48
+ generate?: GenerateCourseDescriptionsOptions["generate"];
49
+ }): Promise<PublishDescriptionsStageResult>;
50
+ /**
51
+ * Entry point for the detached run after publishing. Never throws: it is not
52
+ * awaited by anyone, so an escaping rejection would only surface as an
53
+ * unhandled promise.
54
+ */
55
+ export declare function runDescriptionsStageAfterPublish(params: {
56
+ courseSlug: string;
57
+ gcsBucket?: Bucket;
58
+ journal: PublishJournal;
59
+ journalStorage: JournalStorage;
60
+ hasWork: boolean;
61
+ /** Present when the caller announced `descriptions: "queued"`. */
62
+ eventContext?: PublishEventContext;
63
+ breathecodeToken?: string;
64
+ publishedStorage?: CourseDescriptionsStorage;
65
+ draftStorage?: CourseDescriptionsStorage;
66
+ generate?: GenerateCourseDescriptionsOptions["generate"];
67
+ /** Injection point for tests. */
68
+ emit?: typeof sendPublishEvent;
69
+ }): Promise<PublishDescriptionsStageResult>;