@orkestrel/scaffold 0.0.1

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/bin/scaffold.js +1539 -0
  4. package/dist/bin/scaffold.js.map +1 -0
  5. package/dist/host/AGENTS.md +939 -0
  6. package/dist/host/CLAUDE.md +495 -0
  7. package/dist/host/LICENSE +21 -0
  8. package/dist/host/claude/agents/builder.md +48 -0
  9. package/dist/host/claude/agents/checker.md +37 -0
  10. package/dist/host/claude/agents/composer.md +64 -0
  11. package/dist/host/claude/agents/grok.md +50 -0
  12. package/dist/host/claude/agents/orkestrel.md +236 -0
  13. package/dist/host/claude/agents/planner.md +44 -0
  14. package/dist/host/claude/agents/researcher.md +38 -0
  15. package/dist/host/claude/agents/reviewer.md +47 -0
  16. package/dist/host/claude/agents/scout.md +35 -0
  17. package/dist/host/claude/agents/verifier.md +34 -0
  18. package/dist/host/claude/settings.json +26 -0
  19. package/dist/host/dotfiles/editorconfig +17 -0
  20. package/dist/host/dotfiles/gitattributes +3 -0
  21. package/dist/host/dotfiles/gitignore +40 -0
  22. package/dist/host/dotfiles/oxfmtrc.json +18 -0
  23. package/dist/host/dotfiles/oxlintignore +20 -0
  24. package/dist/host/dotfiles/oxlintrc.json +58 -0
  25. package/dist/host/dotfiles/prettierignore +5 -0
  26. package/dist/host/github/workflows/ci.yml +64 -0
  27. package/dist/host/guides/src/guide.md +312 -0
  28. package/dist/host/guides/src/scaffold.md +2152 -0
  29. package/dist/host/manifest.json +137 -0
  30. package/dist/host/scripts/cursor.sh +74 -0
  31. package/dist/host/scripts/deps.sh +38 -0
  32. package/dist/host/scripts/ollama.sh +163 -0
  33. package/dist/src/core/index.cjs +3728 -0
  34. package/dist/src/core/index.cjs.map +1 -0
  35. package/dist/src/core/index.d.cts +1941 -0
  36. package/dist/src/core/index.d.ts +1941 -0
  37. package/dist/src/core/index.js +3636 -0
  38. package/dist/src/core/index.js.map +1 -0
  39. package/dist/src/server/index.cjs +1595 -0
  40. package/dist/src/server/index.cjs.map +1 -0
  41. package/dist/src/server/index.d.cts +779 -0
  42. package/dist/src/server/index.d.ts +779 -0
  43. package/dist/src/server/index.js +1572 -0
  44. package/dist/src/server/index.js.map +1 -0
  45. package/package.json +113 -0
@@ -0,0 +1,3728 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_contract = require("@orkestrel/contract");
3
+ let _orkestrel_markdown = require("@orkestrel/markdown");
4
+ let _orkestrel_template = require("@orkestrel/template");
5
+ let _orkestrel_emitter = require("@orkestrel/emitter");
6
+ //#region src/core/constants.ts
7
+ /** The three `Surface` values, frozen — compose with `literalOf(...)` / `parseEnum(...)`. */
8
+ var SURFACES = Object.freeze([
9
+ "core",
10
+ "browser",
11
+ "server"
12
+ ]);
13
+ /** The three `Origin` values, frozen. */
14
+ var ORIGINS = Object.freeze([
15
+ "host",
16
+ "template",
17
+ "computed"
18
+ ]);
19
+ /** The seven `Group` values, frozen — the artifact-group selection vocabulary. */
20
+ var GROUPS = Object.freeze([
21
+ "manifest",
22
+ "configs",
23
+ "source",
24
+ "tests",
25
+ "guides",
26
+ "docs",
27
+ "orchestration"
28
+ ]);
29
+ /** The four `Category` values, frozen. */
30
+ var CATEGORIES = Object.freeze([
31
+ "type",
32
+ "constant",
33
+ "factory",
34
+ "entity"
35
+ ]);
36
+ /** The four `Freshness` values, frozen — the currency axis `Sync` reports on. */
37
+ var FRESHNESS = Object.freeze([
38
+ "current",
39
+ "behind",
40
+ "missing",
41
+ "failed"
42
+ ]);
43
+ /** The pipeline phases in order, frozen. */
44
+ var COMPILE_STAGES = Object.freeze([
45
+ "draft",
46
+ "gate",
47
+ "pin"
48
+ ]);
49
+ /**
50
+ * The per-surface variant matrix as data: per `Surface`, its `configs/src`
51
+ * files, Vitest project label, `exports` subpath, and build formats — the
52
+ * per-surface layer `blueprintToPlan` reads BENEATH the manifest/exports
53
+ * combination rules it applies on top.
54
+ */
55
+ var SURFACE_MATRIX = Object.freeze({
56
+ core: Object.freeze({
57
+ configs: Object.freeze(["configs/src/vite.core.config.ts", "configs/src/tsconfig.core.json"]),
58
+ project: "src:core",
59
+ path: ".",
60
+ formats: Object.freeze(["es", "cjs"])
61
+ }),
62
+ browser: Object.freeze({
63
+ configs: Object.freeze(["configs/src/vite.browser.config.ts", "configs/src/tsconfig.browser.json"]),
64
+ project: "src:browser",
65
+ path: "./browser",
66
+ formats: Object.freeze(["es"])
67
+ }),
68
+ server: Object.freeze({
69
+ configs: Object.freeze(["configs/src/vite.server.config.ts", "configs/src/tsconfig.server.json"]),
70
+ project: "src:server",
71
+ path: "./server",
72
+ formats: Object.freeze(["es", "cjs"])
73
+ })
74
+ });
75
+ /**
76
+ * The byte-copied host artifact paths, frozen.
77
+ *
78
+ * @remarks
79
+ * The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`, `.claude`, the three
80
+ * SessionStart hook scripts (`scripts/deps.sh` / `scripts/cursor.sh` /
81
+ * `scripts/ollama.sh`), the line's seven byte-identical root dotfiles,
82
+ * `.github/workflows/ci.yml`, and the two guides-grouped mirrors every repo
83
+ * carries: the line-wide dev-tooling guide (`guides/src/guide.md`) and the
84
+ * scaffold engine's own self-guide (`guides/src/scaffold.md`).
85
+ */
86
+ var HOST_PATHS = Object.freeze([
87
+ "AGENTS.md",
88
+ "CLAUDE.md",
89
+ "LICENSE",
90
+ ".claude",
91
+ "scripts/deps.sh",
92
+ "scripts/cursor.sh",
93
+ "scripts/ollama.sh",
94
+ ".editorconfig",
95
+ ".gitattributes",
96
+ ".gitignore",
97
+ ".oxfmtrc.json",
98
+ ".oxlintrc.json",
99
+ ".oxlintignore",
100
+ ".prettierignore",
101
+ ".github/workflows/ci.yml",
102
+ "guides/src/guide.md",
103
+ "guides/src/scaffold.md"
104
+ ]);
105
+ /** The package-name RegExp — lowercase alphanumeric-with-hyphens, letter-first. */
106
+ var NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
107
+ /**
108
+ * The `@orkestrel/*` dependency-name RegExp — every `Dependency.name` must be
109
+ * scoped to `@orkestrel` and NAME_PATTERN-shaped after the scope, closing the
110
+ * traversal vector a hand-built `../`-laced name would open through
111
+ * `Compiler.#pointerArtifacts`' `guides/src/<short>.md` path derivation.
112
+ */
113
+ var DEPENDENCY_NAME_PATTERN = /^@orkestrel\/[a-z][a-z0-9-]*$/;
114
+ /**
115
+ * The `extras` dependency-name RegExp — a strict npm package-name shape: an
116
+ * optional single `@scope/` prefix, then lowercase letters, digits, hyphens,
117
+ * dots, and underscores (never leading, never adjacent to the scope slash).
118
+ * Broader than `DEPENDENCY_NAME_PATTERN` on purpose: `extras` names are
119
+ * manifest-content only (`devDependenciesFor` keys `devDependencies` with
120
+ * them, `Compiler.#pointerArtifacts` never reads them for a path), so they
121
+ * carry no traversal vector — no `..`, no backslash, and the single optional
122
+ * `/` is fixed to the one scope boundary, so the shape stays structurally
123
+ * incapable of escaping a derived path even though it accepts any valid npm
124
+ * package name (unscoped or externally-scoped), not just `@orkestrel/*`.
125
+ */
126
+ var EXTRA_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
127
+ /** The starting version the `blueprint` builder fills. */
128
+ var DEFAULT_VERSION = "0.0.1";
129
+ /** The `engines.node` range the `blueprint` builder fills. */
130
+ var DEFAULT_ENGINES = ">=22";
131
+ /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
132
+ var SCAFFOLD_RANGE = "^0.0.1";
133
+ /** The default id for a `Compiler` orchestrator. */
134
+ var COMPILER_ID = "compiler";
135
+ //#endregion
136
+ //#region src/core/errors.ts
137
+ /**
138
+ * Carries a `ScaffoldErrorCode` + optional `context` (AGENTS §12).
139
+ *
140
+ * @remarks
141
+ * Throws are reserved for caller misuse: `createBlueprint` on off-contract
142
+ * data throws `INVALID`, any method after `destroy()` throws `DESTROYED`, and
143
+ * on the server surface a non-vacant target throws `TARGET` while a failed
144
+ * write throws `WRITE`. A failing gate is NOT an error — it fails closed into
145
+ * an incomplete `Scaffolding` whose `failures` carry a `BLOCKED` marker.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * import { ScaffoldError, isScaffoldError } from '@orkestrel/scaffold'
150
+ *
151
+ * try {
152
+ * throw new ScaffoldError('INVALID', 'Blueprint failed the exact-record contract')
153
+ * } catch (error) {
154
+ * if (isScaffoldError(error)) error.code // 'INVALID'
155
+ * }
156
+ * ```
157
+ */
158
+ var ScaffoldError = class extends Error {
159
+ code;
160
+ context;
161
+ constructor(code, message, context) {
162
+ super(message);
163
+ this.name = "ScaffoldError";
164
+ this.code = code;
165
+ this.context = context;
166
+ }
167
+ };
168
+ /**
169
+ * Narrow a caught value to a `ScaffoldError`.
170
+ *
171
+ * @param value - The caught value to narrow.
172
+ * @returns `true` when `value` is a {@link ScaffoldError}.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * import { isScaffoldError } from '@orkestrel/scaffold'
177
+ *
178
+ * isScaffoldError(new Error('plain')) // false
179
+ * ```
180
+ */
181
+ function isScaffoldError(value) {
182
+ return value instanceof ScaffoldError;
183
+ }
184
+ //#endregion
185
+ //#region src/core/shapers.ts
186
+ /**
187
+ * Build the `Dependency` object shape.
188
+ *
189
+ * @returns A fresh `ContractShape` describing `{ name, range, optional? }`.
190
+ */
191
+ function dependencyShape() {
192
+ return (0, _orkestrel_contract.objectShape)({
193
+ name: (0, _orkestrel_contract.stringShape)({ min: 1 }),
194
+ range: (0, _orkestrel_contract.stringShape)({ min: 1 }),
195
+ optional: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)())
196
+ });
197
+ }
198
+ /**
199
+ * Build the `Override` object shape.
200
+ *
201
+ * @returns A fresh `ContractShape` describing `{ path, content }`.
202
+ */
203
+ function overrideShape() {
204
+ return (0, _orkestrel_contract.objectShape)({
205
+ path: (0, _orkestrel_contract.stringShape)({ min: 1 }),
206
+ content: (0, _orkestrel_contract.stringShape)({ min: 1 })
207
+ });
208
+ }
209
+ /**
210
+ * Build the `Blueprint` object shape.
211
+ *
212
+ * @remarks
213
+ * `surfaces` is a `literalShape(SURFACES)` array with `min: 1`; `name` is a
214
+ * plain `min: 1` string, NOT pattern-constrained, so `generate` stays
215
+ * satisfiable — the `NAME_PATTERN` law lives in the semantic pass
216
+ * (`validateBlueprint`), never in this compiled contract. `peers` and `extras`
217
+ * are `dependencyShape()` arrays alongside `dependencies` — the cross-array
218
+ * uniqueness and overlap rules also live in `validateBlueprint`.
219
+ *
220
+ * @returns A fresh `ContractShape` describing the closed `Blueprint` spec.
221
+ */
222
+ function blueprintShape() {
223
+ return (0, _orkestrel_contract.objectShape)({
224
+ name: (0, _orkestrel_contract.stringShape)({ min: 1 }),
225
+ description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
226
+ keywords: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)()),
227
+ surfaces: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.literalShape)(SURFACES), { min: 1 }),
228
+ dependencies: (0, _orkestrel_contract.arrayShape)(dependencyShape()),
229
+ peers: (0, _orkestrel_contract.arrayShape)(dependencyShape()),
230
+ extras: (0, _orkestrel_contract.arrayShape)(dependencyShape()),
231
+ version: (0, _orkestrel_contract.stringShape)({ min: 1 }),
232
+ engines: (0, _orkestrel_contract.stringShape)({ min: 1 }),
233
+ overrides: (0, _orkestrel_contract.arrayShape)(overrideShape())
234
+ });
235
+ }
236
+ /**
237
+ * Build the `Member` object shape.
238
+ *
239
+ * @returns A fresh `ContractShape` describing `{ name, category, summary, surface }`.
240
+ */
241
+ function memberShape() {
242
+ return (0, _orkestrel_contract.objectShape)({
243
+ name: (0, _orkestrel_contract.stringShape)({ min: 1 }),
244
+ category: (0, _orkestrel_contract.literalShape)(CATEGORIES),
245
+ summary: (0, _orkestrel_contract.stringShape)({ min: 1 }),
246
+ surface: (0, _orkestrel_contract.literalShape)(SURFACES)
247
+ });
248
+ }
249
+ /**
250
+ * Build the `Artifact` object shape.
251
+ *
252
+ * @remarks
253
+ * `origin` is a `literalShape(ORIGINS)`; `content` and `source` are both
254
+ * optional (the `origin` axis decides which one a given artifact carries).
255
+ *
256
+ * @returns A fresh `ContractShape` describing one planned file.
257
+ */
258
+ function artifactShape() {
259
+ return (0, _orkestrel_contract.objectShape)({
260
+ path: (0, _orkestrel_contract.stringShape)({ min: 1 }),
261
+ group: (0, _orkestrel_contract.literalShape)(GROUPS),
262
+ origin: (0, _orkestrel_contract.literalShape)(ORIGINS),
263
+ surface: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.literalShape)(SURFACES)),
264
+ content: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
265
+ source: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)())
266
+ });
267
+ }
268
+ /**
269
+ * Build the whole `Plan` object shape.
270
+ *
271
+ * @remarks
272
+ * Composes {@link blueprintShape} and {@link artifactShape}; `trace` and
273
+ * `hash` are optional (filled by the pin).
274
+ *
275
+ * @returns A fresh `ContractShape` describing the compiled, ordered plan.
276
+ */
277
+ function planShape() {
278
+ return (0, _orkestrel_contract.objectShape)({
279
+ blueprint: blueprintShape(),
280
+ groups: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.literalShape)(GROUPS)),
281
+ artifacts: (0, _orkestrel_contract.arrayShape)(artifactShape()),
282
+ trace: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
283
+ hash: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)())
284
+ });
285
+ }
286
+ /**
287
+ * Build the `SyncReport` object shape.
288
+ *
289
+ * @remarks
290
+ * `guides` / `versions` are array sub-shapes, each with a
291
+ * `literalShape(FRESHNESS)` `freshness` field; `isSyncReport` /
292
+ * `parseSyncReport` compile from it.
293
+ *
294
+ * @returns A fresh `ContractShape` describing the whole sync outcome.
295
+ */
296
+ function syncReportShape() {
297
+ return (0, _orkestrel_contract.objectShape)({
298
+ target: (0, _orkestrel_contract.stringShape)({ min: 1 }),
299
+ guides: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
300
+ name: (0, _orkestrel_contract.stringShape)({ min: 1 }),
301
+ path: (0, _orkestrel_contract.stringShape)({ min: 1 }),
302
+ content: (0, _orkestrel_contract.stringShape)(),
303
+ freshness: (0, _orkestrel_contract.literalShape)(FRESHNESS),
304
+ note: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)())
305
+ })),
306
+ versions: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
307
+ name: (0, _orkestrel_contract.stringShape)({ min: 1 }),
308
+ range: (0, _orkestrel_contract.stringShape)({ min: 1 }),
309
+ latest: (0, _orkestrel_contract.stringShape)(),
310
+ freshness: (0, _orkestrel_contract.literalShape)(FRESHNESS),
311
+ note: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)())
312
+ })),
313
+ clean: (0, _orkestrel_contract.booleanShape)(),
314
+ failed: (0, _orkestrel_contract.integerShape)({ min: 0 })
315
+ });
316
+ }
317
+ //#endregion
318
+ //#region src/core/validators.ts
319
+ /**
320
+ * Narrow a value to a `Dependency` — `name` and `range` non-empty strings.
321
+ *
322
+ * @remarks
323
+ * Compiled from {@link dependencyShape} via `createContract` (AGENTS §14) — a
324
+ * total `Guard`, adversarial input returns `false`, never throws.
325
+ */
326
+ var isDependency = (0, _orkestrel_contract.createContract)(dependencyShape()).is;
327
+ /**
328
+ * Narrow a value to an `Override` — `path` and `content` non-empty strings.
329
+ *
330
+ * @remarks
331
+ * Compiled from {@link overrideShape} via `createContract` (AGENTS §14) — a
332
+ * total `Guard`, adversarial input returns `false`, never throws.
333
+ */
334
+ var isOverride = (0, _orkestrel_contract.createContract)(overrideShape()).is;
335
+ /**
336
+ * Narrow a value to a `Blueprint` — `surfaces` on-vocabulary and non-empty,
337
+ * `name` a non-empty string.
338
+ *
339
+ * @remarks
340
+ * Compiled from {@link blueprintShape} via `createContract` (AGENTS §14) —
341
+ * the `NAME_PATTERN` law is the semantic pass's (`validateBlueprint`), not
342
+ * this shape's; a total `Guard`, adversarial input returns `false`, never
343
+ * throws.
344
+ */
345
+ var isBlueprint = (0, _orkestrel_contract.createContract)(blueprintShape()).is;
346
+ /**
347
+ * Narrow a value to a `Member` — `category` and `surface` on-vocabulary.
348
+ *
349
+ * @remarks
350
+ * Compiled from {@link memberShape} via `createContract` (AGENTS §14) — a
351
+ * total `Guard`, adversarial input returns `false`, never throws.
352
+ */
353
+ var isMember = (0, _orkestrel_contract.createContract)(memberShape()).is;
354
+ /**
355
+ * Narrow a value to an `Artifact` — `group` / `origin` on-vocabulary.
356
+ *
357
+ * @remarks
358
+ * Compiled from {@link artifactShape} via `createContract` (AGENTS §14) — a
359
+ * total `Guard`, adversarial input returns `false`, never throws.
360
+ */
361
+ var isArtifact = (0, _orkestrel_contract.createContract)(artifactShape()).is;
362
+ /**
363
+ * Narrow a value to a `Plan` — the whole exact-record contract, section
364
+ * guards composed.
365
+ *
366
+ * @remarks
367
+ * Compiled from {@link planShape} via `createContract` (AGENTS §14) — a
368
+ * total `Guard`, adversarial input returns `false`, never throws.
369
+ */
370
+ var isPlan = (0, _orkestrel_contract.createContract)(planShape()).is;
371
+ /**
372
+ * Narrow a value to a `SyncReport` — the whole exact-record sync contract,
373
+ * `guide` / `version` sections composed.
374
+ *
375
+ * @remarks
376
+ * Compiled from {@link syncReportShape} via `createContract` (AGENTS §14) — a
377
+ * total `Guard`, adversarial input returns `false`, never throws.
378
+ */
379
+ var isSyncReport = (0, _orkestrel_contract.createContract)(syncReportShape()).is;
380
+ //#endregion
381
+ //#region src/core/parsers.ts
382
+ /**
383
+ * Parse a `Blueprint` from `unknown` (or a JSON string), else `undefined`.
384
+ *
385
+ * @remarks
386
+ * The coercing counterpart of {@link isBlueprint}, compiled from the same
387
+ * {@link blueprintShape} via `createContract` (AGENTS §14) — a guard-valid
388
+ * value round-trips unchanged, an off-contract value returns `undefined`,
389
+ * and this never throws, including on malformed JSON text.
390
+ *
391
+ * @param input - The value (or JSON string) to parse.
392
+ * @returns A `Blueprint`, else `undefined`.
393
+ */
394
+ var parseBlueprint = ((contract) => (input) => {
395
+ if (typeof input !== "string") return contract.parse(input);
396
+ try {
397
+ return contract.parse(JSON.parse(input));
398
+ } catch {
399
+ return;
400
+ }
401
+ })((0, _orkestrel_contract.createContract)(blueprintShape()));
402
+ /**
403
+ * Parse a `Plan` from `unknown` (or a JSON string), else `undefined`.
404
+ *
405
+ * @remarks
406
+ * The coercing counterpart of {@link isPlan}, compiled from the same
407
+ * {@link planShape} via `createContract` (AGENTS §14) — a guard-valid value
408
+ * round-trips unchanged, an off-contract value returns `undefined`, and this
409
+ * never throws, including on malformed JSON text.
410
+ *
411
+ * @param input - The value (or JSON string) to parse.
412
+ * @returns A `Plan`, else `undefined`.
413
+ */
414
+ var parsePlan = ((contract) => (input) => {
415
+ if (typeof input !== "string") return contract.parse(input);
416
+ try {
417
+ return contract.parse(JSON.parse(input));
418
+ } catch {
419
+ return;
420
+ }
421
+ })((0, _orkestrel_contract.createContract)(planShape()));
422
+ /**
423
+ * Parse a `SyncReport` from `unknown` (or a JSON string), else `undefined`.
424
+ *
425
+ * @remarks
426
+ * The coercing counterpart of {@link isSyncReport}, compiled from the same
427
+ * {@link syncReportShape} via `createContract` (AGENTS §14) — a guard-valid
428
+ * value round-trips unchanged, an off-contract value returns `undefined`,
429
+ * and this never throws, including on malformed JSON text.
430
+ *
431
+ * @param input - The value (or JSON string) to parse.
432
+ * @returns A `SyncReport`, else `undefined`.
433
+ */
434
+ var parseSyncReport = ((contract) => (input) => {
435
+ if (typeof input !== "string") return contract.parse(input);
436
+ try {
437
+ return contract.parse(JSON.parse(input));
438
+ } catch {
439
+ return;
440
+ }
441
+ })((0, _orkestrel_contract.createContract)(syncReportShape()));
442
+ //#endregion
443
+ //#region src/core/helpers.ts
444
+ /**
445
+ * Build a fresh `Dependency`.
446
+ *
447
+ * @param name - The `@orkestrel/*` package name.
448
+ * @param range - The semver range.
449
+ * @param optional - Whether this dependency is optional; meaningful only when
450
+ * used as a `Blueprint` peer. Omitted entirely when absent.
451
+ * @returns A `Dependency` with `name` / `range` set, `optional` included only when passed.
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * import { dependency } from '@orkestrel/scaffold'
456
+ *
457
+ * dependency('@orkestrel/contract', '^0.0.5') // { name: '@orkestrel/contract', range: '^0.0.5' }
458
+ * dependency('@orkestrel/database', '^0.0.5', true) // optional: true
459
+ * ```
460
+ */
461
+ function dependency(name, range, optional) {
462
+ return optional === void 0 ? {
463
+ name,
464
+ range
465
+ } : {
466
+ name,
467
+ range,
468
+ optional
469
+ };
470
+ }
471
+ /**
472
+ * Build a fresh `Override`.
473
+ *
474
+ * @param path - The artifact-relative path the override replaces.
475
+ * @param content - The replacement content.
476
+ * @returns An `Override` with both fields set.
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * import { override } from '@orkestrel/scaffold'
481
+ *
482
+ * override('README.md', '# router\n') // { path: 'README.md', content: '# router\n' }
483
+ * ```
484
+ */
485
+ function override(path, content) {
486
+ return {
487
+ path,
488
+ content
489
+ };
490
+ }
491
+ /**
492
+ * Build a fresh `Member`.
493
+ *
494
+ * @param name - The declared export name.
495
+ * @param category - The `Member`'s `Category`.
496
+ * @param summary - A one-line description.
497
+ * @param surface - The owning `Surface`; defaults `'core'`.
498
+ * @returns A `Member` with every field set.
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * import { member } from '@orkestrel/scaffold'
503
+ *
504
+ * member('RouterOptions', 'type', 'Options for creating a Router.') // surface: 'core'
505
+ * ```
506
+ */
507
+ function member(name, category, summary, surface = "core") {
508
+ return {
509
+ name,
510
+ category,
511
+ summary,
512
+ surface
513
+ };
514
+ }
515
+ /**
516
+ * Build a fresh `Blueprint` from a name and a partial of the rest.
517
+ *
518
+ * @param name - The package name.
519
+ * @param options - A partial of the remaining `Blueprint` fields.
520
+ * @remarks
521
+ * `version` / `engines` default `DEFAULT_VERSION` / `DEFAULT_ENGINES`, `surfaces`
522
+ * defaults `['core']`, and `keywords` / `dependencies` / `peers` / `extras` /
523
+ * `overrides` default `[]`. `description` is OMITTED entirely when absent, so
524
+ * the result round-trips the exact-record `Blueprint` guard.
525
+ * @returns A complete `Blueprint`.
526
+ *
527
+ * @example
528
+ * ```ts
529
+ * import { blueprint } from '@orkestrel/scaffold'
530
+ *
531
+ * blueprint('router').version // '0.0.1'
532
+ * ```
533
+ */
534
+ function blueprint(name, options) {
535
+ const base = {
536
+ name,
537
+ keywords: options?.keywords ?? [],
538
+ surfaces: options?.surfaces ?? ["core"],
539
+ dependencies: options?.dependencies ?? [],
540
+ peers: options?.peers ?? [],
541
+ extras: options?.extras ?? [],
542
+ version: options?.version ?? "0.0.1",
543
+ engines: options?.engines ?? ">=22",
544
+ overrides: options?.overrides ?? []
545
+ };
546
+ return options?.description === void 0 ? base : {
547
+ ...base,
548
+ description: options.description
549
+ };
550
+ }
551
+ /**
552
+ * Derive the PascalCase entity name from a lowercase-hyphen package name.
553
+ *
554
+ * @param name - A lowercase-hyphen package name.
555
+ * @returns The PascalCase entity name — hyphens are word breaks.
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * import { pascalCase } from '@orkestrel/scaffold'
560
+ *
561
+ * pascalCase('my-router') // 'MyRouter'
562
+ * ```
563
+ */
564
+ function pascalCase(name) {
565
+ return name.split("-").filter((word) => word.length > 0).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
566
+ }
567
+ /**
568
+ * Derive the declared public `Member[]` from a blueprint.
569
+ *
570
+ * @param spec - The blueprint to derive members from.
571
+ * @remarks
572
+ * The canonical per-surface inventory is the four `Category` buckets applied to
573
+ * the package's PascalCase entity name: an `Options` type, an `Interface` type,
574
+ * a `create*` factory, a default-id constant, and the entity itself. Standalone
575
+ * helpers, validators, and shapers are hand-authored in implementation, not
576
+ * scaffolded.
577
+ * @returns The declared `Member[]`, one set per surface.
578
+ *
579
+ * @example
580
+ * ```ts
581
+ * import { blueprint, blueprintToMembers } from '@orkestrel/scaffold'
582
+ *
583
+ * blueprintToMembers(blueprint('router'))[0] // { name: 'Router', category: 'entity', … }
584
+ * ```
585
+ */
586
+ function blueprintToMembers(spec) {
587
+ const pascal = pascalCase(spec.name);
588
+ const screaming = pascal.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
589
+ const members = [];
590
+ for (const surface of spec.surfaces) {
591
+ members.push(member(pascal, "entity", `The ${pascal} entity.`, surface));
592
+ members.push(member(`${pascal}Options`, "type", `Options for creating a ${pascal}.`, surface));
593
+ members.push(member(`${pascal}Interface`, "type", `The ${pascal} contract.`, surface));
594
+ members.push(member(`create${pascal}`, "factory", `Create a ${pascal}.`, surface));
595
+ members.push(member(`${screaming}_ID`, "constant", `The default id for a ${pascal}.`, surface));
596
+ }
597
+ return members;
598
+ }
599
+ /**
600
+ * Extract the `@orkestrel/<name>` package names from a catalog markdown
601
+ * block/table, in row order.
602
+ *
603
+ * @param text - The markdown block/table text (the `orkestrel.md` embedded
604
+ * catalog shape — GFM table rows opening `| @orkestrel/<name>`).
605
+ * @remarks
606
+ * Pure line-scan: a row matches when, after trimming, it starts with
607
+ * `| @orkestrel/` followed by a `NAME_PATTERN`-shaped short name and a cell
608
+ * boundary (`|` or whitespace) — the same row shape `runCatalog`'s shrink
609
+ * count previously matched inline; this is the single source both consume.
610
+ * Returns `[]` when the text has no markers/rows (never throws).
611
+ * @returns The full `@orkestrel/<name>` names found, in order.
612
+ *
613
+ * @example
614
+ * ```ts
615
+ * import { catalogNames } from '@orkestrel/scaffold'
616
+ *
617
+ * catalogNames('| @orkestrel/contract | ... |\n| @orkestrel/emitter | ... |')
618
+ * // ['@orkestrel/contract', '@orkestrel/emitter']
619
+ * ```
620
+ */
621
+ function catalogNames(text) {
622
+ const rowPattern = /^\|\s*(@orkestrel\/[a-z][a-z0-9-]*)(?=\s|\|)/;
623
+ const names = [];
624
+ for (const line of text.split("\n")) {
625
+ const match = rowPattern.exec(line.trimStart());
626
+ if (match !== null && match[1] !== void 0) names.push(match[1]);
627
+ }
628
+ return names;
629
+ }
630
+ /**
631
+ * Build a formatter-width-aligned GFM table string from header and row cells.
632
+ *
633
+ * @param header - The header cell strings, in column order.
634
+ * @param rows - The body rows, each a list of cell strings matching `header`'s column count.
635
+ * @param align - Optional per-column alignment; defaults every column to `'none'`.
636
+ * @remarks
637
+ * Builds a `TableNode` (each cell parsed with `parseInline`) and serializes it
638
+ * through `renderMarkdown`, which contributes the structure — `\|`-escaping any
639
+ * literal pipe and emitting the alignment delimiter row — at a flat 1-space
640
+ * cell padding. This function then re-pads BOTH the cells AND the delimiter row
641
+ * to per-column codepoint width, matching oxfmt's markdown re-padding.
642
+ * @returns The aligned GFM table string.
643
+ *
644
+ * @example
645
+ * ```ts
646
+ * import { alignTable } from '@orkestrel/scaffold'
647
+ *
648
+ * alignTable(['API', 'Kind'], [['`createRouter`', 'function']])
649
+ * // '| API | Kind |\n| --------------- | -------- |\n| `createRouter` | function |'
650
+ * ```
651
+ */
652
+ function alignTable(header, rows, align) {
653
+ const columns = header.length;
654
+ const alignment = align ?? header.map(() => "none");
655
+ const lines = (0, _orkestrel_markdown.renderMarkdown)({
656
+ element: "table",
657
+ header: header.map((cell) => (0, _orkestrel_markdown.parseInline)(cell)),
658
+ rows: rows.map((row) => row.map((cell) => (0, _orkestrel_markdown.parseInline)(cell))),
659
+ align: alignment
660
+ }).split("\n");
661
+ const headerCells = splitTableRow(lines[0] ?? "");
662
+ const bodyCells = lines.slice(2).map((line) => splitTableRow(line));
663
+ const widths = [];
664
+ for (let column = 0; column < columns; column += 1) {
665
+ let width = Array.from(headerCells[column] ?? "").length;
666
+ for (const row of bodyCells) {
667
+ const length = Array.from(row[column] ?? "").length;
668
+ if (length > width) width = length;
669
+ }
670
+ widths.push(Math.max(3, width));
671
+ }
672
+ return [
673
+ `| ${headerCells.map((cell, index) => padCell(cell, widths[index] ?? 3)).join(" | ")} |`,
674
+ `| ${alignment.map((columnAlign, index) => delimiterCell(columnAlign, widths[index] ?? 3)).join(" | ")} |`,
675
+ ...bodyCells.map((row) => `| ${row.map((cell, index) => padCell(cell, widths[index] ?? 3)).join(" | ")} |`)
676
+ ].join("\n");
677
+ }
678
+ /**
679
+ * Split one rendered GFM table row into its trimmed cell strings.
680
+ *
681
+ * @param line - A single rendered table line (header, delimiter, or body row).
682
+ * @remarks
683
+ * Splits on an UNESCAPED `|` (a `\|` is a literal pipe inside a cell, not a
684
+ * column boundary), then drops the leading/trailing empty segments the
685
+ * boundary pipes produce and trims each remaining cell.
686
+ * @returns The row's cell strings, in column order.
687
+ *
688
+ * @example
689
+ * ```ts
690
+ * import { splitTableRow } from '@orkestrel/scaffold'
691
+ *
692
+ * splitTableRow('| a | b |') // ['a', 'b']
693
+ * ```
694
+ */
695
+ function splitTableRow(line) {
696
+ return line.split(/(?<!\\)\|/).slice(1, -1).map((part) => part.trim());
697
+ }
698
+ /**
699
+ * Right-pad a cell to a codepoint width, oxfmt-style.
700
+ *
701
+ * @param text - The cell text.
702
+ * @param width - The target codepoint width.
703
+ * @remarks
704
+ * Measures via `Array.from` (codepoints, not UTF-16 code units) so a
705
+ * surrogate-pair or wide codepoint counts once, matching oxfmt's own
706
+ * width math. A cell already at or past `width` is returned unchanged.
707
+ * @returns `text` padded with trailing spaces to `width` codepoints.
708
+ *
709
+ * @example
710
+ * ```ts
711
+ * import { padCell } from '@orkestrel/scaffold'
712
+ *
713
+ * padCell('ab', 5) // 'ab '
714
+ * ```
715
+ */
716
+ function padCell(text, width) {
717
+ const length = Array.from(text).length;
718
+ return length >= width ? text : text + " ".repeat(width - length);
719
+ }
720
+ /**
721
+ * Build one delimiter-row cell for a GFM table column.
722
+ *
723
+ * @param columnAlign - The column's `TableAlign`.
724
+ * @param width - The column's codepoint width.
725
+ * @remarks
726
+ * `'left'` prefixes `:`, `'right'` suffixes `:`, `'center'` wraps both ends,
727
+ * `'none'` is plain dashes — one dash per width unit, `:` markers consuming
728
+ * a dash slot rather than adding to `width`.
729
+ * @returns The delimiter cell string for this column.
730
+ *
731
+ * @example
732
+ * ```ts
733
+ * import { delimiterCell } from '@orkestrel/scaffold'
734
+ *
735
+ * delimiterCell('left', 5) // ':----'
736
+ * ```
737
+ */
738
+ function delimiterCell(columnAlign, width) {
739
+ if (columnAlign === "left") return `:${"-".repeat(width - 1)}`;
740
+ if (columnAlign === "right") return `${"-".repeat(width - 1)}:`;
741
+ if (columnAlign === "center") return `:${"-".repeat(width - 2)}:`;
742
+ return "-".repeat(width);
743
+ }
744
+ /**
745
+ * Project a `Plan` into a `PlanSummary`.
746
+ *
747
+ * @param plan - The plan to summarize.
748
+ * @returns The artifact tally by `origin`, the surfaces, and the covered groups.
749
+ *
750
+ * @example
751
+ * ```ts
752
+ * import { planToSummary } from '@orkestrel/scaffold'
753
+ *
754
+ * planToSummary(plan) // { name: 'router', artifacts: 21, host: 12, template: 6, computed: 3, … }
755
+ * ```
756
+ */
757
+ function planToSummary(plan) {
758
+ let host = 0;
759
+ let template = 0;
760
+ let computed = 0;
761
+ for (const artifact of plan.artifacts) if (artifact.origin === "host") host += 1;
762
+ else if (artifact.origin === "template") template += 1;
763
+ else computed += 1;
764
+ return {
765
+ name: plan.blueprint.name,
766
+ surfaces: plan.blueprint.surfaces,
767
+ groups: plan.groups,
768
+ artifacts: plan.artifacts.length,
769
+ host,
770
+ template,
771
+ computed
772
+ };
773
+ }
774
+ /**
775
+ * Project a `Plan` into a copy-ready markdown review document.
776
+ *
777
+ * @param plan - The plan to review.
778
+ * @returns The artifact table by group, the members table, and the summary — the diff-first dry run.
779
+ *
780
+ * @example
781
+ * ```ts
782
+ * import { planToReview } from '@orkestrel/scaffold'
783
+ *
784
+ * planToReview(plan) // '# Scaffolding router\n## Artifacts\n| Path | Group | Origin |\n…'
785
+ * ```
786
+ */
787
+ function planToReview(plan) {
788
+ const summary = planToSummary(plan);
789
+ const members = blueprintToMembers(plan.blueprint);
790
+ const artifactTable = alignTable([
791
+ "Path",
792
+ "Group",
793
+ "Origin"
794
+ ], plan.artifacts.map((artifact) => [
795
+ artifact.path,
796
+ artifact.group,
797
+ artifact.origin
798
+ ]));
799
+ const memberTable = alignTable([
800
+ "Name",
801
+ "Category",
802
+ "Surface"
803
+ ], members.map((entry) => [
804
+ entry.name,
805
+ entry.category,
806
+ entry.surface
807
+ ]));
808
+ return [
809
+ `# Scaffolding ${plan.blueprint.name}`,
810
+ "",
811
+ "## Artifacts",
812
+ "",
813
+ artifactTable,
814
+ "",
815
+ "## Members",
816
+ "",
817
+ memberTable,
818
+ "",
819
+ "## Summary",
820
+ "",
821
+ `- surfaces: ${summary.surfaces.join(", ")}`,
822
+ `- groups: ${summary.groups.join(", ")}`,
823
+ `- artifacts: ${summary.artifacts} (host: ${summary.host}, template: ${summary.template}, computed: ${summary.computed})`
824
+ ].join("\n");
825
+ }
826
+ /**
827
+ * Project an `Audit` into a markdown drift report.
828
+ *
829
+ * @param audit - The audit to report.
830
+ * @returns Findings grouped by `drift`, `aligned` entries elided — what `repair` will touch.
831
+ *
832
+ * @example
833
+ * ```ts
834
+ * import { auditToReview } from '@orkestrel/scaffold'
835
+ *
836
+ * auditToReview(audit) // '# Audit\n\n- clean: false\n…\n## stale\n\n| Path | Group |\n…'
837
+ * ```
838
+ */
839
+ function auditToReview(audit) {
840
+ const groups = {
841
+ aligned: [],
842
+ stale: [],
843
+ missing: [],
844
+ foreign: []
845
+ };
846
+ for (const finding of audit.findings) groups[finding.drift].push(finding);
847
+ const sections = [
848
+ "# Audit",
849
+ "",
850
+ `- clean: ${audit.clean}`,
851
+ `- drifted: ${audit.drifted}`,
852
+ `- missing: ${audit.missing}`,
853
+ `- foreign: ${audit.foreign}`
854
+ ];
855
+ for (const drift of [
856
+ "stale",
857
+ "missing",
858
+ "foreign"
859
+ ]) {
860
+ const findings = groups[drift];
861
+ if (findings.length === 0) continue;
862
+ sections.push("", `## ${drift}`, "", alignTable(["Path", "Group"], findings.map((finding) => [finding.path, finding.group])));
863
+ }
864
+ return sections.join("\n");
865
+ }
866
+ /**
867
+ * Test whether a `Freshness` verdict counts toward "behind".
868
+ *
869
+ * @param freshness - The freshness verdict to test.
870
+ * @returns `true` iff `freshness` is `'behind'`.
871
+ *
872
+ * @example
873
+ * ```ts
874
+ * import { isBehind } from '@orkestrel/scaffold'
875
+ *
876
+ * isBehind('behind') // true
877
+ * isBehind('current') // false
878
+ * ```
879
+ */
880
+ function isBehind(freshness) {
881
+ return freshness === "behind";
882
+ }
883
+ /**
884
+ * Project a `SyncReport` into a markdown freshness report.
885
+ *
886
+ * @param report - The sync report to render.
887
+ * @returns Guides and versions each in their own table, via `alignTable` — the sibling of `auditToReview`.
888
+ *
889
+ * @example
890
+ * ```ts
891
+ * import { syncToReview } from '@orkestrel/scaffold'
892
+ *
893
+ * syncToReview(report) // '# Sync — 2 behind\n## Guides\n| Name | Freshness |\n…'
894
+ * ```
895
+ */
896
+ function syncToReview(report) {
897
+ const sections = [
898
+ `# Sync — ${report.guides.filter((guide) => isBehind(guide.freshness)).length + report.versions.filter((version) => isBehind(version.freshness)).length} behind`,
899
+ "",
900
+ `- clean: ${report.clean}`,
901
+ `- failed: ${report.failed}`
902
+ ];
903
+ if (report.guides.length > 0) sections.push("", "## Guides", "", alignTable(["Name", "Freshness"], report.guides.map((guide) => [guide.name, guide.freshness])));
904
+ if (report.versions.length > 0) sections.push("", "## Versions", "", alignTable([
905
+ "Name",
906
+ "Range",
907
+ "Latest",
908
+ "Freshness"
909
+ ], report.versions.map((version) => [
910
+ version.name,
911
+ version.range,
912
+ version.latest,
913
+ version.freshness
914
+ ])));
915
+ return sections.join("\n");
916
+ }
917
+ /**
918
+ * Project a fleet package catalog into a markdown table — the block
919
+ * `.claude/agents/orkestrel.md`'s catalog markers wrap.
920
+ *
921
+ * @param entries - The catalog rows to render.
922
+ * @remarks
923
+ * Deduplicated by `name` (a later entry for a repeated name wins), then
924
+ * code-unit sorted by `name`. An empty `description` renders as `—` (an em
925
+ * dash), never a blank cell. Deterministic — same input, same output, every
926
+ * time — via `alignTable`; trailing-newline terminated.
927
+ * @returns The aligned GFM table string.
928
+ *
929
+ * @example
930
+ * ```ts
931
+ * import { catalogToBlock } from '@orkestrel/scaffold'
932
+ *
933
+ * catalogToBlock([
934
+ * { name: '@orkestrel/router', version: '0.0.5', description: 'A tiny hash-router.' },
935
+ * { name: '@orkestrel/contract', version: '0.0.5', description: '' },
936
+ * ])
937
+ * // '| Package | Version | Description |\n| … |\n| @orkestrel/contract | 0.0.5 | — |\n…'
938
+ * ```
939
+ */
940
+ function catalogToBlock(entries) {
941
+ const merged = /* @__PURE__ */ new Map();
942
+ for (const entry of entries) merged.set(entry.name, entry);
943
+ return `${alignTable([
944
+ "Package",
945
+ "Version",
946
+ "Description"
947
+ ], [...merged.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((entry) => [
948
+ entry.name,
949
+ entry.version,
950
+ entry.description.length === 0 ? "—" : entry.description
951
+ ]))}\n`;
952
+ }
953
+ /**
954
+ * Infer a foreign path's `Group` from its leading path segment.
955
+ *
956
+ * @param path - The target-relative path to classify.
957
+ * @remarks
958
+ * Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`,
959
+ * then `.github/` / `scripts/` as `'orchestration'`, then the two manifest
960
+ * files by exact name. Anything else (a root-level, prefix-less file) falls
961
+ * through to `'configs'`.
962
+ * @returns The inferred `Group` for `path`.
963
+ *
964
+ * @example
965
+ * ```ts
966
+ * import { inferGroup } from '@orkestrel/scaffold'
967
+ *
968
+ * inferGroup('src/core/index.ts') // 'source'
969
+ * inferGroup('mystery.config.ts') // 'configs'
970
+ * ```
971
+ */
972
+ function inferGroup(path) {
973
+ if (path.startsWith("src/")) return "source";
974
+ if (path.startsWith("tests/")) return "tests";
975
+ if (path.startsWith("guides/")) return "guides";
976
+ if (path.startsWith("docs/")) return "docs";
977
+ if (path.startsWith("configs/")) return "configs";
978
+ if (path.startsWith(".github/") || path.startsWith("scripts/")) return "orchestration";
979
+ if (path === "package.json" || path === "package-lock.json") return "manifest";
980
+ return "configs";
981
+ }
982
+ /**
983
+ * Diff a plan's artifacts against a target's current content.
984
+ *
985
+ * @param plan - The plan whose artifacts are the source of truth.
986
+ * @param current - The target's current content, keyed by artifact-relative path.
987
+ * @remarks
988
+ * A `template` / `computed` artifact whose rendered content the target does not
989
+ * match is `stale`; one the target lacks is `missing`; a target file the plan
990
+ * does not own is `foreign`. A `host`-origin artifact is audited by PRESENCE
991
+ * only — `missing` or `aligned`, never `stale` — UNLESS it has been hydrated
992
+ * with its real host bytes (`hydratePlan`'s `content`), in which case it is
993
+ * content-compared exactly like a `template` / `computed` artifact and CAN be
994
+ * `stale`. A degrade-path or directory-shaped host artifact (never hydrated)
995
+ * stays presence-only.
996
+ * @returns The `Audit` of drift findings — pure, no I/O.
997
+ *
998
+ * @example
999
+ * ```ts
1000
+ * import { diffPlan } from '@orkestrel/scaffold'
1001
+ *
1002
+ * diffPlan(plan, current) // { findings: [...], clean: false, complete: true, drifted: 1, missing: 20, foreign: 0 }
1003
+ * ```
1004
+ */
1005
+ function diffPlan(plan, current) {
1006
+ const findings = [];
1007
+ const owned = /* @__PURE__ */ new Set();
1008
+ for (const artifact of plan.artifacts) {
1009
+ owned.add(artifact.path);
1010
+ const seen = current[artifact.path];
1011
+ if (artifact.origin === "host") {
1012
+ let drift;
1013
+ if (seen === void 0) drift = "missing";
1014
+ else if (artifact.content === void 0) drift = "aligned";
1015
+ else drift = seen === artifact.content ? "aligned" : "stale";
1016
+ findings.push({
1017
+ path: artifact.path,
1018
+ group: artifact.group,
1019
+ drift
1020
+ });
1021
+ continue;
1022
+ }
1023
+ if (seen === void 0) findings.push({
1024
+ path: artifact.path,
1025
+ group: artifact.group,
1026
+ drift: "missing"
1027
+ });
1028
+ else if (seen === artifact.content) findings.push({
1029
+ path: artifact.path,
1030
+ group: artifact.group,
1031
+ drift: "aligned"
1032
+ });
1033
+ else findings.push({
1034
+ path: artifact.path,
1035
+ group: artifact.group,
1036
+ drift: "stale"
1037
+ });
1038
+ }
1039
+ for (const path of Object.keys(current)) {
1040
+ if (owned.has(path)) continue;
1041
+ findings.push({
1042
+ path,
1043
+ group: inferGroup(path),
1044
+ drift: "foreign"
1045
+ });
1046
+ }
1047
+ let drifted = 0;
1048
+ let missing = 0;
1049
+ let foreign = 0;
1050
+ for (const finding of findings) if (finding.drift === "stale") drifted += 1;
1051
+ else if (finding.drift === "missing") missing += 1;
1052
+ else if (finding.drift === "foreign") foreign += 1;
1053
+ return {
1054
+ findings,
1055
+ clean: drifted === 0 && missing === 0 && foreign === 0,
1056
+ complete: true,
1057
+ questions: [],
1058
+ drifted,
1059
+ missing,
1060
+ foreign
1061
+ };
1062
+ }
1063
+ /**
1064
+ * Validate one dependency-shaped array under the name/range/duplicate rules.
1065
+ *
1066
+ * @param field - The `Question.field` to attribute a violation to (`'dependencies'` / `'peers'` / `'extras'`).
1067
+ * @param items - The `Dependency[]` to check.
1068
+ * @remarks
1069
+ * Pure — takes no closed-over `questions` array to mutate; the caller
1070
+ * concatenates the returned `questions` and inspects the returned `seen` set
1071
+ * to apply the cross-array (`dependencies` vs `peers` vs `extras`) overlap
1072
+ * rules `validateBlueprint` layers on top. `field === 'extras'` validates
1073
+ * names against `EXTRA_NAME_PATTERN` (broader — any valid npm package name);
1074
+ * `'dependencies'` and `'peers'` keep `DEPENDENCY_NAME_PATTERN` (closed to
1075
+ * `@orkestrel/*`) — the path-derived arrays stay orkestrel-closed, since only
1076
+ * `dependencies`/`peers` names ever reach `Compiler.#pointerArtifacts`' path
1077
+ * derivation; `extras` names are manifest-content only.
1078
+ * @returns The violations found and the set of names seen, in encounter order.
1079
+ *
1080
+ * @example
1081
+ * ```ts
1082
+ * import { validateDependencyArray } from '@orkestrel/scaffold'
1083
+ *
1084
+ * validateDependencyArray('dependencies', [{ name: '', range: '^1' }])
1085
+ * // { questions: [{ field: 'dependencies', text: 'A dependency name must not be empty', … }], seen: Set(0) {} }
1086
+ * ```
1087
+ */
1088
+ function validateDependencyArray(field, items) {
1089
+ const pattern = field === "extras" ? EXTRA_NAME_PATTERN : DEPENDENCY_NAME_PATTERN;
1090
+ const questions = [];
1091
+ const seen = /* @__PURE__ */ new Set();
1092
+ for (const item of items) {
1093
+ if (item.name.length === 0) questions.push({
1094
+ field,
1095
+ text: "A dependency name must not be empty",
1096
+ blocking: true
1097
+ });
1098
+ else if (!pattern.test(item.name)) questions.push({
1099
+ field,
1100
+ text: `Dependency name "${item.name}" must match ${pattern.source}`,
1101
+ blocking: true
1102
+ });
1103
+ if (item.range.length === 0) questions.push({
1104
+ field,
1105
+ text: `Dependency "${item.name}" is missing a version range`,
1106
+ blocking: true
1107
+ });
1108
+ if (seen.has(item.name)) questions.push({
1109
+ field,
1110
+ text: `Dependency "${item.name}" is declared more than once`,
1111
+ blocking: true
1112
+ });
1113
+ seen.add(item.name);
1114
+ }
1115
+ return {
1116
+ questions,
1117
+ seen
1118
+ };
1119
+ }
1120
+ /**
1121
+ * The semantic pass over a blueprint.
1122
+ *
1123
+ * @param spec - The blueprint to validate.
1124
+ * @remarks
1125
+ * Checks the name against `NAME_PATTERN`, non-empty on-vocabulary `surfaces`
1126
+ * with no repeats (a repeat would produce duplicate members); a single
1127
+ * surface — `core`-only, `server`-only, `browser`-only — is a fully
1128
+ * first-class declaration (`rootViteConfig` retargets the root export and
1129
+ * runs the surface's own factory as the base, no `core` involved), but a
1130
+ * `browser`+`server` declaration with no `core` has no defined configuration
1131
+ * class `rootViteConfig` / `singleSurfaceViteConfig` can shape — that ONE
1132
+ * exemplar-less combination is a blocking question (without this gate it
1133
+ * would silently drop a surface at `rootViteConfig`'s dispatch while the
1134
+ * manifest still references it). And well-formed `dependencies` / `peers` /
1135
+ * `extras` (non-empty name/range, no duplicate names within an array):
1136
+ * `dependencies` and `peers` names are shaped `DEPENDENCY_NAME_PATTERN`
1137
+ * (closed to `@orkestrel/*`) — a NAME-shaped law at the gate that closes the
1138
+ * traversal vector a hand-built `../`-laced dependency name would open
1139
+ * through `Compiler.#pointerArtifacts`'s path derivation; `extras` names are
1140
+ * shaped `EXTRA_NAME_PATTERN` instead — broader (any valid npm package name),
1141
+ * safe because `extras` never feeds a path, only `devDependencies` content. A
1142
+ * name appearing in both `dependencies` and `peers` is a blocking question
1143
+ * (npm forbids sensibly declaring the same package both ways), and an
1144
+ * `extras` name may overlap neither `dependencies` nor `peers`.
1145
+ * @returns A `Validation` — never throws.
1146
+ *
1147
+ * @example
1148
+ * ```ts
1149
+ * import { validateBlueprint } from '@orkestrel/scaffold'
1150
+ *
1151
+ * validateBlueprint(blueprint('router')) // { valid: true, questions: [], warnings: [] }
1152
+ * ```
1153
+ */
1154
+ function validateBlueprint(spec) {
1155
+ const MAX_NAME_LENGTH = 203;
1156
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
1157
+ const ENGINES_PATTERN = /^>=\d+$/;
1158
+ const questions = [];
1159
+ if (!NAME_PATTERN.test(spec.name)) questions.push({
1160
+ field: "name",
1161
+ text: `Name "${spec.name}" must match ${NAME_PATTERN.source}`,
1162
+ blocking: true
1163
+ });
1164
+ if (spec.name.length > MAX_NAME_LENGTH) questions.push({
1165
+ field: "name",
1166
+ text: `Name "${spec.name}" is ${spec.name.length} characters — the published @orkestrel/<name> must fit npm's 214-character limit (max ${MAX_NAME_LENGTH})`,
1167
+ blocking: true
1168
+ });
1169
+ if (!VERSION_PATTERN.test(spec.version)) questions.push({
1170
+ field: "version",
1171
+ text: `Version "${spec.version}" must match ${VERSION_PATTERN.source}`,
1172
+ blocking: true
1173
+ });
1174
+ if (!ENGINES_PATTERN.test(spec.engines)) questions.push({
1175
+ field: "engines",
1176
+ text: `Engines "${spec.engines}" must match ${ENGINES_PATTERN.source}`,
1177
+ blocking: true
1178
+ });
1179
+ const seenOverridePaths = /* @__PURE__ */ new Set();
1180
+ for (const item of spec.overrides) {
1181
+ if (seenOverridePaths.has(item.path)) questions.push({
1182
+ field: "overrides",
1183
+ text: `Override path "${item.path}" is declared more than once`,
1184
+ blocking: true
1185
+ });
1186
+ seenOverridePaths.add(item.path);
1187
+ if (item.content.length === 0) questions.push({
1188
+ field: "overrides",
1189
+ text: `Override path "${item.path}" has empty content`,
1190
+ blocking: true
1191
+ });
1192
+ }
1193
+ if (spec.surfaces.length === 0) questions.push({
1194
+ field: "surfaces",
1195
+ text: "At least one surface is required",
1196
+ blocking: true
1197
+ });
1198
+ else {
1199
+ for (const surface of spec.surfaces) if (!SURFACES.includes(surface)) questions.push({
1200
+ field: "surfaces",
1201
+ text: `Surface "${surface}" is not recognized`,
1202
+ blocking: true,
1203
+ candidates: [...SURFACES]
1204
+ });
1205
+ if (new Set(spec.surfaces).size !== spec.surfaces.length) questions.push({
1206
+ field: "surfaces",
1207
+ text: "Surfaces must not repeat — a repeat produces duplicate members",
1208
+ blocking: true
1209
+ });
1210
+ if (spec.surfaces.length > 1 && !spec.surfaces.includes("core")) questions.push({
1211
+ field: "surfaces",
1212
+ text: "The browser+server combination without core has no defined configuration class — declare core alongside them, or declare a single surface",
1213
+ blocking: true
1214
+ });
1215
+ }
1216
+ const dependenciesResult = validateDependencyArray("dependencies", spec.dependencies);
1217
+ const peersResult = validateDependencyArray("peers", spec.peers);
1218
+ const extrasResult = validateDependencyArray("extras", spec.extras);
1219
+ questions.push(...dependenciesResult.questions, ...peersResult.questions, ...extrasResult.questions);
1220
+ const seenDependencies = dependenciesResult.seen;
1221
+ const seenPeers = peersResult.seen;
1222
+ const seenExtras = extrasResult.seen;
1223
+ for (const name of seenPeers) if (seenDependencies.has(name)) questions.push({
1224
+ field: "peers",
1225
+ text: `Dependency "${name}" is declared in both "dependencies" and "peers"`,
1226
+ blocking: true
1227
+ });
1228
+ for (const name of seenExtras) {
1229
+ if (seenDependencies.has(name)) questions.push({
1230
+ field: "extras",
1231
+ text: `Dependency "${name}" is declared in both "dependencies" and "extras"`,
1232
+ blocking: true
1233
+ });
1234
+ if (seenPeers.has(name)) questions.push({
1235
+ field: "extras",
1236
+ text: `Dependency "${name}" is declared in both "peers" and "extras"`,
1237
+ blocking: true
1238
+ });
1239
+ }
1240
+ return {
1241
+ valid: questions.length === 0,
1242
+ questions,
1243
+ warnings: []
1244
+ };
1245
+ }
1246
+ /**
1247
+ * Narrow an unknown value to a plain (non-array, non-null) JSON object.
1248
+ *
1249
+ * @param value - The value to narrow.
1250
+ * @returns `true` iff `value` is a non-null, non-array object.
1251
+ *
1252
+ * @example
1253
+ * ```ts
1254
+ * import { isRecord } from '@orkestrel/scaffold'
1255
+ *
1256
+ * isRecord({ a: 1 }) // true
1257
+ * isRecord([1, 2]) // false
1258
+ * isRecord(null) // false
1259
+ * ```
1260
+ */
1261
+ function isRecord(value) {
1262
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1263
+ }
1264
+ /**
1265
+ * Parse a `package.json` text into its declared `@orkestrel/*` dependencies.
1266
+ *
1267
+ * @param manifestText - The `package.json` file content.
1268
+ * @remarks
1269
+ * Reads `dependencies`, `devDependencies`, and `peerDependencies` (ALL three,
1270
+ * in that order), keeps only `DEPENDENCY_NAME_PATTERN`-shaped names,
1271
+ * deduplicated (first occurrence wins). Malformed JSON, a non-object root, or
1272
+ * a non-object/non-string section entry is skipped, never thrown.
1273
+ * @returns The declared `Dependency[]` — pure, never throws.
1274
+ *
1275
+ * @example
1276
+ * ```ts
1277
+ * import { manifestToDependencies } from '@orkestrel/scaffold'
1278
+ *
1279
+ * manifestToDependencies('{"dependencies":{"@orkestrel/contract":"^0.0.5"}}')
1280
+ * // [{ name: '@orkestrel/contract', range: '^0.0.5' }]
1281
+ * ```
1282
+ */
1283
+ function manifestToDependencies(manifestText) {
1284
+ let parsed;
1285
+ try {
1286
+ parsed = JSON.parse(manifestText);
1287
+ } catch {
1288
+ return [];
1289
+ }
1290
+ if (!isRecord(parsed)) return [];
1291
+ const seen = /* @__PURE__ */ new Set();
1292
+ const dependencies = [];
1293
+ for (const section of [
1294
+ "dependencies",
1295
+ "devDependencies",
1296
+ "peerDependencies"
1297
+ ]) {
1298
+ const entries = parsed[section];
1299
+ if (!isRecord(entries)) continue;
1300
+ for (const [name, range] of Object.entries(entries)) {
1301
+ if (typeof range !== "string") continue;
1302
+ if (!DEPENDENCY_NAME_PATTERN.test(name)) continue;
1303
+ if (seen.has(name)) continue;
1304
+ seen.add(name);
1305
+ dependencies.push({
1306
+ name,
1307
+ range
1308
+ });
1309
+ }
1310
+ }
1311
+ return dependencies;
1312
+ }
1313
+ /**
1314
+ * Compare a declared range to the registry latest.
1315
+ *
1316
+ * @param range - The declared semver range.
1317
+ * @param latest - The registry's latest published version.
1318
+ * @remarks
1319
+ * The `0.0.x` exact-pin law: `'current'` iff `range`'s `^0.0.N` exact pin
1320
+ * equals `latest`, else `'behind'`. The `'missing'` / `'failed'` verdicts
1321
+ * come from the fetch layer, never this pure comparison.
1322
+ * @returns `'current'` or `'behind'`.
1323
+ *
1324
+ * @example
1325
+ * ```ts
1326
+ * import { rangeToFreshness } from '@orkestrel/scaffold'
1327
+ *
1328
+ * rangeToFreshness('^0.0.5', '0.0.5') // 'current' — pinned to latest
1329
+ * rangeToFreshness('^0.0.5', '0.0.7') // 'behind' — a newer patch is published
1330
+ * ```
1331
+ */
1332
+ function rangeToFreshness(range, latest) {
1333
+ return range.replace(/^\^/, "") === latest ? "current" : "behind";
1334
+ }
1335
+ /**
1336
+ * Compute a canonical FNV-1a digest of a text string.
1337
+ *
1338
+ * @param text - The text to digest.
1339
+ * @remarks
1340
+ * The 32-bit FNV-1a offset basis/prime, `Math.imul` for the wraparound
1341
+ * multiply, rendered as an 8-hex-digit zero-padded lowercase string —
1342
+ * deterministic, no clocks or randomness.
1343
+ * @returns The 8-hex-digit FNV-1a digest of `text`.
1344
+ *
1345
+ * @example
1346
+ * ```ts
1347
+ * import { computeHash } from '@orkestrel/scaffold'
1348
+ *
1349
+ * computeHash('hello-world') // '428d118e'
1350
+ * ```
1351
+ */
1352
+ function computeHash(text) {
1353
+ let hash = 2166136261;
1354
+ for (let index = 0; index < text.length; index += 1) {
1355
+ hash ^= text.charCodeAt(index);
1356
+ hash = Math.imul(hash, 16777619);
1357
+ }
1358
+ return (hash >>> 0).toString(16).padStart(8, "0");
1359
+ }
1360
+ /**
1361
+ * Serialize a value to a canonical, key-order-INDEPENDENT JSON-like string.
1362
+ *
1363
+ * @param value - The value to stringify.
1364
+ * @remarks
1365
+ * Object keys sort code-unit; array order is preserved. So two
1366
+ * logically-equal blueprints built with their fields in a different
1367
+ * construction order still hash identically once fed through `computeHash`.
1368
+ * @returns The canonical string form of `value`.
1369
+ *
1370
+ * @example
1371
+ * ```ts
1372
+ * import { stableStringify } from '@orkestrel/scaffold'
1373
+ *
1374
+ * stableStringify({ b: 1, a: 2 }) // '{"a":2,"b":1}'
1375
+ * ```
1376
+ */
1377
+ function stableStringify(value) {
1378
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
1379
+ if (typeof value === "object" && value !== null) return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
1380
+ return JSON.stringify(value);
1381
+ }
1382
+ /**
1383
+ * Return a fresh `Plan` with `trace` and `hash` filled.
1384
+ *
1385
+ * @param plan - The plan to pin.
1386
+ * @remarks
1387
+ * `hash` is a canonical `computeHash` digest of the plan's
1388
+ * blueprint/groups/artifacts, serialized through `stableStringify` —
1389
+ * deterministic, no clocks or randomness. `trace` is a one-line derivation
1390
+ * summary built from the plan's own `PlanSummary`.
1391
+ * @returns The plan with `trace` and `hash` filled.
1392
+ *
1393
+ * @example
1394
+ * ```ts
1395
+ * import { pinPlan } from '@orkestrel/scaffold'
1396
+ *
1397
+ * pinPlan(plan).trace // 'router · core+browser · groups:7 · artifacts:21'
1398
+ * ```
1399
+ */
1400
+ function pinPlan(plan) {
1401
+ const canonical = stableStringify({
1402
+ blueprint: plan.blueprint,
1403
+ groups: plan.groups,
1404
+ artifacts: plan.artifacts
1405
+ });
1406
+ const summary = planToSummary(plan);
1407
+ const trace = `${plan.blueprint.name} · ${summary.surfaces.join("+")} · groups:${summary.groups.length} · artifacts:${summary.artifacts}`;
1408
+ return {
1409
+ ...plan,
1410
+ trace,
1411
+ hash: computeHash(canonical)
1412
+ };
1413
+ }
1414
+ //#endregion
1415
+ //#region src/core/templates.ts
1416
+ /**
1417
+ * The shipped, versioned `TemplateDefinition` data behind every
1418
+ * `template`-origin artifact `blueprintToPlan` renders.
1419
+ *
1420
+ * @remarks
1421
+ * The generated-minimal stub prose/source, expressed as `{{name}}` /
1422
+ * `{{pascal}}` `{{token}}` placeholders for `@orkestrel/template`'s pure
1423
+ * `fillTemplate` LEAF. Only genuinely templated PROSE / source artifacts live
1424
+ * here — the token-collision boundary (AGENTS §14, this guide's Contract
1425
+ * invariant 3) keeps every STRUCTURAL file (`package.json`, the tsconfigs,
1426
+ * the vite configs) `computed` inside `blueprintToPlan` instead, so a literal
1427
+ * `{{…}}` in a config can never be mistaken for a placeholder. A convention
1428
+ * change here is a version bump of this package, never a hand-edit of a
1429
+ * scaffolded repo's copy.
1430
+ */
1431
+ var TEMPLATES = (() => {
1432
+ const EXPORT_KEYWORD = "export";
1433
+ const CONST_KEYWORD = "const";
1434
+ const IMPORT_KEYWORD = "import";
1435
+ const FUNCTION_KEYWORD = "function";
1436
+ return Object.freeze({
1437
+ readme: Object.freeze({
1438
+ id: "readme",
1439
+ name: "readme",
1440
+ summary: "The package root README — install, usage, and the guide's pointer.",
1441
+ category: "docs",
1442
+ placeholders: Object.freeze([Object.freeze({
1443
+ name: "name",
1444
+ description: "The lowercase-hyphen package name."
1445
+ }), Object.freeze({
1446
+ name: "pascal",
1447
+ description: "The PascalCase entity name."
1448
+ })]),
1449
+ content: `# @orkestrel/{{name}}
1450
+
1451
+ TODO: one-line description. Part of the \`@orkestrel\` line.
1452
+
1453
+ ## Install
1454
+
1455
+ \`\`\`sh
1456
+ npm install @orkestrel/{{name}}
1457
+ \`\`\`
1458
+
1459
+ ## Usage
1460
+
1461
+ \`\`\`ts
1462
+ import { create{{pascal}} } from '@orkestrel/{{name}}'
1463
+
1464
+ ${CONST_KEYWORD} instance = create{{pascal}}({ id: 'example' })
1465
+ \`\`\`
1466
+
1467
+ ## Guide
1468
+
1469
+ For the full surface, see [\`guides/src/{{name}}.md\`](guides/src/{{name}}.md).
1470
+
1471
+ ## License
1472
+
1473
+ MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
1474
+ `
1475
+ }),
1476
+ guide: Object.freeze({
1477
+ id: "guide",
1478
+ name: "guide",
1479
+ summary: "The package's own guide stub, with its Surface tables filled in.",
1480
+ category: "guides",
1481
+ placeholders: Object.freeze([
1482
+ Object.freeze({
1483
+ name: "name",
1484
+ description: "The lowercase-hyphen package name."
1485
+ }),
1486
+ Object.freeze({
1487
+ name: "pascal",
1488
+ description: "The PascalCase entity name."
1489
+ }),
1490
+ Object.freeze({
1491
+ name: "primary",
1492
+ description: "The primary Surface (`core` when declared, else the sole surface)."
1493
+ }),
1494
+ Object.freeze({
1495
+ name: "source",
1496
+ description: "The rendered \"Source: …\" fragment over every declared surface."
1497
+ }),
1498
+ Object.freeze({
1499
+ name: "barrel",
1500
+ description: "The rendered \"Surfaced through …\" sentence."
1501
+ }),
1502
+ Object.freeze({
1503
+ name: "tests",
1504
+ description: "The rendered per-surface Tests section body."
1505
+ }),
1506
+ Object.freeze({
1507
+ name: "factories",
1508
+ description: "The rendered Factories surface table."
1509
+ }),
1510
+ Object.freeze({
1511
+ name: "entities",
1512
+ description: "The rendered Entities surface table."
1513
+ }),
1514
+ Object.freeze({
1515
+ name: "types",
1516
+ description: "The rendered Types surface table."
1517
+ })
1518
+ ]),
1519
+ content: `# {{pascal}}
1520
+
1521
+ > TODO: one-paragraph description of \`{{pascal}}\` — what it is, what problem it
1522
+ > solves, and how it fits the \`@orkestrel\` line. Source: {{source}}.
1523
+ > {{barrel}}
1524
+
1525
+ ## Surface
1526
+
1527
+ TODO: a short intro line, then a minimal usage example:
1528
+
1529
+ \`\`\`ts
1530
+ import { create{{pascal}} } from '@src/{{primary}}'
1531
+
1532
+ ${CONST_KEYWORD} instance = create{{pascal}}({ id: 'example' })
1533
+ \`\`\`
1534
+
1535
+ ### Factories
1536
+
1537
+ {{factories}}
1538
+
1539
+ ### Entities
1540
+
1541
+ {{entities}}
1542
+
1543
+ ### Types
1544
+
1545
+ {{types}}
1546
+
1547
+ ## Tests
1548
+
1549
+ {{tests}}
1550
+
1551
+ ## See also
1552
+
1553
+ - [\`AGENTS.md\`](../../AGENTS.md) — the rules.
1554
+ - [\`guide.md\`](guide.md) — the mirrored guide for \`@orkestrel/guide\`, the
1555
+ devDependency powering this repo's guides-parity test suite.
1556
+ - [\`README.md\`](../README.md) — the guides index.
1557
+ `
1558
+ }),
1559
+ guidesReadme: Object.freeze({
1560
+ id: "guidesReadme",
1561
+ name: "guidesReadme",
1562
+ summary: "The dual-axis guides index — by concept and by directory.",
1563
+ category: "guides",
1564
+ placeholders: Object.freeze([Object.freeze({
1565
+ name: "concept",
1566
+ description: "The rendered by-concept index table."
1567
+ }), Object.freeze({
1568
+ name: "directory",
1569
+ description: "The rendered by-directory index table."
1570
+ })]),
1571
+ content: `# Guides
1572
+
1573
+ A dual-axis index into this repository's guides — by concept, and by directory (AGENTS §22).
1574
+
1575
+ ## By concept
1576
+
1577
+ {{concept}}
1578
+
1579
+ ## By directory
1580
+
1581
+ {{directory}}
1582
+
1583
+ ## Dependency reference
1584
+
1585
+ [\`src/guide.md\`](src/guide.md) is a byte-identical mirror of the guide for
1586
+ \`@orkestrel/guide\` — the devDependency powering this repo's guides-parity test
1587
+ suite (\`tests/guides/src/parity.test.ts\`). It documents **that package's**
1588
+ surface (\`Guide\` / \`Source\`, the manifest and comparison helpers), not anything
1589
+ sourced in this repo; it is kept here so a reader of the parity suite can see
1590
+ the primitives it is built from without leaving this guide set.
1591
+
1592
+ ## See also
1593
+
1594
+ - [\`AGENTS.md\`](../AGENTS.md) — the rules; §22 documentation-as-contracts.
1595
+ `
1596
+ }),
1597
+ types: Object.freeze({
1598
+ id: "types",
1599
+ name: "types",
1600
+ summary: "The generated-minimal `src/core/types.ts` stub.",
1601
+ category: "source",
1602
+ placeholders: Object.freeze([Object.freeze({
1603
+ name: "pascal",
1604
+ description: "The PascalCase entity name."
1605
+ })]),
1606
+ content: `/** Options for \`create{{pascal}}\`. */
1607
+ ${EXPORT_KEYWORD} interface {{pascal}}Options {
1608
+ readonly id?: string
1609
+ }
1610
+
1611
+ /** A working \`{{pascal}}\` — pure data, no behavior. */
1612
+ ${EXPORT_KEYWORD} interface {{pascal}}Interface {
1613
+ readonly id: string
1614
+ }
1615
+ `
1616
+ }),
1617
+ entity: Object.freeze({
1618
+ id: "entity",
1619
+ name: "entity",
1620
+ summary: "The generated-minimal `src/core/{Pascal}.ts` entity stub.",
1621
+ category: "source",
1622
+ placeholders: Object.freeze([Object.freeze({
1623
+ name: "pascal",
1624
+ description: "The PascalCase entity name."
1625
+ })]),
1626
+ content: `import type { {{pascal}}Interface, {{pascal}}Options } from './types.js'
1627
+
1628
+ /**
1629
+ * A working \`{{pascal}}\` — pure data, no behavior.
1630
+ *
1631
+ * @example
1632
+ * \`\`\`ts
1633
+ * const instance = new {{pascal}}({ id: 'example' })
1634
+ * \`\`\`
1635
+ */
1636
+ ${EXPORT_KEYWORD} class {{pascal}} implements {{pascal}}Interface {
1637
+ readonly id: string
1638
+
1639
+ constructor(options: {{pascal}}Options = {}) {
1640
+ this.id = typeof options.id === 'string' ? options.id : crypto.randomUUID()
1641
+ }
1642
+ }
1643
+ `
1644
+ }),
1645
+ factories: Object.freeze({
1646
+ id: "factories",
1647
+ name: "factories",
1648
+ summary: "The generated-minimal `src/core/factories.ts` stub.",
1649
+ category: "source",
1650
+ placeholders: Object.freeze([Object.freeze({
1651
+ name: "pascal",
1652
+ description: "The PascalCase entity name."
1653
+ })]),
1654
+ content: `import type { {{pascal}}Interface, {{pascal}}Options } from './types.js'
1655
+ import { {{pascal}} } from './{{pascal}}.js'
1656
+
1657
+ /**
1658
+ * Create a \`{{pascal}}Interface\`.
1659
+ *
1660
+ * @param options - An optional \`id\` (defaults to a random UUID)
1661
+ * @returns A working {@link {{pascal}}Interface}
1662
+ *
1663
+ * @example
1664
+ * \`\`\`ts
1665
+ * import { create{{pascal}} } from '@src/core'
1666
+ *
1667
+ * ${CONST_KEYWORD} instance = create{{pascal}}({ id: 'example' })
1668
+ * \`\`\`
1669
+ */
1670
+ ${EXPORT_KEYWORD} function create{{pascal}}(options: {{pascal}}Options = {}): {{pascal}}Interface {
1671
+ return new {{pascal}}(options)
1672
+ }
1673
+ `
1674
+ }),
1675
+ index: Object.freeze({
1676
+ id: "index",
1677
+ name: "index",
1678
+ summary: "The generated-minimal `src/core/index.ts` barrel stub.",
1679
+ category: "source",
1680
+ placeholders: Object.freeze([Object.freeze({
1681
+ name: "pascal",
1682
+ description: "The PascalCase entity name."
1683
+ })]),
1684
+ content: `export type * from './types.js'
1685
+ export * from './{{pascal}}.js'
1686
+ export * from './factories.js'
1687
+ `
1688
+ }),
1689
+ setup: Object.freeze({
1690
+ id: "setup",
1691
+ name: "setup",
1692
+ summary: "The generated-minimal `tests/setup.ts` recorder helper — no placeholders.",
1693
+ category: "tests",
1694
+ placeholders: Object.freeze([]),
1695
+ content: `// ── Call recorder (a real callback, not a mock) ──────────────────────────────
1696
+ //
1697
+ // AGENTS §16.1: when a test only needs to count calls or inspect arguments, use a
1698
+ // recorder — a real listener that records every invocation — rather than a test-
1699
+ // framework spy. \`handler\` is a genuine callback; \`calls\` is each invocation's
1700
+ // argument tuple, in order.
1701
+
1702
+ /** A real call-recording callback over an argument tuple (AGENTS §16.1). */
1703
+ ${EXPORT_KEYWORD} interface TestRecorderInterface<TArgs extends readonly unknown[]> {
1704
+ readonly calls: readonly TArgs[]
1705
+ readonly count: number
1706
+ readonly handler: (...args: TArgs) => void
1707
+ clear(): void
1708
+ }
1709
+
1710
+ /**
1711
+ * Create a {@link TestRecorderInterface} — a real callback that records each
1712
+ * invocation's arguments, for asserting what fired and with what (AGENTS §16.1).
1713
+ *
1714
+ * @typeParam TArgs - The argument tuple the recorded handler receives
1715
+ * @returns A recorder whose \`handler\` records into \`calls\`
1716
+ */
1717
+ ${EXPORT_KEYWORD} function createRecorder<TArgs extends readonly unknown[]>(): TestRecorderInterface<TArgs> {
1718
+ const calls: TArgs[] = []
1719
+ return {
1720
+ get calls() {
1721
+ return calls
1722
+ },
1723
+ get count() {
1724
+ return calls.length
1725
+ },
1726
+ handler(...args: TArgs) {
1727
+ calls.push(args)
1728
+ },
1729
+ clear() {
1730
+ calls.length = 0
1731
+ },
1732
+ }
1733
+ }
1734
+ `
1735
+ }),
1736
+ setupServer: Object.freeze({
1737
+ id: "setupServer",
1738
+ name: "setupServer",
1739
+ summary: "The generated-minimal `tests/setupServer.ts` node-only helper — no placeholders.",
1740
+ category: "tests",
1741
+ placeholders: Object.freeze([]),
1742
+ content: `// AGENTS §16.1: node-only test helpers anchor \`node:fs\` fixture loaders to
1743
+ // this workspace root rather than a relative path, so a loader works
1744
+ // regardless of the running test file's directory depth. Add server-specific
1745
+ // fixtures/helpers here as this surface grows beyond the shared recorder in
1746
+ // tests/setup.ts.
1747
+
1748
+ ${IMPORT_KEYWORD} { fileURLToPath } from 'node:url'
1749
+
1750
+ /** The workspace root, for anchoring \`node:fs\` fixture loaders (AGENTS §16.1). */
1751
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} WORKSPACE_ROOT = fileURLToPath(new URL('../', import.meta.url))
1752
+ `
1753
+ }),
1754
+ setupBrowser: Object.freeze({
1755
+ id: "setupBrowser",
1756
+ name: "setupBrowser",
1757
+ summary: "The generated-minimal `tests/setupBrowser.ts` DOM-only helper — no placeholders.",
1758
+ category: "tests",
1759
+ placeholders: Object.freeze([]),
1760
+ content: `// AGENTS §16.1: DOM/browser-only test helpers (builders, CSS assertion
1761
+ // primitives) go here as this surface grows fixtures beyond the shared
1762
+ // recorder in tests/setup.ts.
1763
+
1764
+ // TODO: [Browser] add browser/DOM test helpers as this surface grows.
1765
+ export {}
1766
+ `
1767
+ }),
1768
+ entityTest: Object.freeze({
1769
+ id: "entityTest",
1770
+ name: "entityTest",
1771
+ summary: "The generated-minimal `tests/src/<surface>/{Pascal}.test.ts` stub.",
1772
+ category: "tests",
1773
+ placeholders: Object.freeze([Object.freeze({
1774
+ name: "pascal",
1775
+ description: "The PascalCase entity name."
1776
+ }), Object.freeze({
1777
+ name: "surface",
1778
+ description: "The owning Surface (`core`/`browser`/`server`)."
1779
+ })]),
1780
+ content: `import type { {{pascal}}Interface } from '@src/{{surface}}'
1781
+ import { {{pascal}} } from '@src/{{surface}}'
1782
+ import { describe, expect, it } from 'vitest'
1783
+
1784
+ // The {{pascal}} entity — id assignment (explicit / generated) and independence
1785
+ // across instances. Factory-level assertions live in factories.test.ts.
1786
+
1787
+ describe('{{pascal}}', () => {
1788
+ it('round-trips an explicit id', () => {
1789
+ const instance: {{pascal}}Interface = new {{pascal}}({ id: 'example' })
1790
+
1791
+ expect(instance.id).toBe('example')
1792
+ })
1793
+
1794
+ it('generates a non-empty id when none is given', () => {
1795
+ const instance = new {{pascal}}()
1796
+
1797
+ expect(typeof instance.id).toBe('string')
1798
+ expect(instance.id.length).toBeGreaterThan(0)
1799
+ })
1800
+
1801
+ it('gives distinct instances distinct generated ids', () => {
1802
+ const a = new {{pascal}}()
1803
+ const b = new {{pascal}}()
1804
+
1805
+ expect(a.id).not.toBe(b.id)
1806
+ })
1807
+ })
1808
+ `
1809
+ }),
1810
+ factoriesTest: Object.freeze({
1811
+ id: "factoriesTest",
1812
+ name: "factoriesTest",
1813
+ summary: "The generated-minimal `tests/src/<surface>/factories.test.ts` stub.",
1814
+ category: "tests",
1815
+ placeholders: Object.freeze([Object.freeze({
1816
+ name: "pascal",
1817
+ description: "The PascalCase entity name."
1818
+ }), Object.freeze({
1819
+ name: "surface",
1820
+ description: "The owning Surface (`core`/`browser`/`server`)."
1821
+ })]),
1822
+ content: `import type { {{pascal}}Interface } from '@src/{{surface}}'
1823
+ import { create{{pascal}}, {{pascal}} } from '@src/{{surface}}'
1824
+ import { describe, expect, expectTypeOf, it } from 'vitest'
1825
+
1826
+ // The {{pascal}} factory — that \`create{{pascal}}\` returns a working {{pascal}}Interface
1827
+ // backed by a real {{pascal}} instance.
1828
+
1829
+ describe('create{{pascal}}', () => {
1830
+ it('returns a {{pascal}} instance', () => {
1831
+ const instance = create{{pascal}}()
1832
+
1833
+ expect(instance).toBeInstanceOf({{pascal}})
1834
+ })
1835
+
1836
+ it('honors the id option', () => {
1837
+ ${CONST_KEYWORD} instance = create{{pascal}}({ id: 'example' })
1838
+
1839
+ expect(instance.id).toBe('example')
1840
+ })
1841
+
1842
+ it('create{{pascal}} returns a {{pascal}}Interface', () => {
1843
+ expectTypeOf(create{{pascal}}()).toEqualTypeOf<{{pascal}}Interface>()
1844
+ })
1845
+ })
1846
+ `
1847
+ }),
1848
+ parityTest: Object.freeze({
1849
+ id: "parityTest",
1850
+ name: "parityTest",
1851
+ summary: "The consumer-side guides-parity drop-in — `tests/guides/src/parity.test.ts`.",
1852
+ category: "tests",
1853
+ placeholders: Object.freeze([Object.freeze({
1854
+ name: "name",
1855
+ description: "The lowercase-hyphen package name."
1856
+ }), Object.freeze({
1857
+ name: "specifiers",
1858
+ description: "The computed SELF_SPECIFIERS / SPECIFIER_MODULES / exportsFor block, one shape for every surface count."
1859
+ })]),
1860
+ content: `// The consumer-side guides-parity drop-in: runs \`@orkestrel/guide\`'s
1861
+ // checks against this repo's own \`guides/README.md\` manifest.
1862
+
1863
+ ${IMPORT_KEYWORD} { describe, expect, it } from 'vitest'
1864
+ ${IMPORT_KEYWORD} { readdirSync, readFileSync } from 'node:fs'
1865
+ ${IMPORT_KEYWORD} { fileURLToPath } from 'node:url'
1866
+ ${IMPORT_KEYWORD} { join } from 'node:path'
1867
+ ${IMPORT_KEYWORD} {
1868
+ createGuide,
1869
+ createSource,
1870
+ fenceImports,
1871
+ findMissing,
1872
+ findUnexampled,
1873
+ isExternalLink,
1874
+ missingSymbols,
1875
+ parseManifest,
1876
+ resolveLink,
1877
+ symbolKey,
1878
+ } from '@orkestrel/guide'
1879
+
1880
+ ${CONST_KEYWORD} ROOT = fileURLToPath(new URL('../../../', import.meta.url))
1881
+ ${CONST_KEYWORD} WALK_DIRS = ['src', 'guides', 'tests']
1882
+
1883
+ ${FUNCTION_KEYWORD} walk(dir: string, acc: Record<string, string>): void {
1884
+ for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
1885
+ const relative = \`\${dir}/\${entry.name}\`
1886
+ if (entry.isDirectory()) {
1887
+ walk(relative, acc)
1888
+ continue
1889
+ }
1890
+ if (!entry.name.endsWith('.ts') && !entry.name.endsWith('.md')) continue
1891
+ acc[relative] = readFileSync(join(ROOT, relative), 'utf8')
1892
+ }
1893
+ }
1894
+
1895
+ ${CONST_KEYWORD} files: Record<string, string> = {}
1896
+ for (const dir of WALK_DIRS) walk(dir, files)
1897
+ files['AGENTS.md'] = readFileSync(join(ROOT, 'AGENTS.md'), 'utf8')
1898
+
1899
+ ${FUNCTION_KEYWORD} readText(relative: string): string {
1900
+ const text = files[relative]
1901
+ if (text === undefined) throw new Error(\`Missing file: \${relative}\`)
1902
+ return text
1903
+ }
1904
+
1905
+ ${CONST_KEYWORD} manifest = parseManifest(readText('guides/README.md'), 'guides')
1906
+
1907
+ {{specifiers}}
1908
+
1909
+ it('manifest lists at least one guide', () => {
1910
+ expect(manifest.length).toBeGreaterThan(0)
1911
+ })
1912
+
1913
+ for (const entry of manifest) {
1914
+ const guide = createGuide(readText(entry.spec))
1915
+ const source = createSource({ files, module: entry.source })
1916
+
1917
+ describe(\`\${entry.concept}\`, () => {
1918
+ it('extracts a non-empty documented surface', () => {
1919
+ expect(guide.surface().length).toBeGreaterThan(0)
1920
+ })
1921
+ it('documents every source export', () => {
1922
+ expect(missingSymbols(source.exports(), guide.surface())).toEqual([])
1923
+ })
1924
+ it('documents only real exports', () => {
1925
+ expect(missingSymbols(guide.surface(), source.exports())).toEqual([])
1926
+ })
1927
+
1928
+ it('exposes no hidden module-scope declarations', () => {
1929
+ expect(source.hidden().map(symbolKey)).toEqual([])
1930
+ })
1931
+
1932
+ for (const group of guide.methods()) {
1933
+ const members = source.methods(group.interface)
1934
+ const entity = group.interface.replace(/Interface$/, '')
1935
+ describe(\`\${group.interface}\`, () => {
1936
+ it('documents at least one method', () => {
1937
+ expect(group.methods.length).toBeGreaterThan(0)
1938
+ })
1939
+ it('documents every interface method', () => {
1940
+ expect(findMissing(members, group.methods)).toEqual([])
1941
+ })
1942
+ it('documents no phantom method', () => {
1943
+ expect(findMissing(group.methods, members)).toEqual([])
1944
+ })
1945
+ it(\`\${entity} exposes no undocumented method\`, () => {
1946
+ const extra =
1947
+ entity === group.interface ? [] : findMissing(source.methods(entity), group.methods)
1948
+ expect(extra).toEqual([])
1949
+ })
1950
+ })
1951
+ }
1952
+
1953
+ it('documents an example for every Surface function', () => {
1954
+ const fences = guide.patterns()
1955
+ const names = guide
1956
+ .surface()
1957
+ .filter((symbol) => symbol.kind === 'function')
1958
+ .map((symbol) => symbol.name)
1959
+ expect(findUnexampled(names, fences, source.examples())).toEqual([])
1960
+ })
1961
+
1962
+ for (const group of guide.methods()) {
1963
+ const entity = group.interface.replace(/Interface$/, '')
1964
+ describe(\`\${group.interface} examples\`, () => {
1965
+ it('documents an example for every method', () => {
1966
+ const fences = guide.patterns()
1967
+ const examples =
1968
+ entity === group.interface
1969
+ ? source.examples(group.interface)
1970
+ : source.examples(group.interface).concat(source.examples(entity))
1971
+ expect(findUnexampled(group.methods, fences, examples)).toEqual([])
1972
+ })
1973
+ })
1974
+ }
1975
+
1976
+ it('imports only real exports in every \`\`\`ts fence', () => {
1977
+ for (const fence of guide.patterns()) {
1978
+ for (const { specifier, names } of fenceImports(fence)) {
1979
+ if (!SELF_SPECIFIERS.includes(specifier)) continue
1980
+ expect(findMissing(names, exportsFor(specifier))).toEqual([])
1981
+ }
1982
+ }
1983
+ })
1984
+
1985
+ it('resolves every relative link', () => {
1986
+ const broken = guide
1987
+ .links()
1988
+ .filter((href) => !isExternalLink(href))
1989
+ .map((href) => resolveLink(entry.spec, href))
1990
+ .filter((path) => !source.exists(path))
1991
+ expect(broken).toEqual([])
1992
+ })
1993
+ it('links only to test files that exist', () => {
1994
+ const missing = guide
1995
+ .tests()
1996
+ .map((href) => resolveLink(entry.spec, href))
1997
+ .filter((path) => !source.exists(path))
1998
+ expect(missing).toEqual([])
1999
+ })
2000
+ })
2001
+ }
2002
+ `
2003
+ })
2004
+ });
2005
+ })();
2006
+ //#endregion
2007
+ //#region src/core/compilers.ts
2008
+ /**
2009
+ * Resolve the `Group` a byte-copied `HOST_PATHS` entry belongs to.
2010
+ *
2011
+ * @param path - A `HOST_PATHS` entry.
2012
+ * @returns The owning `Group`.
2013
+ *
2014
+ * @example
2015
+ * ```ts
2016
+ * hostGroup('AGENTS.md') // 'docs'
2017
+ * hostGroup('.claude') // 'orchestration'
2018
+ * ```
2019
+ */
2020
+ function hostGroup(path) {
2021
+ if (path === "AGENTS.md" || path === "CLAUDE.md" || path === "LICENSE") return "docs";
2022
+ if (path === ".claude" || path === "scripts/deps.sh" || path === "scripts/cursor.sh" || path === "scripts/ollama.sh" || path === ".github/workflows/ci.yml") return "orchestration";
2023
+ if (path === "guides/src/guide.md" || path === "guides/src/scaffold.md") return "guides";
2024
+ return "configs";
2025
+ }
2026
+ /**
2027
+ * Fill one `TEMPLATES` entry into a `template`-origin `Artifact`, optionally
2028
+ * tagged with the owning `Surface` (source/tests artifacts that live under a
2029
+ * declared surface's tree).
2030
+ *
2031
+ * @param path - The artifact's output path.
2032
+ * @param group - The artifact's `Group`.
2033
+ * @param id - The `TEMPLATES` entry id to fill.
2034
+ * @param values - The placeholder values to fill the template with.
2035
+ * @param surface - The owning `Surface`, when the artifact lives under a declared surface's tree.
2036
+ * @returns The filled `template`-origin `Artifact`.
2037
+ *
2038
+ * @example
2039
+ * ```ts
2040
+ * fillArtifact('README.md', 'docs', 'readme', { name: 'router', pascal: 'Router' })
2041
+ * // { path: 'README.md', group: 'docs', origin: 'template', content: '# router\n…' }
2042
+ * ```
2043
+ */
2044
+ function fillArtifact(path, group, id, values, surface) {
2045
+ const definition = TEMPLATES[id];
2046
+ if (!definition) throw new Error(`Unknown template id: ${id}`);
2047
+ const content = (0, _orkestrel_template.fillTemplate)(definition.content, values, {
2048
+ missing: "error",
2049
+ placeholders: definition.placeholders
2050
+ });
2051
+ return surface === void 0 ? {
2052
+ path,
2053
+ group,
2054
+ origin: "template",
2055
+ content
2056
+ } : {
2057
+ path,
2058
+ group,
2059
+ origin: "template",
2060
+ surface,
2061
+ content
2062
+ };
2063
+ }
2064
+ /**
2065
+ * Classify a blueprint's surfaces into the manifest/exports variant class.
2066
+ *
2067
+ * @param surfaces - The declared `Surface[]`.
2068
+ * @returns The sole declared `Surface`, or `'multi'` when two or more are declared.
2069
+ *
2070
+ * @example
2071
+ * ```ts
2072
+ * surfaceVariant(['core']) // 'core'
2073
+ * surfaceVariant(['core', 'server']) // 'multi'
2074
+ * ```
2075
+ */
2076
+ function surfaceVariant(surfaces) {
2077
+ if (surfaces.length > 1) return "multi";
2078
+ const [only] = surfaces;
2079
+ return only ?? "core";
2080
+ }
2081
+ /**
2082
+ * Build the `main` / `module` / top-level `types` entry fields.
2083
+ *
2084
+ * @param surfaces - The declared `Surface[]`.
2085
+ * @returns The `package.json` `main` / `module` / optional `types` fields.
2086
+ *
2087
+ * @example
2088
+ * ```ts
2089
+ * entryFields(['browser']).main // './dist/src/browser/index.js'
2090
+ * ```
2091
+ */
2092
+ function entryFields(surfaces) {
2093
+ const variant = surfaceVariant(surfaces);
2094
+ if (variant === "multi") return {
2095
+ main: "./dist/src/core/index.cjs",
2096
+ module: "./dist/src/core/index.js"
2097
+ };
2098
+ const root = variant;
2099
+ if (root === "browser") return {
2100
+ main: "./dist/src/browser/index.js",
2101
+ module: "./dist/src/browser/index.js",
2102
+ types: "./dist/src/browser/index.d.ts"
2103
+ };
2104
+ if (root === "server") return {
2105
+ main: "./dist/src/server/index.cjs",
2106
+ module: "./dist/src/server/index.js",
2107
+ types: "./dist/src/server/index.d.ts"
2108
+ };
2109
+ return {
2110
+ main: "./dist/src/core/index.cjs",
2111
+ module: "./dist/src/core/index.js",
2112
+ types: "./dist/src/core/index.d.ts"
2113
+ };
2114
+ }
2115
+ /**
2116
+ * One dual-format (`import` + `require`) `exports` condition block.
2117
+ *
2118
+ * @param path - The extensionless dist path to point both conditions at.
2119
+ * @returns The dual `import`/`require` exports condition object.
2120
+ *
2121
+ * @example
2122
+ * ```ts
2123
+ * dualCondition('./dist/src/core/index')
2124
+ * // { import: { types: '….d.ts', default: '….js' }, require: { types: '….d.cts', default: '….cjs' } }
2125
+ * ```
2126
+ */
2127
+ function dualCondition(path) {
2128
+ return {
2129
+ import: {
2130
+ types: `${path}.d.ts`,
2131
+ default: `${path}.js`
2132
+ },
2133
+ require: {
2134
+ types: `${path}.d.cts`,
2135
+ default: `${path}.cjs`
2136
+ }
2137
+ };
2138
+ }
2139
+ /**
2140
+ * Build the `package.json` `exports` map.
2141
+ *
2142
+ * @param surfaces - The declared `Surface[]`.
2143
+ * @returns The `package.json` `exports` map.
2144
+ *
2145
+ * @example
2146
+ * ```ts
2147
+ * exportsMap(['core'])['.'] // dual import/require condition block
2148
+ * ```
2149
+ */
2150
+ function exportsMap(surfaces) {
2151
+ const variant = surfaceVariant(surfaces);
2152
+ if (variant === "browser") return {
2153
+ ".": {
2154
+ types: "./dist/src/browser/index.d.ts",
2155
+ import: "./dist/src/browser/index.js",
2156
+ default: "./dist/src/browser/index.js"
2157
+ },
2158
+ "./package.json": "./package.json"
2159
+ };
2160
+ if (variant === "server") return {
2161
+ ".": dualCondition("./dist/src/server/index"),
2162
+ "./package.json": "./package.json"
2163
+ };
2164
+ if (variant === "core") return {
2165
+ ".": dualCondition("./dist/src/core/index"),
2166
+ "./package.json": "./package.json"
2167
+ };
2168
+ const map = { ".": dualCondition("./dist/src/core/index") };
2169
+ for (const surface of surfaces) {
2170
+ if (surface === "core") continue;
2171
+ const row = SURFACE_MATRIX[surface];
2172
+ if (surface === "browser") {
2173
+ map[row.path] = { import: {
2174
+ types: "./dist/src/browser/index.d.ts",
2175
+ default: "./dist/src/browser/index.js"
2176
+ } };
2177
+ continue;
2178
+ }
2179
+ map[row.path] = dualCondition(`./dist/src/${surface}/index`);
2180
+ }
2181
+ map["./package.json"] = "./package.json";
2182
+ return map;
2183
+ }
2184
+ /**
2185
+ * A code-unit (not locale-sensitive) comparator — matches the `keywords` sort
2186
+ * and keeps ordering stable across locales/environments.
2187
+ *
2188
+ * @param a - The first string.
2189
+ * @param b - The second string.
2190
+ * @returns `-1` / `0` / `1` per code-unit order.
2191
+ *
2192
+ * @example
2193
+ * ```ts
2194
+ * [...['b', 'a']].sort(compareCodeUnit) // ['a', 'b']
2195
+ * ```
2196
+ */
2197
+ function compareCodeUnit(a, b) {
2198
+ return a < b ? -1 : a > b ? 1 : 0;
2199
+ }
2200
+ /**
2201
+ * The devDependency baseline — every repo in the line carries the same set
2202
+ * (`@vitest/browser-playwright` included regardless of a browser surface: both
2203
+ * @orkestrel/middleware, core+server, and @orkestrel/router, core+browser+server,
2204
+ * ship it — grounded, not conditional). A package's `extras` (code-unit sorted)
2205
+ * merge in on top, the extras' declared range winning on a name collision with
2206
+ * the baseline.
2207
+ *
2208
+ * @param extras - The blueprint's package-specific `extras` `Dependency[]`.
2209
+ * @returns The merged `devDependencies` record.
2210
+ *
2211
+ * @example
2212
+ * ```ts
2213
+ * devDependenciesFor([])['typescript'] // '^6.0.3'
2214
+ * ```
2215
+ */
2216
+ function devDependenciesFor(extras) {
2217
+ const baseline = {
2218
+ "@microsoft/api-extractor": "^7.58.11",
2219
+ "@orkestrel/guide": "^0.0.5",
2220
+ "@orkestrel/scaffold": SCAFFOLD_RANGE,
2221
+ "@types/node": "^26.1.1",
2222
+ "@vitest/browser-playwright": "^4.1.10",
2223
+ oxfmt: "^0.59.0",
2224
+ oxlint: "^1.74.0",
2225
+ typescript: "^6.0.3",
2226
+ vite: "^8.1.5",
2227
+ "vite-plugin-dts": "^5.0.3",
2228
+ vitest: "^4.1.10"
2229
+ };
2230
+ for (const extra of [...extras].sort((a, b) => compareCodeUnit(a.name, b.name))) baseline[extra.name] = extra.range;
2231
+ return baseline;
2232
+ }
2233
+ /**
2234
+ * Compute the `package.json` artifact's `content`, applying the manifest and
2235
+ * exports combination rules over a blueprint's surfaces — grounded against the
2236
+ * live @orkestrel/middleware (core+server) and @orkestrel/router
2237
+ * (core+browser+server) exemplars.
2238
+ *
2239
+ * @param spec - The `Blueprint` to derive the manifest from.
2240
+ * @returns The `package.json` file content, newline-terminated.
2241
+ *
2242
+ * @example
2243
+ * ```ts
2244
+ * packageManifest(blueprint('router')) // '{\n\t"name": "@orkestrel/router",\n…}\n'
2245
+ * ```
2246
+ */
2247
+ function packageManifest(spec) {
2248
+ const entry = entryFields(spec.surfaces);
2249
+ const dependencies = {};
2250
+ for (const dep of [...spec.dependencies].sort((a, b) => compareCodeUnit(a.name, b.name))) dependencies[dep.name] = dep.range;
2251
+ const peerDependencies = {};
2252
+ for (const peer of [...spec.peers].sort((a, b) => compareCodeUnit(a.name, b.name))) peerDependencies[peer.name] = peer.range;
2253
+ const peerDependenciesMeta = {};
2254
+ for (const peer of spec.peers) if (peer.optional === true) peerDependenciesMeta[peer.name] = { optional: true };
2255
+ const scripts = {
2256
+ clean: "node -e \"try{require('node:fs').rmSync('dist',{recursive:true,force:true})}catch{}\"",
2257
+ copy: "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
2258
+ "tmp:txt": "node -e \"const fs=require('node:fs'),p=require('node:path');function walk(d){for(const e of fs.readdirSync(d,{withFileTypes:true})){const f=p.join(d,e.name);if(e.isDirectory()){walk(f)}else if(!e.name.endsWith('.md')&&!e.name.endsWith('.txt')){const t=f+'.txt';if(!fs.existsSync(t)){fs.renameSync(f,t)}else{console.warn('Skipping '+f+' — target exists: '+t)}}}}try{walk('tmp')}catch(e){if(e.code!=='ENOENT')throw e}\"",
2259
+ scaffold: "scaffold",
2260
+ lint: "oxlint --config .oxlintrc.json --fix .",
2261
+ check: "tsc --noEmit --project tsconfig.json && npm run check:src",
2262
+ "check:src": spec.surfaces.map((surface) => `npm run check:src:${surface}`).join(" && ")
2263
+ };
2264
+ for (const surface of spec.surfaces) scripts[`check:src:${surface}`] = `tsc --noEmit -p configs/src/tsconfig.${surface}.json`;
2265
+ scripts.format = "oxfmt --config .oxfmtrc.json --write .";
2266
+ scripts["format:check"] = "oxfmt --config .oxfmtrc.json --check .";
2267
+ scripts["lint:check"] = "oxlint --config .oxlintrc.json .";
2268
+ scripts.test = "npm run test:src && npm run test:guides";
2269
+ scripts["test:src"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.surfaces.map((surface) => `--project src:${surface}`).join(" ");
2270
+ for (const surface of spec.surfaces) scripts[`test:src:${surface}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project src:${surface}`;
2271
+ scripts["test:guides"] = "vitest run --config vite.config.ts --reporter=dot --project guides";
2272
+ scripts.build = "npm run clean && npm run build:src";
2273
+ scripts["build:src"] = spec.surfaces.map((surface) => `npm run build:src:${surface}`).join(" && ");
2274
+ for (const surface of spec.surfaces) scripts[`build:src:${surface}`] = surface === "browser" ? `vite build --config configs/src/vite.${surface}.config.ts` : `vite build --config configs/src/vite.${surface}.config.ts && npm run copy dist/src/${surface}/index.d.ts dist/src/${surface}/index.d.cts`;
2275
+ scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test";
2276
+ const manifest = {
2277
+ name: `@orkestrel/${spec.name}`,
2278
+ version: spec.version,
2279
+ description: spec.description ?? "TODO: one-line description. Part of the @orkestrel line.",
2280
+ keywords: [...spec.keywords].sort(),
2281
+ homepage: `https://github.com/orkestrel/${spec.name}#readme`,
2282
+ bugs: `https://github.com/orkestrel/${spec.name}/issues`,
2283
+ license: "MIT",
2284
+ repository: {
2285
+ type: "git",
2286
+ url: `git+https://github.com/orkestrel/${spec.name}.git`
2287
+ },
2288
+ files: ["dist", "README.md"],
2289
+ type: "module",
2290
+ sideEffects: false,
2291
+ main: entry.main,
2292
+ module: entry.module,
2293
+ ...entry.types ? { types: entry.types } : {},
2294
+ exports: exportsMap(spec.surfaces),
2295
+ publishConfig: { access: "public" },
2296
+ scripts,
2297
+ dependencies,
2298
+ devDependencies: devDependenciesFor(spec.extras),
2299
+ ...Object.keys(peerDependencies).length > 0 ? { peerDependencies } : {},
2300
+ ...Object.keys(peerDependenciesMeta).length > 0 ? { peerDependenciesMeta } : {},
2301
+ engines: { node: spec.engines }
2302
+ };
2303
+ return `${JSON.stringify(manifest, void 0, " ")}\n`;
2304
+ }
2305
+ /**
2306
+ * The root `tsconfig.json` — one `@src/<surface>` path alias per declared
2307
+ * surface, in declared order.
2308
+ *
2309
+ * @param surfaces - The declared `Surface[]`.
2310
+ * @returns The root `tsconfig.json` file content, newline-terminated.
2311
+ *
2312
+ * @example
2313
+ * ```ts
2314
+ * rootTsconfig(['core']) // '{\n\t"compilerOptions": {…}\n}\n'
2315
+ * ```
2316
+ */
2317
+ function rootTsconfig(surfaces) {
2318
+ const paths = {};
2319
+ for (const surface of surfaces) paths[`@src/${surface}`] = [`./src/${surface}/index.ts`];
2320
+ return `${JSON.stringify({
2321
+ compilerOptions: {
2322
+ target: "ESNext",
2323
+ module: "ESNext",
2324
+ moduleResolution: "bundler",
2325
+ lib: [
2326
+ "ESNext",
2327
+ "DOM",
2328
+ "DOM.Iterable"
2329
+ ],
2330
+ types: [
2331
+ "node",
2332
+ "vite/client",
2333
+ "vitest/globals"
2334
+ ],
2335
+ moduleDetection: "force",
2336
+ resolveJsonModule: true,
2337
+ strict: true,
2338
+ noImplicitOverride: true,
2339
+ noFallthroughCasesInSwitch: true,
2340
+ forceConsistentCasingInFileNames: true,
2341
+ skipLibCheck: true,
2342
+ noEmit: true,
2343
+ paths
2344
+ },
2345
+ exclude: [
2346
+ "node_modules",
2347
+ "dist",
2348
+ "tmp"
2349
+ ]
2350
+ }, void 0, " ")}\n`;
2351
+ }
2352
+ /**
2353
+ * The rendered import / `resolve` header block every `rootViteConfig` shape
2354
+ * prefixes — the Playwright import lines + `createBrowserProvider` appear
2355
+ * only when `needsPlaywright`, per the three grounded `rootViteConfig`
2356
+ * shapes: unconditional for a multi-surface blueprint, conditional on the
2357
+ * sole surface being `'browser'` for a single non-`core` surface, absent for
2358
+ * `core`-only.
2359
+ *
2360
+ * @param needsPlaywright - Whether this shape ships a browser test project (and so needs Playwright).
2361
+ * @returns The rendered header block, newline-terminated.
2362
+ *
2363
+ * @example
2364
+ * ```ts
2365
+ * viteHeader(false).includes('@vitest/browser-playwright') // false
2366
+ * viteHeader(true).includes('@vitest/browser-playwright') // true
2367
+ * ```
2368
+ */
2369
+ function viteHeader(needsPlaywright) {
2370
+ const EXPORT_KEYWORD = "export";
2371
+ return `import type { UserConfig } from 'vite'
2372
+ import { defineConfig, mergeConfig } from 'vitest/config'
2373
+ import tsconfig from './tsconfig.json' with { type: 'json' }
2374
+ import { fileURLToPath, URL } from 'node:url'
2375
+ ${needsPlaywright ? `import { globSync } from 'node:fs'
2376
+ import { playwright } from '@vitest/browser-playwright'
2377
+ ` : ""}
2378
+ ${EXPORT_KEYWORD} function resolveWorkspacePath(relativePath: string): string {
2379
+ return fileURLToPath(new URL(relativePath, import.meta.url))
2380
+ }
2381
+ ${needsPlaywright ? `
2382
+ ${EXPORT_KEYWORD} function createBrowserProvider() {
2383
+ const { PLAYWRIGHT_EXECUTABLE_PATH, PLAYWRIGHT_WS_ENDPOINT, PLAYWRIGHT_CHANNEL } = process.env
2384
+ if (PLAYWRIGHT_EXECUTABLE_PATH)
2385
+ return playwright({ launchOptions: { executablePath: PLAYWRIGHT_EXECUTABLE_PATH } })
2386
+ if (PLAYWRIGHT_WS_ENDPOINT)
2387
+ return playwright({ connectOptions: { wsEndpoint: PLAYWRIGHT_WS_ENDPOINT } })
2388
+ if (PLAYWRIGHT_CHANNEL) return playwright({ launchOptions: { channel: PLAYWRIGHT_CHANNEL } })
2389
+ if (process.platform === 'linux') {
2390
+ for (const pattern of [
2391
+ '/opt/pw-browsers/chromium',
2392
+ '/opt/pw-browsers/chromium-*/chrome-linux64/chrome',
2393
+ '/opt/pw-browsers/chromium-*/chrome-linux/chrome',
2394
+ ]) {
2395
+ const [executablePath] = globSync(pattern).sort().reverse()
2396
+ if (executablePath) return playwright({ launchOptions: { executablePath } })
2397
+ }
2398
+ }
2399
+ const channel = process.platform === 'win32' ? 'msedge' : 'chrome'
2400
+ return playwright({ launchOptions: { channel } })
2401
+ }
2402
+ ` : ""}
2403
+ const resolve = {
2404
+ alias: Object.entries(tsconfig.compilerOptions.paths).reduce(
2405
+ (a, [k, v]) => Object.assign(a, { [k]: resolveWorkspacePath(v[0]) }),
2406
+ {},
2407
+ ),
2408
+ }
2409
+ `;
2410
+ }
2411
+ /**
2412
+ * The single non-`core` surface's factory IS the base (Shape 3 of
2413
+ * `rootViteConfig`) — the surface's own `viteHeader` (Playwright only when
2414
+ * `surface === 'browser'`, per the live sqlite/indexeddb exemplars) prefixes
2415
+ * the surface-specific `srcBrowser` / `srcServer` + `guides` projects export.
2416
+ *
2417
+ * @param surface - The sole declared non-`core` surface.
2418
+ * @returns The root `vite.config.ts` file content for a single non-`core` surface, newline-terminated.
2419
+ *
2420
+ * @example
2421
+ * ```ts
2422
+ * singleSurfaceViteConfig('server').includes('srcServer') // true
2423
+ * ```
2424
+ */
2425
+ function singleSurfaceViteConfig(surface) {
2426
+ const EXPORT_KEYWORD = "export";
2427
+ const header = viteHeader(surface === "browser");
2428
+ if (surface === "browser") return `${header}
2429
+ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
2430
+ mergeConfig(
2431
+ {
2432
+ resolve,
2433
+ build: {
2434
+ emptyOutDir: true,
2435
+ sourcemap: true,
2436
+ minify: false,
2437
+ lib: {
2438
+ entry: resolveWorkspacePath('src/browser/index.ts'),
2439
+ formats: ['es'],
2440
+ fileName: () => 'index.js',
2441
+ },
2442
+ outDir: 'dist/src/browser',
2443
+ rolldownOptions: {
2444
+ external: (id: string) => id.startsWith('@orkestrel/'),
2445
+ },
2446
+ },
2447
+ test: {
2448
+ name: { label: 'src:browser', color: 'yellow' },
2449
+ include: ['tests/src/browser/**/*.test.ts'],
2450
+ setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
2451
+ browser: {
2452
+ enabled: true,
2453
+ provider: createBrowserProvider(),
2454
+ instances: [{ browser: 'chromium', headless: true }],
2455
+ },
2456
+ fileParallelism: false,
2457
+ },
2458
+ },
2459
+ config ?? {},
2460
+ )
2461
+
2462
+ ${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
2463
+ srcBrowser(
2464
+ mergeConfig(
2465
+ {
2466
+ test: {
2467
+ name: { label: 'guides', color: 'green' },
2468
+ include: ['tests/guides/**/*.test.ts'],
2469
+ exclude: ['tests/src/**/*.test.ts', 'tests/setup.test.ts'],
2470
+ environment: 'node',
2471
+ browser: { enabled: false },
2472
+ },
2473
+ },
2474
+ config ?? {},
2475
+ ),
2476
+ )
2477
+
2478
+ export default defineConfig({
2479
+ resolve,
2480
+ test: {
2481
+ projects: [srcBrowser, guides],
2482
+ },
2483
+ })
2484
+ `;
2485
+ return `${header}
2486
+ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
2487
+ mergeConfig(
2488
+ {
2489
+ resolve,
2490
+ build: {
2491
+ emptyOutDir: true,
2492
+ sourcemap: true,
2493
+ minify: false,
2494
+ lib: {
2495
+ entry: resolveWorkspacePath('src/server/index.ts'),
2496
+ formats: ['es', 'cjs'],
2497
+ fileName: (format: string) => (format === 'es' ? 'index.js' : 'index.cjs'),
2498
+ },
2499
+ outDir: 'dist/src/server',
2500
+ target: 'node24',
2501
+ rolldownOptions: {
2502
+ external: (id: string) => id.startsWith('node:') || id.startsWith('@orkestrel/'),
2503
+ },
2504
+ },
2505
+ test: {
2506
+ name: { label: 'src:server', color: 'red' },
2507
+ include: ['tests/src/server/**/*.test.ts'],
2508
+ setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
2509
+ environment: 'node',
2510
+ browser: { enabled: false },
2511
+ },
2512
+ },
2513
+ config ?? {},
2514
+ )
2515
+
2516
+ ${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
2517
+ srcServer(
2518
+ mergeConfig(
2519
+ {
2520
+ test: {
2521
+ name: { label: 'guides', color: 'green' },
2522
+ include: ['tests/guides/**/*.test.ts'],
2523
+ exclude: ['tests/src/**/*.test.ts', 'tests/setup.test.ts'],
2524
+ },
2525
+ },
2526
+ config ?? {},
2527
+ ),
2528
+ )
2529
+
2530
+ export default defineConfig({
2531
+ resolve,
2532
+ test: {
2533
+ projects: [srcServer, guides],
2534
+ },
2535
+ })
2536
+ `;
2537
+ }
2538
+ /**
2539
+ * The root `vite.config.ts` — three grounded shapes, chosen by a blueprint's
2540
+ * `surfaces`:
2541
+ * 1. `core`-only — `srcCore` + `guides`, no Playwright at all (the live
2542
+ * timeout exemplar: no browser project exists anywhere in the file).
2543
+ * 2. Multi-surface (2+ surfaces, always including `core` per the live
2544
+ * middleware/router exemplars) — `srcCore` is the shared base;
2545
+ * `srcBrowser` / `srcServer` extend it and externalize `@src/core` to
2546
+ * the sibling build. Playwright ships UNCONDITIONALLY (middleware
2547
+ * carries it with no browser surface — grounded, not conditional).
2548
+ * 3. A single non-`core` surface (`browser`-only / `server`-only) — the
2549
+ * surface factory itself IS the base (no `srcCore` to extend, so no
2550
+ * dead `@src/core` externalize/remap either — there is no sibling
2551
+ * core build), per the live sqlite (server-only) / indexeddb
2552
+ * (browser-only) exemplars. Playwright ships only when the sole
2553
+ * surface is `browser` (it must run its own tests in a real browser).
2554
+ *
2555
+ * @param surfaces - The declared `Surface[]`.
2556
+ * @returns The root `vite.config.ts` file content, newline-terminated.
2557
+ *
2558
+ * @example
2559
+ * ```ts
2560
+ * rootViteConfig(['core']).includes('srcCore') // true
2561
+ * ```
2562
+ */
2563
+ function rootViteConfig(surfaces) {
2564
+ const EXPORT_KEYWORD = "export";
2565
+ const hasCore = surfaces.includes("core");
2566
+ const nonCore = surfaces.filter((surface) => surface !== "core");
2567
+ const header = viteHeader(surfaces.length > 1 || surfaces.includes("browser"));
2568
+ if (!hasCore) {
2569
+ const [onlySurface] = nonCore;
2570
+ if (onlySurface === "browser" || onlySurface === "server") return singleSurfaceViteConfig(onlySurface);
2571
+ }
2572
+ const browserBlock = `
2573
+ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
2574
+ srcCore(
2575
+ mergeConfig(
2576
+ {
2577
+ build: {
2578
+ lib: {
2579
+ entry: resolveWorkspacePath('src/browser/index.ts'),
2580
+ formats: ['es'],
2581
+ fileName: () => 'index.js',
2582
+ },
2583
+ outDir: 'dist/src/browser',
2584
+ rolldownOptions: {
2585
+ external: (id: string) => id === '@src/core' || id.startsWith('@orkestrel/'),
2586
+ output: { paths: { '@src/core': '../core/index.js' } },
2587
+ },
2588
+ },
2589
+ test: {
2590
+ name: { label: 'src:browser', color: 'yellow' },
2591
+ include: ['tests/src/browser/**/*.test.ts'],
2592
+ exclude: ['tests/src/core/**/*.test.ts'],
2593
+ setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
2594
+ browser: {
2595
+ enabled: true,
2596
+ provider: createBrowserProvider(),
2597
+ instances: [{ browser: 'chromium', headless: true }],
2598
+ },
2599
+ fileParallelism: false,
2600
+ },
2601
+ },
2602
+ config ?? {},
2603
+ ),
2604
+ )
2605
+ `;
2606
+ const serverBlock = `
2607
+ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
2608
+ srcCore(
2609
+ mergeConfig(
2610
+ {
2611
+ build: {
2612
+ lib: {
2613
+ entry: resolveWorkspacePath('src/server/index.ts'),
2614
+ formats: ['es', 'cjs'],
2615
+ fileName: (format: string) => (format === 'es' ? 'index.js' : 'index.cjs'),
2616
+ },
2617
+ outDir: 'dist/src/server',
2618
+ target: 'node24',
2619
+ rolldownOptions: {
2620
+ external: (id: string) =>
2621
+ id === '@src/core' || id.startsWith('node:') || id.startsWith('@orkestrel/'),
2622
+ output: [
2623
+ {
2624
+ format: 'es',
2625
+ entryFileNames: 'index.js',
2626
+ paths: { '@src/core': '../core/index.js' },
2627
+ },
2628
+ {
2629
+ format: 'cjs',
2630
+ entryFileNames: 'index.cjs',
2631
+ paths: { '@src/core': '../core/index.cjs' },
2632
+ },
2633
+ ],
2634
+ },
2635
+ },
2636
+ test: {
2637
+ name: { label: 'src:server', color: 'red' },
2638
+ include: ['tests/src/server/**/*.test.ts'],
2639
+ exclude: ['tests/src/core/**/*.test.ts'],
2640
+ setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
2641
+ },
2642
+ },
2643
+ config ?? {},
2644
+ ),
2645
+ )
2646
+ `;
2647
+ return `${header}
2648
+ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
2649
+ mergeConfig(
2650
+ {
2651
+ resolve,
2652
+ build: {
2653
+ emptyOutDir: true,
2654
+ sourcemap: true,
2655
+ minify: false,
2656
+ },
2657
+ test: {
2658
+ name: { label: 'src:core', color: 'magenta' },
2659
+ include: ['tests/src/core/**/*.test.ts'],
2660
+ setupFiles: ['./tests/setup.ts'],
2661
+ environment: 'node',
2662
+ browser: { enabled: false },
2663
+ },
2664
+ },
2665
+ config ?? {},
2666
+ )
2667
+
2668
+ ${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
2669
+ srcCore(
2670
+ mergeConfig(
2671
+ {
2672
+ test: {
2673
+ name: { label: 'guides', color: 'green' },
2674
+ include: ['tests/guides/**/*.test.ts'],
2675
+ exclude: ['tests/src/**/*.test.ts', 'tests/setup.test.ts'],
2676
+ },
2677
+ },
2678
+ config ?? {},
2679
+ ),
2680
+ )
2681
+ ${nonCore.map((surface) => surface === "browser" ? browserBlock : serverBlock).join("")}
2682
+ export default defineConfig({
2683
+ resolve,
2684
+ test: {
2685
+ projects: [${[
2686
+ ...hasCore ? ["srcCore"] : [],
2687
+ ...nonCore.map((surface) => `src${pascalCase(surface)}`),
2688
+ "guides"
2689
+ ].join(", ")}],
2690
+ },
2691
+ })
2692
+ `;
2693
+ }
2694
+ /**
2695
+ * `configs/src/tsconfig.core.json` — unchanged core shape.
2696
+ *
2697
+ * @returns The core surface `tsconfig` file content, newline-terminated.
2698
+ *
2699
+ * @example
2700
+ * ```ts
2701
+ * coreTsconfig().includes('"rootDir": "../../src/core"') // true
2702
+ * ```
2703
+ */
2704
+ function coreTsconfig() {
2705
+ return `${JSON.stringify({
2706
+ extends: "../../tsconfig.json",
2707
+ compilerOptions: {
2708
+ lib: ["ESNext"],
2709
+ noEmit: false,
2710
+ declaration: true,
2711
+ emitDeclarationOnly: true,
2712
+ rootDir: "../../src/core",
2713
+ outDir: "../../dist/src/core"
2714
+ },
2715
+ include: ["../../src/core/**/*.ts"]
2716
+ }, void 0, " ")}\n`;
2717
+ }
2718
+ /**
2719
+ * `configs/src/vite.core.config.ts` — inlines its own `build.lib` /
2720
+ * `rollupOptions` (core's `srcCore` root export carries no build.lib).
2721
+ *
2722
+ * @returns The core surface `vite.config.ts` file content, newline-terminated.
2723
+ *
2724
+ * @example
2725
+ * ```ts
2726
+ * coreViteConfig().includes('srcCore(') // true
2727
+ * ```
2728
+ */
2729
+ function coreViteConfig() {
2730
+ return `import { defineConfig } from 'vite'
2731
+ import dts from 'vite-plugin-dts'
2732
+ import { srcCore, resolveWorkspacePath } from '../../vite.config'
2733
+
2734
+ export default defineConfig(
2735
+ srcCore({
2736
+ plugins: [
2737
+ dts({
2738
+ tsconfigPath: resolveWorkspacePath('configs/src/tsconfig.core.json'),
2739
+ bundleTypes: true,
2740
+ }),
2741
+ ],
2742
+ build: {
2743
+ lib: {
2744
+ entry: resolveWorkspacePath('src/core/index.ts'),
2745
+ formats: ['es', 'cjs'],
2746
+ fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs'),
2747
+ },
2748
+ outDir: 'dist/src/core',
2749
+ rollupOptions: {
2750
+ external: [/^node:/, /^@orkestrel\\//],
2751
+ },
2752
+ },
2753
+ }),
2754
+ )
2755
+ `;
2756
+ }
2757
+ /**
2758
+ * `configs/src/tsconfig.<browser|server>.json` — `rootDir`/`outDir` point at
2759
+ * the whole `src`/`dist/src` tree (not a per-surface subfolder), per the live
2760
+ * middleware/router exemplars.
2761
+ *
2762
+ * @param surface - The non-`core` surface to derive the `tsconfig` for.
2763
+ * @returns The surface `tsconfig` file content, newline-terminated.
2764
+ *
2765
+ * @example
2766
+ * ```ts
2767
+ * surfaceTsconfig('server').includes('"rootDir": "../../src"') // true
2768
+ * ```
2769
+ */
2770
+ function surfaceTsconfig(surface) {
2771
+ const config = {
2772
+ extends: "../../tsconfig.json",
2773
+ compilerOptions: {
2774
+ lib: surface === "browser" ? [
2775
+ "ESNext",
2776
+ "DOM",
2777
+ "DOM.Iterable"
2778
+ ] : ["ESNext"],
2779
+ types: surface === "browser" ? ["vite/client"] : ["node"],
2780
+ noEmit: false,
2781
+ declaration: true,
2782
+ emitDeclarationOnly: true,
2783
+ rootDir: "../../src",
2784
+ outDir: "../../dist/src"
2785
+ },
2786
+ include: [`../../src/${surface}/**/*.ts`]
2787
+ };
2788
+ return `${JSON.stringify(config, void 0, " ")}\n`;
2789
+ }
2790
+ /**
2791
+ * `configs/src/vite.<browser|server>.config.ts` — a thin `dts`-only wrapper;
2792
+ * `build.lib` / externals live in the root `srcBrowser` / `srcServer` export
2793
+ * instead (per the live exemplars).
2794
+ *
2795
+ * @param surface - The non-`core` surface to derive the `vite.config.ts` for.
2796
+ * @returns The surface `vite.config.ts` file content, newline-terminated.
2797
+ *
2798
+ * @example
2799
+ * ```ts
2800
+ * surfaceViteConfig('browser').includes('srcBrowser') // true
2801
+ * ```
2802
+ */
2803
+ function surfaceViteConfig(surface) {
2804
+ const anchor = surface === "browser" ? "srcBrowser" : "srcServer";
2805
+ return `import { defineConfig } from 'vite'
2806
+ import dts from 'vite-plugin-dts'
2807
+ import { ${anchor}, resolveWorkspacePath } from '../../vite.config'
2808
+
2809
+ // Types are bundled inline by vite-plugin-dts (see configs/src/vite.core.config.ts
2810
+ // for the same pattern).
2811
+ export default defineConfig(
2812
+ ${anchor}({
2813
+ plugins: [
2814
+ dts({
2815
+ tsconfigPath: resolveWorkspacePath('configs/src/tsconfig.${surface}.json'),
2816
+ bundleTypes: true,
2817
+ }),
2818
+ ],
2819
+ }),
2820
+ )
2821
+ `;
2822
+ }
2823
+ /**
2824
+ * Draft the `configs` group's `computed` artifacts — the root
2825
+ * `tsconfig.json` / `vite.config.ts` plus each declared surface's
2826
+ * `configs/src/*` pair, grounded against the live middleware (core+server)
2827
+ * and router (core+browser+server) exemplars.
2828
+ *
2829
+ * @param spec - The `Blueprint` to derive config artifacts from.
2830
+ * @returns The `configs` group's `Artifact[]`.
2831
+ *
2832
+ * @example
2833
+ * ```ts
2834
+ * configArtifacts(blueprint('router')).length // 4
2835
+ * ```
2836
+ */
2837
+ function configArtifacts(spec) {
2838
+ const artifacts = [{
2839
+ path: "tsconfig.json",
2840
+ group: "configs",
2841
+ origin: "computed",
2842
+ content: rootTsconfig(spec.surfaces)
2843
+ }, {
2844
+ path: "vite.config.ts",
2845
+ group: "configs",
2846
+ origin: "computed",
2847
+ content: rootViteConfig(spec.surfaces)
2848
+ }];
2849
+ for (const surface of spec.surfaces) {
2850
+ const row = SURFACE_MATRIX[surface];
2851
+ for (const path of row.configs) {
2852
+ const isTsconfig = path.endsWith(".json");
2853
+ const content = surface === "core" ? isTsconfig ? coreTsconfig() : coreViteConfig() : isTsconfig ? surfaceTsconfig(surface) : surfaceViteConfig(surface);
2854
+ artifacts.push({
2855
+ path,
2856
+ group: "configs",
2857
+ origin: "computed",
2858
+ surface,
2859
+ content
2860
+ });
2861
+ }
2862
+ }
2863
+ return artifacts;
2864
+ }
2865
+ /**
2866
+ * Draft the `source` group's `template` artifacts — the generated-minimal
2867
+ * `src/<surface>/*` stubs, one full {types, <Pascal>, factories, index} set
2868
+ * PER declared surface (never assuming `core`), filled from `TEMPLATES` with
2869
+ * `missing: 'error'`. `blueprintToMembers` already declares a full entity +
2870
+ * factory per surface (AGENTS §5's per-surface centralized-file pattern), so
2871
+ * every surface gets the same uniform stub shape.
2872
+ *
2873
+ * @param spec - The `Blueprint` to derive source stubs from.
2874
+ * @param pascal - The package's PascalCase entity name.
2875
+ * @returns The `source` group's `Artifact[]`.
2876
+ *
2877
+ * @example
2878
+ * ```ts
2879
+ * sourceArtifacts(blueprint('router'), 'Router').length // 4
2880
+ * ```
2881
+ */
2882
+ function sourceArtifacts(spec, pascal) {
2883
+ const values = { pascal };
2884
+ const artifacts = [];
2885
+ for (const surface of spec.surfaces) artifacts.push(fillArtifact(`src/${surface}/types.ts`, "source", "types", values, surface), fillArtifact(`src/${surface}/${pascal}.ts`, "source", "entity", values, surface), fillArtifact(`src/${surface}/factories.ts`, "source", "factories", values, surface), fillArtifact(`src/${surface}/index.ts`, "source", "index", values, surface));
2886
+ return artifacts;
2887
+ }
2888
+ /**
2889
+ * Build the computed `SELF_SPECIFIERS` / `SPECIFIER_MODULES` / `exportsFor`
2890
+ * block the `parityTest` template's `{{specifiers}}` placeholder fills —
2891
+ * ONE shape for every surface count (grounded against the live single-surface
2892
+ * websocket/indexeddb and multi-surface router/middleware exemplars, which
2893
+ * both resolve a fence's specifier through a `SPECIFIER_MODULES` map rather
2894
+ * than a single-module lookup). The bare `@orkestrel/<name>` specifier
2895
+ * resolves to the PRIMARY surface — `core` when declared, else the sole
2896
+ * declared surface.
2897
+ *
2898
+ * @param spec - The `Blueprint` to derive the parity specifiers block from.
2899
+ * @returns The computed `parityTest` `{{specifiers}}` block content.
2900
+ *
2901
+ * @example
2902
+ * ```ts
2903
+ * paritySpecifiers(blueprint('router')).includes('SELF_SPECIFIERS') // true
2904
+ * ```
2905
+ */
2906
+ function paritySpecifiers(spec) {
2907
+ const CONST_KEYWORD = "const";
2908
+ const FUNCTION_KEYWORD = "function";
2909
+ const primary = spec.surfaces.includes("core") ? "core" : spec.surfaces[0] ?? "core";
2910
+ const packageSpecifier = `@orkestrel/${spec.name}`;
2911
+ const selfSpecifiers = [packageSpecifier, ...spec.surfaces.map((surface) => `@src/${surface}`)];
2912
+ const modules = { [packageSpecifier]: `src/${primary}` };
2913
+ for (const surface of spec.surfaces) modules[`@src/${surface}`] = `src/${surface}`;
2914
+ return `const SELF_SPECIFIERS = [${selfSpecifiers.map((specifier) => `'${specifier}'`).join(", ")}]
2915
+
2916
+ ${CONST_KEYWORD} SPECIFIER_MODULES: Readonly<Record<string, string>> = {
2917
+ ${Object.entries(modules).map(([specifier, module]) => `\t'${specifier}': '${module}',`).join("\n")}
2918
+ }
2919
+ ${CONST_KEYWORD} specifierSources = new Map<string, ReturnType<typeof createSource>>()
2920
+ ${FUNCTION_KEYWORD} exportsFor(specifier: string): readonly string[] {
2921
+ const module = SPECIFIER_MODULES[specifier]
2922
+ if (module === undefined) return []
2923
+ let source = specifierSources.get(module)
2924
+ if (source === undefined) {
2925
+ source = createSource({ files, module })
2926
+ specifierSources.set(module, source)
2927
+ }
2928
+ return source.exports().map((symbol) => symbol.name)
2929
+ }`;
2930
+ }
2931
+ /**
2932
+ * Draft the `tests` group's `template` artifacts — the shared recorder
2933
+ * setup, one environment-specific setup file per non-`core` surface
2934
+ * (`setupServer.ts` / `setupBrowser.ts`, grounded against the live
2935
+ * exemplars' setup-file naming), the generated-minimal entity / factory test
2936
+ * stubs PER declared surface, and the surface-aware guides-parity drop-in.
2937
+ *
2938
+ * @param spec - The `Blueprint` to derive test stubs from.
2939
+ * @param pascal - The package's PascalCase entity name.
2940
+ * @returns The `tests` group's `Artifact[]`.
2941
+ *
2942
+ * @example
2943
+ * ```ts
2944
+ * testArtifacts(blueprint('router'), 'Router').length // 3
2945
+ * ```
2946
+ */
2947
+ function testArtifacts(spec, pascal) {
2948
+ const artifacts = [fillArtifact("tests/setup.ts", "tests", "setup", {})];
2949
+ if (spec.surfaces.includes("server")) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
2950
+ if (spec.surfaces.includes("browser")) artifacts.push(fillArtifact("tests/setupBrowser.ts", "tests", "setupBrowser", {}, "browser"));
2951
+ for (const surface of spec.surfaces) {
2952
+ const values = {
2953
+ pascal,
2954
+ surface
2955
+ };
2956
+ artifacts.push(fillArtifact(`tests/src/${surface}/${pascal}.test.ts`, "tests", "entityTest", values, surface), fillArtifact(`tests/src/${surface}/factories.test.ts`, "tests", "factoriesTest", values, surface));
2957
+ }
2958
+ artifacts.push(fillArtifact("tests/guides/src/parity.test.ts", "tests", "parityTest", {
2959
+ name: spec.name,
2960
+ specifiers: paritySpecifiers(spec)
2961
+ }));
2962
+ return artifacts;
2963
+ }
2964
+ /**
2965
+ * Build an `alignTable` markdown table over a member category's rows, deduped
2966
+ * by name — `blueprintToMembers` declares one full member set PER surface, so
2967
+ * a multi-surface blueprint carries byte-identical name/summary rows once per
2968
+ * surface; the one guide (AGENTS §22) lists each declared member once,
2969
+ * grouped across its surfaces.
2970
+ *
2971
+ * @param category - The `Member['category']` to filter rows by.
2972
+ * @param members - The blueprint's derived `Member[]` (previously closed over by the caller).
2973
+ * @returns The aligned markdown table for the category's deduped members.
2974
+ *
2975
+ * @example
2976
+ * ```ts
2977
+ * guideMemberTable('entity', blueprintToMembers(blueprint('router'))).includes('Router') // true
2978
+ * ```
2979
+ */
2980
+ function guideMemberTable(category, members) {
2981
+ const seen = /* @__PURE__ */ new Set();
2982
+ const rows = [];
2983
+ for (const item of members) {
2984
+ if (item.category !== category) continue;
2985
+ if (seen.has(item.name)) continue;
2986
+ seen.add(item.name);
2987
+ rows.push([`\`${item.name}\``, item.summary]);
2988
+ }
2989
+ return alignTable(["API", "Summary"], rows);
2990
+ }
2991
+ /**
2992
+ * Draft the `guides` group's artifacts — the package's own filled guide stub,
2993
+ * the guides index, and any vendored dependency guide mirrors.
2994
+ *
2995
+ * @param spec - The `Blueprint` to derive guide artifacts from.
2996
+ * @param pascal - The package's PascalCase entity name.
2997
+ * @param members - The blueprint's derived `Member[]`.
2998
+ * @returns The `guides` group's `Artifact[]`.
2999
+ *
3000
+ * @example
3001
+ * ```ts
3002
+ * guideArtifacts(blueprint('router'), 'Router', blueprintToMembers(blueprint('router'))).length // 2
3003
+ * ```
3004
+ */
3005
+ function guideArtifacts(spec, pascal, members) {
3006
+ const vendoredGuides = [
3007
+ "@orkestrel/contract",
3008
+ "@orkestrel/emitter",
3009
+ "@orkestrel/markdown",
3010
+ "@orkestrel/template",
3011
+ "@orkestrel/terminal",
3012
+ "@orkestrel/console",
3013
+ "@orkestrel/guide"
3014
+ ];
3015
+ const primary = spec.surfaces.includes("core") ? "core" : spec.surfaces[0] ?? "core";
3016
+ const source = spec.surfaces.map((surface) => `[\`src/${surface}\`](../../src/${surface})`).join(", ");
3017
+ const barrel = spec.surfaces.length === 1 ? `Surfaced through the \`@src/${primary}\` barrel.` : `Surfaced through the \`@orkestrel/${spec.name}\` barrel (aliased ${spec.surfaces.map((surface) => `\`@src/${surface}\``).join(" / ")} inside this repo).`;
3018
+ const tests = spec.surfaces.map((surface) => `- [\`tests/src/${surface}/${pascal}.test.ts\`](../../tests/src/${surface}/${pascal}.test.ts) —\n id assignment (explicit / generated) and independence across instances.\n- [\`tests/src/${surface}/factories.test.ts\`](../../tests/src/${surface}/factories.test.ts) —\n \`create${pascal}\` returns a working \`${pascal}Interface\` backed by a real \`${pascal}\`.`).join("\n");
3019
+ const artifacts = [fillArtifact(`guides/src/${spec.name}.md`, "guides", "guide", {
3020
+ name: spec.name,
3021
+ pascal,
3022
+ primary,
3023
+ source,
3024
+ barrel,
3025
+ tests,
3026
+ factories: guideMemberTable("factory", members),
3027
+ entities: guideMemberTable("entity", members),
3028
+ types: guideMemberTable("type", members)
3029
+ }), fillArtifact("guides/README.md", "guides", "guidesReadme", {
3030
+ concept: alignTable([
3031
+ "Concept",
3032
+ "Spec",
3033
+ "Source",
3034
+ "Tests"
3035
+ ], [[
3036
+ pascal,
3037
+ `[\`${spec.name}.md\`](src/${spec.name}.md)`,
3038
+ spec.surfaces.map((surface) => `[\`src/${surface}\`](../src/${surface})`).join(", "),
3039
+ spec.surfaces.map((surface) => `[\`tests/src/${surface}\`](../tests/src/${surface})`).join(", ")
3040
+ ]]),
3041
+ directory: alignTable(["Directory", "Guide"], spec.surfaces.map((surface) => [`src/${surface}`, `[\`${spec.name}.md\`](src/${spec.name}.md)`]))
3042
+ })];
3043
+ for (const dep of spec.dependencies) {
3044
+ if (!vendoredGuides.includes(dep.name)) continue;
3045
+ const short = dep.name.replace("@orkestrel/", "");
3046
+ artifacts.push({
3047
+ path: `guides/src/${short}.md`,
3048
+ group: "guides",
3049
+ origin: "host",
3050
+ source: `guides/src/${short}.md`
3051
+ });
3052
+ }
3053
+ return artifacts;
3054
+ }
3055
+ /**
3056
+ * Apply a blueprint's `overrides` over a drafted artifact list — an override
3057
+ * REPLACES the matching artifact's `content` in place; an override matching
3058
+ * no planned artifact, or targeting a `host`-origin path, is left unapplied
3059
+ * here (the gate stage surfaces it as a blocking question — this leaf only
3060
+ * performs the replacement half of the rule).
3061
+ *
3062
+ * @param artifacts - The drafted `Artifact[]`.
3063
+ * @param overrides - The blueprint's `overrides`.
3064
+ * @returns The artifact list with matching overrides applied.
3065
+ *
3066
+ * @example
3067
+ * ```ts
3068
+ * applyOverrides(artifacts, [override('README.md', '# custom')])[0].content // '# custom'
3069
+ * ```
3070
+ */
3071
+ function applyOverrides(artifacts, overrides) {
3072
+ if (overrides.length === 0) return artifacts;
3073
+ const byPath = new Map(overrides.map((override) => [override.path, override.content]));
3074
+ return artifacts.map((artifact) => {
3075
+ if (artifact.origin === "host") return artifact;
3076
+ const content = byPath.get(artifact.path);
3077
+ return content === void 0 ? artifact : {
3078
+ ...artifact,
3079
+ content
3080
+ };
3081
+ });
3082
+ }
3083
+ /**
3084
+ * The full pure compilation: draft a blueprint's artifacts — the manifest and
3085
+ * exports combination rules over the per-surface `SURFACE_MATRIX` rows, plus
3086
+ * `HOST_PATHS` and `overrides` — then pin.
3087
+ *
3088
+ * @param blueprint - The `Blueprint` to compile.
3089
+ * @param groups - An optional `Group[]` selection (default: all groups).
3090
+ * @returns The drafted, pinned `Plan`.
3091
+ *
3092
+ * @example
3093
+ * ```ts
3094
+ * const plan = blueprintToPlan(blueprint('router', { surfaces: ['core'] }))
3095
+ * plan.artifacts.length // every file the package needs
3096
+ * ```
3097
+ */
3098
+ function blueprintToPlan(blueprint, groups) {
3099
+ const selected = groups && groups.length > 0 ? groups : GROUPS;
3100
+ const pascal = pascalCase(blueprint.name);
3101
+ const members = blueprintToMembers(blueprint);
3102
+ const artifacts = [];
3103
+ if (selected.includes("manifest")) artifacts.push({
3104
+ path: "package.json",
3105
+ group: "manifest",
3106
+ origin: "computed",
3107
+ content: packageManifest(blueprint)
3108
+ });
3109
+ if (selected.includes("configs")) artifacts.push(...configArtifacts(blueprint));
3110
+ if (selected.includes("source")) artifacts.push(...sourceArtifacts(blueprint, pascal));
3111
+ if (selected.includes("tests")) artifacts.push(...testArtifacts(blueprint, pascal));
3112
+ if (selected.includes("guides")) artifacts.push(...guideArtifacts(blueprint, pascal, members));
3113
+ if (selected.includes("docs")) artifacts.push(fillArtifact("README.md", "docs", "readme", {
3114
+ name: blueprint.name,
3115
+ pascal
3116
+ }));
3117
+ for (const path of HOST_PATHS) {
3118
+ const group = hostGroup(path);
3119
+ if (!selected.includes(group)) continue;
3120
+ artifacts.push({
3121
+ path,
3122
+ group,
3123
+ origin: "host",
3124
+ source: path
3125
+ });
3126
+ }
3127
+ return pinPlan({
3128
+ blueprint,
3129
+ groups: [...selected],
3130
+ artifacts: applyOverrides(artifacts, blueprint.overrides)
3131
+ });
3132
+ }
3133
+ //#endregion
3134
+ //#region src/core/Compiler.ts
3135
+ /**
3136
+ * The compilation orchestrator — runs the fixed three-stage `[draft, gate,
3137
+ * pin]` pipeline over a `Blueprint` and the pure `audit` projection, owning a
3138
+ * typed `emitter` (AGENTS §13).
3139
+ *
3140
+ * @remarks
3141
+ * `compile` and `audit` are genuinely synchronous and pure; the gate fails
3142
+ * CLOSED — a blueprint failing `validateBlueprint`, or carrying an override
3143
+ * that matches no planned artifact or targets a `host`-origin path, yields a
3144
+ * visible incomplete `Scaffolding` (`plan` absent, `questions` populated)
3145
+ * rather than throwing. A dependency outside the vendored guide set surfaces
3146
+ * a non-blocking `Question` and a `host`-origin pointer artifact instead of a
3147
+ * fabricated mirror. `compile` emits `compile` only for a complete
3148
+ * compilation and `block` for a gated one; `audit` emits `block` (when gated)
3149
+ * then `audit`, never `compile`. After `destroy()` every method but the
3150
+ * getter and `destroy` itself throws `ScaffoldError('DESTROYED', …)`.
3151
+ *
3152
+ * @example
3153
+ * ```ts
3154
+ * import { blueprint, Compiler } from '@src/core'
3155
+ *
3156
+ * const compiler = new Compiler()
3157
+ * const scaffolding = compiler.compile(blueprint('router', { surfaces: ['core'] }))
3158
+ * scaffolding.complete // true
3159
+ * compiler.destroy()
3160
+ * ```
3161
+ */
3162
+ var Compiler = class Compiler {
3163
+ static #vendored = Object.freeze([
3164
+ "@orkestrel/contract",
3165
+ "@orkestrel/emitter",
3166
+ "@orkestrel/markdown",
3167
+ "@orkestrel/template",
3168
+ "@orkestrel/terminal",
3169
+ "@orkestrel/console",
3170
+ "@orkestrel/guide"
3171
+ ]);
3172
+ #emitter;
3173
+ #destroyed = false;
3174
+ constructor(options) {
3175
+ this.#emitter = new _orkestrel_emitter.Emitter({
3176
+ on: options?.on,
3177
+ error: options?.error
3178
+ });
3179
+ }
3180
+ get emitter() {
3181
+ return this.#emitter;
3182
+ }
3183
+ /**
3184
+ * Run the three-stage pipeline over a `Blueprint`, returning a complete or
3185
+ * visible-incomplete `Scaffolding`.
3186
+ *
3187
+ * @param blueprint - The `Blueprint` to compile.
3188
+ * @param groups - Optional `Group` selection scoping the plan to those
3189
+ * artifact groups; absent means the full plan.
3190
+ * @returns The `Scaffolding` outcome of this compile.
3191
+ *
3192
+ * @example
3193
+ * ```ts
3194
+ * const scaffolding = compiler.compile(blueprint('timeout', { surfaces: ['core'] }))
3195
+ * scaffolding.stages.map((record) => record.stage) // ['draft', 'gate', 'pin']
3196
+ * ```
3197
+ */
3198
+ compile(blueprint, groups) {
3199
+ this.#assertAlive();
3200
+ const scaffolding = this.#run(blueprint, groups);
3201
+ if (scaffolding.complete) this.#emitter.emit("compile", scaffolding);
3202
+ else this.#emitter.emit("block", scaffolding.questions);
3203
+ return scaffolding;
3204
+ }
3205
+ /**
3206
+ * Compile the blueprint, then diff the resulting plan against the
3207
+ * caller-supplied current target content.
3208
+ *
3209
+ * @param blueprint - The `Blueprint` to compile and audit.
3210
+ * @param current - The target's current content, keyed by artifact path.
3211
+ * @param groups - Optional `Group` selection scoping the audit to those
3212
+ * artifact groups; absent means the full plan.
3213
+ * @returns The `Audit` outcome — a gated blueprint returns `complete: false`
3214
+ * with the gate's blocking `questions` and zero findings.
3215
+ *
3216
+ * @example
3217
+ * ```ts
3218
+ * const audit = compiler.audit(blueprint('timeout', { surfaces: ['core'] }), {})
3219
+ * audit.missing // every artifact — nothing exists at the target yet
3220
+ * ```
3221
+ */
3222
+ audit(blueprint, current, groups) {
3223
+ this.#assertAlive();
3224
+ const scaffolding = this.#run(blueprint, groups);
3225
+ if (!scaffolding.complete || scaffolding.plan === void 0) {
3226
+ this.#emitter.emit("block", scaffolding.questions);
3227
+ const result = {
3228
+ findings: [],
3229
+ clean: false,
3230
+ complete: false,
3231
+ questions: scaffolding.questions,
3232
+ drifted: 0,
3233
+ missing: 0,
3234
+ foreign: 0
3235
+ };
3236
+ this.#emitter.emit("audit", result);
3237
+ return result;
3238
+ }
3239
+ const result = diffPlan(scaffolding.plan, current);
3240
+ this.#emitter.emit("audit", result);
3241
+ return result;
3242
+ }
3243
+ /** Idempotent teardown — emits `destroy`, then destroys the emitter LAST. */
3244
+ destroy() {
3245
+ if (this.#destroyed) return;
3246
+ this.#destroyed = true;
3247
+ this.#emitter.emit("destroy");
3248
+ this.#emitter.destroy();
3249
+ }
3250
+ #run(blueprint, groups) {
3251
+ const stages = [];
3252
+ const failures = [];
3253
+ let draft;
3254
+ try {
3255
+ draft = blueprintToPlan(blueprint, groups);
3256
+ stages.push({
3257
+ stage: "draft",
3258
+ input: blueprint,
3259
+ output: draft,
3260
+ failed: false
3261
+ });
3262
+ } catch (error) {
3263
+ const message = error instanceof Error ? error.message : String(error);
3264
+ stages.push({
3265
+ stage: "draft",
3266
+ input: blueprint,
3267
+ output: void 0,
3268
+ failed: true,
3269
+ error: message
3270
+ });
3271
+ failures.push({
3272
+ stage: "draft",
3273
+ code: "INVALID",
3274
+ message
3275
+ });
3276
+ this.#emitter.emit("error", error);
3277
+ stages.push({
3278
+ stage: "gate",
3279
+ input: void 0,
3280
+ output: void 0,
3281
+ failed: true,
3282
+ error: "Skipped: draft failed"
3283
+ });
3284
+ stages.push({
3285
+ stage: "pin",
3286
+ input: void 0,
3287
+ output: void 0,
3288
+ failed: true,
3289
+ error: "Skipped: draft failed"
3290
+ });
3291
+ return {
3292
+ blueprint,
3293
+ questions: [],
3294
+ stages,
3295
+ failures,
3296
+ complete: false,
3297
+ digest: ""
3298
+ };
3299
+ }
3300
+ const validation = validateBlueprint(blueprint);
3301
+ const overrideQuestions = this.#overrideQuestions(blueprint, draft.artifacts);
3302
+ const dependencyQuestions = this.#dependencyQuestions(blueprint);
3303
+ const blocking = [...validation.questions, ...overrideQuestions];
3304
+ const questions = [...blocking, ...dependencyQuestions];
3305
+ stages.push({
3306
+ stage: "gate",
3307
+ input: draft,
3308
+ output: {
3309
+ blocking: blocking.length,
3310
+ questions
3311
+ },
3312
+ failed: blocking.length > 0
3313
+ });
3314
+ if (blocking.length > 0) {
3315
+ const message = `${blocking.length} blocking question${blocking.length === 1 ? "" : "s"}`;
3316
+ failures.push({
3317
+ stage: "gate",
3318
+ code: "BLOCKED",
3319
+ message
3320
+ });
3321
+ stages.push({
3322
+ stage: "pin",
3323
+ input: void 0,
3324
+ output: void 0,
3325
+ failed: true,
3326
+ error: "Skipped: gate blocked"
3327
+ });
3328
+ return {
3329
+ blueprint,
3330
+ questions,
3331
+ stages,
3332
+ failures,
3333
+ complete: false,
3334
+ digest: ""
3335
+ };
3336
+ }
3337
+ try {
3338
+ const pointers = this.#pointerArtifacts(blueprint);
3339
+ const plan = pinPlan({
3340
+ ...draft,
3341
+ artifacts: [...draft.artifacts, ...pointers]
3342
+ });
3343
+ stages.push({
3344
+ stage: "pin",
3345
+ input: draft,
3346
+ output: plan,
3347
+ failed: false
3348
+ });
3349
+ return {
3350
+ blueprint,
3351
+ plan,
3352
+ questions,
3353
+ stages,
3354
+ failures,
3355
+ complete: true,
3356
+ digest: plan.hash ?? ""
3357
+ };
3358
+ } catch (error) {
3359
+ const message = error instanceof Error ? error.message : String(error);
3360
+ stages.push({
3361
+ stage: "pin",
3362
+ input: draft,
3363
+ output: void 0,
3364
+ failed: true,
3365
+ error: message
3366
+ });
3367
+ failures.push({
3368
+ stage: "pin",
3369
+ code: "INVALID",
3370
+ message
3371
+ });
3372
+ this.#emitter.emit("error", error);
3373
+ return {
3374
+ blueprint,
3375
+ questions,
3376
+ stages,
3377
+ failures,
3378
+ complete: false,
3379
+ digest: ""
3380
+ };
3381
+ }
3382
+ }
3383
+ #overrideQuestions(blueprint, artifacts) {
3384
+ const byPath = new Map(artifacts.map((artifact) => [artifact.path, artifact]));
3385
+ const questions = [];
3386
+ for (const item of blueprint.overrides) {
3387
+ const artifact = byPath.get(item.path);
3388
+ if (artifact === void 0) {
3389
+ questions.push({
3390
+ field: "overrides",
3391
+ text: `Override path "${item.path}" matches no planned artifact`,
3392
+ blocking: true
3393
+ });
3394
+ continue;
3395
+ }
3396
+ if (artifact.origin === "host") questions.push({
3397
+ field: "overrides",
3398
+ text: `Override path "${item.path}" targets a host-origin artifact`,
3399
+ blocking: true
3400
+ });
3401
+ }
3402
+ return questions;
3403
+ }
3404
+ #dependencyQuestions(blueprint) {
3405
+ const questions = [];
3406
+ for (const item of blueprint.dependencies) {
3407
+ if (Compiler.#vendored.includes(item.name)) continue;
3408
+ questions.push({
3409
+ field: "dependencies",
3410
+ text: `Dependency "${item.name}" is not vendored — sync its guides/src mirror from that repo at HEAD`,
3411
+ blocking: false
3412
+ });
3413
+ }
3414
+ return questions;
3415
+ }
3416
+ #pointerArtifacts(blueprint) {
3417
+ const artifacts = [];
3418
+ for (const item of blueprint.dependencies) {
3419
+ if (Compiler.#vendored.includes(item.name)) continue;
3420
+ const path = `guides/src/${item.name.replace("@orkestrel/", "")}.md`;
3421
+ artifacts.push({
3422
+ path,
3423
+ group: "guides",
3424
+ origin: "host",
3425
+ source: path
3426
+ });
3427
+ }
3428
+ return artifacts;
3429
+ }
3430
+ #assertAlive() {
3431
+ if (this.#destroyed) throw new ScaffoldError("DESTROYED", "Compiler has been destroyed");
3432
+ }
3433
+ };
3434
+ //#endregion
3435
+ //#region src/core/PlanManager.ts
3436
+ /**
3437
+ * The self-owning, versioned/hashed plan registry (AGENTS §9).
3438
+ *
3439
+ * @remarks
3440
+ * `add` re-pins the plan and mints the record's `id` from its own content
3441
+ * `hash` — deterministic, no randomness. Re-adding a plan whose content is
3442
+ * unchanged resolves to the SAME id and returns the existing record
3443
+ * untouched (`version` stays put); a plan whose content differs mints a
3444
+ * fresh id at `version: 1`. The array overload of `remove` is declared FIRST
3445
+ * (AGENTS §9.2) so an id list resolves to the batch form; the batch form is
3446
+ * ALL-OR-NOTHING. After `destroy()` every method but the getters and
3447
+ * `destroy` itself throws `ScaffoldError('DESTROYED', …)`.
3448
+ *
3449
+ * @example
3450
+ * ```ts
3451
+ * import { blueprint, blueprintToPlan, PlanManager } from '@src/core'
3452
+ *
3453
+ * const plans = new PlanManager()
3454
+ * const record = plans.add(blueprintToPlan(blueprint('budget', { surfaces: ['core'] })))
3455
+ * record.id === record.hash // true — id minted from content
3456
+ * plans.destroy()
3457
+ * ```
3458
+ */
3459
+ var PlanManager = class {
3460
+ #plans = /* @__PURE__ */ new Map();
3461
+ #emitter;
3462
+ #destroyed = false;
3463
+ constructor(options) {
3464
+ this.#emitter = new _orkestrel_emitter.Emitter({
3465
+ on: options?.on,
3466
+ error: options?.error
3467
+ });
3468
+ for (const plan of options?.plans ?? []) {
3469
+ const record = this.#pin(plan);
3470
+ this.#plans.set(record.id, record);
3471
+ }
3472
+ }
3473
+ get emitter() {
3474
+ return this.#emitter;
3475
+ }
3476
+ get size() {
3477
+ return this.#plans.size;
3478
+ }
3479
+ /**
3480
+ * Whether a plan with the given id is registered.
3481
+ *
3482
+ * @param id - The plan record id.
3483
+ * @returns `true` when `id` is registered.
3484
+ */
3485
+ has(id) {
3486
+ this.#assertAlive();
3487
+ return this.#plans.has(id);
3488
+ }
3489
+ /**
3490
+ * Look up one registered plan record by id (AGENTS §9.1 singular accessor).
3491
+ *
3492
+ * @param id - The plan record id.
3493
+ * @returns The `PlanRecord`, or `undefined` when unregistered.
3494
+ */
3495
+ plan(id) {
3496
+ this.#assertAlive();
3497
+ return this.#plans.get(id);
3498
+ }
3499
+ /**
3500
+ * List every registered plan record (AGENTS §9.1 plural accessor).
3501
+ *
3502
+ * @returns A snapshot array of every registered `PlanRecord`.
3503
+ */
3504
+ plans() {
3505
+ this.#assertAlive();
3506
+ return [...this.#plans.values()];
3507
+ }
3508
+ /**
3509
+ * Register (or re-register) one plan, mints the record's id from its
3510
+ * content hash.
3511
+ *
3512
+ * @param plan - The `Plan` to register.
3513
+ * @returns The registered `PlanRecord`.
3514
+ *
3515
+ * @example
3516
+ * ```ts
3517
+ * const record = plans.add(blueprintToPlan(blueprint('budget', { surfaces: ['core'] })))
3518
+ * record.version // 1
3519
+ * ```
3520
+ */
3521
+ add(plan) {
3522
+ this.#assertAlive();
3523
+ const record = this.#pin(plan);
3524
+ const final = this.#plans.get(record.id) ?? record;
3525
+ this.#plans.set(final.id, final);
3526
+ this.#emitter.emit("add", final.id);
3527
+ return final;
3528
+ }
3529
+ remove(target) {
3530
+ this.#assertAlive();
3531
+ if (target === void 0) {
3532
+ for (const id of this.#plans.keys()) this.#emitter.emit("remove", id);
3533
+ this.#plans.clear();
3534
+ return;
3535
+ }
3536
+ if (typeof target === "string") {
3537
+ if (!this.#plans.has(target)) return false;
3538
+ this.#plans.delete(target);
3539
+ this.#emitter.emit("remove", target);
3540
+ return true;
3541
+ }
3542
+ for (const id of target) if (!this.#plans.has(id)) return false;
3543
+ for (const id of target) {
3544
+ this.#plans.delete(id);
3545
+ this.#emitter.emit("remove", id);
3546
+ }
3547
+ return true;
3548
+ }
3549
+ /** Idempotent teardown — clears the collection, emits `destroy`, then destroys the emitter LAST. */
3550
+ destroy() {
3551
+ if (this.#destroyed) return;
3552
+ this.#destroyed = true;
3553
+ this.#plans.clear();
3554
+ this.#emitter.emit("destroy");
3555
+ this.#emitter.destroy();
3556
+ }
3557
+ #pin(plan) {
3558
+ const pinned = pinPlan(plan);
3559
+ const hash = pinned.hash ?? "";
3560
+ return {
3561
+ id: hash,
3562
+ plan: pinned,
3563
+ version: 1,
3564
+ hash
3565
+ };
3566
+ }
3567
+ #assertAlive() {
3568
+ if (this.#destroyed) throw new ScaffoldError("DESTROYED", "PlanManager has been destroyed");
3569
+ }
3570
+ };
3571
+ //#endregion
3572
+ //#region src/core/factories.ts
3573
+ /**
3574
+ * Create a `CompilerInterface` — the compilation orchestrator.
3575
+ *
3576
+ * @param options - `CompilerOptions` — `on` initial event listeners, `error` the listener-error handler.
3577
+ * @returns A fresh `Compiler`.
3578
+ *
3579
+ * @example
3580
+ * ```ts
3581
+ * import { createCompiler } from '@src/core'
3582
+ *
3583
+ * const compiler = createCompiler()
3584
+ * compiler.destroy()
3585
+ * ```
3586
+ */
3587
+ function createCompiler(options) {
3588
+ return new Compiler(options);
3589
+ }
3590
+ /**
3591
+ * Create a working `PlanManagerInterface`.
3592
+ *
3593
+ * @param options - `PlanManagerOptions` — `plans` to seed the registry, `on` / `error` for the emitter.
3594
+ * @returns A fresh `PlanManager`.
3595
+ *
3596
+ * @example
3597
+ * ```ts
3598
+ * import { createPlanManager } from '@src/core'
3599
+ *
3600
+ * const plans = createPlanManager()
3601
+ * plans.size // 0
3602
+ * plans.destroy()
3603
+ * ```
3604
+ */
3605
+ function createPlanManager(options) {
3606
+ return new PlanManager(options);
3607
+ }
3608
+ /**
3609
+ * Validate and return a `Blueprint` from plain data.
3610
+ *
3611
+ * @param data - A `name` plus a partial of the remaining `Blueprint` fields.
3612
+ * @remarks
3613
+ * Fills the builder defaults, then checks BOTH the exact-record shape
3614
+ * (`isBlueprint`) and the semantic pass (`validateBlueprint`) — so an
3615
+ * off-`NAME_PATTERN` name throws here too.
3616
+ * @returns The validated `Blueprint`.
3617
+ * @throws {@link ScaffoldError} coded `INVALID` when the structure or the
3618
+ * semantic pass fails.
3619
+ *
3620
+ * @example
3621
+ * ```ts
3622
+ * import { createBlueprint } from '@src/core'
3623
+ *
3624
+ * createBlueprint({ name: 'Router', surfaces: [] }) // throws ScaffoldError('INVALID', …)
3625
+ * ```
3626
+ */
3627
+ function createBlueprint(data) {
3628
+ const candidate = blueprint(data.name, data);
3629
+ if (!isBlueprint(candidate)) throw new ScaffoldError("INVALID", "Blueprint failed the exact-record contract", { name: data.name });
3630
+ const validation = validateBlueprint(candidate);
3631
+ if (!validation.valid) throw new ScaffoldError("INVALID", "Blueprint failed validation", { questions: validation.questions });
3632
+ return candidate;
3633
+ }
3634
+ //#endregion
3635
+ exports.CATEGORIES = CATEGORIES;
3636
+ exports.COMPILER_ID = COMPILER_ID;
3637
+ exports.COMPILE_STAGES = COMPILE_STAGES;
3638
+ exports.Compiler = Compiler;
3639
+ exports.DEFAULT_ENGINES = DEFAULT_ENGINES;
3640
+ exports.DEFAULT_VERSION = DEFAULT_VERSION;
3641
+ exports.DEPENDENCY_NAME_PATTERN = DEPENDENCY_NAME_PATTERN;
3642
+ exports.EXTRA_NAME_PATTERN = EXTRA_NAME_PATTERN;
3643
+ exports.FRESHNESS = FRESHNESS;
3644
+ exports.GROUPS = GROUPS;
3645
+ exports.HOST_PATHS = HOST_PATHS;
3646
+ exports.NAME_PATTERN = NAME_PATTERN;
3647
+ exports.ORIGINS = ORIGINS;
3648
+ exports.PlanManager = PlanManager;
3649
+ exports.SCAFFOLD_RANGE = SCAFFOLD_RANGE;
3650
+ exports.SURFACES = SURFACES;
3651
+ exports.SURFACE_MATRIX = SURFACE_MATRIX;
3652
+ exports.ScaffoldError = ScaffoldError;
3653
+ exports.TEMPLATES = TEMPLATES;
3654
+ exports.alignTable = alignTable;
3655
+ exports.applyOverrides = applyOverrides;
3656
+ exports.artifactShape = artifactShape;
3657
+ exports.auditToReview = auditToReview;
3658
+ exports.blueprint = blueprint;
3659
+ exports.blueprintShape = blueprintShape;
3660
+ exports.blueprintToMembers = blueprintToMembers;
3661
+ exports.blueprintToPlan = blueprintToPlan;
3662
+ exports.catalogNames = catalogNames;
3663
+ exports.catalogToBlock = catalogToBlock;
3664
+ exports.compareCodeUnit = compareCodeUnit;
3665
+ exports.computeHash = computeHash;
3666
+ exports.configArtifacts = configArtifacts;
3667
+ exports.coreTsconfig = coreTsconfig;
3668
+ exports.coreViteConfig = coreViteConfig;
3669
+ exports.createBlueprint = createBlueprint;
3670
+ exports.createCompiler = createCompiler;
3671
+ exports.createPlanManager = createPlanManager;
3672
+ exports.delimiterCell = delimiterCell;
3673
+ exports.dependency = dependency;
3674
+ exports.dependencyShape = dependencyShape;
3675
+ exports.devDependenciesFor = devDependenciesFor;
3676
+ exports.diffPlan = diffPlan;
3677
+ exports.dualCondition = dualCondition;
3678
+ exports.entryFields = entryFields;
3679
+ exports.exportsMap = exportsMap;
3680
+ exports.fillArtifact = fillArtifact;
3681
+ exports.guideArtifacts = guideArtifacts;
3682
+ exports.guideMemberTable = guideMemberTable;
3683
+ exports.hostGroup = hostGroup;
3684
+ exports.inferGroup = inferGroup;
3685
+ exports.isArtifact = isArtifact;
3686
+ exports.isBehind = isBehind;
3687
+ exports.isBlueprint = isBlueprint;
3688
+ exports.isDependency = isDependency;
3689
+ exports.isMember = isMember;
3690
+ exports.isOverride = isOverride;
3691
+ exports.isPlan = isPlan;
3692
+ exports.isRecord = isRecord;
3693
+ exports.isScaffoldError = isScaffoldError;
3694
+ exports.isSyncReport = isSyncReport;
3695
+ exports.manifestToDependencies = manifestToDependencies;
3696
+ exports.member = member;
3697
+ exports.memberShape = memberShape;
3698
+ exports.override = override;
3699
+ exports.overrideShape = overrideShape;
3700
+ exports.packageManifest = packageManifest;
3701
+ exports.padCell = padCell;
3702
+ exports.paritySpecifiers = paritySpecifiers;
3703
+ exports.parseBlueprint = parseBlueprint;
3704
+ exports.parsePlan = parsePlan;
3705
+ exports.parseSyncReport = parseSyncReport;
3706
+ exports.pascalCase = pascalCase;
3707
+ exports.pinPlan = pinPlan;
3708
+ exports.planShape = planShape;
3709
+ exports.planToReview = planToReview;
3710
+ exports.planToSummary = planToSummary;
3711
+ exports.rangeToFreshness = rangeToFreshness;
3712
+ exports.rootTsconfig = rootTsconfig;
3713
+ exports.rootViteConfig = rootViteConfig;
3714
+ exports.singleSurfaceViteConfig = singleSurfaceViteConfig;
3715
+ exports.sourceArtifacts = sourceArtifacts;
3716
+ exports.splitTableRow = splitTableRow;
3717
+ exports.stableStringify = stableStringify;
3718
+ exports.surfaceTsconfig = surfaceTsconfig;
3719
+ exports.surfaceVariant = surfaceVariant;
3720
+ exports.surfaceViteConfig = surfaceViteConfig;
3721
+ exports.syncReportShape = syncReportShape;
3722
+ exports.syncToReview = syncToReview;
3723
+ exports.testArtifacts = testArtifacts;
3724
+ exports.validateBlueprint = validateBlueprint;
3725
+ exports.validateDependencyArray = validateDependencyArray;
3726
+ exports.viteHeader = viteHeader;
3727
+
3728
+ //# sourceMappingURL=index.cjs.map