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