@ramplab/generator 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/dist/agentSdkRunner.d.ts +64 -0
  4. package/dist/agentSdkRunner.d.ts.map +1 -0
  5. package/dist/agentSdkRunner.js +158 -0
  6. package/dist/agentSdkRunner.js.map +1 -0
  7. package/dist/assembleStage.d.ts +41 -0
  8. package/dist/assembleStage.d.ts.map +1 -0
  9. package/dist/assembleStage.js +33 -0
  10. package/dist/assembleStage.js.map +1 -0
  11. package/dist/authorStage.d.ts +123 -0
  12. package/dist/authorStage.d.ts.map +1 -0
  13. package/dist/authorStage.js +284 -0
  14. package/dist/authorStage.js.map +1 -0
  15. package/dist/cloneRepo.d.ts +72 -0
  16. package/dist/cloneRepo.d.ts.map +1 -0
  17. package/dist/cloneRepo.js +104 -0
  18. package/dist/cloneRepo.js.map +1 -0
  19. package/dist/flagship.d.ts +62 -0
  20. package/dist/flagship.d.ts.map +1 -0
  21. package/dist/flagship.js +216 -0
  22. package/dist/flagship.js.map +1 -0
  23. package/dist/generateLab.d.ts +151 -0
  24. package/dist/generateLab.d.ts.map +1 -0
  25. package/dist/generateLab.js +291 -0
  26. package/dist/generateLab.js.map +1 -0
  27. package/dist/index.d.ts +22 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +21 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/intake.d.ts +31 -0
  32. package/dist/intake.d.ts.map +1 -0
  33. package/dist/intake.js +45 -0
  34. package/dist/intake.js.map +1 -0
  35. package/dist/live.d.ts +2 -0
  36. package/dist/live.d.ts.map +1 -0
  37. package/dist/live.js +213 -0
  38. package/dist/live.js.map +1 -0
  39. package/dist/mapStage.d.ts +84 -0
  40. package/dist/mapStage.d.ts.map +1 -0
  41. package/dist/mapStage.js +241 -0
  42. package/dist/mapStage.js.map +1 -0
  43. package/dist/pass1.d.ts +85 -0
  44. package/dist/pass1.d.ts.map +1 -0
  45. package/dist/pass1.js +81 -0
  46. package/dist/pass1.js.map +1 -0
  47. package/dist/pipeline.d.ts +73 -0
  48. package/dist/pipeline.d.ts.map +1 -0
  49. package/dist/pipeline.js +70 -0
  50. package/dist/pipeline.js.map +1 -0
  51. package/dist/planStage.d.ts +141 -0
  52. package/dist/planStage.d.ts.map +1 -0
  53. package/dist/planStage.js +275 -0
  54. package/dist/planStage.js.map +1 -0
  55. package/dist/progress.d.ts +45 -0
  56. package/dist/progress.d.ts.map +1 -0
  57. package/dist/progress.js +2 -0
  58. package/dist/progress.js.map +1 -0
  59. package/dist/repoCommit.d.ts +41 -0
  60. package/dist/repoCommit.d.ts.map +1 -0
  61. package/dist/repoCommit.js +84 -0
  62. package/dist/repoCommit.js.map +1 -0
  63. package/dist/repoSize.d.ts +23 -0
  64. package/dist/repoSize.d.ts.map +1 -0
  65. package/dist/repoSize.js +74 -0
  66. package/dist/repoSize.js.map +1 -0
  67. package/dist/resolveAnchors.d.ts +142 -0
  68. package/dist/resolveAnchors.d.ts.map +1 -0
  69. package/dist/resolveAnchors.js +242 -0
  70. package/dist/resolveAnchors.js.map +1 -0
  71. package/dist/verifyStage.d.ts +150 -0
  72. package/dist/verifyStage.d.ts.map +1 -0
  73. package/dist/verifyStage.js +461 -0
  74. package/dist/verifyStage.js.map +1 -0
  75. package/package.json +47 -0
@@ -0,0 +1,284 @@
1
+ import { z } from 'zod';
2
+ import { moduleSchema } from '@ramplab/spec';
3
+ import { extractJson } from './mapStage.js';
4
+ import { mapWithConcurrency } from './pipeline.js';
5
+ /**
6
+ * The author stage (PLAN.md §4, pass 2): fan out one authoring agent per
7
+ * planned module, in parallel under a small concurrency cap. Each agent
8
+ * writes a full module — a mix of anchored widgets (callouts, code
9
+ * walkthroughs, code figures, diagrams, and a closing quiz) — validated
10
+ * against `@ramplab/spec`'s module schema with the same bounded retry the
11
+ * other stages use.
12
+ *
13
+ * Diagrams are budgeted, not left over (founder, 2026-07-25): the Edition v2
14
+ * threads gave every module several prose obligations inside an unchanged
15
+ * 4-to-8 widget budget, and the only widget family described as optional —
16
+ * the diagrams — is what got squeezed out. Live editions pressed on 2026-07-13
17
+ * carried 3-5 system maps; those pressed on 2026-07-14 carried 1-2, with the
18
+ * callout share climbing from 29% to 50% across the same five editions. The
19
+ * range now starts higher, the diagram catalogue sits above code-figure and
20
+ * quiz rather than last, and {@link SHAPE_CONTRACT} states the expectation
21
+ * outright. The escape hatch stays honest: no diagram where the code has no
22
+ * shape, and every node still carries its own anchor.
23
+ *
24
+ * Narrative continuity without serializing the fan-out (issue #18): each
25
+ * author prompt carries its module's **learner-state ledger slice** — what
26
+ * the learner has already seen (`assumes`, plus the titles of earlier
27
+ * modules it may reference), and what this module must add (`teaches`) —
28
+ * plus the **style contract** (concrete before abstract, jargon defined at
29
+ * first use, analogies anchored, mechanism-and-why over structural survey).
30
+ *
31
+ * Failure policy (deliberate, documented): a module that is still
32
+ * schema-invalid after its retries **fails the whole stage** with an error
33
+ * naming the module — there is no silent-skip mode. A planned module is a
34
+ * curriculum commitment; dropping it quietly would ship a lab with a hole
35
+ * the lead never sees. Other in-flight modules finish before the error is
36
+ * raised, so all failures are reported at once.
37
+ */
38
+ /** Default cap on concurrent authoring agents. */
39
+ export const DEFAULT_AUTHOR_CONCURRENCY = 4;
40
+ /**
41
+ * Widgets per authored chapter. The floor clears the module's mandatory prose
42
+ * — a quiz, the capability callout, the "when it breaks" warning, the threads
43
+ * the plan assigned — so a diagram has somewhere to go.
44
+ */
45
+ export const DEFAULT_WIDGET_RANGE = { min: 6, max: 10 };
46
+ /** Widgets per chapter for repos deep enough to earn a 7-module plan. */
47
+ export const DEEP_WIDGET_RANGE = { min: 7, max: 12 };
48
+ /** Raised when at least one planned module never produced a valid module. */
49
+ export class AuthorStageError extends Error {
50
+ failures;
51
+ constructor(failures) {
52
+ const detail = failures
53
+ .map((f) => `- module "${f.moduleId}" failed after ${f.attempts} attempt` +
54
+ `${f.attempts === 1 ? '' : 's'}:\n${indent(f.lastFailure)}`)
55
+ .join('\n');
56
+ super(`Author stage failed: ${failures.length} planned module` +
57
+ `${failures.length === 1 ? '' : 's'} did not produce valid output.\n${detail}`);
58
+ this.name = 'AuthorStageError';
59
+ this.failures = failures;
60
+ }
61
+ }
62
+ function indent(text) {
63
+ return text
64
+ .split('\n')
65
+ .map((line) => ` ${line}`)
66
+ .join('\n');
67
+ }
68
+ export async function runAuthorStage(runner, repoDir, options) {
69
+ const concurrency = Math.max(1, options.concurrency ?? DEFAULT_AUTHOR_CONCURRENCY);
70
+ const planned = options.plan.modules;
71
+ // Each module's ledger slice includes the titles of the modules before it —
72
+ // any chapters preceding this plan entirely, then the plan's own earlier
73
+ // modules — the names an author may reference for continuity.
74
+ const preceding = options.precedingTitles ?? [];
75
+ const entries = planned.map((module, index) => ({
76
+ module,
77
+ earlierTitles: [...preceding, ...planned.slice(0, index).map((earlier) => earlier.title)],
78
+ }));
79
+ const outcomes = await mapWithConcurrency(entries, concurrency, (entry) => authorModule(runner, repoDir, entry.module, entry.earlierTitles, options));
80
+ let costUsd;
81
+ const modules = [];
82
+ const failures = [];
83
+ for (const outcome of outcomes) {
84
+ if (outcome.costUsd !== undefined) {
85
+ costUsd = (costUsd ?? 0) + outcome.costUsd;
86
+ }
87
+ if (outcome.ok) {
88
+ modules.push({ module: outcome.module, attempts: outcome.attempts });
89
+ }
90
+ else {
91
+ failures.push(outcome.failure);
92
+ }
93
+ }
94
+ if (failures.length > 0) {
95
+ throw new AuthorStageError(failures);
96
+ }
97
+ return { modules, costUsd };
98
+ }
99
+ async function authorModule(runner, repoDir, planned, earlierTitles, options) {
100
+ const maxRetries = options.maxRetries ?? 1;
101
+ const maxAttempts = 1 + Math.max(0, maxRetries);
102
+ const range = options.widgetRange ?? DEFAULT_WIDGET_RANGE;
103
+ const basePrompt = buildAuthorPrompt(planned, earlierTitles, range);
104
+ let costUsd;
105
+ let lastFailure = 'no attempt was made';
106
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
107
+ const prompt = attempt === 1 ? basePrompt : `${basePrompt}\n\n${retryPreamble(lastFailure)}`;
108
+ const response = await runner.runStage({
109
+ stage: 'author',
110
+ model: options.model,
111
+ repoDir,
112
+ prompt,
113
+ attempt,
114
+ });
115
+ if (response.costUsd !== undefined) {
116
+ costUsd = (costUsd ?? 0) + response.costUsd;
117
+ }
118
+ const outcome = assembleModule(response.output, planned);
119
+ if (outcome.ok) {
120
+ options.onModuleAuthored?.({ module: outcome.module, attempts: attempt });
121
+ return { ok: true, module: outcome.module, attempts: attempt, costUsd };
122
+ }
123
+ lastFailure = outcome.failure;
124
+ }
125
+ return {
126
+ ok: false,
127
+ failure: { moduleId: planned.id, attempts: maxAttempts, lastFailure },
128
+ costUsd,
129
+ };
130
+ }
131
+ /**
132
+ * Parse the model's text as the author payload and assemble the module
133
+ * around the plan's stable id/title. Validation is a single path: the
134
+ * assembled module must pass `@ramplab/spec`'s module schema, plus a
135
+ * non-empty-widgets floor (an empty module teaches nothing).
136
+ */
137
+ function assembleModule(output, planned) {
138
+ const json = extractJson(output);
139
+ if (json === undefined) {
140
+ return { ok: false, failure: 'The output did not contain a parseable JSON object.' };
141
+ }
142
+ if (typeof json !== 'object' || json === null || Array.isArray(json)) {
143
+ return { ok: false, failure: 'The output JSON must be an object.' };
144
+ }
145
+ const payload = json;
146
+ const candidate = {
147
+ id: planned.id,
148
+ title: planned.title,
149
+ ...(typeof payload['summary'] === 'string' && payload['summary'].length > 0
150
+ ? { summary: payload['summary'] }
151
+ : {}),
152
+ widgets: payload['widgets'],
153
+ };
154
+ const parsed = moduleSchema.safeParse(candidate);
155
+ if (!parsed.success) {
156
+ return { ok: false, failure: z.prettifyError(parsed.error) };
157
+ }
158
+ if (parsed.data.widgets.length === 0) {
159
+ return { ok: false, failure: 'The module must contain at least one widget.' };
160
+ }
161
+ return { ok: true, module: parsed.data };
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Prompting
165
+ // ---------------------------------------------------------------------------
166
+ function retryPreamble(failure) {
167
+ return (`IMPORTANT — your previous attempt was rejected because it did not ` +
168
+ `validate against the lab module schema:\n\n${failure}\n\n` +
169
+ `Fix these problems and respond again with ONLY the corrected JSON object.`);
170
+ }
171
+ /** The ledger slice rendered into one author's prompt (issue #18). */
172
+ function renderLedgerSection(planned, earlierTitles) {
173
+ const alreadySeen = planned.assumes.length > 0
174
+ ? planned.assumes.map((a) => ` - ${a}`).join('\n')
175
+ : ' - nothing about this codebase yet — this is the learner\'s first hands-on module';
176
+ const references = earlierTitles.length > 0
177
+ ? earlierTitles.map((t) => `"${t}"`).join(', ')
178
+ : '(none — this is the first module)';
179
+ // The opening chapter is the flagship trace, and the catalog lights ITS
180
+ // system-map as the edition's cover journey (apps/web featured.ts gates
181
+ // hero eligibility on module 0 carrying a map). Left unstated, that map
182
+ // came out a coin flip: two Caddy runs hours apart produced four maps and
183
+ // one. Ask for it by name.
184
+ const flagship = earlierTitles.length === 0
185
+ ? `\n- THIS IS THE OPENING CHAPTER — the flagship trace, the first thing anyone reads. Close the trace with a system-map that recaps the path just walked: one node per component the trace actually passed through, edges in the order control moved between them. The catalog lights this map as the edition's cover, so it must be THIS chapter's journey — not a general architecture diagram of the repo.`
186
+ : '';
187
+ return `Learner-state ledger — this module is one stop on a longer journey:${flagship}
188
+ - The learner has already seen:
189
+ ${alreadySeen}
190
+ Do not re-explain any of it; build on it (e.g. "you saw X in an earlier module — here is who calls it").
191
+ - This module must teach:
192
+ ${planned.teaches.map((t) => ` - ${t}`).join('\n')}
193
+ - Earlier modules you may reference by title: ${references}.${earlierTitles.length > 0
194
+ ? `
195
+ - CARRY THE THREAD FORWARD. At least once, name a specific thing an earlier chapter already showed the reader — a function, struct, field, or moment, by name — and say what THIS chapter adds to it ("you watched \`ServeHTTP\` hand off to the route chain in ${earlierTitles[0] !== undefined ? `"${earlierTitles[0]}"` : 'the opening chapter'}; here is who builds that chain"). A generic nod ("as we saw earlier") does not count: the reader must be able to place it. Restating something at the same altitude the reader already met it is the failure mode.`
196
+ : ''}
197
+ - The last widget before the closing quiz must be a callout stating plainly what the learner can now DO that they could not before this module — concrete capabilities ("You can now trace any request from listener to handler", "You can now add and register a config adapter"), not a list of topics covered. Every chapter carries one; a chapter that ends without it breaks the pattern the reader has learned to expect.`;
198
+ }
199
+ /** The style contract every author writes under (issue #18). */
200
+ const STYLE_CONTRACT = `Style contract — how to write every word of this module:
201
+ - Write for a sharp engineer who knows nothing about THIS codebase. Never dumb anything down, and never assume repo-specific knowledge beyond the ledger above.
202
+ - Introduce every abstraction through a concrete instance first: show a real value, call, or file, then name the pattern it is an instance of.
203
+ - Define project-specific jargon the first time it appears.
204
+ - Reach for an analogy whenever it makes a mechanism click — a good module has one or two. Every analogy must be immediately followed by the precise mechanism it stands for, anchored to the code.
205
+ - Teach mechanism and why, not structure alone. "X calls Y" by itself is the failure mode: say what X passes, what Y decides or returns, and why the code is built that way.
206
+ - Teach the craft alongside the behavior. Where a key file exhibits a real code pattern, name it at the moment it appears and anchor it; where the code embodies a system-design decision or an architectural tradeoff, say what was traded away and why this shape won. "why" callouts are the natural home for these. Only name patterns and decisions the code in front of you actually shows — never import textbook material the repo does not have.
207
+ - In prose, wrap every code identifier, symbol, type, function, method, variable, file path, and package name in backticks the moment it appears — e.g. \`ServeHTTP\`, \`caddyhttp.Server\`, \`changeConfig\`, \`internal/caddyhttp/server.go\`. The renderer sets backticked spans as inline code; unbacked identifiers read as ordinary prose. This applies to every prose field (callout/plate bodies, walkthrough commentary, figure captions, quiz explanations, node/state/stage descriptions and edge labels, data-model annotations) — never to the verbatim "code" lines, which are already code.`;
208
+ const CONTENT_CONTRACT = `Field threads — the details a colleague at the next desk would actually give you. The plan assigns each module the threads that are load-bearing THERE (look for them in this module's focus); carry those, and add another only where the material demands it. THE WEAVE RULE governs all of them: a thread is part of this chapter's story, told at the moment it becomes relevant, never appended as a checklist section — and where it touches something an earlier chapter taught, it says so by that chapter's name ("the config reload you traced in Chapter II is what this timeout guards"). Never fabricate:
209
+ - RUN IT. When the module teaches something observable, show how to see it live: the repo's OWN install/run/demo commands, taken verbatim from its README, package scripts, Makefile, or docs, anchored to where they are written. A reader should be able to run what they just learned. Never invent a command the repo does not document.
210
+ - THE NUMBERS THAT MATTER. Surface the constants the code actually embeds — timeouts, sizes, retry counts, thresholds, magic values — anchored, each with why that value matters to behavior the reader has already met.
211
+ - WHEN IT BREAKS. One "warning" callout naming this module's debugging front door: the file, log line, flag, or state a person should inspect FIRST when this subsystem misbehaves, and why, anchored — tied to the mechanism this chapter just taught, not generic advice.
212
+ - HOW IT IS TESTED. Where this module's key files are tested, what those tests pin down, and the exact command the repo's own tooling uses to run just them, anchored to the test files. If the repo has no tests for this area, say nothing rather than inventing.`;
213
+ /**
214
+ * The diagram budget (founder, 2026-07-25). Prose and code teach only what a
215
+ * reader can hold in a line; the shapes are what a newcomer draws on a napkin
216
+ * and keeps. Stated as its own contract so a diagram competes with the
217
+ * threads for a slot instead of taking whatever is left after them.
218
+ */
219
+ const SHAPE_CONTRACT = `Draw the shape. A chapter of nothing but prose and code teaches only what a reader can hold in a single line; the shapes are what they will redraw from memory a week later. Where this module's material HAS a shape, carry at least one diagram widget for it — and where it has two, carry two:
220
+ - components and the calls between them → system-map
221
+ - one thing that occupies several states and moves between them → state-machine
222
+ - a fixed sequence of stages, each handing to the next → pipeline
223
+ - a payload, config, or record whose fields carry meaning → data-model
224
+ Skip the diagram only when the module is genuinely one linear read of one file. Never invent structure the code does not have: every node, state, stage, and field is a claim like any other and carries its own anchor, so a shape you cannot anchor is a shape you must not draw.
225
+
226
+ SHOW THE CODE. Shapes and prose orient the reader; only the source convinces them. At least TWO widgets in this chapter must put real lines in front of the reader — code-walkthrough or code-figure, copied verbatim from the files. A chapter that describes a codebase without ever showing it is a summary, not a lab, however many diagrams and callouts it carries. The only chapter that may fall below two is one whose subject genuinely is not code (a contribution-process chapter, say) — and even there, prefer showing the config, the test, or the CI file to describing it.`;
227
+ /** Exported for prompt-contract tests; not part of the stage's runtime API. */
228
+ export function buildAuthorPrompt(planned, earlierTitles, range) {
229
+ return `You are authoring one module of a codebase onboarding lab for the repository in your working directory.
230
+
231
+ Module id: ${planned.id}
232
+ Module title: ${planned.title}
233
+ Focus: ${planned.focus}
234
+ Key files to teach from:
235
+ ${planned.keyFiles.map((f) => `- ${f}`).join('\n')}
236
+
237
+ ${renderLedgerSection(planned, earlierTitles)}
238
+
239
+ ${STYLE_CONTRACT}
240
+
241
+ ${CONTENT_CONTRACT}
242
+
243
+ ${SHAPE_CONTRACT}
244
+
245
+ Explore the repository with the read-only tools available to you (Read, Glob, Grep) — start from the key files, follow the code where the focus leads. Do not modify anything.
246
+
247
+ When you are done, respond with ONLY a single JSON object (no prose before or after):
248
+
249
+ {
250
+ "summary": "<2-3 sentence module summary for a new engineer>",
251
+ "widgets": [ ...${range.min} to ${range.max} widgets, mixing the types below... ]
252
+ }
253
+
254
+ Widget types (choose what fits the material; close with the capability callout, then one quiz):
255
+
256
+ 1. Callout — one teachable insight:
257
+ { "id": "<kebab-id>", "type": "callout", "kind": "why" | "warning" | "connects-to", "title": "<optional>", "body": "<insight>", "anchors": [<anchor>, ...] }
258
+
259
+ 2. Code walkthrough — step through an excerpt:
260
+ { "id": "<kebab-id>", "type": "code-walkthrough", "title": "<optional>", "code": ["<line 1>", "<line 2>", ...], "source": <anchor for where the excerpt lives>, "steps": [{ "lines": { "start": N, "end": M }, "commentary": { "body": "<what these lines do>", "anchors": [<anchor>] } }] }
261
+ Copy "code" lines verbatim from the file; step "lines" are 1-based within the excerpt and must not exceed its length.
262
+
263
+ 3. Diagram widgets — one per shape the module's material actually has (see the shape contract above):
264
+ - system-map: { "id", "type": "system-map", "title", "nodes": [{ "id", "label", "description": "<plain string>", "anchors": [<anchor>, ...] }], "edges": [{ "from", "to", "label": "<plain string>", "anchors": [<anchor>, ...] }] } — CAUTION: unlike the widgets below, a node "description" and an edge "label" are PLAIN STRINGS, never { "body", "anchors" } objects; their anchors go in the sibling "anchors" array. A node with a description (or an edge with a label) requires at least one anchor there. Edge endpoints must name existing node ids.
265
+ - state-machine: { "id", "type": "state-machine", "title", "states": [{ "id", "label", "description": { "body", "anchors": [...] } }], "transitions": [{ "from", "to", "trigger", "commentary": { "body", "anchors": [...] } }] }
266
+ - pipeline: { "id", "type": "pipeline", "title", "stages": [{ "id", "label", "description": { "body", "anchors": [...] }, "flow": { "in": "...", "out": "..." } }] }
267
+ - data-model: { "id", "type": "data-model", "title", "source": <anchor>, "nodes": [{ "name", "type", "example": <optional>, "annotation": { "body", "anchors": [...] }, "children": [...] }] }
268
+
269
+ 4. Code figure — one highlighted excerpt with an anchored caption:
270
+ { "id": "<kebab-id>", "type": "code-figure", "code": ["..."], "source": <anchor>, "caption": { "body": "<the point of this excerpt>", "anchors": [<anchor>] }, "highlight": { "start": N, "end": M } }
271
+
272
+ 5. Quiz — end-of-module checkpoint (2-4 questions; at least one must ask the reader to PREDICT what the code does for a concrete input or event, not recall what the prose said):
273
+ { "id": "<kebab-id>", "type": "quiz", "title": "<optional>", "questions": [{ "id": "<kebab-id>", "prompt": "<question>", "options": [{ "id": "<kebab-id>", "label": "<answer>" }, ...>= 2], "correctOptionId": "<option id>", "explanation": { "body": "<why>", "anchors": [<anchor>] } }] }
274
+
275
+ An <anchor> is { "file": "<repo-relative path>", "symbol": "<identifier>", "lines": { "start": N, "end": M } } — "symbol" and "lines" are optional but sharpen the reference.
276
+
277
+ Hard requirements — the JSON is machine-validated and rejected on any violation:
278
+ - Every "id" is kebab-case (lowercase letters, digits, hyphens) and unique within its scope.
279
+ - Every teachable claim (callout, commentary, caption, explanation, description, annotation) carries at least one anchor. Where the claim is a { "body", "anchors" } object the anchors go inside it; where it is a plain string (system-map node descriptions, system-map edge labels) the anchors go in the sibling "anchors" array.
280
+ - Anchors are mechanically verified against the repository: "file" must exist repo-relative, "lines" must be a valid 1-based inclusive range in that file, and "symbol" must appear as a whole token inside the anchored region. Only anchor code you actually read — content whose anchors fail verification is dropped from the lab.
281
+ - Do not include a "fingerprint" field on any anchor.
282
+ - Strings are non-empty. Produce ${range.min} to ${range.max} widgets and at least one quiz.`;
283
+ }
284
+ //# sourceMappingURL=authorStage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorStage.js","sourceRoot":"","sources":["../src/authorStage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Ephemeral clone sandbox for untrusted public repos (Slice 2, #30).
3
+ *
4
+ * v1 only **reads** repo files (the generator never executes the repo), so
5
+ * the threat model is resource abuse and bad URLs — not RCE. This is a
6
+ * shallow clone into a temp dir with a hard timeout and depth cap; stronger
7
+ * per-job microVM isolation is deferred to the v2 live-logic widget
8
+ * (`docs/infra-cost-options.md` §5). `git` itself is injected (`exec`) so the
9
+ * flow tests without touching the network.
10
+ */
11
+ export interface CloneResult {
12
+ /** The local checkout directory (hand to the generator). */
13
+ dir: string;
14
+ /** Remove the checkout. Always call it (a `finally`). */
15
+ cleanup: () => Promise<void>;
16
+ }
17
+ export type ExecResult = {
18
+ code: number;
19
+ stderr: string;
20
+ };
21
+ export type ExecFn = (cmd: string, args: readonly string[], opts: {
22
+ timeoutMs: number;
23
+ }) => Promise<ExecResult>;
24
+ export interface CloneOptions {
25
+ /** Abort the clone after this long. @default 120_000 */
26
+ timeoutMs?: number;
27
+ /** Inject the git runner (tests). @default real `git` via spawn. */
28
+ exec?: ExecFn;
29
+ /** Temp root for the checkout. @default os.tmpdir(). */
30
+ tmpRoot?: string;
31
+ /**
32
+ * Installation access token for a private clone (see
33
+ * `docs/private-repos-design.md`). Passed to git as an `Authorization`
34
+ * extraheader — never in the URL, so it can't leak into error messages or
35
+ * the dedup key; `validateRepoUrl` keeps rejecting credentials-in-URL.
36
+ */
37
+ accessToken?: string;
38
+ /**
39
+ * Check out this exact commit instead of the default branch's tip.
40
+ *
41
+ * Verifying a claimed edition means resolving its anchors against the tree
42
+ * they were written against; clone `HEAD` instead and the repository has
43
+ * moved on, honest anchors drift, and a sound edition is rejected for a
44
+ * reason that is not its fault. Must be a full sha — an abbreviation cannot
45
+ * be fetched by name.
46
+ */
47
+ commit?: string;
48
+ }
49
+ export type UrlCheck = {
50
+ ok: true;
51
+ url: string;
52
+ repoKey: string;
53
+ } | {
54
+ ok: false;
55
+ error: string;
56
+ };
57
+ /**
58
+ * Accept only `https://github.com/<owner>/<repo>`. Rejects other hosts, SSH,
59
+ * credentials-in-URL, and path traversal. Returns the normalized clone URL
60
+ * and a stable `repoKey` (`owner/repo`) for dedup.
61
+ */
62
+ export declare function validateRepoUrl(input: string): UrlCheck;
63
+ /**
64
+ * Validate the URL, shallow-clone into a fresh temp dir, and return the dir
65
+ * plus a cleanup. On any failure the temp dir is removed before throwing, so
66
+ * a failed clone never leaks disk.
67
+ *
68
+ * With `commit`, the fetch asks for that object by name rather than cloning a
69
+ * branch: one commit, one tree, no history.
70
+ */
71
+ export declare function cloneRepo(repoUrl: string, options?: CloneOptions): Promise<CloneResult>;
72
+ //# sourceMappingURL=cloneRepo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloneRepo.d.ts","sourceRoot":"","sources":["../src/cloneRepo.ts"],"names":[],"mappings":"AAKA;;;;;;;;;GASG;AAEH,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAC1D,MAAM,MAAM,MAAM,GAAG,CACnB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,KACxB,OAAO,CAAC,UAAU,CAAC,CAAC;AAEzB,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,QAAQ,GAChB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CA2BvD;AAqBD;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAiDjG"}
@@ -0,0 +1,104 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ /**
6
+ * Accept only `https://github.com/<owner>/<repo>`. Rejects other hosts, SSH,
7
+ * credentials-in-URL, and path traversal. Returns the normalized clone URL
8
+ * and a stable `repoKey` (`owner/repo`) for dedup.
9
+ */
10
+ export function validateRepoUrl(input) {
11
+ let parsed;
12
+ try {
13
+ parsed = new URL(input.trim());
14
+ }
15
+ catch {
16
+ return { ok: false, error: `Not a URL: ${input}` };
17
+ }
18
+ if (parsed.protocol !== 'https:') {
19
+ return { ok: false, error: 'Only https:// GitHub URLs are allowed.' };
20
+ }
21
+ if (parsed.hostname !== 'github.com') {
22
+ return { ok: false, error: 'Only github.com repositories are supported.' };
23
+ }
24
+ if (parsed.username || parsed.password) {
25
+ return { ok: false, error: 'URL must not contain credentials.' };
26
+ }
27
+ const segments = parsed.pathname.split('/').filter(Boolean);
28
+ if (segments.length !== 2) {
29
+ return { ok: false, error: 'Expected https://github.com/<owner>/<repo>.' };
30
+ }
31
+ const [owner, repoRaw] = segments;
32
+ const repo = repoRaw.replace(/\.git$/, '');
33
+ const nameRe = /^[A-Za-z0-9._-]+$/;
34
+ if (!nameRe.test(owner) || !nameRe.test(repo) || repo === '.' || repo === '..') {
35
+ return { ok: false, error: 'Invalid owner or repository name.' };
36
+ }
37
+ return { ok: true, url: `https://github.com/${owner}/${repo}.git`, repoKey: `${owner}/${repo}` };
38
+ }
39
+ /** Default git runner: spawn, capture stderr, enforce a kill-on-timeout. */
40
+ const realExec = (cmd, args, opts) => new Promise((resolve) => {
41
+ const child = spawn(cmd, args, { stdio: ['ignore', 'ignore', 'pipe'] });
42
+ let stderr = '';
43
+ const timer = setTimeout(() => child.kill('SIGKILL'), opts.timeoutMs);
44
+ child.stderr?.on('data', (chunk) => {
45
+ stderr += chunk.toString();
46
+ });
47
+ child.on('error', (err) => {
48
+ clearTimeout(timer);
49
+ resolve({ code: 1, stderr: err.message });
50
+ });
51
+ child.on('close', (code) => {
52
+ clearTimeout(timer);
53
+ resolve({ code: code ?? 1, stderr });
54
+ });
55
+ });
56
+ /**
57
+ * Validate the URL, shallow-clone into a fresh temp dir, and return the dir
58
+ * plus a cleanup. On any failure the temp dir is removed before throwing, so
59
+ * a failed clone never leaks disk.
60
+ *
61
+ * With `commit`, the fetch asks for that object by name rather than cloning a
62
+ * branch: one commit, one tree, no history.
63
+ */
64
+ export async function cloneRepo(repoUrl, options = {}) {
65
+ const check = validateRepoUrl(repoUrl);
66
+ if (!check.ok)
67
+ throw new Error(`Invalid repo URL: ${check.error}`);
68
+ if (options.commit !== undefined && !/^[0-9a-f]{40}$/.test(options.commit)) {
69
+ throw new Error('A commit to clone must be a full 40-character sha.');
70
+ }
71
+ const exec = options.exec ?? realExec;
72
+ const timeoutMs = options.timeoutMs ?? 120_000;
73
+ const dir = await mkdtemp(join(options.tmpRoot ?? tmpdir(), 'ramplab-clone-'));
74
+ const cleanup = () => rm(dir, { recursive: true, force: true });
75
+ // GitHub installation tokens authenticate as `x-access-token:<token>` over
76
+ // Basic auth. The extraheader config is scoped to github.com only.
77
+ const authArgs = options.accessToken === undefined
78
+ ? []
79
+ : [
80
+ '-c',
81
+ `http.https://github.com/.extraheader=Authorization: Basic ${Buffer.from(`x-access-token:${options.accessToken}`).toString('base64')}`,
82
+ ];
83
+ const run = async (args, what) => {
84
+ const result = await exec('git', args, { timeoutMs });
85
+ if (result.code !== 0) {
86
+ await cleanup();
87
+ throw new Error(`git ${what} failed (exit ${result.code}): ${result.stderr.trim()}`);
88
+ }
89
+ };
90
+ if (options.commit === undefined) {
91
+ await run([...authArgs, 'clone', '--depth', '1', '--single-branch', '--no-tags', check.url, dir], 'clone');
92
+ return { dir, cleanup };
93
+ }
94
+ // Fetching one object by sha, rather than cloning and then looking for it:
95
+ // GitHub serves any commit it has by name, so this is a single round trip
96
+ // for exactly the tree the edition was pressed against, however far the
97
+ // default branch has moved since.
98
+ await run(['init', '--quiet', dir], 'init');
99
+ await run(['-C', dir, 'remote', 'add', 'origin', check.url], 'remote add');
100
+ await run([...authArgs, '-C', dir, 'fetch', '--depth', '1', 'origin', options.commit], 'fetch');
101
+ await run(['-C', dir, 'checkout', '--detach', 'FETCH_HEAD'], 'checkout');
102
+ return { dir, cleanup };
103
+ }
104
+ //# sourceMappingURL=cloneRepo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloneRepo.js","sourceRoot":"","sources":["../src/cloneRepo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { LabSpec } from '@ramplab/spec';
2
+ import { type LeadIntake } from './intake.js';
3
+ import type { TraceCandidate } from './mapStage.js';
4
+ import { type PlannedModule } from './planStage.js';
5
+ /**
6
+ * Pass-1 flagship selection (PLAN.md §4, issue #18): pick the ONE module
7
+ * worth authoring in the 3–5 minute preview. The flagship is a **trace**:
8
+ * the most representative candidate "front door" user action the map stage
9
+ * proposed, authored as "follow this action end to end through the code".
10
+ *
11
+ * The selection is mechanical — no model call — and documented here because
12
+ * it IS the product decision:
13
+ *
14
+ * 1. **Trace candidates first.** When the map proposed candidates
15
+ * (`MapStageResult.traceCandidates`), the flagship is one of them:
16
+ * - **Intake override**: the lead's answers are scanned in priority order
17
+ * (`weekOneMastery` before `activeDevelopment`; `gotchas` flavor
18
+ * prompts but never pick) and the first candidate whose action/
19
+ * description/key files match a hint wins (`basis: 'trace-intake'`).
20
+ * - **Otherwise the first candidate wins** (`basis: 'trace'`): the map
21
+ * prompt orders candidates most-representative-first, so map order is
22
+ * the representativeness ranking.
23
+ * 2. **Fallback: subsystem centrality over the system map** — the pre-#18
24
+ * heuristic, kept for maps with no usable candidates (including reused
25
+ * map results captured before trace candidates existed). Candidates are
26
+ * anchored nodes; an intake hint match overrides (`basis: 'intake'`);
27
+ * otherwise the highest-degree node wins (`basis: 'centrality'`, ties
28
+ * keep map order).
29
+ *
30
+ * The winner is packaged as a `PlannedModule` (same brief the plan stage
31
+ * would emit, ledger included) so pass 1 reuses the author stage unchanged,
32
+ * and pass 2's plan stage pins its module 1 to the same id/title/focus.
33
+ */
34
+ /** Why the flagship won — trace-based first, node-based fallback. */
35
+ export type FlagshipBasis = 'trace' | 'trace-intake' | 'intake' | 'centrality';
36
+ export interface FlagshipSelection {
37
+ /** Why it won (see the module doc for the selection order). */
38
+ basis: FlagshipBasis;
39
+ /** Trace-based selection: the chosen front-door action. */
40
+ traceAction?: string;
41
+ /** Node-based fallback: the chosen system-map node. */
42
+ nodeId?: string;
43
+ /** Node-based fallback: incident edge count of the chosen node. */
44
+ degree?: number;
45
+ /** When `basis` is `trace-intake` or `intake`: the hint that matched. */
46
+ matchedHint?: string;
47
+ /** The authoring brief handed to the author stage. */
48
+ plannedModule: PlannedModule;
49
+ }
50
+ /** Raised when the map offers nothing teachable to author. */
51
+ export declare class FlagshipSelectionError extends Error {
52
+ constructor(message: string);
53
+ }
54
+ export declare function selectFlagshipModule(mapSpec: LabSpec, intake?: LeadIntake, traceCandidates?: readonly TraceCandidate[]): FlagshipSelection;
55
+ /**
56
+ * Select the flagship trace from the map's candidate actions, or `undefined`
57
+ * when there are no candidates (callers fall back to centrality). Exported
58
+ * separately because pass 2 uses it too: the plan stage pins its module 1 to
59
+ * the same selection pass 1 previewed.
60
+ */
61
+ export declare function selectTraceFlagship(traceCandidates: readonly TraceCandidate[] | undefined, intake?: LeadIntake): FlagshipSelection | undefined;
62
+ //# sourceMappingURL=flagship.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flagship.d.ts","sourceRoot":"","sources":["../src/flagship.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAiD,MAAM,eAAe,CAAC;AAC5F,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,qEAAqE;AACrE,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,cAAc,GAAG,QAAQ,GAAG,YAAY,CAAC;AAE/E,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,KAAK,EAAE,aAAa,CAAC;IACrB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,8DAA8D;AAC9D,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,UAAU,EACnB,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,GAC1C,iBAAiB,CAInB;AAMD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS,EACtD,MAAM,CAAC,EAAE,UAAU,GAClB,iBAAiB,GAAG,SAAS,CAsB/B"}