@orkestrel/scaffold 0.0.27 → 0.0.28

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.
@@ -110,12 +110,21 @@ environment:
110
110
  The second axis is cross-cutting workspace proofs. Each one covers the whole workspace rather than
111
111
  one environment, so each is its own project:
112
112
 
113
- | Project | Files | Proves | In `test` |
114
- | ------------- | --------------------------- | -------------------------------------------------------------- | --------- |
115
- | `policy` | `tests/policy.test.ts` | Every source file obeys the syntactic coding and placement law | Yes |
116
- | `config` | `tests/config.test.ts` | Root configuration resolves its aliases, projects, and outputs | Yes |
117
- | `guides` | `tests/guides.test.ts` | Every documented API exists and every public API is documented | Yes |
118
- | `integration` | `tests/integration.test.ts` | The built package works when installed and driven from outside | No |
113
+ | Project | Files | Proves | In `test` |
114
+ | ------------- | ---------------------------- | --------------------------------------------------------------- | --------- |
115
+ | `policy` | `tests/policy.test.ts` | Every source file obeys the syntactic coding and placement law | Yes |
116
+ | `config` | `tests/config.test.ts` | Root configuration resolves its aliases, projects, and outputs | Yes |
117
+ | `guides` | `tests/guides.test.ts` | Every documented API exists and every public API is documented | Yes |
118
+ | `conformance` | `tests/conformance.test.ts` | Where this package drifts from the official tooling it tracks | Yes |
119
+ | `integration` | `tests/integration.test.ts` | The built package works when installed and driven from outside | No |
120
+ | `service` | `tests/service/**/*.test.ts` | The live external services this package drives, driven for real | No |
121
+
122
+ `conformance` and `service` are two subjects, not two names for one. `conformance` measures this
123
+ package against an official artifact it stays compatible with and drives nothing external: the
124
+ tooling it measures against is installed, and any server it drives is one the proof starts itself.
125
+ That is what makes it hermetic and keeps it in `test`. `service` drives the real thing, so it takes
126
+ `tests/setupService.ts` for readiness, longer timeouts, and no file parallelism, and it leaves
127
+ `test` for `prepublishOnly`.
119
128
 
120
129
  One project sits on neither axis. `probe` includes `tmp/probe/**/*.test.ts` so an agent can run a
121
130
  throwaway instrument against real sources, aliases and setup. Declare no proof there. Every test
@@ -123,8 +132,9 @@ script names its project, so no gate runs it; its directory is ignored by git; a
123
132
  `.claude/rules/tests.md` governs what may live there.
124
133
 
125
134
  - Define a cross-cutting project only for a proof the package actually has.
126
- - A live-service project is the fifth kind. It is named for the service it drives, and
127
- `.claude/rules/tests.md` governs it.
135
+ - A live-service project is the fifth kind. It is the `service` project above, `scripts/service.sh`
136
+ provisions what it drives, and `.claude/rules/tests.md` governs it. Name it `service` whatever it
137
+ drives.
128
138
  - A project leaves the default run for one of two reasons: it drives a live external service, or it
129
139
  is hermetic but slow — it spawns processes, packs, installs, or drives a real build.
130
140
  - Every isolated project has its own script, is excluded from `test`, and runs in `prepublishOnly`.
@@ -137,7 +147,7 @@ Setup assets:
137
147
 
138
148
  Scope with `test:src`, `test:src:core`, `test:app`, `test:app:server`, and equivalent scripts. Each
139
149
  cross-cutting project has its own script too: `test:policy`, `test:config`, `test:guides`,
140
- `test:integration`.
150
+ `test:conformance`, `test:integration`, `test:service`.
141
151
 
142
152
  ## Typechecking and environment isolation
143
153
 
@@ -211,3 +221,4 @@ Run `show` only **after** formatting. The committed `demo/showcase.html` is gene
211
221
  - Store text as UTF-8.
212
222
  - Before accepting broad generated or migrated edits, scan changed text for replacement characters, mojibake, unintended control characters, and accidental trailing debris.
213
223
  - Preserve intentional Unicode punctuation and symbols; do not “clean” valid text merely because it is non-ASCII.
224
+ - Never renormalize Unicode while rewriting a file. Retyping a line can silently fold a decomposed sequence into its precomposed form — `e` + U+0301 becoming U+00E9 — and the two render identically, so the diff reads as a no-op and review sees nothing. Where the exact code points are the subject, as in an encoding or transport proof, that fold deletes the case the test exists for while leaving it green and named. Move such a line rather than retyping it, and compare bytes with `od -c` or a code-point dump rather than by eye.
@@ -92,6 +92,7 @@ Exported from `@orkestrel/scaffold`, and reachable from
92
92
  | `BIN_ENTRY_PATH` | const | The executable entry whose presence makes a workspace `bin`. |
93
93
  | `CATALOG_AGENT_PATH` | const | The agent file whose marker-bounded package table the catalog verb alone owns. |
94
94
  | `CONFIG_TEMPLATES` | const | Formatter-stable template text for every configuration artifact. |
95
+ | `CONFORMANCE_TEST_PATH` | const | The official-tooling drift proof whose presence makes a workspace `conformance`. |
95
96
  | `CONTROL_CHARACTER_PATTERN` | const | Unicode controls, formatting controls, and line and paragraph separators rejected in text. |
96
97
  | `DEFAULT_ENGINES` | const | The `engines.node` range a workspace starts with. |
97
98
  | `DEFAULT_VERSION` | const | The version a workspace starts at. |
@@ -124,6 +125,8 @@ Exported from `@orkestrel/scaffold`, and reachable from
124
125
  | `ORCHESTRATION_PATH_PREFIXES` | const | The path prefixes whose contents instruct or wire an agent, frozen. |
125
126
  | `ORKESTREL_RANGE_PATTERN` | const | The exact caret-pinned pre-1.0 range accepted for an `@orkestrel/*` runtime dependency. |
126
127
  | `SERVICE_SCRIPT_PATH` | const | The provisioner skeleton a workspace with declared service vendors is given once. |
128
+ | `SERVICE_SETUP_PATH` | const | The live-service readiness module whose presence makes a workspace `service`. |
129
+ | `SERVICE_TEST_INCLUDE` | const | The include the live-service project covers, which is a directory rather than one proof. |
127
130
  | `SHOWCASE_CONFIG_PATH` | const | The Vite wrapper whose presence makes a workspace `showcase`. |
128
131
  | `SHOWCASE_DEV_DEPENDENCIES` | const | The development dependency used only by the optional single-file showcase build. |
129
132
  | `SOURCE_BROWSER_DEV_DEPENDENCIES` | const | The development dependencies a published browser `src` environment adds. |
@@ -456,25 +459,33 @@ workspace would otherwise declare a dependency that does not resolve.
456
459
 
457
460
  `new --bin` creates the executable entry, its test, and its scoped Vite and TypeScript wrappers.
458
461
  The other structural facts do not need creation flags. Add `tests/integration.test.ts` for
459
- `integration`, `tests/setupGlobal.ts` for `global`, and
460
- `configs/app/vite.showcase.config.ts` for `showcase`; reading verbs detect each exact-case file and
461
- register its fixed machinery. Add `scripts/service.sh` for `services`. Reading verbs preserve and
462
- protect that birth-owned script, but do not infer its service list from edited text.
462
+ `integration`, `tests/conformance.test.ts` for `conformance`, `tests/setupService.ts` for `service`,
463
+ `tests/setupGlobal.ts` for `global`, and `configs/app/vite.showcase.config.ts` for `showcase`;
464
+ reading verbs detect each exact-case file and register its fixed machinery. Add `scripts/service.sh`
465
+ for `vendors`. Reading verbs preserve and protect that birth-owned script, but do not infer its
466
+ vendor list from edited text.
463
467
 
464
468
  ### Reading a target
465
469
 
466
470
  `audit`, `repair`, `catalog`, and `overwrite` derive the blueprint from the target itself. The name
467
471
  and the declared `@orkestrel/*` packages come from `package.json`. The two environment axes come
468
472
  from the directories the target actually ships, because a directory is the fact and a declaration
469
- beside it could disagree. Four more facts come from exact-case files: `src/bin/main.ts` selects
470
- `bin`, `tests/integration.test.ts` selects `integration`, `tests/setupGlobal.ts` selects `global`,
473
+ beside it could disagree. Six more facts come from exact-case files: `src/bin/main.ts` selects
474
+ `bin`, `tests/integration.test.ts` selects `integration`, `tests/conformance.test.ts` selects
475
+ `conformance`, `tests/setupService.ts` selects `service`, `tests/setupGlobal.ts` selects `global`,
471
476
  and `configs/app/vite.showcase.config.ts` selects `showcase`. A containing directory does not select
472
477
  the fact by itself.
473
478
 
474
- `services` is not reconstructed. Its only artifact, `scripts/service.sh`, is birth-owned, so edited
475
- script text is not a trustworthy declaration of a service list. A present script remains in the
479
+ `vendors` is not reconstructed. Its only artifact, `scripts/service.sh`, is birth-owned, so edited
480
+ script text is not a trustworthy declaration of a vendor list. A present script remains in the
476
481
  target and remains protected from deletion through the owned scripts inventory, but a reading verb
477
- does not infer services from it.
482
+ does not infer vendors from it.
483
+
484
+ That is why the live-service project follows `service` rather than `vendors`. A reading verb has to
485
+ plan the project before it can say anything about a target that runs one, and a vendor list it
486
+ cannot recover would leave every such workspace unplannable. `tests/setupService.ts` is recoverable,
487
+ is the module the root configuration names by path, and is what a live proof needs anyway, so it
488
+ carries the fact and the vendor list keeps its own separate job.
478
489
 
479
490
  The root Vite configuration always defines the fixed `guides` project and selects it at
480
491
  configuration load only when `tests/guides.test.ts` is a physical file with that exact path case. A
@@ -560,9 +571,16 @@ because the shape is chosen once and read afterwards: `new` refuses the advisory
560
571
  `repair` need the plan to describe and restore a target that already has that shape. A library
561
572
  caller creating a workspace holds the same refusal, and the Compile section below states it.
562
573
 
563
- `bin`, `integration`, `services`, `global`, and `showcase` are structural facts. Each is set only
564
- when the workspace physically ships the directory or exact-case file that defines it, never because
565
- of the workspace's name and never because a sibling fact is set.
574
+ `bin`, `integration`, `conformance`, `service`, `vendors`, `global`, and `showcase` are structural
575
+ facts. Each is set only when the workspace physically ships the directory or exact-case file that
576
+ defines it, never because of the workspace's name and never because a sibling fact is set.
577
+
578
+ `service` says the workspace runs a live-service Vitest project over `tests/service`, and it alone
579
+ registers that project, its `test:service` script, and the `tests/setupService.ts` readiness module
580
+ the project names. `vendors` names each external service the workspace drives and emits
581
+ `scripts/service.sh`, the provisioner that starts them. Neither is derivable from the other: a
582
+ workspace may declare vendors before it writes a suite, and a suite may drive a service the skeleton
583
+ does not start.
566
584
 
567
585
  An axis-dependent structural fact projects only when its required axis exists. `integration`
568
586
  projects a published `src`, and `showcase` projects the browser `app` environment. When that axis is
@@ -674,7 +692,8 @@ barrels, the tests, `README.md`, and `guides/README.md` are written once and are
674
692
  a later verb.
675
693
 
676
694
  Content ownership does not preserve an arbitrary custom Vitest project. Fixed optional proofs are
677
- selected by their defining paths, as `guides` is. A workspace that needs other local configuration
695
+ selected by their defining paths, as `guides`, `conformance`, and `service` are. A workspace that
696
+ needs other local configuration
678
697
  must keep those edits outside a content-owned file; `repair` restores that file to the canonical
679
698
  project set.
680
699
 
@@ -960,7 +979,7 @@ are thrown, so an observer sees a refusal even where the caller catches it.
960
979
 
961
980
  ## Limits
962
981
 
963
- Six things a reader will look for and not find.
982
+ Seven things a reader will look for and not find.
964
983
 
965
984
  **A code fence in this guide is unverified.** [`tests/guides.test.ts`](../tests/guides.test.ts)
966
985
  proves that every fence imports only real exports of the two barrels, and that every backticked name
@@ -1012,6 +1031,30 @@ nothing. This is deliberate: a generated sample entity is repeatedly mistaken fo
1012
1031
  implementation. What a consumer does first is write the module's `types.ts`, then the
1013
1032
  implementation that conforms to it, then export both from the barrel — the order `AGENTS.md` fixes.
1014
1033
 
1034
+ **A conformance or live-service proof is registered, but neither is written for you.** Scaffold
1035
+ registers the `conformance` and `service` projects, their scripts, and the gate each belongs to. It
1036
+ emits no proof into either, because both name something only the package knows: the official
1037
+ artifact a conformance check measures against, and the service a live proof drives. A generated
1038
+ placeholder would read as a proof while measuring nothing, so the file a consumer writes is the file
1039
+ that selects the project.
1040
+
1041
+ The consequence is one empty-project case in each direction. A blueprint carrying `conformance` with
1042
+ no `tests/conformance.test.ts` registers a project whose include resolves to nothing, and Vitest
1043
+ exits non-zero on it. A blueprint carrying `service` gets `tests/setupService.ts` — the root
1044
+ configuration names that module by path, so an absent one fails the project's load rather than its
1045
+ run — and still no suite beneath `tests/service`, so `test:service` reports no test files until the
1046
+ consumer writes the first one. Both cases are visible the first time the script runs, which is why
1047
+ neither is silent.
1048
+
1049
+ Neither project folds into `integration`, which means something narrower: the built package works
1050
+ when installed and driven from outside. Two fleet packages hold the distinction. `@orkestrel/ollama`
1051
+ drives a real Ollama daemon through a `service` project, so a real service answers it and it runs
1052
+ from `prepublishOnly`. `@orkestrel/mcp` measures its server against the specification's own runner,
1053
+ `@modelcontextprotocol/conformance`, through a `conformance` project. It pins that runner as a
1054
+ development dependency and resolves it out of `node_modules`, and the server the runner drives is
1055
+ one the fixture starts itself on a loopback port, so the run drives nothing external and stays in
1056
+ `test`.
1057
+
1015
1058
  ## Tests
1016
1059
 
1017
1060
  - [`tests/src/core/Compiler.test.ts`](../tests/src/core/Compiler.test.ts) — the three stages, the
@@ -102,13 +102,22 @@ describe('root configuration', () => {
102
102
  setup: ['./tests/setup.ts', './tests/setupServer.ts'],
103
103
  })
104
104
  }
105
- for (const label of ['policy', 'config', 'guides', 'integration']) {
105
+ for (const label of ['policy', 'config', 'guides', 'conformance', 'integration']) {
106
106
  if (!existsSync(resolve(root, `tests/${label}.test.ts`))) continue
107
107
  expected.set(label, {
108
108
  include: `tests/${label}.test.ts`,
109
109
  setup: ['./tests/setup.ts'],
110
110
  })
111
111
  }
112
+ // The live-service project covers a directory rather than one proof, so its
113
+ // readiness module is the fact that selects it. A suite beneath
114
+ // `tests/service` with no setup module is a project nothing configures.
115
+ if (existsSync(resolve(root, 'tests/setupService.ts'))) {
116
+ expected.set('service', {
117
+ include: 'tests/service/**/*.test.ts',
118
+ setup: ['./tests/setup.ts', './tests/setupService.ts'],
119
+ })
120
+ }
112
121
  expected.set('probe', { include: 'tmp/probe/**/*.test.ts', setup: ['./tests/setup.ts'] })
113
122
  // A row that is a configuration rather than a factory. A workspace with a
114
123
  // browser application emits one, because that factory refuses overrides and
@@ -353,8 +362,35 @@ describe('root configuration', () => {
353
362
  const test = Object.getOwnPropertyDescriptor(scripts, 'test')?.value
354
363
  const config = Object.getOwnPropertyDescriptor(scripts, 'test:config')?.value
355
364
  const integration = Object.getOwnPropertyDescriptor(scripts, 'test:integration')?.value
365
+ const conformance = Object.getOwnPropertyDescriptor(scripts, 'test:conformance')?.value
366
+ const service = Object.getOwnPropertyDescriptor(scripts, 'test:service')?.value
356
367
  const publish = Object.getOwnPropertyDescriptor(scripts, 'prepublishOnly')?.value
357
368
  const hasIntegration = existsSync(resolve(root, 'tests/integration.test.ts'))
369
+ // The optional proofs are read off the registered project set rather than off
370
+ // their files, because the defect this measures is a registered project no
371
+ // gate runs. A project selected by a path that is not yet there is still
372
+ // registered, and it is exactly the one whose script goes missing.
373
+ const rows = configuration.test?.projects
374
+ if (!Array.isArray(rows)) throw new Error('The root configuration carries no projects')
375
+ const registered = new Set<string>()
376
+ for (const row of rows) {
377
+ if (typeof row === 'function') {
378
+ registered.add(row.name)
379
+ continue
380
+ }
381
+ if (typeof row !== 'object' || row === null) continue
382
+ const block: unknown = Object.getOwnPropertyDescriptor(row, 'test')?.value
383
+ if (typeof block !== 'object' || block === null) continue
384
+ const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
385
+ if (typeof named !== 'object' || named === null) continue
386
+ const label: unknown = Object.getOwnPropertyDescriptor(named, 'label')?.value
387
+ if (typeof label === 'string') registered.add(label)
388
+ }
389
+ // The population must be able to answer both ways before either answer counts.
390
+ expect(registered.has('config')).toBe(true)
391
+ expect(registered.has('control')).toBe(false)
392
+ const hasConformance = registered.has('conformance')
393
+ const hasService = registered.has('service')
358
394
  expect(config).toBe(
359
395
  'vitest run --config vite.config.ts --no-cache --reporter=dot --project config',
360
396
  )
@@ -368,6 +404,25 @@ describe('root configuration', () => {
368
404
  expect(typeof publish === 'string' && publish.includes('npm run test:integration')).toBe(
369
405
  hasIntegration,
370
406
  )
407
+ // A registered project no gate runs is a proof that never executes, and it
408
+ // never fails, so the suite reports green while carrying it. Conformance is
409
+ // hermetic and belongs to `test`; the live-service project drives a real
410
+ // service and belongs to `prepublishOnly` alone.
411
+ expect(conformance).toBe(
412
+ hasConformance
413
+ ? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project conformance'
414
+ : undefined,
415
+ )
416
+ expect(typeof test === 'string' && test.includes('npm run test:conformance')).toBe(
417
+ hasConformance,
418
+ )
419
+ expect(service).toBe(
420
+ hasService
421
+ ? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project service'
422
+ : undefined,
423
+ )
424
+ expect(typeof test === 'string' && test.includes('test:service')).toBe(false)
425
+ expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(hasService)
371
426
  })
372
427
  })
373
428
 
@@ -201,6 +201,12 @@ var GLOBAL_SETUP_PATH = "tests/setupGlobal.ts";
201
201
  var GUIDES_TEST_PATH = "tests/guides.test.ts";
202
202
  /** The installed-package proof whose presence makes a workspace `integration`. */
203
203
  var INTEGRATION_TEST_PATH = "tests/integration.test.ts";
204
+ /** The official-tooling drift proof whose presence makes a workspace `conformance`. */
205
+ var CONFORMANCE_TEST_PATH = "tests/conformance.test.ts";
206
+ /** The live-service readiness module whose presence makes a workspace `service`. */
207
+ var SERVICE_SETUP_PATH = "tests/setupService.ts";
208
+ /** The include the live-service project covers, which is a directory rather than one proof. */
209
+ var SERVICE_TEST_INCLUDE = "tests/service/**/*.test.ts";
204
210
  /** The Vite wrapper whose presence makes a workspace `showcase`. */
205
211
  var SHOWCASE_CONFIG_PATH = "configs/app/vite.showcase.config.ts";
206
212
  /** The bare workspace name syntax: lowercase alphanumeric with hyphens, letter first. */
@@ -284,7 +290,7 @@ var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
284
290
  var BASE_DEV_DEPENDENCIES = Object.freeze({
285
291
  "@microsoft/api-extractor": "^7.58.12",
286
292
  "@orkestrel/guide": "^0.0.10",
287
- "@orkestrel/scaffold": "^0.0.27",
293
+ "@orkestrel/scaffold": "^0.0.28",
288
294
  "@types/node": "^26.2.0",
289
295
  oxfmt: "^0.62.0",
290
296
  oxlint: "^1.77.0",
@@ -676,6 +682,44 @@ export function appBrowser(...options: never[]): UserConfig {
676
682
  },
677
683
  options ?? {},
678
684
  )
685
+ `,
686
+ conformance: `// Where this package drifts from the official tooling it stays compatible with.
687
+ // The subject is this package, so the proof is hermetic and stays in \`npm test\`.
688
+ export const conformance = (options?: UserConfig): UserConfig =>
689
+ mergeConfig(
690
+ {
691
+ resolve,
692
+ test: {
693
+ name: { label: 'conformance', color: 'magenta' },
694
+ include: ['${CONFORMANCE_TEST_PATH}'],
695
+ setupFiles: ['./tests/setup.ts'],
696
+ environment: 'node',
697
+ browser: { enabled: false },
698
+ },
699
+ },
700
+ options ?? {},
701
+ )
702
+ `,
703
+ service: `// The live external services this package drives. It starts nothing itself:
704
+ // \`scripts/service.sh\` provisions, \`tests/setupService.ts\` proves readiness, and
705
+ // the project stays out of \`npm test\` because a real service answers it.
706
+ export const service = (options?: UserConfig): UserConfig =>
707
+ mergeConfig(
708
+ {
709
+ resolve,
710
+ test: {
711
+ name: { label: 'service', color: 'red' },
712
+ include: ['${SERVICE_TEST_INCLUDE}'],
713
+ setupFiles: ['./tests/setup.ts', './tests/setupService.ts'],
714
+ environment: 'node',
715
+ browser: { enabled: false },
716
+ testTimeout: 120_000,
717
+ hookTimeout: 120_000,
718
+ fileParallelism: false,
719
+ },
720
+ },
721
+ options ?? {},
722
+ )
679
723
  `,
680
724
  probe: `// A workbench, not a proof. No gate selects this project.
681
725
  export const probe = (options?: UserConfig): UserConfig =>
@@ -1150,7 +1194,7 @@ npm test
1150
1194
  set -eu
1151
1195
 
1152
1196
  printf '%s\\n' \\
1153
- {{services}}
1197
+ {{vendors}}
1154
1198
  ` })
1155
1199
  });
1156
1200
  //#endregion
@@ -1437,7 +1481,9 @@ var isBlueprint = (0, _orkestrel_contract.recordOf)({
1437
1481
  overrides: (0, _orkestrel_contract.andOf)(isCollection, (0, _orkestrel_contract.arrayOf)(isOverride)),
1438
1482
  bin: _orkestrel_contract.isBoolean,
1439
1483
  integration: _orkestrel_contract.isBoolean,
1440
- services: (0, _orkestrel_contract.andOf)(isCollection, (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)),
1484
+ conformance: _orkestrel_contract.isBoolean,
1485
+ service: _orkestrel_contract.isBoolean,
1486
+ vendors: (0, _orkestrel_contract.andOf)(isCollection, (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)),
1441
1487
  global: _orkestrel_contract.isBoolean,
1442
1488
  showcase: _orkestrel_contract.isBoolean
1443
1489
  }, ["description"]);
@@ -2673,8 +2719,14 @@ function blueprintToDevDependencies(blueprint) {
2673
2719
  * to the axes the blueprint declares: a check and a test script per declared
2674
2720
  * environment, an aggregate over each axis, the policy and configuration
2675
2721
  * proofs every workspace can pass before it has a public API, and one build per
2676
- * target that actually builds. The isolated installed-package integration
2677
- * proof stays out of `test` and runs from `prepublishOnly` instead.
2722
+ * target that actually builds.
2723
+ *
2724
+ * A proof leaves `test` when a real service or a real install answers it. The
2725
+ * installed-package integration proof and the live-service proof therefore run
2726
+ * from `prepublishOnly` instead. The conformance proof stays in `test`, because
2727
+ * it measures this package against official tooling and drives nothing external:
2728
+ * a conformance run may start a server, but it starts its own and reaches it
2729
+ * over loopback, so the run stays hermetic.
2678
2730
  *
2679
2731
  * The configuration paths interpolated here are the same ones `SRC_MATRIX` and
2680
2732
  * `APP_MATRIX` list as each environment's configuration files, so a rename in
@@ -2721,7 +2773,8 @@ function blueprintToScripts(blueprint) {
2721
2773
  ...compiles ? ["npm run test:src"] : [],
2722
2774
  ...blueprint.app.length > 0 ? ["npm run test:app"] : [],
2723
2775
  "npm run test:policy",
2724
- "npm run test:config"
2776
+ "npm run test:config",
2777
+ ...blueprint.conformance ? ["npm run test:conformance"] : []
2725
2778
  ].join(" && ");
2726
2779
  if (compiles) {
2727
2780
  scripts["test:src"] = [
@@ -2738,8 +2791,10 @@ function blueprintToScripts(blueprint) {
2738
2791
  }
2739
2792
  scripts["test:policy"] = `${vitest} --project policy`;
2740
2793
  scripts["test:config"] = `${vitest} --project config`;
2794
+ if (blueprint.conformance) scripts["test:conformance"] = `${vitest} --project conformance`;
2741
2795
  scripts["test:probe"] = "vitest run --config vite.config.ts --no-cache --reporter=verbose --project probe";
2742
2796
  if (integrates) scripts["test:integration"] = `${vitest} --project integration`;
2797
+ if (blueprint.service) scripts["test:service"] = `${vitest} --project service`;
2743
2798
  scripts.build = [
2744
2799
  "npm run clean",
2745
2800
  ...compiles ? ["npm run build:src"] : [],
@@ -2769,7 +2824,11 @@ function blueprintToScripts(blueprint) {
2769
2824
  scripts.serve = "node dist/app/server/main.cjs";
2770
2825
  scripts["serve:build"] = "npm run build:app:server && npm run serve";
2771
2826
  }
2772
- scripts.prepublishOnly = ["npm run format:check && npm run lint:check && npm run check && npm run build && npm test", ...integrates ? ["npm run test:integration"] : []].join(" && ");
2827
+ scripts.prepublishOnly = [
2828
+ "npm run format:check && npm run lint:check && npm run check && npm run build && npm test",
2829
+ ...integrates ? ["npm run test:integration"] : [],
2830
+ ...blueprint.service ? ["npm run test:service"] : []
2831
+ ].join(" && ");
2773
2832
  return scripts;
2774
2833
  }
2775
2834
  /**
@@ -3051,6 +3110,14 @@ export function appShowcase(...options: never[]): UserConfig {
3051
3110
  projects.push("config");
3052
3111
  factories.push(CONFIG_TEMPLATES.factories.guides);
3053
3112
  projects.push(`...(isExactCaseFile(resolveWorkspacePath('${GUIDES_TEST_PATH}')) ? [guides] : [])`);
3113
+ if (blueprint.conformance) {
3114
+ factories.push(CONFIG_TEMPLATES.factories.conformance);
3115
+ projects.push("conformance");
3116
+ }
3117
+ if (blueprint.service) {
3118
+ factories.push(CONFIG_TEMPLATES.factories.service);
3119
+ projects.push("service");
3120
+ }
3054
3121
  if (blueprint.src.length > 0 && blueprint.integration) {
3055
3122
  factories.push((0, _orkestrel_template.fillTemplate)(CONFIG_TEMPLATES.factories.integration, { global: blueprint.global ? " globalSetup: ['./tests/setupGlobal.ts'],\n" : "" }));
3056
3123
  projects.push("integration");
@@ -3271,6 +3338,12 @@ function blueprintToSourceArtifacts(blueprint) {
3271
3338
  * workspace and therefore follows the `src` axis as well as its structural
3272
3339
  * flag.
3273
3340
  *
3341
+ * The conformance proof is not emitted either, for the reason the guide proof
3342
+ * is not: it names an official artifact only the package knows, so a generated
3343
+ * placeholder would read as a proof while measuring nothing. `service` emits its
3344
+ * readiness setup alone, because the root configuration names that module by
3345
+ * path.
3346
+ *
3274
3347
  * @example
3275
3348
  * ```ts
3276
3349
  * import { blueprintToTestArtifacts, createBlueprint } from '@orkestrel/scaffold'
@@ -3304,6 +3377,13 @@ function blueprintToTestArtifacts(blueprint) {
3304
3377
  environment: "server",
3305
3378
  content: ARTIFACT_TEMPLATES.tests.setup
3306
3379
  });
3380
+ if (blueprint.service) artifacts.push({
3381
+ path: SERVICE_SETUP_PATH,
3382
+ group: "tests",
3383
+ ownership: "birth",
3384
+ origin: "template",
3385
+ content: ARTIFACT_TEMPLATES.tests.setup
3386
+ });
3307
3387
  if (blueprint.global) artifacts.push({
3308
3388
  path: GLOBAL_SETUP_PATH,
3309
3389
  group: "tests",
@@ -3417,22 +3497,22 @@ function blueprintToDocumentArtifacts(blueprint) {
3417
3497
  * Compile the blueprint-dependent orchestration artifacts.
3418
3498
  *
3419
3499
  * @param blueprint - The workspace specification.
3420
- * @returns A service inventory script when services are declared, otherwise none.
3500
+ * @returns A vendor inventory script when vendors are declared, otherwise none.
3421
3501
  *
3422
3502
  * @remarks
3423
- * A service name does not describe startup, readiness, or cleanup. The script
3503
+ * A vendor name does not describe startup, readiness, or cleanup. The script
3424
3504
  * therefore records only the declared inventory and does not invent a service
3425
3505
  * runner or test project.
3426
3506
  */
3427
3507
  function blueprintToOrchestrationArtifacts(blueprint) {
3428
- if (blueprint.services.length === 0) return [];
3429
- const services = blueprint.services.map((service, index) => `\t'${service}'${index === blueprint.services.length - 1 ? "" : " \\"}`).join("\n");
3508
+ if (blueprint.vendors.length === 0) return [];
3509
+ const vendors = blueprint.vendors.map((vendor, index) => `\t'${vendor}'${index === blueprint.vendors.length - 1 ? "" : " \\"}`).join("\n");
3430
3510
  return [{
3431
3511
  path: SERVICE_SCRIPT_PATH,
3432
3512
  group: "orchestration",
3433
3513
  ownership: "birth",
3434
3514
  origin: "template",
3435
- content: (0, _orkestrel_template.fillTemplate)(ARTIFACT_TEMPLATES.orchestration.service, { services })
3515
+ content: (0, _orkestrel_template.fillTemplate)(ARTIFACT_TEMPLATES.orchestration.service, { vendors })
3436
3516
  }];
3437
3517
  }
3438
3518
  /**
@@ -3788,19 +3868,19 @@ function blueprintToQuestions(blueprint) {
3788
3868
  message: "integration projects a published src, and this workspace declares none, so it emits nothing.",
3789
3869
  blocking: false
3790
3870
  });
3791
- const services = /* @__PURE__ */ new Set();
3792
- for (const service of blueprint.services) {
3793
- if (!NAME_PATTERN.test(service)) questions.push({
3794
- field: "services",
3795
- message: `${service} is not a lowercase alphanumeric service name starting with a letter.`,
3871
+ const vendors = /* @__PURE__ */ new Set();
3872
+ for (const vendor of blueprint.vendors) {
3873
+ if (!NAME_PATTERN.test(vendor)) questions.push({
3874
+ field: "vendors",
3875
+ message: `${vendor} is not a lowercase alphanumeric vendor name starting with a letter.`,
3796
3876
  blocking: true
3797
3877
  });
3798
- else if (services.has(service)) questions.push({
3799
- field: "services",
3800
- message: `${service} is declared more than once on services.`,
3878
+ else if (vendors.has(vendor)) questions.push({
3879
+ field: "vendors",
3880
+ message: `${vendor} is declared more than once on vendors.`,
3801
3881
  blocking: true
3802
3882
  });
3803
- services.add(service);
3883
+ vendors.add(vendor);
3804
3884
  }
3805
3885
  if (blueprint.showcase && !blueprint.app.includes("browser")) questions.push({
3806
3886
  field: "showcase",
@@ -4268,8 +4348,8 @@ var Compiler = class {
4268
4348
  * blueprint.
4269
4349
  *
4270
4350
  * @remarks
4271
- * A blueprint is a closed record of sixteen fields, and most of them have one
4272
- * sensible starting value: an empty list, a cleared flag, `DEFAULT_VERSION`, and
4351
+ * A blueprint is a closed record, and most of its fields have one sensible
4352
+ * starting value: an empty list, a cleared flag, `DEFAULT_VERSION`, and
4273
4353
  * `DEFAULT_ENGINES`. Filling them here is what lets a caller state only what its
4274
4354
  * workspace actually declares.
4275
4355
  *
@@ -4310,7 +4390,9 @@ function createBlueprint(name, input) {
4310
4390
  overrides: input?.overrides ?? [],
4311
4391
  bin: input?.bin ?? false,
4312
4392
  integration: input?.integration ?? false,
4313
- services: input?.services ?? [],
4393
+ conformance: input?.conformance ?? false,
4394
+ service: input?.service ?? false,
4395
+ vendors: input?.vendors ?? [],
4314
4396
  global: input?.global ?? false,
4315
4397
  showcase: input?.showcase ?? false
4316
4398
  }));
@@ -4347,6 +4429,7 @@ exports.BIN_CONFIGS = BIN_CONFIGS;
4347
4429
  exports.BIN_ENTRY_PATH = BIN_ENTRY_PATH;
4348
4430
  exports.CATALOG_AGENT_PATH = CATALOG_AGENT_PATH;
4349
4431
  exports.CONFIG_TEMPLATES = CONFIG_TEMPLATES;
4432
+ exports.CONFORMANCE_TEST_PATH = CONFORMANCE_TEST_PATH;
4350
4433
  exports.CONTROL_CHARACTER_PATTERN = CONTROL_CHARACTER_PATTERN;
4351
4434
  exports.Compiler = Compiler;
4352
4435
  exports.DEFAULT_ENGINES = DEFAULT_ENGINES;
@@ -4380,6 +4463,8 @@ exports.ORCHESTRATION_PATH_NAMES = ORCHESTRATION_PATH_NAMES;
4380
4463
  exports.ORCHESTRATION_PATH_PREFIXES = ORCHESTRATION_PATH_PREFIXES;
4381
4464
  exports.ORKESTREL_RANGE_PATTERN = ORKESTREL_RANGE_PATTERN;
4382
4465
  exports.SERVICE_SCRIPT_PATH = SERVICE_SCRIPT_PATH;
4466
+ exports.SERVICE_SETUP_PATH = SERVICE_SETUP_PATH;
4467
+ exports.SERVICE_TEST_INCLUDE = SERVICE_TEST_INCLUDE;
4383
4468
  exports.SHOWCASE_CONFIG_PATH = SHOWCASE_CONFIG_PATH;
4384
4469
  exports.SHOWCASE_DEV_DEPENDENCIES = SHOWCASE_DEV_DEPENDENCIES;
4385
4470
  exports.SOURCE_BROWSER_DEV_DEPENDENCIES = SOURCE_BROWSER_DEV_DEPENDENCIES;