@learnpack/learnpack 5.0.362 → 5.0.364

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 (55) hide show
  1. package/lib/commands/serve.js +444 -279
  2. package/lib/creatorDist/assets/{index-B7s5Nmq8.css → index-Cco3XhDR.css} +96 -0
  3. package/lib/creatorDist/assets/{index-CKg5uuoj.js → index-o3Cw0bsF.js} +69620 -66788
  4. package/lib/creatorDist/index.html +2 -2
  5. package/lib/models/creator.d.ts +1 -0
  6. package/lib/utils/api.d.ts +0 -9
  7. package/lib/utils/api.js +1 -65
  8. package/lib/utils/generationRetry.d.ts +48 -0
  9. package/lib/utils/generationRetry.js +81 -0
  10. package/lib/utils/interactivity/componentsGuide.d.ts +50 -0
  11. package/lib/utils/interactivity/componentsGuide.js +193 -0
  12. package/lib/utils/interactivity/interactivityPlan.d.ts +20 -0
  13. package/lib/utils/interactivity/interactivityPlan.js +33 -0
  14. package/lib/utils/interactivity/structuredComponents.d.ts +34 -0
  15. package/lib/utils/interactivity/structuredComponents.js +792 -0
  16. package/lib/utils/rigoActions.d.ts +1 -1
  17. package/lib/utils/rigoActions.js +2 -2
  18. package/package.json +1 -1
  19. package/src/commands/serve.ts +653 -397
  20. package/src/creator/package-lock.json +297 -3
  21. package/src/creator/package.json +1 -0
  22. package/src/creator/src/App.test.tsx +124 -1
  23. package/src/creator/src/App.tsx +52 -36
  24. package/src/creator/src/components/FileCard.tsx +63 -19
  25. package/src/creator/src/components/FileReadListener.test.tsx +128 -0
  26. package/src/creator/src/components/FileReadListener.tsx +75 -0
  27. package/src/creator/src/components/FileReviewModal.test.tsx +193 -0
  28. package/src/creator/src/components/FileReviewModal.tsx +128 -0
  29. package/src/creator/src/components/FileUploader.test.tsx +229 -0
  30. package/src/creator/src/components/FileUploader.tsx +212 -58
  31. package/src/creator/src/components/GenerationErrorCard.tsx +57 -0
  32. package/src/creator/src/components/MarkdownRenderer.tsx +4 -2
  33. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -9
  34. package/src/creator/src/index.css +45 -0
  35. package/src/creator/src/locales/en.json +27 -5
  36. package/src/creator/src/locales/es.json +27 -5
  37. package/src/creator/src/main.tsx +2 -0
  38. package/src/creator/src/utils/completionResult.test.ts +61 -0
  39. package/src/creator/src/utils/completionResult.ts +61 -2
  40. package/src/creator/src/utils/courseLanguage.test.ts +6 -0
  41. package/src/creator/src/utils/creationRequest.test.ts +53 -18
  42. package/src/creator/src/utils/creationRequest.ts +17 -12
  43. package/src/creator/src/utils/fileRead.ts +60 -0
  44. package/src/creator/src/utils/lib.ts +0 -5
  45. package/src/creator/src/utils/store.ts +0 -10
  46. package/src/creatorDist/assets/{index-B7s5Nmq8.css → index-Cco3XhDR.css} +96 -0
  47. package/src/creatorDist/assets/{index-CKg5uuoj.js → index-o3Cw0bsF.js} +69620 -66788
  48. package/src/creatorDist/index.html +2 -2
  49. package/src/models/creator.ts +87 -84
  50. package/src/utils/api.ts +0 -83
  51. package/src/utils/generationRetry.ts +100 -0
  52. package/src/utils/interactivity/componentsGuide.ts +294 -0
  53. package/src/utils/interactivity/interactivityPlan.ts +57 -0
  54. package/src/utils/interactivity/structuredComponents.ts +1004 -0
  55. package/src/utils/rigoActions.ts +3 -2
@@ -10,8 +10,8 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-CKg5uuoj.js"></script>
14
- <link rel="stylesheet" crossorigin href="/creator/assets/index-B7s5Nmq8.css">
13
+ <script type="module" crossorigin src="/creator/assets/index-o3Cw0bsF.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/creator/assets/index-Cco3XhDR.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -36,6 +36,7 @@ export interface Lesson {
36
36
  startedAt: number;
37
37
  completedAt?: number;
38
38
  error?: boolean;
39
+ autoRetries?: number;
39
40
  description?: string | null;
40
41
  descriptionStatus?: "generated" | "pending" | "error" | "none";
41
42
  sourceContentHash?: string;
@@ -110,13 +110,6 @@ export declare const getAssetById: (token: string, assetId: number) => Promise<A
110
110
  * @returns `found` with the asset, `missing` on 404, `error` when it could not be checked.
111
111
  */
112
112
  export declare const getAssetBySlug: (token: string, assetSlug: string) => Promise<AssetLookup>;
113
- type TTechnology = {
114
- slug: string;
115
- lang: string;
116
- };
117
- export declare const fetchTechnologies: () => Promise<any[]>;
118
- declare function updateTechnologiesPeriodically(): Promise<void>;
119
- export declare const getCurrentTechnologies: () => TTechnology[];
120
113
  declare const _default: {
121
114
  login: (identification: string, password: string) => Promise<any>;
122
115
  publish: (config: any) => Promise<any>;
@@ -144,7 +137,5 @@ declare const _default: {
144
137
  asset_ids?: number[];
145
138
  }) => Promise<any>;
146
139
  createRigoPackage: (token: string, slug: string, config: any) => Promise<any>;
147
- getCurrentTechnologies: () => TTechnology[];
148
- updateTechnologiesPeriodically: typeof updateTechnologiesPeriodically;
149
140
  };
150
141
  export default _default;
package/lib/utils/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getCurrentTechnologies = exports.fetchTechnologies = exports.getAssetBySlug = exports.getAssetById = exports.createAsset = exports.validateToken = exports.listUserAcademies = exports.getConsumable = exports.countConsumables = exports.RIGOBOT_REALTIME_HOST = exports.RIGOBOT_HOST = exports.BREATHECODE_EVENTS_URL = exports.BREATHECODE_TELEMETRY_URL = exports.BREATHECODE_HOST = void 0;
3
+ exports.getAssetBySlug = exports.getAssetById = exports.createAsset = exports.validateToken = exports.listUserAcademies = exports.getConsumable = exports.countConsumables = exports.RIGOBOT_REALTIME_HOST = exports.RIGOBOT_HOST = exports.BREATHECODE_EVENTS_URL = exports.BREATHECODE_TELEMETRY_URL = exports.BREATHECODE_HOST = void 0;
4
4
  exports.describeRequestError = describeRequestError;
5
5
  exports.getRigoPackage = getRigoPackage;
6
6
  const console_1 = require("../utils/console");
@@ -592,68 +592,6 @@ const createRigoPackage = async (token, slug, config) => {
592
592
  throw error;
593
593
  }
594
594
  };
595
- let technologiesCache = [];
596
- /**
597
- * Strip .env comments (e.g. "token # comment") so the token is sent without spaces.
598
- * @param token - Raw token value from env, possibly with trailing comment.
599
- * @returns Trimmed token with inline `#` comments removed, or empty string if missing.
600
- */
601
- function sanitizeToken(token) {
602
- if (!token)
603
- return "";
604
- const trimmed = token.trim();
605
- const beforeComment = trimmed.split(/\s+#/)[0].trim();
606
- return beforeComment;
607
- }
608
- const fetchTechnologies = async () => {
609
- const rawToken = process.env.BREATHECODE_PERMANENT_TOKEN;
610
- const BREATHECODE_PERMANENT_TOKEN = sanitizeToken(rawToken);
611
- const LANGS = ["en", "es"];
612
- if (!BREATHECODE_PERMANENT_TOKEN) {
613
- throw new Error("BREATHECODE_PERMANENT_TOKEN is not defined in environment variables");
614
- }
615
- const headers = {
616
- Authorization: `Token ${BREATHECODE_PERMANENT_TOKEN}`,
617
- };
618
- const results = await Promise.all(LANGS.map(lang => axios_1.default
619
- .get(`${HOST}/v1/registry/technology?lang=${lang}`, { headers })
620
- .then(res => {
621
- return res.data;
622
- })
623
- .then(data => data.map((item) => ({
624
- slug: item.slug,
625
- lang: lang,
626
- })))));
627
- const allItems = results.flat();
628
- // Remove duplicates by slug+lang combination
629
- const unique = [];
630
- const seen = new Set();
631
- for (const item of allItems) {
632
- const key = `${item.slug}:${item.lang}`;
633
- if (!seen.has(key)) {
634
- seen.add(key);
635
- unique.push(item);
636
- }
637
- }
638
- return unique;
639
- };
640
- exports.fetchTechnologies = fetchTechnologies;
641
- // Function to update the cache and schedule the next update
642
- async function updateTechnologiesPeriodically() {
643
- try {
644
- technologiesCache = await (0, exports.fetchTechnologies)();
645
- // Uncomment for debugging:
646
- // console.log('Technologies list updated:', technologiesCache);
647
- }
648
- catch (error) {
649
- console.error("Error updating technologies list:", error);
650
- }
651
- finally {
652
- setTimeout(updateTechnologiesPeriodically, 24 * 60 * 60 * 1000);
653
- }
654
- }
655
- const getCurrentTechnologies = () => technologiesCache;
656
- exports.getCurrentTechnologies = getCurrentTechnologies;
657
595
  exports.default = {
658
596
  login,
659
597
  publish,
@@ -673,6 +611,4 @@ exports.default = {
673
611
  getCategories,
674
612
  updateRigoPackage,
675
613
  createRigoPackage,
676
- getCurrentTechnologies: exports.getCurrentTechnologies,
677
- updateTechnologiesPeriodically,
678
614
  };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Automatic retries for the two lesson generation phases. Without a cap, an
3
+ * error that always repeats, like a broken prompt template, starts jobs
4
+ * forever: Rigobot fails in under a second and the webhook starts another one.
5
+ */
6
+ /** Automatic retries allowed per phase, after the first job. */
7
+ export declare const MAX_AUTO_RETRIES = 2;
8
+ /**
9
+ * Whether a failed job would fail the same way if started again.
10
+ * @param statusText - The `status_text` Rigobot sends with the error.
11
+ * @returns True when retrying cannot help.
12
+ */
13
+ export declare const isPermanentGenerationError: (statusText: unknown) => boolean;
14
+ type LessonState = {
15
+ status?: string;
16
+ translations?: {
17
+ [language: string]: {
18
+ completionId?: number;
19
+ autoRetries?: number;
20
+ };
21
+ };
22
+ };
23
+ /**
24
+ * Whether a webhook belongs to a job the lesson no longer waits for. That
25
+ * happens when Rigobot sends a second webhook for a finished job, or when an
26
+ * old job answers after a newer one started.
27
+ * @param lesson - The lesson as saved in the syllabus.
28
+ * @param language - The course language.
29
+ * @param jobId - The id of the job the webhook reports.
30
+ * @returns True when the webhook must not change the lesson.
31
+ */
32
+ export declare const isStaleWebhook: (lesson: LessonState, language: string, jobId: unknown) => boolean;
33
+ export type RetryPlan = {
34
+ retry: true;
35
+ attempt: number;
36
+ } | {
37
+ retry: false;
38
+ reason: "permanent" | "exhausted";
39
+ };
40
+ /**
41
+ * Decides whether a failed job starts another one.
42
+ * @param lesson - The lesson as saved in the syllabus.
43
+ * @param language - The course language.
44
+ * @param statusText - The `status_text` Rigobot sends with the error.
45
+ * @returns The retry to start, numbered from 1, or why there is none.
46
+ */
47
+ export declare const planGenerationRetry: (lesson: LessonState, language: string, statusText: unknown) => RetryPlan;
48
+ export {};
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * Automatic retries for the two lesson generation phases. Without a cap, an
4
+ * error that always repeats, like a broken prompt template, starts jobs
5
+ * forever: Rigobot fails in under a second and the webhook starts another one.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.planGenerationRetry = exports.isStaleWebhook = exports.isPermanentGenerationError = exports.MAX_AUTO_RETRIES = void 0;
9
+ /** Automatic retries allowed per phase, after the first job. */
10
+ exports.MAX_AUTO_RETRIES = 2;
11
+ /**
12
+ * Errors from Rigobot that repeat on every attempt, so retrying only spends
13
+ * jobs. They come from the template, the inputs or the model configuration.
14
+ */
15
+ const PERMANENT_ERRORS = [
16
+ // A template placeholder without an input.
17
+ /^Missing key /,
18
+ // Braces in a template body that Python's str.format cannot parse.
19
+ /format string|in field name|out of range for positional args/,
20
+ // The purpose's engine cannot return the template's fields.
21
+ /doesn't support structured outputs/,
22
+ // The model API rejected the request itself. 429 and 5xx are not here,
23
+ // because they can pass.
24
+ /^Error code: (400|401|403|404|422)\b/,
25
+ ];
26
+ /**
27
+ * Whether a failed job would fail the same way if started again.
28
+ * @param statusText - The `status_text` Rigobot sends with the error.
29
+ * @returns True when retrying cannot help.
30
+ */
31
+ const isPermanentGenerationError = (statusText) => {
32
+ if (typeof statusText !== "string")
33
+ return false;
34
+ const text = statusText.trim();
35
+ // A Python KeyError prints as its quoted key: a placeholder in the template
36
+ // that matches no input.
37
+ if (/^["']/.test(text) || text.startsWith("{'"))
38
+ return true;
39
+ return PERMANENT_ERRORS.some(pattern => pattern.test(text));
40
+ };
41
+ exports.isPermanentGenerationError = isPermanentGenerationError;
42
+ /**
43
+ * Whether a webhook belongs to a job the lesson no longer waits for. That
44
+ * happens when Rigobot sends a second webhook for a finished job, or when an
45
+ * old job answers after a newer one started.
46
+ * @param lesson - The lesson as saved in the syllabus.
47
+ * @param language - The course language.
48
+ * @param jobId - The id of the job the webhook reports.
49
+ * @returns True when the webhook must not change the lesson.
50
+ */
51
+ const isStaleWebhook = (lesson, language, jobId) => {
52
+ var _a, _b;
53
+ if (lesson.status === "DONE")
54
+ return true;
55
+ const current = (_b = (_a = lesson.translations) === null || _a === void 0 ? void 0 : _a[language]) === null || _b === void 0 ? void 0 : _b.completionId;
56
+ // Job ids grow, so a lower id is an older job. Without both ids, nothing
57
+ // tells the webhook apart, and it is handled.
58
+ return (typeof jobId === "number" &&
59
+ typeof current === "number" &&
60
+ current > 0 &&
61
+ jobId < current);
62
+ };
63
+ exports.isStaleWebhook = isStaleWebhook;
64
+ /**
65
+ * Decides whether a failed job starts another one.
66
+ * @param lesson - The lesson as saved in the syllabus.
67
+ * @param language - The course language.
68
+ * @param statusText - The `status_text` Rigobot sends with the error.
69
+ * @returns The retry to start, numbered from 1, or why there is none.
70
+ */
71
+ const planGenerationRetry = (lesson, language, statusText) => {
72
+ var _a, _b, _c;
73
+ if ((0, exports.isPermanentGenerationError)(statusText)) {
74
+ return { retry: false, reason: "permanent" };
75
+ }
76
+ const used = (_c = (_b = (_a = lesson.translations) === null || _a === void 0 ? void 0 : _a[language]) === null || _b === void 0 ? void 0 : _b.autoRetries) !== null && _c !== void 0 ? _c : 0;
77
+ if (used >= exports.MAX_AUTO_RETRIES)
78
+ return { retry: false, reason: "exhausted" };
79
+ return { retry: true, attempt: used + 1 };
80
+ };
81
+ exports.planGenerationRetry = planGenerationRetry;
@@ -0,0 +1,50 @@
1
+ import { DecidedBy } from "./structuredComponents";
2
+ export declare const COMPONENTS_V2_URL = "https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/components_v2.yml";
3
+ export interface GuideComponent {
4
+ name: string;
5
+ category: string;
6
+ decidedBy: DecidedBy;
7
+ choose: {
8
+ summary: string;
9
+ goodFor?: string[];
10
+ avoid?: string[];
11
+ tests?: string[];
12
+ };
13
+ write: {
14
+ fields: Record<string, string>;
15
+ rules?: string[];
16
+ };
17
+ visualStyle?: string;
18
+ }
19
+ export interface ComponentsGuide {
20
+ version: string;
21
+ components: GuideComponent[];
22
+ }
23
+ export declare class ComponentsGuideUnavailableError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ export declare const parseComponentsGuide: (text: string) => {
27
+ guide: ComponentsGuide;
28
+ warnings: string[];
29
+ };
30
+ export declare const buildInteractivityGuide: (guide: ComponentsGuide) => string;
31
+ export declare const decidedByFromGuide: (guide: ComponentsGuide) => Record<string, DecidedBy>;
32
+ type Fetcher = (url: string, timeoutMs: number) => Promise<string>;
33
+ export declare const resolveComponentsGuideUrl: (env?: Record<string, string | undefined>) => string;
34
+ export interface GuideLoaderOptions {
35
+ url?: string;
36
+ fetcher?: Fetcher;
37
+ timeoutMs?: number;
38
+ retryDelaysMs?: number[];
39
+ maxAgeMs?: number;
40
+ now?: () => number;
41
+ sleep?: (ms: number) => Promise<void>;
42
+ log?: (message: string) => void;
43
+ }
44
+ export interface GuideLoader {
45
+ load(): Promise<ComponentsGuide>;
46
+ peek(): ComponentsGuide | null;
47
+ }
48
+ export declare const createGuideLoader: (options?: GuideLoaderOptions) => GuideLoader;
49
+ export declare const componentsGuideLoader: GuideLoader;
50
+ export {};
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ // Loads docs/components_v2.yml from learnpack/ide and turns it into the
3
+ // COMPONENTS input of the structured interactivity template.
4
+ //
5
+ // The file is the single source of truth for component names and for the
6
+ // guidance the model receives. Without it the model does not know what each
7
+ // field means, so a generation must not start when no valid copy is available.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.componentsGuideLoader = exports.createGuideLoader = exports.resolveComponentsGuideUrl = exports.decidedByFromGuide = exports.buildInteractivityGuide = exports.parseComponentsGuide = exports.ComponentsGuideUnavailableError = exports.COMPONENTS_V2_URL = void 0;
10
+ const axios_1 = require("axios");
11
+ const yaml = require("js-yaml");
12
+ const structuredComponents_1 = require("./structuredComponents");
13
+ exports.COMPONENTS_V2_URL = "https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/components_v2.yml";
14
+ class ComponentsGuideUnavailableError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "ComponentsGuideUnavailableError";
18
+ Object.setPrototypeOf(this, ComponentsGuideUnavailableError.prototype);
19
+ }
20
+ }
21
+ exports.ComponentsGuideUnavailableError = ComponentsGuideUnavailableError;
22
+ const isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
23
+ const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
24
+ // Parses and checks the guide. It is lenient on purpose: the IDE repository
25
+ // can add or rename a component before this CLI knows it, and that must not
26
+ // stop every generation. Unknown or incomplete components are skipped with a
27
+ // warning; the guide is unavailable only when it does not parse or has no
28
+ // usable component.
29
+ const parseComponentsGuide = (text) => {
30
+ let document;
31
+ try {
32
+ document = yaml.load(text);
33
+ }
34
+ catch (error) {
35
+ throw new ComponentsGuideUnavailableError(`components_v2.yml is not valid YAML: ${error.message}`);
36
+ }
37
+ if (!isPlainObject(document) || !Array.isArray(document.components)) {
38
+ throw new ComponentsGuideUnavailableError("components_v2.yml has no components list");
39
+ }
40
+ const warnings = [];
41
+ const known = new Set(structuredComponents_1.COMPONENT_NAMES);
42
+ const seen = new Set();
43
+ const components = [];
44
+ for (const raw of document.components) {
45
+ const name = isPlainObject(raw) && isNonEmptyString(raw.name) ? raw.name : "(no name)";
46
+ if (!known.has(name)) {
47
+ warnings.push(`components_v2.yml: "${name}" is not a component this CLI can render; skipped`);
48
+ continue;
49
+ }
50
+ if (seen.has(name)) {
51
+ warnings.push(`components_v2.yml: "${name}" appears twice; the second one is skipped`);
52
+ continue;
53
+ }
54
+ const decidedBy = raw.decidedBy;
55
+ if ((decidedBy !== "syllabus" && decidedBy !== "interactivity") ||
56
+ !isPlainObject(raw.choose) ||
57
+ !isNonEmptyString(raw.choose.summary) ||
58
+ !isPlainObject(raw.write) ||
59
+ !isPlainObject(raw.write.fields)) {
60
+ warnings.push(`components_v2.yml: "${name}" lacks decidedBy, choose.summary or write.fields; skipped`);
61
+ continue;
62
+ }
63
+ seen.add(name);
64
+ components.push(raw);
65
+ }
66
+ if (components.length === 0) {
67
+ throw new ComponentsGuideUnavailableError("components_v2.yml has no usable component");
68
+ }
69
+ for (const name of structuredComponents_1.COMPONENT_NAMES) {
70
+ if (!seen.has(name)) {
71
+ warnings.push(`components_v2.yml: "${name}" is missing; the model can still return it, without guidance`);
72
+ }
73
+ }
74
+ return {
75
+ guide: { version: String(document.version || ""), components },
76
+ warnings,
77
+ };
78
+ };
79
+ exports.parseComponentsGuide = parseComponentsGuide;
80
+ // The COMPONENTS input of the template: what each field means and the
81
+ // writing rules, plus the choice criteria. Full criteria only go to the
82
+ // components this step decides; for the ones the syllabus plans, the summary
83
+ // is enough to pick a format when a step has no plan. Syntax never goes in.
84
+ const buildInteractivityGuide = (guide) => {
85
+ const components = guide.components.map(component => {
86
+ const choose = component.decidedBy === "interactivity" ?
87
+ component.choose :
88
+ { summary: component.choose.summary };
89
+ const entry = {
90
+ name: component.name,
91
+ choose,
92
+ write: component.write,
93
+ };
94
+ return entry;
95
+ });
96
+ return yaml.dump({ components }, { lineWidth: -1, noRefs: true });
97
+ };
98
+ exports.buildInteractivityGuide = buildInteractivityGuide;
99
+ const decidedByFromGuide = (guide) => {
100
+ const result = {};
101
+ for (const component of guide.components)
102
+ result[component.name] = component.decidedBy;
103
+ return result;
104
+ };
105
+ exports.decidedByFromGuide = decidedByFromGuide;
106
+ const defaultFetcher = async (url, timeoutMs) => {
107
+ const response = await axios_1.default.get(url, {
108
+ timeout: timeoutMs,
109
+ responseType: "text",
110
+ transformResponse: [(data) => data],
111
+ });
112
+ return String(response.data);
113
+ };
114
+ // Where the guide is downloaded from. COMPONENTS_V2_URL overrides master, for
115
+ // example with the raw URL of a branch, to test a guide before merging it:
116
+ // master is read by every hosted generation. Never set it in production.
117
+ const resolveComponentsGuideUrl = (env = process.env) => env.COMPONENTS_V2_URL || exports.COMPONENTS_V2_URL;
118
+ exports.resolveComponentsGuideUrl = resolveComponentsGuideUrl;
119
+ // Keeps the last valid copy in memory (per process). A copy older than
120
+ // maxAgeMs is still served while a new one is fetched in the background
121
+ // (stale-while-revalidate), so a GitHub hiccup does not stop a generation that
122
+ // already had the guide. Concurrent calls share one download.
123
+ const createGuideLoader = (options = {}) => {
124
+ var _a, _b;
125
+ const fetcher = options.fetcher || defaultFetcher;
126
+ const timeoutMs = (_a = options.timeoutMs) !== null && _a !== void 0 ? _a : 5000;
127
+ const retryDelaysMs = options.retryDelaysMs || [1000, 2000];
128
+ const maxAgeMs = (_b = options.maxAgeMs) !== null && _b !== void 0 ? _b : 10 * 60 * 1000;
129
+ const now = options.now || Date.now;
130
+ const sleep = options.sleep ||
131
+ ((ms) => new Promise(resolve => {
132
+ setTimeout(resolve, ms);
133
+ }));
134
+ const log = options.log || ((message) => console.warn(message));
135
+ let cached = null;
136
+ let inFlight = null;
137
+ const fetchAndParse = async () => {
138
+ const url = options.url || (0, exports.resolveComponentsGuideUrl)();
139
+ if (url !== exports.COMPONENTS_V2_URL) {
140
+ log(`Loading the components guide from ${url} instead of master`);
141
+ }
142
+ let lastError = "unknown error";
143
+ for (let attempt = 0; attempt <= retryDelaysMs.length; attempt++) {
144
+ // eslint-disable-next-line no-await-in-loop -- attempts are sequential on purpose
145
+ if (attempt > 0)
146
+ await sleep(retryDelaysMs[attempt - 1]);
147
+ let text;
148
+ try {
149
+ // eslint-disable-next-line no-await-in-loop -- attempts are sequential on purpose
150
+ text = await fetcher(url, timeoutMs);
151
+ }
152
+ catch (error) {
153
+ lastError = `could not download ${url}: ${error.message}`;
154
+ continue;
155
+ }
156
+ // A file that does not parse will not parse on the next attempt either.
157
+ const { guide, warnings } = (0, exports.parseComponentsGuide)(text);
158
+ for (const warning of warnings)
159
+ log(warning);
160
+ return guide;
161
+ }
162
+ throw new ComponentsGuideUnavailableError(lastError);
163
+ };
164
+ const refresh = () => {
165
+ if (!inFlight) {
166
+ inFlight = fetchAndParse().then(guide => {
167
+ cached = { guide, fetchedAt: now() };
168
+ inFlight = null;
169
+ return guide;
170
+ }, error => {
171
+ inFlight = null;
172
+ throw error;
173
+ });
174
+ }
175
+ return inFlight;
176
+ };
177
+ return {
178
+ async load() {
179
+ if (cached) {
180
+ if (now() - cached.fetchedAt > maxAgeMs) {
181
+ refresh().catch(error => log(`Could not refresh the components guide, using the copy in memory: ${error.message}`));
182
+ }
183
+ return cached.guide;
184
+ }
185
+ return refresh();
186
+ },
187
+ peek() {
188
+ return cached ? cached.guide : null;
189
+ },
190
+ };
191
+ };
192
+ exports.createGuideLoader = createGuideLoader;
193
+ exports.componentsGuideLoader = (0, exports.createGuideLoader)();
@@ -0,0 +1,20 @@
1
+ export declare const INTERACTIVITY_V1_SLUG = "add-lesson-interactivy";
2
+ export declare const INTERACTIVITY_V2_SLUG = "add-lesson-interactivity-v2";
3
+ export type InteractivityTemplate = "v1" | "v2";
4
+ export declare const isInteractivityV2Enabled: (env?: Record<string, string | undefined>) => boolean;
5
+ export interface InteractivityRequest {
6
+ tpl: InteractivityTemplate;
7
+ slug: string;
8
+ webhookQuery: string;
9
+ }
10
+ export declare const planInteractivityRequest: ({ v2Enabled, forceV1, }: {
11
+ v2Enabled: boolean;
12
+ forceV1?: boolean;
13
+ }) => InteractivityRequest;
14
+ export interface InteractivityFailure {
15
+ fromV2: boolean;
16
+ retryStatusText: unknown;
17
+ }
18
+ export declare const describeInteractivityFailure: (query: {
19
+ tpl?: unknown;
20
+ }, statusText: unknown) => InteractivityFailure;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ // Which interactivity template a lesson uses, and what happens when it fails.
3
+ //
4
+ // v2 (structured) returns component data that the CLI renders; v1 is the
5
+ // original template, which writes the component markdown itself. The webhook
6
+ // URL carries the template, because rigobot posts back to that exact URL and
7
+ // its error payload does not say it. How many retries a lesson gets is decided
8
+ // by utils/generationRetry.ts, as for the other generation phases.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.describeInteractivityFailure = exports.planInteractivityRequest = exports.isInteractivityV2Enabled = exports.INTERACTIVITY_V2_SLUG = exports.INTERACTIVITY_V1_SLUG = void 0;
11
+ exports.INTERACTIVITY_V1_SLUG = "add-lesson-interactivy";
12
+ exports.INTERACTIVITY_V2_SLUG = "add-lesson-interactivity-v2";
13
+ const isInteractivityV2Enabled = (env = process.env) => env.INTERACTIVITY_V2 === "true";
14
+ exports.isInteractivityV2Enabled = isInteractivityV2Enabled;
15
+ const planInteractivityRequest = ({ v2Enabled, forceV1 = false, }) => {
16
+ const tpl = v2Enabled && !forceV1 ? "v2" : "v1";
17
+ return {
18
+ tpl,
19
+ slug: tpl === "v2" ? exports.INTERACTIVITY_V2_SLUG : exports.INTERACTIVITY_V1_SLUG,
20
+ webhookQuery: `?tpl=${tpl}`,
21
+ };
22
+ };
23
+ exports.planInteractivityRequest = planInteractivityRequest;
24
+ // After a failed job. Every retry uses v1, so a problem of the new template
25
+ // never blocks a lesson. An error that would repeat normally stops the retries,
26
+ // but when the v2 template fails that says nothing about v1, so the error is
27
+ // not passed on and v1 gets its retry within the usual cap. Jobs started
28
+ // before this change have no query and count as v1.
29
+ const describeInteractivityFailure = (query, statusText) => {
30
+ const fromV2 = query.tpl === "v2";
31
+ return { fromV2, retryStatusText: fromV2 ? undefined : statusText };
32
+ };
33
+ exports.describeInteractivityFailure = describeInteractivityFailure;
@@ -0,0 +1,34 @@
1
+ export declare const COMPONENT_NAMES: readonly ["multiple_choice_question", "select_the_blank", "ordering", "open_text_question", "code_challenge_proposal", "ask_rigo_button", "mermaid_diagram", "technical_diagram", "conceptual_illustration", "realistic_photography"];
2
+ export type ComponentName = typeof COMPONENT_NAMES[number];
3
+ export type DecidedBy = "syllabus" | "interactivity";
4
+ export declare const DEFAULT_DECIDED_BY: Record<ComponentName, DecidedBy>;
5
+ export interface Issue {
6
+ component?: ComponentName;
7
+ id?: string;
8
+ message: string;
9
+ }
10
+ export interface ProcessContext {
11
+ stepType?: string;
12
+ isFirstOrLast?: boolean;
13
+ lessonId?: string;
14
+ decidedBy?: Partial<Record<string, DecidedBy>>;
15
+ }
16
+ export interface ProcessResult {
17
+ markdown: string;
18
+ usedComponents: ComponentName[];
19
+ assessmentCount: number;
20
+ dropped: Issue[];
21
+ warnings: Issue[];
22
+ }
23
+ export declare const collapse: (value: unknown) => string;
24
+ export declare const stripBlockPrefix: (value: unknown) => string;
25
+ export declare const encodeMetaValue: (value: unknown) => string;
26
+ export declare const fence: (language: string, meta: string, body: string) => string;
27
+ export declare const toSlug: (value: string) => string;
28
+ export declare const seededShuffle: <T>(items: T[], seed: string) => T[];
29
+ export declare const removeCopiedSyntax: (text: string) => {
30
+ text: string;
31
+ removed: string[];
32
+ };
33
+ export declare const processInteractivityResult: (parsed: any, ctx?: ProcessContext) => ProcessResult;
34
+ export declare const isImageComponent: (name: string) => boolean;