@orkestrel/scaffold 0.0.27 → 0.0.29

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.
@@ -36,12 +36,14 @@ paths:
36
36
  A proof that covers the workspace instead of one module has a fixed location, so no package invents
37
37
  its own:
38
38
 
39
- | Path | Proves |
40
- | --------------------------- | -------------------------------------------------------------- |
41
- | `tests/policy.test.ts` | Every source file obeys the syntactic coding and placement law |
42
- | `tests/config.test.ts` | Root configuration resolves its aliases, projects, and outputs |
43
- | `tests/guides.test.ts` | Every documented API exists and every public API is documented |
44
- | `tests/integration.test.ts` | The built package works when installed and driven from outside |
39
+ | Path | Proves |
40
+ | ---------------------------- | --------------------------------------------------------------- |
41
+ | `tests/policy.test.ts` | Every source file obeys the syntactic coding and placement law |
42
+ | `tests/config.test.ts` | Root configuration resolves its aliases, projects, and outputs |
43
+ | `tests/guides.test.ts` | Every documented API exists and every public API is documented |
44
+ | `tests/conformance.test.ts` | Where this package drifts from the official tooling it tracks |
45
+ | `tests/integration.test.ts` | The built package works when installed and driven from outside |
46
+ | `tests/service/**/*.test.ts` | The live external services this package drives, driven for real |
45
47
 
46
48
  - `.claude/rules/workspace.md` names the Vitest project each location belongs to.
47
49
  - `integration.test.ts` is a reserved filename at any level. It names a scope rather than a module,
@@ -80,9 +82,11 @@ Three rules bind every probe:
80
82
 
81
83
  Live external services/models are the deliberate exception to fast hermetic defaults:
82
84
 
83
- - Put them in a dedicated isolated Vitest project with its own setup and longer timeout.
85
+ - Put them in the `service` project, under `tests/service/`, with `tests/setupService.ts` for setup
86
+ and a longer timeout. That module's presence is what registers the project, so a live proof with
87
+ no readiness setup is a project nothing configures.
84
88
  - Keep them out of the default run.
85
- - Warm and verify service readiness in setup.
89
+ - Warm and verify service readiness in `tests/setupService.ts`.
86
90
  - Hard-require readiness: throw loudly; never silently skip.
87
91
  - Verify service-dependent logic through that service's project, not unrelated module tests or scattered conditional skips.
88
92
  - Tune each request to the smallest input/context/output that proves one behavior without becoming brittle or expensive.
@@ -193,6 +197,14 @@ Coverage rules:
193
197
  Before acceptance:
194
198
 
195
199
  - prove every intended test file is discovered by the correct project;
200
+ - prove every declared project is reachable from a gate. A project registered in the root
201
+ configuration with no script, or with a script no chain runs, is a proof that never executes — and
202
+ because it never executes it never fails, so the suite reports green while carrying it. Read the
203
+ chain, not the exit code: run each project directly once and compare that list against what `test`
204
+ actually invokes. The gate that would report this gap is the gate that is missing.
205
+ - prove a declared project's include resolves to a real file. An empty project is not a passing
206
+ project: Vitest exits non-zero on "no test files found", so a project aimed at a path that was
207
+ never created stays invisible until something finally runs it.
196
208
  - inspect actual test counts and environments;
197
209
  - audit `.todo`, `.skip`, conditional skips, retries, and inflated timeouts;
198
210
  - confirm each assertion would fail for the defect it claims to catch, and that it fails rather than passes when its population is empty;
@@ -60,9 +60,13 @@ Define aliases in `tsconfig.json` first. `vite.config.ts` derives from `compiler
60
60
  - `*/types.ts`: public API contracts.
61
61
  - `configs/src/` and `configs/app/`: thin per-target wrappers, including optional
62
62
  `configs/src/*bin*` files. Shared logic remains in root configs.
63
- - `configs/helpers.ts`: the one permitted leaf under `configs/`. It imports nothing from the
64
- workspace, which is what keeps it a leaf. Each `configs/src/*.config.ts` imports the root config
65
- rather than the leaf, so shared build logic stays in one place.
63
+ - `configs/helpers.ts` and `configs/browsers.ts`: the only permitted leaves under `configs/`. Each
64
+ imports nothing from the workspace, which is what keeps it a leaf. Each `configs/src/*.config.ts`
65
+ imports the root config rather than a leaf, so shared build logic stays in one place.
66
+ - Keep `configs/helpers.ts` free of any dependency a core-only workspace does not declare. It is
67
+ vendored byte-identical to every workspace, so an import there must resolve in all of them.
68
+ `configs/browsers.ts` exists for that reason: it imports `playwright` and
69
+ `@vitest/browser-playwright`, and only a workspace with a browser environment is given it.
66
70
 
67
71
  Environment rules:
68
72
 
@@ -110,12 +114,21 @@ environment:
110
114
  The second axis is cross-cutting workspace proofs. Each one covers the whole workspace rather than
111
115
  one environment, so each is its own project:
112
116
 
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 |
117
+ | Project | Files | Proves | In `test` |
118
+ | ------------- | ---------------------------- | --------------------------------------------------------------- | --------- |
119
+ | `policy` | `tests/policy.test.ts` | Every source file obeys the syntactic coding and placement law | Yes |
120
+ | `config` | `tests/config.test.ts` | Root configuration resolves its aliases, projects, and outputs | Yes |
121
+ | `guides` | `tests/guides.test.ts` | Every documented API exists and every public API is documented | Yes |
122
+ | `conformance` | `tests/conformance.test.ts` | Where this package drifts from the official tooling it tracks | Yes |
123
+ | `integration` | `tests/integration.test.ts` | The built package works when installed and driven from outside | No |
124
+ | `service` | `tests/service/**/*.test.ts` | The live external services this package drives, driven for real | No |
125
+
126
+ `conformance` and `service` are two subjects, not two names for one. `conformance` measures this
127
+ package against an official artifact it stays compatible with and drives nothing external: the
128
+ tooling it measures against is installed, and any server it drives is one the proof starts itself.
129
+ That is what makes it hermetic and keeps it in `test`. `service` drives the real thing, so it takes
130
+ `tests/setupService.ts` for readiness, longer timeouts, and no file parallelism, and it leaves
131
+ `test` for `prepublishOnly`.
119
132
 
120
133
  One project sits on neither axis. `probe` includes `tmp/probe/**/*.test.ts` so an agent can run a
121
134
  throwaway instrument against real sources, aliases and setup. Declare no proof there. Every test
@@ -123,8 +136,9 @@ script names its project, so no gate runs it; its directory is ignored by git; a
123
136
  `.claude/rules/tests.md` governs what may live there.
124
137
 
125
138
  - 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.
139
+ - A live-service project is the fifth kind. It is the `service` project above, `scripts/service.sh`
140
+ provisions what it drives, and `.claude/rules/tests.md` governs it. Name it `service` whatever it
141
+ drives.
128
142
  - A project leaves the default run for one of two reasons: it drives a live external service, or it
129
143
  is hermetic but slow — it spawns processes, packs, installs, or drives a real build.
130
144
  - Every isolated project has its own script, is excluded from `test`, and runs in `prepublishOnly`.
@@ -137,7 +151,7 @@ Setup assets:
137
151
 
138
152
  Scope with `test:src`, `test:src:core`, `test:app`, `test:app:server`, and equivalent scripts. Each
139
153
  cross-cutting project has its own script too: `test:policy`, `test:config`, `test:guides`,
140
- `test:integration`.
154
+ `test:conformance`, `test:integration`, `test:service`.
141
155
 
142
156
  ## Typechecking and environment isolation
143
157
 
@@ -211,3 +225,4 @@ Run `show` only **after** formatting. The committed `demo/showcase.html` is gene
211
225
  - Store text as UTF-8.
212
226
  - Before accepting broad generated or migrated edits, scan changed text for replacement characters, mojibake, unintended control characters, and accidental trailing debris.
213
227
  - Preserve intentional Unicode punctuation and symbols; do not “clean” valid text merely because it is non-ASCII.
228
+ - 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. |
@@ -188,6 +191,7 @@ Exported from `@orkestrel/scaffold`, and reachable from
188
191
  | `matchesOrchestrationPath` | function | Test whether a path instructs or wires an agent rather than the toolchain. |
189
192
  | `matchesRange` | function | Test whether a declared range already admits a published version. |
190
193
  | `nameToGuide` | function | Derive the guide mirror path a package name answers for. |
194
+ | `nameToRewrite` | function | Derive the declaration rewrite a published face roll-up applies. |
191
195
  | `planToSummary` | function | Project a plan into its tally by artifact origin. |
192
196
  | `selectGroups` | function | Select the groups a compile covers, in plan order. |
193
197
  | `selectHostPaths` | function | Select the host paths a named workspace vendors. |
@@ -456,25 +460,33 @@ workspace would otherwise declare a dependency that does not resolve.
456
460
 
457
461
  `new --bin` creates the executable entry, its test, and its scoped Vite and TypeScript wrappers.
458
462
  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.
463
+ `integration`, `tests/conformance.test.ts` for `conformance`, `tests/setupService.ts` for `service`,
464
+ `tests/setupGlobal.ts` for `global`, and `configs/app/vite.showcase.config.ts` for `showcase`;
465
+ reading verbs detect each exact-case file and register its fixed machinery. Add `scripts/service.sh`
466
+ for `vendors`. Reading verbs preserve and protect that birth-owned script, but do not infer its
467
+ vendor list from edited text.
463
468
 
464
469
  ### Reading a target
465
470
 
466
471
  `audit`, `repair`, `catalog`, and `overwrite` derive the blueprint from the target itself. The name
467
472
  and the declared `@orkestrel/*` packages come from `package.json`. The two environment axes come
468
473
  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`,
474
+ beside it could disagree. Six more facts come from exact-case files: `src/bin/main.ts` selects
475
+ `bin`, `tests/integration.test.ts` selects `integration`, `tests/conformance.test.ts` selects
476
+ `conformance`, `tests/setupService.ts` selects `service`, `tests/setupGlobal.ts` selects `global`,
471
477
  and `configs/app/vite.showcase.config.ts` selects `showcase`. A containing directory does not select
472
478
  the fact by itself.
473
479
 
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
480
+ `vendors` is not reconstructed. Its only artifact, `scripts/service.sh`, is birth-owned, so edited
481
+ script text is not a trustworthy declaration of a vendor list. A present script remains in the
476
482
  target and remains protected from deletion through the owned scripts inventory, but a reading verb
477
- does not infer services from it.
483
+ does not infer vendors from it.
484
+
485
+ That is why the live-service project follows `service` rather than `vendors`. A reading verb has to
486
+ plan the project before it can say anything about a target that runs one, and a vendor list it
487
+ cannot recover would leave every such workspace unplannable. `tests/setupService.ts` is recoverable,
488
+ is the module the root configuration names by path, and is what a live proof needs anyway, so it
489
+ carries the fact and the vendor list keeps its own separate job.
478
490
 
479
491
  The root Vite configuration always defines the fixed `guides` project and selects it at
480
492
  configuration load only when `tests/guides.test.ts` is a physical file with that exact path case. A
@@ -560,9 +572,16 @@ because the shape is chosen once and read afterwards: `new` refuses the advisory
560
572
  `repair` need the plan to describe and restore a target that already has that shape. A library
561
573
  caller creating a workspace holds the same refusal, and the Compile section below states it.
562
574
 
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.
575
+ `bin`, `integration`, `conformance`, `service`, `vendors`, `global`, and `showcase` are structural
576
+ facts. Each is set only when the workspace physically ships the directory or exact-case file that
577
+ defines it, never because of the workspace's name and never because a sibling fact is set.
578
+
579
+ `service` says the workspace runs a live-service Vitest project over `tests/service`, and it alone
580
+ registers that project, its `test:service` script, and the `tests/setupService.ts` readiness module
581
+ the project names. `vendors` names each external service the workspace drives and emits
582
+ `scripts/service.sh`, the provisioner that starts them. Neither is derivable from the other: a
583
+ workspace may declare vendors before it writes a suite, and a suite may drive a service the skeleton
584
+ does not start.
566
585
 
567
586
  An axis-dependent structural fact projects only when its required axis exists. `integration`
568
587
  projects a published `src`, and `showcase` projects the browser `app` environment. When that axis is
@@ -674,7 +693,8 @@ barrels, the tests, `README.md`, and `guides/README.md` are written once and are
674
693
  a later verb.
675
694
 
676
695
  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
696
+ selected by their defining paths, as `guides`, `conformance`, and `service` are. A workspace that
697
+ needs other local configuration
678
698
  must keep those edits outside a content-owned file; `repair` restores that file to the canonical
679
699
  project set.
680
700
 
@@ -807,6 +827,17 @@ except the manifest.
807
827
  - One template artifact per configuration file the selection needs: the root `tsconfig.json` and
808
828
  `vite.config.ts`, plus a Vite config and a scoped TypeScript config per selected environment, and
809
829
  two more when `bin` is set.
830
+ - One template artifact, `configs/browsers.ts`, for a workspace selecting `browser` on either axis.
831
+ It resolves the Chromium the Playwright provider launches, and the root `vite.config.ts` calls it
832
+ once into `browserOptions` and passes that to every `playwright()` provider it configures. The
833
+ precedence is `PLAYWRIGHT_EXECUTABLE_PATH`, `PLAYWRIGHT_WS_ENDPOINT`, `PLAYWRIGHT_CHANNEL`, the
834
+ managed Playwright Chromium, the container's bundled Chromium, a verified system channel, then the
835
+ platform default. An installed pinned revision returns empty options, so Playwright keeps its own
836
+ launch defaults. A pinned revision that is not installed falls through to a `chromium` alias or a
837
+ sibling `chromium-*` revision under the same browsers directory, because a managed container ships
838
+ one usable build for many Playwright versions. It is its own file rather than a block in the
839
+ vendored `configs/helpers.ts`, which every workspace receives byte-identical while only a browser
840
+ selection declares the `playwright` this module imports.
810
841
  - One template artifact per source and test file the selection needs: an `index.ts` barrel per
811
842
  selected environment, `main.ts` and `index.html` for an application browser, `tests/setup.ts`
812
843
  plus the host setup modules the selection reaches, and one entry test per axis project.
@@ -960,7 +991,7 @@ are thrown, so an observer sees a refusal even where the caller catches it.
960
991
 
961
992
  ## Limits
962
993
 
963
- Six things a reader will look for and not find.
994
+ Seven things a reader will look for and not find.
964
995
 
965
996
  **A code fence in this guide is unverified.** [`tests/guides.test.ts`](../tests/guides.test.ts)
966
997
  proves that every fence imports only real exports of the two barrels, and that every backticked name
@@ -1012,6 +1043,30 @@ nothing. This is deliberate: a generated sample entity is repeatedly mistaken fo
1012
1043
  implementation. What a consumer does first is write the module's `types.ts`, then the
1013
1044
  implementation that conforms to it, then export both from the barrel — the order `AGENTS.md` fixes.
1014
1045
 
1046
+ **A conformance or live-service proof is registered, but neither is written for you.** Scaffold
1047
+ registers the `conformance` and `service` projects, their scripts, and the gate each belongs to. It
1048
+ emits no proof into either, because both name something only the package knows: the official
1049
+ artifact a conformance check measures against, and the service a live proof drives. A generated
1050
+ placeholder would read as a proof while measuring nothing, so the file a consumer writes is the file
1051
+ that selects the project.
1052
+
1053
+ The consequence is one empty-project case in each direction. A blueprint carrying `conformance` with
1054
+ no `tests/conformance.test.ts` registers a project whose include resolves to nothing, and Vitest
1055
+ exits non-zero on it. A blueprint carrying `service` gets `tests/setupService.ts` — the root
1056
+ configuration names that module by path, so an absent one fails the project's load rather than its
1057
+ run — and still no suite beneath `tests/service`, so `test:service` reports no test files until the
1058
+ consumer writes the first one. Both cases are visible the first time the script runs, which is why
1059
+ neither is silent.
1060
+
1061
+ Neither project folds into `integration`, which means something narrower: the built package works
1062
+ when installed and driven from outside. Two fleet packages hold the distinction. `@orkestrel/ollama`
1063
+ drives a real Ollama daemon through a `service` project, so a real service answers it and it runs
1064
+ from `prepublishOnly`. `@orkestrel/mcp` measures its server against the specification's own runner,
1065
+ `@modelcontextprotocol/conformance`, through a `conformance` project. It pins that runner as a
1066
+ development dependency and resolves it out of `node_modules`, and the server the runner drives is
1067
+ one the fixture starts itself on a loopback port, so the run drives nothing external and stays in
1068
+ `test`.
1069
+
1015
1070
  ## Tests
1016
1071
 
1017
1072
  - [`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