@orkestrel/scaffold 0.0.9 → 0.0.11

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.
@@ -134,7 +134,7 @@ From [`types.ts`](../../src/core/types.ts).
134
134
  | `SrcDefinition` | interface |
135
135
  | `AppDefinition` | interface |
136
136
  | `ViteMachinery` | interface |
137
- | `ViteAxes` | interface |
137
+ | `ViteFacts` | interface |
138
138
  | `ViteProjectRegistration` | interface |
139
139
  | `Origin` | type |
140
140
  | `Group` | type |
@@ -192,8 +192,10 @@ single-file-component, HTML, and development-server machinery an application bro
192
192
  needs, and `output` for build-output containment. It never selects a boundary guarantee — those ship
193
193
  in every shape, as the compilers section sets out.
194
194
 
195
- `ViteAxes` is the optional structural-project slice shared by every root Vite compiler:
196
- `bin`, `integration`, and `service` each select their matching standalone project when `true`.
195
+ `ViteFacts` is the optional structural-fact slice shared by every root Vite compiler:
196
+ `bin`, `integration`, and `service` each select their matching standalone project when `true`;
197
+ `global` records the exact-case consumer-owned global-setup module and wires it into each eligible
198
+ project.
197
199
 
198
200
  `ViteProjectRegistration` carries one generated project factory identifier and its optional browser
199
201
  label. Root configuration renderers preserve that browser ownership as data through registration
@@ -217,6 +219,7 @@ interface Blueprint {
217
219
  readonly bin: boolean
218
220
  readonly integration: boolean
219
221
  readonly service: boolean
222
+ readonly global: boolean
220
223
  }
221
224
  ```
222
225
 
@@ -227,11 +230,11 @@ packages — a peer flagged `optional` also gets a `peerDependenciesMeta` entry.
227
230
  package-specific development dependencies merged over the generated baseline, and may carry any
228
231
  valid npm package name.
229
232
 
230
- `bin`, `integration`, and `service` are the three structural axes, and they obey one law: each is
231
- `true` only when the workspace physically ships the directory that defines it — never because of
232
- the workspace's name, and never because a sibling axis is set. `deriveBlueprint` probes exactly
233
- those directories, so a fresh compile and an audit of a mature repository agree on what the
234
- workspace is.
233
+ `bin`, `integration`, `service`, and `global` are structural project facts. All four obey one law:
234
+ each is `true` only when the workspace physically ships the directory or exact-case file that
235
+ defines it — never because of the workspace's name, and never because a sibling fact is set.
236
+ `deriveBlueprint` probes those paths, so a fresh compile and an audit of a mature repository agree
237
+ on what the workspace is.
235
238
 
236
239
  - **`bin`** — `src/bin/` exists. It alone turns on the self-hosting extras: the manifest's `bin`
237
240
  entry, the `scaffold` script pointed at the built executable, the bin check, test, and build
@@ -245,6 +248,10 @@ workspace is.
245
248
  running process, outside the default run: a standalone `service` project including
246
249
  `tests/service/**/*.test.ts`, with `tests/setupService.ts` after the shared setup, and the
247
250
  isolated `test:service` script.
251
+ - **`global`** — the physical, exact-case `tests/setupGlobal.ts` file exists. It is the single
252
+ governing setup-presence fact. A declared `src/browser` project runs that consumer-owned module
253
+ as `globalSetup`; integration runs it only when `bin` and `integration` are also true.
254
+ Application-browser, styles, service, and unrelated proof projects never receive it.
248
255
 
249
256
  A service workspace owes two companion files beside that directory, and derivation requires both
250
257
  physically present: `tests/setupService.ts` and `scripts/service.sh`. Either missing companion is a
@@ -360,6 +367,7 @@ From [`constants.ts`](../../src/core/constants.ts).
360
367
  | `APP_MATRIX` | const |
361
368
  | `HOST_PATHS` | const |
362
369
  | `SERVICE_SCRIPT_PATH` | const |
370
+ | `GLOBAL_SETUP_PATH` | const |
363
371
  | `NAME_PATTERN` | const |
364
372
  | `MAX_NAME_LENGTH` | const |
365
373
  | `MAX_DEPENDENCY_NAME_LENGTH` | const |
@@ -410,7 +418,8 @@ axis's computed `tsconfig` and Vite wrapper pair. `HOST_PATHS` is the ordered li
410
418
  host artifacts, and it is the staging manifest rather than the per-plan carried set:
411
419
  `stageHost` vendors every path on it, while each plan carries the subset `selectHostPaths` selects
412
420
  for that one workspace. `SERVICE_SCRIPT_PATH` names the consumer-owned provisioner a service
413
- workspace's audit expects.
421
+ workspace's audit expects, and `GLOBAL_SETUP_PATH` names the consumer-owned Vitest global-setup
422
+ module that independently selected projects can load.
414
423
 
415
424
  The bounds are public because they are part of the contract, not implementation trivia.
416
425
  `MAX_ARTIFACT_BYTES` caps one artifact at 5 MiB and `MAX_TOTAL_ARTIFACT_BYTES` caps one blueprint,
@@ -789,6 +798,7 @@ From [`helpers.ts`](../../src/core/helpers.ts).
789
798
  | `stableStringify` | function |
790
799
  | `planPayload` | function |
791
800
  | `computeColumnWidth` | function |
801
+ | `fitsPrintWidth` | function |
792
802
  | `renderArray` | function |
793
803
  | `renderObject` | function |
794
804
  | `renderValue` | function |
@@ -800,10 +810,11 @@ own data descriptor, so parsed JSON cannot acquire manifest fields through a pol
800
810
  and accessors are never invoked. Each builder omits an absent optional
801
811
  field entirely rather than writing `undefined`, so a built value round-trips its own exact-record
802
812
  guard. `blueprint` fills the defaults: `version` and `engines` from their constants, `src` to
803
- `['core']`, and every other collection to empty. `pascalCase` derives the entity name from a
804
- lowercase-hyphen package name, and `blueprintToMembers` derives the declared public `Member[]` — a
805
- full entity, options type, interface, and factory per published environment, plus the exact declaration
806
- inventory each selected application environment contributes.
813
+ `['core']`, every other collection to empty, and every structural fact to `false`. `pascalCase`
814
+ derives the entity name from a lowercase-hyphen package name, and `blueprintToMembers` derives the
815
+ declared public `Member[]` — a full entity, options type, interface, and factory per published
816
+ environment, plus the exact declaration inventory each selected application environment
817
+ contributes.
807
818
 
808
819
  `escapeHtmlText` and `serializeTypeScriptString` are the two escaping leaves used when a
809
820
  caller-supplied name reaches generated HTML or generated TypeScript source; the latter preserves
@@ -853,7 +864,7 @@ marks an empty axis). `PlanManager` compares the canonical payload whenever an
853
864
  id is already registered: an identical plan is idempotent, while a distinct payload with the same
854
865
  32-bit digest fails closed with `ScaffoldError('INVALID', 'Plan hash collision')`.
855
866
  `formatJson` and its leaves — `renderValue`,
856
- `renderArray`, `renderObject`, and `computeColumnWidth` — emit JSON that matches the fleet
867
+ `renderArray`, `renderObject`, `computeColumnWidth`, and `fitsPrintWidth` — emit JSON that matches the fleet
857
868
  formatter byte for byte, collapsing a short array onto one line and breaking a long one, so
858
869
  computed configuration JSON is format-stable by construction.
859
870
 
@@ -949,10 +960,11 @@ omitting an absent path entirely. `readManifest` reads `package.json` text, and
949
960
 
950
961
  `deriveBlueprint` is the faithful inverse an audit needs: it reconstructs a blueprint from an
951
962
  existing workspace so a mature package is diffed against its own would-be scaffold rather than a
952
- dependency-less stand-in. Environments come from `src/<environment>/` and `app/<environment>/`, and
953
- the three structural axes from the directory probes and the service companion law the blueprint
954
- section states every one of them a reading of the filesystem, never of the name. Dependencies and
955
- peers come from the manifest's scoped entries, with an optional peer recovered from
963
+ dependency-less stand-in. Environments come from `src/<environment>/` and `app/<environment>/`, the
964
+ three structural project facts from their directory probes, and `global` from the physical,
965
+ exact-case `tests/setupGlobal.ts` file; the service companion law remains the one the blueprint
966
+ section states every fact is a reading of the filesystem, never of the name. Dependencies and peers come
967
+ from the manifest's scoped entries, with an optional peer recovered from
956
968
  `peerDependenciesMeta`; and `extras` is every development dependency minus the complete set
957
969
  `devDependenciesFor` emits for those environments and structural axes, and minus anything already
958
970
  declared as a dependency or peer. An axis-emitted dependency is therefore never double-counted,
@@ -1095,13 +1107,17 @@ without output containment — and it still carries every boundary guarantee abo
1095
1107
 
1096
1108
  `renderViteTest` is the single root-project renderer. It consumes ordered `ViteProjectRegistration`
1097
1109
  data and emits either the plain project list or the browser gate, keeping source and application
1098
- root configurations byte-consistent without reconstructing browser ownership.
1110
+ root configurations byte-consistent without reconstructing browser ownership. Both forms use the
1111
+ formatter's 100-column fixed point: a complete registration-array line, including indentation and
1112
+ its trailing comma, stays collapsed when it fits and expands one entry per line otherwise.
1099
1113
  `viteProjectRegistrations` is the one registration derivation every root shape consumes: it derives
1100
1114
  the selected source and application projects from the canonical environment order, then appends
1101
1115
  `policy`, `guides`, and the optional `srcBin`, `integration`, and `service` projects.
1102
- `viteProjectDefinitions` renders the standalone proof and structural-axis definitions in that same
1103
- order with one blank line between declarations. Both consume `ViteAxes`, so each optional project is
1104
- controlled only by its matching `bin`, `integration`, or `service` blueprint axis.
1116
+ `viteProjectDefinitions` renders the standalone proof and structural-fact definitions in that same
1117
+ order with one blank line between declarations. Both consume `ViteFacts`, so each optional project
1118
+ is controlled only by its matching `bin`, `integration`, or `service` blueprint fact; the same
1119
+ slice carries `global` to integration and the source-browser compiler without adding another
1120
+ project.
1105
1121
 
1106
1122
  `coreViteConfig`, `srcViteConfig`, `binViteConfig`, and `appViteConfig` emit the thin per-target
1107
1123
  wrappers, while `binTsconfig` emits the executable declaration scope; `rootViteConfig`,
@@ -1115,9 +1131,12 @@ or application environment project. The guides project therefore uses only `test
1115
1131
  `tests/app/**/*.test.ts` exclude rows are uniform across all root shapes by design, including
1116
1132
  core-only workspaces where one row cannot currently match. Integration and service use 120-second
1117
1133
  test and hook timeouts with file parallelism disabled, and service alone layers
1118
- `tests/setupService.ts` onto the shared setup. Where a bin workspace also ships the integration
1119
- project, that project wires `tests/setupIntegration.ts` as its global setup for the shared
1120
- template-registry harness; bin-less integration workspaces do not.
1134
+ `tests/setupService.ts` onto the shared setup. The integration project wires
1135
+ `tests/setupGlobal.ts` for the shared template-registry harness exactly when `bin`, `integration`,
1136
+ and `global` are all true. Independently, a `global` source-browser project places
1137
+ `globalSetup: ['./tests/setupGlobal.ts']` immediately before its ordinary `setupFiles` row (and
1138
+ after the core-test exclusion where that row exists). Application browser projects never receive
1139
+ that field.
1121
1140
 
1122
1141
  `configArtifacts`, `sourceArtifacts`, `applicationArtifacts`, `testArtifacts`, and `guideArtifacts`
1123
1142
  are the per-group drafters. When `bin` is selected, `configArtifacts` includes
@@ -1666,9 +1685,14 @@ readonly, that privacy is a runtime `#` field rather than a TypeScript modifier,
1666
1685
  re-exports only through `export *`, that a core source never names a worker-only global the
1667
1686
  `WebWorker` declarations expose, and that a computed dynamic import cannot smuggle a
1668
1687
  cross-environment dependency past the declared import rules. Vue components are inspected for the
1669
- same evasions. It is a complement to lint and typecheck, never a second type system, and it is not a
1670
- general-purpose source analyzer. Generated workspaces receive the same exported policy module as a
1671
- host-origin file and run it as a dedicated Node-only `policy` test project over
1688
+ same evasions. A self-contained runtime entrypoint may be exempt from module-scope placement only
1689
+ when it is not a centralized kind file and has at least one real `node:` value import. Erased
1690
+ type-only imports may reference sibling contracts; any non-`node:` static value import,
1691
+ `export … from` re-export, or dynamic `import(...)` disqualifies the exemption. An importless file
1692
+ does not qualify, and centralized declarations remain subject to their export law. Every other
1693
+ policy law still applies. It is a complement to lint and typecheck, never a second type system, and
1694
+ it is not a general-purpose source analyzer. Generated workspaces receive the same exported policy
1695
+ module as a host-origin file and run it as a dedicated Node-only `policy` test project over
1672
1696
  `tests/policy.test.ts`.
1673
1697
 
1674
1698
  **Real browser capability.** Browser test projects are gated on the real executable: the generated
@@ -1683,6 +1707,15 @@ Node projects. One printed warning names every gated project label. A machine wi
1683
1707
  registers and runs the real browser suites unchanged; a machine without one runs the remaining
1684
1708
  projects and says so.
1685
1709
 
1710
+ **Consumer-owned global setup.** The single mechanism-named `tests/setupGlobal.ts` module may
1711
+ prepare a shared integration registry, a real Node-side counterpart for source-browser tests such
1712
+ as a WebSocket fixture server, or both. The scaffold does not emit or replace it. Derivation
1713
+ records its exact-case physical presence as `global`, the single governing fact. Integration
1714
+ consumes it only when `bin` and `integration` are also true; a declared `src/browser` independently
1715
+ wires it to `srcBrowser`. Removing the file removes both eligible rows from regenerated
1716
+ configuration byte-for-byte. Application browser, styles, and service readiness setup remain
1717
+ isolated from this seam.
1718
+
1686
1719
  **Continuous integration.** The generated workflow runs on push and pull request, on
1687
1720
  `ubuntu-latest`, with read-only contents permission, a 60-minute timeout, and a matrix that **tests
1688
1721
  Node `22.12.0` and `26`** with fail-fast disabled. Checkout and Node setup are pinned to immutable
@@ -1981,6 +2014,7 @@ import {
1981
2014
  compareCodeUnit,
1982
2015
  computeColumnWidth,
1983
2016
  escapeHtmlText,
2017
+ fitsPrintWidth,
1984
2018
  formatJson,
1985
2019
  renderArray,
1986
2020
  renderObject,
@@ -1993,6 +2027,7 @@ renderValue('ESNext', '', '', '') // '"ESNext"'
1993
2027
  renderArray(['ESNext', 'DOM'], '', '', '') // '["ESNext", "DOM"]'
1994
2028
  renderObject({ lib: ['ESNext'] }, '') // '{\n\t"lib": ["ESNext"]\n}'
1995
2029
  computeColumnWidth('\t"a"') // 3
2030
+ fitsPrintWidth('\t["ESNext"],') // true
1996
2031
 
1997
2032
  escapeHtmlText('<app & "team">') // '&lt;app &amp; &quot;team&quot;&gt;'
1998
2033
  serializeTypeScriptString("app's") // "'app\\'s'"
@@ -2172,7 +2207,7 @@ appViteConfig('server')
2172
2207
  policyViteProject()
2173
2208
  guidesViteProject()
2174
2209
  binViteProject()
2175
- integrationViteProject({ bin: true, integration: true })
2210
+ integrationViteProject({ bin: true, integration: true, global: true })
2176
2211
  serviceViteProject()
2177
2212
  viteProjectDefinitions({ integration: true }).includes('export const integration =') // true
2178
2213
  viteProjectRegistrations(['core'], [], { integration: true }).map(({ project }) => project)
@@ -1,4 +1,5 @@
1
1
  import { globSync, readFileSync } from 'node:fs'
2
+ import { isBuiltin } from 'node:module'
2
3
  import { basename, join } from 'node:path'
3
4
  import * as ts from 'typescript'
4
5
 
@@ -199,6 +200,77 @@ export function hasAllowedTripleSlashReference(path: string, source: ts.SourceFi
199
200
  )
200
201
  }
201
202
 
203
+ /**
204
+ * Whether a source is self-contained around a positively identified Node runtime dependency.
205
+ *
206
+ * @param source - The parsed source file to inspect.
207
+ * @returns `true` when at least one value import names a real `node:` builtin and no sibling,
208
+ * re-exported, or dynamic runtime dependency is present; type-only imports are erased.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const source = ts.createSourceFile(
213
+ * 'serve.ts',
214
+ * "import { parentPort } from 'node:worker_threads'",
215
+ * ts.ScriptTarget.Latest,
216
+ * true,
217
+ * )
218
+ * isSelfContained(source) // true
219
+ * ```
220
+ */
221
+ export function isSelfContained(source: ts.SourceFile): boolean {
222
+ const pending: ts.Node[] = [source]
223
+ while (pending.length > 0) {
224
+ const node = pending.pop()
225
+ if (node === undefined) continue
226
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
227
+ return false
228
+ }
229
+ ts.forEachChild(node, (child) => {
230
+ pending.push(child)
231
+ })
232
+ }
233
+
234
+ let builtin = false
235
+ for (const statement of source.statements) {
236
+ if (ts.isExportDeclaration(statement) && statement.moduleSpecifier !== undefined) {
237
+ return false
238
+ }
239
+ if (ts.isImportDeclaration(statement)) {
240
+ const clause = statement.importClause
241
+ const named = clause?.namedBindings
242
+ const erased =
243
+ clause?.isTypeOnly === true ||
244
+ (clause !== undefined &&
245
+ clause.name === undefined &&
246
+ named !== undefined &&
247
+ ts.isNamedImports(named) &&
248
+ named.elements.length > 0 &&
249
+ named.elements.every((element) => element.isTypeOnly))
250
+ if (erased) continue
251
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) return false
252
+ const specifier = statement.moduleSpecifier.text
253
+ if (!specifier.startsWith('node:') || !isBuiltin(specifier)) return false
254
+ builtin = true
255
+ }
256
+ if (ts.isImportEqualsDeclaration(statement)) {
257
+ if (statement.isTypeOnly) continue
258
+ const reference = statement.moduleReference
259
+ if (
260
+ !ts.isExternalModuleReference(reference) ||
261
+ reference.expression === undefined ||
262
+ !ts.isStringLiteral(reference.expression)
263
+ ) {
264
+ return false
265
+ }
266
+ const specifier = reference.expression.text
267
+ if (!specifier.startsWith('node:') || !isBuiltin(specifier)) return false
268
+ builtin = true
269
+ }
270
+ }
271
+ return builtin
272
+ }
273
+
202
274
  /**
203
275
  * Whether the policy compiler can read one source path.
204
276
  *
@@ -425,6 +497,11 @@ export function inspectCodingLaw(path: string, content: string): readonly string
425
497
  if (source === undefined) throw new Error(`Policy source was not bound at ${path}`)
426
498
  const checker = program.getTypeChecker()
427
499
  const file = basename(path)
500
+ const placementExempt =
501
+ !CENTRAL_SOURCE_FILES.includes(file) &&
502
+ !FUNCTION_SOURCE_FILES.includes(file) &&
503
+ !DATA_SOURCE_FILES.includes(file) &&
504
+ isSelfContained(source)
428
505
 
429
506
  if (/\.[cm]?jsx?$/u.test(path)) {
430
507
  violations.push(`${path} production modules use TypeScript source extensions`)
@@ -472,10 +549,18 @@ export function inspectCodingLaw(path: string, content: string): readonly string
472
549
  ) {
473
550
  violations.push(`${path} exports every centralized declaration`)
474
551
  }
475
- if (ts.isFunctionDeclaration(statement) && !FUNCTION_SOURCE_FILES.includes(file)) {
552
+ if (
553
+ !placementExempt &&
554
+ ts.isFunctionDeclaration(statement) &&
555
+ !FUNCTION_SOURCE_FILES.includes(file)
556
+ ) {
476
557
  violations.push(`${path} places module functions in their centralized kind file`)
477
558
  }
478
- if (ts.isVariableStatement(statement) && !DATA_SOURCE_FILES.includes(file)) {
559
+ if (
560
+ !placementExempt &&
561
+ ts.isVariableStatement(statement) &&
562
+ !DATA_SOURCE_FILES.includes(file)
563
+ ) {
479
564
  violations.push(`${path} places module data in its centralized kind file`)
480
565
  }
481
566
  if (