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