@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.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/agentSdkRunner.d.ts +64 -0
- package/dist/agentSdkRunner.d.ts.map +1 -0
- package/dist/agentSdkRunner.js +158 -0
- package/dist/agentSdkRunner.js.map +1 -0
- package/dist/assembleStage.d.ts +41 -0
- package/dist/assembleStage.d.ts.map +1 -0
- package/dist/assembleStage.js +33 -0
- package/dist/assembleStage.js.map +1 -0
- package/dist/authorStage.d.ts +123 -0
- package/dist/authorStage.d.ts.map +1 -0
- package/dist/authorStage.js +284 -0
- package/dist/authorStage.js.map +1 -0
- package/dist/cloneRepo.d.ts +72 -0
- package/dist/cloneRepo.d.ts.map +1 -0
- package/dist/cloneRepo.js +104 -0
- package/dist/cloneRepo.js.map +1 -0
- package/dist/flagship.d.ts +62 -0
- package/dist/flagship.d.ts.map +1 -0
- package/dist/flagship.js +216 -0
- package/dist/flagship.js.map +1 -0
- package/dist/generateLab.d.ts +151 -0
- package/dist/generateLab.d.ts.map +1 -0
- package/dist/generateLab.js +291 -0
- package/dist/generateLab.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/intake.d.ts +31 -0
- package/dist/intake.d.ts.map +1 -0
- package/dist/intake.js +45 -0
- package/dist/intake.js.map +1 -0
- package/dist/live.d.ts +2 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +213 -0
- package/dist/live.js.map +1 -0
- package/dist/mapStage.d.ts +84 -0
- package/dist/mapStage.d.ts.map +1 -0
- package/dist/mapStage.js +241 -0
- package/dist/mapStage.js.map +1 -0
- package/dist/pass1.d.ts +85 -0
- package/dist/pass1.d.ts.map +1 -0
- package/dist/pass1.js +81 -0
- package/dist/pass1.js.map +1 -0
- package/dist/pipeline.d.ts +73 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +70 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/planStage.d.ts +141 -0
- package/dist/planStage.d.ts.map +1 -0
- package/dist/planStage.js +275 -0
- package/dist/planStage.js.map +1 -0
- package/dist/progress.d.ts +45 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +2 -0
- package/dist/progress.js.map +1 -0
- package/dist/repoCommit.d.ts +41 -0
- package/dist/repoCommit.d.ts.map +1 -0
- package/dist/repoCommit.js +84 -0
- package/dist/repoCommit.js.map +1 -0
- package/dist/repoSize.d.ts +23 -0
- package/dist/repoSize.d.ts.map +1 -0
- package/dist/repoSize.js +74 -0
- package/dist/repoSize.js.map +1 -0
- package/dist/resolveAnchors.d.ts +142 -0
- package/dist/resolveAnchors.d.ts.map +1 -0
- package/dist/resolveAnchors.js +242 -0
- package/dist/resolveAnchors.js.map +1 -0
- package/dist/verifyStage.d.ts +150 -0
- package/dist/verifyStage.d.ts.map +1 -0
- package/dist/verifyStage.js +461 -0
- package/dist/verifyStage.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { idSchema } from '@ramplab/spec';
|
|
3
|
+
import { renderIntakeSection } from './intake.js';
|
|
4
|
+
import { extractJson } from './mapStage.js';
|
|
5
|
+
/**
|
|
6
|
+
* The curriculum-plan stage (PLAN.md §4, pass 2): given the map-stage output
|
|
7
|
+
* (the system map is the model's index of the repo's subsystems), an agent
|
|
8
|
+
* plans the lab as a **journey, not a survey** (issue #18). The curriculum
|
|
9
|
+
* contract, enforced by the parse gate where it can be mechanical:
|
|
10
|
+
*
|
|
11
|
+
* - **Module 1 is an end-to-end trace** of one front-door user action. When
|
|
12
|
+
* the flagship trace was already selected (see `flagship.ts`), the plan's
|
|
13
|
+
* first module must carry its id, and its title/focus are canonicalized
|
|
14
|
+
* from the selection after parsing — pass 2's re-authoring of the pass-1
|
|
15
|
+
* preview stays canonical instead of re-negotiated per run.
|
|
16
|
+
* - **Dependency order, concrete to abstract**: each module relies only on
|
|
17
|
+
* what earlier modules taught.
|
|
18
|
+
* - **The learner-state ledger**: every planned module declares `assumes`
|
|
19
|
+
* (what the learner knows on arrival) and `teaches` (what it adds). The
|
|
20
|
+
* author stage hands each parallel author its slice, buying narrative
|
|
21
|
+
* continuity without serializing the fan-out.
|
|
22
|
+
* - **The final module is always "your first contribution"** (id
|
|
23
|
+
* `first-contribution`): starter areas, test conventions, CI gates, PR
|
|
24
|
+
* norms — thin if the repo's material is thin, never fabricated.
|
|
25
|
+
*
|
|
26
|
+
* The plan is **pipeline-internal state** — it never appears in a lab spec,
|
|
27
|
+
* so its schema lives here, not in `@ramplab/spec`. Downstream, the author
|
|
28
|
+
* stage fans out one agent per planned module and the assemble stage merges
|
|
29
|
+
* the authored modules in plan order.
|
|
30
|
+
*
|
|
31
|
+
* Budget semantics: the cap is a **config property, not a repo property**.
|
|
32
|
+
* The prompt asks for at most `budget.maxModules` modules; if the model
|
|
33
|
+
* over-delivers anyway, the plan is truncated to the cap after validation.
|
|
34
|
+
* Truncation drops modules from the end of the middle: the opening trace
|
|
35
|
+
* and the landing `first-contribution` module always survive, because they
|
|
36
|
+
* are curriculum invariants, not centrality picks.
|
|
37
|
+
*/
|
|
38
|
+
/** Default cap on planned modules when the config does not set one. */
|
|
39
|
+
export const DEFAULT_MAX_MODULES = 6;
|
|
40
|
+
/** The map stage owns this module id; planned modules must not collide. */
|
|
41
|
+
const RESERVED_MODULE_IDS = new Set(['repo-overview']);
|
|
42
|
+
/**
|
|
43
|
+
* The mandated landing module (issue #18): every curriculum ends with "your
|
|
44
|
+
* first contribution" under this id, so its presence is a mechanical check.
|
|
45
|
+
*/
|
|
46
|
+
export const LANDING_MODULE_ID = 'first-contribution';
|
|
47
|
+
/**
|
|
48
|
+
* Internal Zod schema for one planned module. Deliberately NOT part of
|
|
49
|
+
* `@ramplab/spec`: the plan (including the learner-state ledger) is an
|
|
50
|
+
* intermediate artifact between stages and never reaches the lab spec.
|
|
51
|
+
*/
|
|
52
|
+
export const plannedModuleSchema = z.object({
|
|
53
|
+
/** Stable kebab-case id — becomes the authored module's id in the spec. */
|
|
54
|
+
id: idSchema,
|
|
55
|
+
title: z.string().min(1, 'planned module title must be non-empty'),
|
|
56
|
+
/** What this module must teach — the author agent's brief. */
|
|
57
|
+
focus: z.string().min(1, 'planned module focus must be non-empty'),
|
|
58
|
+
/** Repo-relative paths the author agent should start from. */
|
|
59
|
+
keyFiles: z.array(z.string().min(1)).min(1, 'a planned module needs at least one key file'),
|
|
60
|
+
/**
|
|
61
|
+
* Ledger: what the learner already knows on arrival — established by
|
|
62
|
+
* earlier modules. Empty for the opening trace module.
|
|
63
|
+
*/
|
|
64
|
+
assumes: z.array(z.string().min(1, 'assumes entries must be non-empty')),
|
|
65
|
+
/** Ledger: what this module adds to the learner's understanding. */
|
|
66
|
+
teaches: z
|
|
67
|
+
.array(z.string().min(1, 'teaches entries must be non-empty'))
|
|
68
|
+
.min(1, 'a planned module must teach at least one thing'),
|
|
69
|
+
});
|
|
70
|
+
export const curriculumPlanSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
modules: z.array(plannedModuleSchema).min(1, 'a plan needs at least one module'),
|
|
73
|
+
})
|
|
74
|
+
.superRefine((plan, ctx) => {
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
plan.modules.forEach((module, index) => {
|
|
77
|
+
if (seen.has(module.id)) {
|
|
78
|
+
ctx.addIssue({
|
|
79
|
+
code: 'custom',
|
|
80
|
+
path: ['modules', index, 'id'],
|
|
81
|
+
message: `duplicate planned module id "${module.id}"`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
seen.add(module.id);
|
|
85
|
+
if (RESERVED_MODULE_IDS.has(module.id)) {
|
|
86
|
+
ctx.addIssue({
|
|
87
|
+
code: 'custom',
|
|
88
|
+
path: ['modules', index, 'id'],
|
|
89
|
+
message: `module id "${module.id}" is reserved for the map stage's overview module`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const last = plan.modules[plan.modules.length - 1];
|
|
94
|
+
if (last !== undefined && last.id !== LANDING_MODULE_ID) {
|
|
95
|
+
ctx.addIssue({
|
|
96
|
+
code: 'custom',
|
|
97
|
+
path: ['modules', plan.modules.length - 1, 'id'],
|
|
98
|
+
message: `the final module must be the "your first contribution" landing module ` +
|
|
99
|
+
`with id "${LANDING_MODULE_ID}" (got "${last.id}")`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
/** Raised when the model's output stays schema-invalid after all retries. */
|
|
104
|
+
export class PlanStageError extends Error {
|
|
105
|
+
attempts;
|
|
106
|
+
lastFailure;
|
|
107
|
+
constructor(attempts, lastFailure) {
|
|
108
|
+
super(`Plan stage failed: model output did not produce a valid curriculum plan after ` +
|
|
109
|
+
`${attempts} attempt${attempts === 1 ? '' : 's'}.\nLast failure:\n${lastFailure}`);
|
|
110
|
+
this.name = 'PlanStageError';
|
|
111
|
+
this.attempts = attempts;
|
|
112
|
+
this.lastFailure = lastFailure;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export async function runPlanStage(runner, repoDir, options) {
|
|
116
|
+
const maxRetries = options.maxRetries ?? 1;
|
|
117
|
+
const maxAttempts = 1 + Math.max(0, maxRetries);
|
|
118
|
+
const maxModules = options.budget?.maxModules ?? DEFAULT_MAX_MODULES;
|
|
119
|
+
const basePrompt = buildPlanPrompt(options.mapSpec, maxModules, options.budget?.scopeHints, options.intake, options.traceModule);
|
|
120
|
+
let costUsd;
|
|
121
|
+
let lastFailure = 'no attempt was made';
|
|
122
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
123
|
+
const prompt = attempt === 1 ? basePrompt : `${basePrompt}\n\n${retryPreamble(lastFailure)}`;
|
|
124
|
+
const response = await runner.runStage({
|
|
125
|
+
stage: 'plan',
|
|
126
|
+
model: options.model,
|
|
127
|
+
repoDir,
|
|
128
|
+
prompt,
|
|
129
|
+
attempt,
|
|
130
|
+
});
|
|
131
|
+
if (response.costUsd !== undefined) {
|
|
132
|
+
costUsd = (costUsd ?? 0) + response.costUsd;
|
|
133
|
+
}
|
|
134
|
+
const outcome = parsePlan(response.output, options.traceModule);
|
|
135
|
+
if (outcome.ok) {
|
|
136
|
+
return { plan: capPlan(outcome.plan, maxModules), attempts: attempt, costUsd };
|
|
137
|
+
}
|
|
138
|
+
lastFailure = outcome.failure;
|
|
139
|
+
}
|
|
140
|
+
throw new PlanStageError(maxAttempts, lastFailure);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Enforce the budget mechanically, preserving the curriculum invariants:
|
|
144
|
+
* the opening trace (head of the plan) and the landing `first-contribution`
|
|
145
|
+
* module (tail) always survive; over-delivery is trimmed from the end of
|
|
146
|
+
* the middle. This makes the cap a config guarantee rather than a hope
|
|
147
|
+
* about model obedience.
|
|
148
|
+
*/
|
|
149
|
+
function capPlan(plan, maxModules) {
|
|
150
|
+
if (plan.modules.length <= maxModules)
|
|
151
|
+
return plan;
|
|
152
|
+
const landing = plan.modules[plan.modules.length - 1];
|
|
153
|
+
return { modules: [...plan.modules.slice(0, Math.max(0, maxModules - 1)), landing] };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The parse gate: schema validation (ledger fields, landing module, ids)
|
|
157
|
+
* plus the flagship-trace contract when a trace module was pre-selected —
|
|
158
|
+
* the first module must carry the trace's id, and its title/focus are then
|
|
159
|
+
* overwritten with the canonical selection so the trace's identity is owned
|
|
160
|
+
* by flagship selection, not re-negotiated by the planner. The planner still
|
|
161
|
+
* owns the trace module's keyFiles (it explored the repo) and its ledger.
|
|
162
|
+
*/
|
|
163
|
+
export function parsePlan(output, traceModule) {
|
|
164
|
+
const json = extractJson(output);
|
|
165
|
+
if (json === undefined) {
|
|
166
|
+
return { ok: false, failure: 'The output did not contain a parseable JSON object.' };
|
|
167
|
+
}
|
|
168
|
+
const parsed = curriculumPlanSchema.safeParse(json);
|
|
169
|
+
if (!parsed.success) {
|
|
170
|
+
return { ok: false, failure: z.prettifyError(parsed.error) };
|
|
171
|
+
}
|
|
172
|
+
const plan = parsed.data;
|
|
173
|
+
if (traceModule !== undefined) {
|
|
174
|
+
const first = plan.modules[0];
|
|
175
|
+
if (first.id !== traceModule.id) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
failure: `The first module must be the already-selected flagship trace module ` +
|
|
179
|
+
`with id "${traceModule.id}" (got "${first.id}").`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
plan.modules[0] = { ...first, title: traceModule.title, focus: traceModule.focus };
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, plan };
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Prompting
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
function retryPreamble(failure) {
|
|
190
|
+
return (`IMPORTANT — your previous attempt was rejected because it did not ` +
|
|
191
|
+
`validate against the curriculum-plan schema:\n\n${failure}\n\n` +
|
|
192
|
+
`Fix these problems and respond again with ONLY the corrected JSON object.`);
|
|
193
|
+
}
|
|
194
|
+
/** Render the map stage's system map as compact context for the planner. */
|
|
195
|
+
function describeSystemMap(mapSpec) {
|
|
196
|
+
const lines = [];
|
|
197
|
+
for (const module of mapSpec.base.modules) {
|
|
198
|
+
for (const widget of module.widgets) {
|
|
199
|
+
if (widget.type !== 'system-map')
|
|
200
|
+
continue;
|
|
201
|
+
for (const node of widget.nodes) {
|
|
202
|
+
const files = (node.anchors ?? []).map((a) => a.file).join(', ');
|
|
203
|
+
lines.push(`- ${node.label} (${node.id})` +
|
|
204
|
+
(node.description !== undefined ? `: ${node.description}` : '') +
|
|
205
|
+
(files.length > 0 ? ` [${files}]` : ''));
|
|
206
|
+
}
|
|
207
|
+
for (const edge of widget.edges) {
|
|
208
|
+
lines.push(`- edge ${edge.from} -> ${edge.to}${edge.label !== undefined ? `: ${edge.label}` : ''}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return lines.length > 0 ? lines.join('\n') : '(no system map available)';
|
|
213
|
+
}
|
|
214
|
+
function buildPlanPrompt(mapSpec, maxModules, scopeHints, intake, traceModule) {
|
|
215
|
+
const hints = scopeHints !== undefined && scopeHints.length > 0
|
|
216
|
+
? `\nScope hints from the onboarding lead — respect them when choosing and ordering modules:\n${scopeHints.map((h) => `- ${h}`).join('\n')}\n`
|
|
217
|
+
: '';
|
|
218
|
+
const traceContract = traceModule !== undefined
|
|
219
|
+
? `The trace has already been selected — your FIRST module MUST use exactly this id (its title and focus are fixed; restate them verbatim):
|
|
220
|
+
- id: ${traceModule.id}
|
|
221
|
+
- title: ${traceModule.title}
|
|
222
|
+
- focus: ${traceModule.focus}
|
|
223
|
+
Plan its "keyFiles" from the files along the action's path (start from: ${traceModule.keyFiles.join(', ')}), and fill in its ledger ("assumes" is [] — the learner arrives knowing nothing).`
|
|
224
|
+
: `Pick ONE "front door" user action — a real scenario a user triggers from outside (a request served, a file sent, a command run) — and make module 1 follow it end to end through the code, entry point to effect, before anything abstract.`;
|
|
225
|
+
return `You are planning the curriculum for a codebase onboarding lab — a JOURNEY that takes a sharp engineer from zero knowledge of this repository to first-meaningful-PR readiness. A previous mapping pass produced this system map of the repository in your working directory:
|
|
226
|
+
|
|
227
|
+
${describeSystemMap(mapSpec)}
|
|
228
|
+
|
|
229
|
+
Explore the repository with the read-only tools available to you (Read, Glob, Grep) to confirm what each module should teach. Do not modify anything.
|
|
230
|
+
${renderIntakeSection(intake)}${hints}
|
|
231
|
+
Curriculum contract — every plan must satisfy all five points:
|
|
232
|
+
|
|
233
|
+
1. MODULE 1 IS AN END-TO-END TRACE. ${traceContract}
|
|
234
|
+
|
|
235
|
+
2. DEPENDENCY ORDER, CONCRETE TO ABSTRACT. Order the modules so each relies only on what earlier modules taught. Concrete mechanisms come before the abstractions and architecture built from them; a learner must meet every concept through a concrete instance before any module treats it abstractly. Do not plan a "most important areas" survey — plan a path.
|
|
236
|
+
|
|
237
|
+
3. THE LEARNER-STATE LEDGER. Every module declares:
|
|
238
|
+
- "assumes": what the learner already knows on arrival — only things an EARLIER module in this plan teaches (empty for module 1).
|
|
239
|
+
- "teaches": what this module adds. Together, the "teaches" lists are the whole journey.
|
|
240
|
+
|
|
241
|
+
4. THE CRAFT IS CURRICULUM. The journey must teach how this codebase is BUILT, not only what it does — woven into the modules where each is most concrete, never quarantined into one abstract survey module:
|
|
242
|
+
- the CODE PATTERNS the repo actually uses, called by their names at the places they occur (a plugin registry, dependency injection, a worker pool — whatever this repo truly exhibits);
|
|
243
|
+
- the SYSTEM DESIGN decisions the code embodies and the tradeoffs they bought;
|
|
244
|
+
- the ARCHITECTURE'S rationale: not just what talks to what, but why it is shaped that way.
|
|
245
|
+
Reflect this in the modules' "focus" and "teaches" entries so the authoring pass delivers it. Only claim patterns and decisions the code actually shows — never import textbook patterns the repo does not have.
|
|
246
|
+
|
|
247
|
+
DISTRIBUTE THE FIELD THREADS. Four field threads run through an edition: RUN IT (the repo's own run/demo commands), THE NUMBERS THAT MATTER (embedded constants and why), WHEN IT BREAKS (the subsystem's debugging front door), and HOW IT IS TESTED (where the tests live and how to run just them). Assign each thread to the ONE OR TWO modules where it is load-bearing — run-it belongs where the reader first has something to run, when-it-breaks where the operational risk lives, how-it-is-tested where the test suite actually concentrates — and NAME the assigned threads in those modules' "focus". A module stamped with all four reads as a checklist and breaks the journey; a thread assigned nowhere is a gap.
|
|
248
|
+
|
|
249
|
+
5. THE FINAL MODULE IS "YOUR FIRST CONTRIBUTION", id exactly "${LANDING_MODULE_ID}". It teaches where starter work lives, the repo's test conventions and how to run them, the CI gates a PR must pass, and contribution/PR norms.${intake !== undefined ? ` The onboarding lead's "active development" and "week-one" answers above tell you which starter areas matter most.` : ''} Ground every claim in material that actually exists in the repo (CONTRIBUTING/docs, CI configs, test directories, tooling). If the repo offers little such material, keep this module thin — NEVER fabricate conventions the repo does not have.
|
|
250
|
+
|
|
251
|
+
Hard budget: plan AT MOST ${maxModules} module${maxModules === 1 ? '' : 's'} INCLUDING the trace and the "${LANDING_MODULE_ID}" module. If you over-deliver, modules are discarded from the end of the middle (the trace and the landing module always survive) — make every middle module earn its slot.
|
|
252
|
+
|
|
253
|
+
When you are done, respond with ONLY a single JSON object (no prose before or after) with exactly this shape:
|
|
254
|
+
|
|
255
|
+
{
|
|
256
|
+
"modules": [
|
|
257
|
+
{
|
|
258
|
+
"id": "<kebab-case-id, stable across regenerations>",
|
|
259
|
+
"title": "<module title a learner sees>",
|
|
260
|
+
"focus": "<2-4 sentences: what this module must teach and why it matters>",
|
|
261
|
+
"keyFiles": ["<repo-relative path>", "..."],
|
|
262
|
+
"assumes": ["<something an earlier module taught>", "..."],
|
|
263
|
+
"teaches": ["<something this module adds>", "..."]
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
Hard requirements — the JSON is machine-validated and rejected on any violation:
|
|
269
|
+
- Every "id" is kebab-case (lowercase letters, digits, hyphens), unique, and NOT "repo-overview" (that id is reserved for the intro module).${traceModule !== undefined ? `\n- The FIRST module's id is "${traceModule.id}".` : ''}
|
|
270
|
+
- The LAST module's id is "${LANDING_MODULE_ID}".
|
|
271
|
+
- Every module has "assumes" (a list, may be empty) and "teaches" (at least one entry).
|
|
272
|
+
- "keyFiles" lists 1-6 repo-relative paths to files that actually exist — only list files you confirmed.
|
|
273
|
+
- Strings are non-empty. At most ${maxModules} entries in "modules".`;
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=planStage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planStage.js","sourceRoot":"","sources":["../src/planStage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { LabSpec } from '@ramplab/spec';
|
|
2
|
+
import type { GenerationStageName } from './pipeline.js';
|
|
3
|
+
/**
|
|
4
|
+
* Progress streaming for the two-pass runtime (PLAN.md §4).
|
|
5
|
+
*
|
|
6
|
+
* The `onProgress` callback is **the streaming seam**: pass 1's "renders as
|
|
7
|
+
* it streams in" experience is a caller subscribing to these events — no
|
|
8
|
+
* network or server transport lives in this package. A server slice later
|
|
9
|
+
* forwards the same events over SSE/WebSocket; the CLI prints them; tests
|
|
10
|
+
* assert on them.
|
|
11
|
+
*
|
|
12
|
+
* Event vocabulary (deliberately small):
|
|
13
|
+
* - `stage-started` / `stage-completed` — pipeline stage boundaries.
|
|
14
|
+
* - `module-authored` — one authored module landed (fan-out progress).
|
|
15
|
+
* - `spec-updated` — carries a **complete, schema-valid, anchor-resolved
|
|
16
|
+
* lab spec** reflecting everything generated so far, so a renderer can
|
|
17
|
+
* display each snapshot as-is without merging deltas.
|
|
18
|
+
*
|
|
19
|
+
* Callbacks are invoked synchronously and are not awaited; a throwing
|
|
20
|
+
* callback would abort the run, so callers should keep handlers cheap.
|
|
21
|
+
*/
|
|
22
|
+
/** Which pass of the two-pass runtime emitted an event. */
|
|
23
|
+
export type GenerationPass = 'pass1' | 'pass2';
|
|
24
|
+
export type GenerationProgressEvent = {
|
|
25
|
+
type: 'stage-started';
|
|
26
|
+
pass: GenerationPass;
|
|
27
|
+
stage: GenerationStageName;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'stage-completed';
|
|
30
|
+
pass: GenerationPass;
|
|
31
|
+
stage: GenerationStageName;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'module-authored';
|
|
34
|
+
pass: GenerationPass;
|
|
35
|
+
moduleId: string;
|
|
36
|
+
/** Attempts spent on this module (1 = no retry needed). */
|
|
37
|
+
attempts: number;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'spec-updated';
|
|
40
|
+
pass: GenerationPass;
|
|
41
|
+
/** A valid, anchor-resolved snapshot — renderable as-is. */
|
|
42
|
+
spec: LabSpec;
|
|
43
|
+
};
|
|
44
|
+
export type ProgressCallback = (event: GenerationProgressEvent) => void;
|
|
45
|
+
//# sourceMappingURL=progress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../src/progress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD;;;;;;;;;;;;;;;;;;GAkBG;AAEH,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAC/B;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,mBAAmB,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,mBAAmB,CAAA;CAAE,GAC7E;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;CAClB,GACD;IACE,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,cAAc,CAAC;IACrB,4DAA4D;IAC5D,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAEN,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC"}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.js","sourceRoot":"","sources":["../src/progress.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commit a repository directory is sitting at (founder, 2026-07-26).
|
|
3
|
+
*
|
|
4
|
+
* Stamped onto a pressed spec as `commit`, because anchors are only checkable
|
|
5
|
+
* against the tree they were written against. Anyone re-verifying a spec later
|
|
6
|
+
* needs to fetch that exact tree; without it they clone `HEAD`, the repository
|
|
7
|
+
* has moved on, and honest anchors fail.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately best-effort and silent: a directory that is not a git checkout
|
|
10
|
+
* is a perfectly valid thing to press (a tarball, a vendored copy), and it
|
|
11
|
+
* would be wrong to fail a pressing over provenance nobody asked for. The
|
|
12
|
+
* consequence is stated where it lands — an edition with no commit cannot be
|
|
13
|
+
* verified later, so it cannot take the public shelf.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readRepoCommit(repoDir: string): string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* True when the working tree has uncommitted changes. A spec pressed from a
|
|
18
|
+
* dirty tree describes code that exists on exactly one machine, so its anchors
|
|
19
|
+
* can never be re-verified from the commit alone — worth knowing before an
|
|
20
|
+
* edition is offered to the public shelf.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isRepoDirty(repoDir: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The repository a checkout came from, canonicalized (founder, 2026-07-26).
|
|
25
|
+
*
|
|
26
|
+
* A pressed spec has to name its repository or nothing can ever check it: the
|
|
27
|
+
* claim route needs somewhere to clone from, and without this every edition
|
|
28
|
+
* pressed on someone's own machine reads as "does not name a repository" and
|
|
29
|
+
* is capped below the public shelf forever. The hosted press has always
|
|
30
|
+
* stamped it from the URL it was given; a local press reads it from the
|
|
31
|
+
* remote.
|
|
32
|
+
*
|
|
33
|
+
* Only GitHub, because that is what the library can actually fetch and
|
|
34
|
+
* re-resolve anchors against. Naming a remote we cannot clone would promise a
|
|
35
|
+
* check we are unable to perform, so a repository elsewhere is left unnamed
|
|
36
|
+
* and the edition stays private or unlisted, which is the honest outcome.
|
|
37
|
+
* `https` and `ssh` remotes of the same project canonicalize to one string, so
|
|
38
|
+
* a spec does not say two different things depending on how someone cloned.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readRepoRemote(repoDir: string): string | undefined;
|
|
41
|
+
//# sourceMappingURL=repoCommit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repoCommit.d.ts","sourceRoot":"","sources":["../src/repoCommit.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAYlE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAYpD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAclE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { parseGithubRepo } from '@ramplab/spec';
|
|
3
|
+
/**
|
|
4
|
+
* The commit a repository directory is sitting at (founder, 2026-07-26).
|
|
5
|
+
*
|
|
6
|
+
* Stamped onto a pressed spec as `commit`, because anchors are only checkable
|
|
7
|
+
* against the tree they were written against. Anyone re-verifying a spec later
|
|
8
|
+
* needs to fetch that exact tree; without it they clone `HEAD`, the repository
|
|
9
|
+
* has moved on, and honest anchors fail.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately best-effort and silent: a directory that is not a git checkout
|
|
12
|
+
* is a perfectly valid thing to press (a tarball, a vendored copy), and it
|
|
13
|
+
* would be wrong to fail a pressing over provenance nobody asked for. The
|
|
14
|
+
* consequence is stated where it lands — an edition with no commit cannot be
|
|
15
|
+
* verified later, so it cannot take the public shelf.
|
|
16
|
+
*/
|
|
17
|
+
export function readRepoCommit(repoDir) {
|
|
18
|
+
try {
|
|
19
|
+
const sha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
20
|
+
cwd: repoDir,
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
timeout: 5_000,
|
|
24
|
+
}).trim();
|
|
25
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined; // not a checkout, no git, shallow oddity: not our problem
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* True when the working tree has uncommitted changes. A spec pressed from a
|
|
33
|
+
* dirty tree describes code that exists on exactly one machine, so its anchors
|
|
34
|
+
* can never be re-verified from the commit alone — worth knowing before an
|
|
35
|
+
* edition is offered to the public shelf.
|
|
36
|
+
*/
|
|
37
|
+
export function isRepoDirty(repoDir) {
|
|
38
|
+
try {
|
|
39
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
40
|
+
cwd: repoDir,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
43
|
+
timeout: 5_000,
|
|
44
|
+
});
|
|
45
|
+
return status.trim().length > 0;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false; // not a checkout: nothing to be dirty about
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The repository a checkout came from, canonicalized (founder, 2026-07-26).
|
|
53
|
+
*
|
|
54
|
+
* A pressed spec has to name its repository or nothing can ever check it: the
|
|
55
|
+
* claim route needs somewhere to clone from, and without this every edition
|
|
56
|
+
* pressed on someone's own machine reads as "does not name a repository" and
|
|
57
|
+
* is capped below the public shelf forever. The hosted press has always
|
|
58
|
+
* stamped it from the URL it was given; a local press reads it from the
|
|
59
|
+
* remote.
|
|
60
|
+
*
|
|
61
|
+
* Only GitHub, because that is what the library can actually fetch and
|
|
62
|
+
* re-resolve anchors against. Naming a remote we cannot clone would promise a
|
|
63
|
+
* check we are unable to perform, so a repository elsewhere is left unnamed
|
|
64
|
+
* and the edition stays private or unlisted, which is the honest outcome.
|
|
65
|
+
* `https` and `ssh` remotes of the same project canonicalize to one string, so
|
|
66
|
+
* a spec does not say two different things depending on how someone cloned.
|
|
67
|
+
*/
|
|
68
|
+
export function readRepoRemote(repoDir) {
|
|
69
|
+
let url;
|
|
70
|
+
try {
|
|
71
|
+
url = execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
72
|
+
cwd: repoDir,
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
75
|
+
timeout: 5_000,
|
|
76
|
+
}).trim();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined; // no remote, no git, not a checkout: not our problem
|
|
80
|
+
}
|
|
81
|
+
const ref = parseGithubRepo(url);
|
|
82
|
+
return ref === undefined ? undefined : `https://github.com/${ref.owner}/${ref.name}`;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=repoCommit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repoCommit.js","sourceRoot":"","sources":["../src/repoCommit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE;YACrD,GAAG,EAAE,OAAO;YACZ,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,OAAO,EAAE,KAAK;SACf,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,CAAC,0DAA0D;IAC9E,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE;YAC5D,GAAG,EAAE,OAAO;YACZ,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC,CAAC,4CAA4C;IAC5D,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE;YACzD,GAAG,EAAE,OAAO;YACZ,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,OAAO,EAAE,KAAK;SACf,CAAC,CAAC,IAAI,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,CAAC,qDAAqD;IACzE,CAAC;IACD,MAAM,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,sBAAsB,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;AACvF,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Right-sizing the curriculum to the repository (founder, 2026-07-14): every
|
|
3
|
+
* edition was exactly the module cap because a budget reads as a quota to the
|
|
4
|
+
* planner, so a thousand-line game and a hundred-thousand-line monorepo got
|
|
5
|
+
* identical spines. The plan budget now scales with what the clone actually
|
|
6
|
+
* holds. Mechanical and cheap: one directory walk, no model involved.
|
|
7
|
+
*/
|
|
8
|
+
export interface RepoSizeStats {
|
|
9
|
+
/** Source-ish files counted (binaries and vendored trees skipped). */
|
|
10
|
+
files: number;
|
|
11
|
+
/** Their summed size in bytes. */
|
|
12
|
+
bytes: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function measureRepo(dir: string): RepoSizeStats;
|
|
15
|
+
/**
|
|
16
|
+
* Planned (authored) modules for a repo of this size. The assembled edition
|
|
17
|
+
* adds the overview module on top, so the reader sees one more chapter than
|
|
18
|
+
* this number: tiers 4/5/6/7 read as editions of 5/6/7/8 chapters. A tier is
|
|
19
|
+
* taken when EITHER measure clears it; both file count and volume signal
|
|
20
|
+
* teachable surface.
|
|
21
|
+
*/
|
|
22
|
+
export declare function plannedModulesFor(stats: RepoSizeStats): number;
|
|
23
|
+
//# sourceMappingURL=repoSize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repoSize.d.ts","sourceRoot":"","sources":["../src/repoSize.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;CACf;AAwBD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CA4BtD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAO9D"}
|
package/dist/repoSize.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Directories that say nothing about how much there is to TEACH. */
|
|
4
|
+
const SKIPPED_DIRS = new Set([
|
|
5
|
+
'.git',
|
|
6
|
+
'node_modules',
|
|
7
|
+
'vendor',
|
|
8
|
+
'dist',
|
|
9
|
+
'build',
|
|
10
|
+
'out',
|
|
11
|
+
'target',
|
|
12
|
+
'.next',
|
|
13
|
+
'coverage',
|
|
14
|
+
]);
|
|
15
|
+
/** Extensions that are clearly not teachable source. */
|
|
16
|
+
const SKIPPED_EXTENSIONS = new Set([
|
|
17
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg',
|
|
18
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
19
|
+
'.mp3', '.mp4', '.ogg', '.wav', '.webm',
|
|
20
|
+
'.zip', '.gz', '.tar', '.jar', '.wasm',
|
|
21
|
+
'.lock', '.min.js', '.map', '.pdf',
|
|
22
|
+
]);
|
|
23
|
+
export function measureRepo(dir) {
|
|
24
|
+
const stats = { files: 0, bytes: 0 };
|
|
25
|
+
const walk = (current) => {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return; // unreadable directory: not teachable either
|
|
32
|
+
}
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
if (!SKIPPED_DIRS.has(entry.name))
|
|
36
|
+
walk(join(current, entry.name));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!entry.isFile())
|
|
40
|
+
continue;
|
|
41
|
+
const name = entry.name.toLowerCase();
|
|
42
|
+
const ext = name.slice(name.lastIndexOf('.'));
|
|
43
|
+
if (SKIPPED_EXTENSIONS.has(ext) || name.endsWith('.min.js'))
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
stats.files += 1;
|
|
47
|
+
stats.bytes += statSync(join(current, entry.name)).size;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
stats.files -= 0; // stat raced a deletion: skip silently
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
walk(dir);
|
|
55
|
+
return stats;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Planned (authored) modules for a repo of this size. The assembled edition
|
|
59
|
+
* adds the overview module on top, so the reader sees one more chapter than
|
|
60
|
+
* this number: tiers 4/5/6/7 read as editions of 5/6/7/8 chapters. A tier is
|
|
61
|
+
* taken when EITHER measure clears it; both file count and volume signal
|
|
62
|
+
* teachable surface.
|
|
63
|
+
*/
|
|
64
|
+
export function plannedModulesFor(stats) {
|
|
65
|
+
const tier = (files, bytes) => stats.files >= files || stats.bytes >= bytes;
|
|
66
|
+
if (tier(900, 8_000_000))
|
|
67
|
+
return 7;
|
|
68
|
+
if (tier(200, 1_500_000))
|
|
69
|
+
return 6;
|
|
70
|
+
if (tier(40, 300_000))
|
|
71
|
+
return 5;
|
|
72
|
+
return 4;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=repoSize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repoSize.js","sourceRoot":"","sources":["../src/repoSize.ts"],"names":[],"mappings":"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"}
|