@orkestrel/scaffold 0.0.30 → 0.0.32

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.
@@ -71,8 +71,8 @@ Exported from `@orkestrel/scaffold`, and reachable from
71
71
  | `HostArtifact` | interface | A file byte-copied from the vendored data root, planned before its bytes are read. |
72
72
  | `HydratedArtifact` | interface | A vendored file whose exact bytes have been read, so its content can be compared. |
73
73
  | `Override` | interface | One artifact override. |
74
- | `PlanSummary` | interface | The tally of one plan by artifact origin. |
75
74
  | `Plan` | interface | The compiled, ordered artifact list and the selection it covers. |
75
+ | `PlanSummary` | interface | The tally of one plan by artifact origin. |
76
76
  | `Question` | interface | One validation issue raised against a blueprint or a plan. |
77
77
  | `Scaffolding` | interface | The replayable outcome of one compile. |
78
78
  | `SrcDefinition` | interface | The build and export settings one published `src` environment contributes. |
@@ -94,20 +94,22 @@ Exported from `@orkestrel/scaffold`, and reachable from
94
94
  | `CONFIG_TEMPLATES` | const | Formatter-stable template text for every configuration artifact. |
95
95
  | `CONFORMANCE_TEST_PATH` | const | The official-tooling drift proof whose presence makes a workspace `conformance`. |
96
96
  | `CONTROL_CHARACTER_PATTERN` | const | Unicode controls, formatting controls, and line and paragraph separators rejected in text. |
97
+ | `DECLARATION_DEV_DEPENDENCIES` | const | The development dependencies that emit declarations for published source or an executable. |
97
98
  | `DEFAULT_ENGINES` | const | The `engines.node` range a workspace starts with. |
98
99
  | `DEFAULT_VERSION` | const | The version a workspace starts at. |
99
100
  | `DEPENDENCY_NAME_PATTERN` | const | The runtime dependency name syntax: the `@orkestrel` scope and a bare name. |
101
+ | `DISTRIBUTION_TEST_PATH` | const | The packed-package proof whose presence makes a workspace `distribution`. |
100
102
  | `ENGINES_PATTERN` | const | The minimum-Node engine syntax a blueprint declares. |
101
103
  | `ENVIRONMENTS` | const | The three `Environment` values, frozen. |
104
+ | `EXECUTABLE_PATHS` | const | The vendored paths a target receives with its executable bit set, frozen. |
102
105
  | `EXTRA_NAME_PATTERN` | const | The development extra name syntax: any valid npm package name. |
103
106
  | `EXTRA_RANGE_PATTERN` | const | The registry-only semver subset accepted for a development extra's range. |
104
107
  | `GLOBAL_SETUP_PATH` | const | The shared Vitest global-setup module whose presence makes a workspace `global`. |
105
- | `GUIDES_TEST_PATH` | const | The guide-parity proof whose physical file selects the fixed `guides` project. |
106
- | `EXECUTABLE_PATHS` | const | The vendored paths a target receives with its executable bit set, frozen. |
107
108
  | `GROUPS` | const | The seven `Group` values in plan order, frozen. |
109
+ | `GUIDES_TEST_PATH` | const | The guide-parity proof whose presence selects the planned `guides` project. |
108
110
  | `HEX_PATTERN` | const | Exact lowercase hexadecimal bytes: two digits per byte, and empty content is valid. |
109
111
  | `HOST_PATHS` | const | The paths byte-copied from the vendored data root, frozen. |
110
- | `INTEGRATION_TEST_PATH` | const | The installed-package proof whose presence makes a workspace `integration`. |
112
+ | `INTEGRATION_TEST_PATH` | const | The cross-environment composition proof whose presence makes a workspace `integration`. |
111
113
  | `INVALID_PATH_CHARACTER_PATTERN` | const | Visible characters a target-relative path and a Markdown path cell both forbid. |
112
114
  | `MAX_ARTIFACT_BYTES` | const | Maximum bytes accepted for one artifact. |
113
115
  | `MAX_ARTIFACT_HEX_LENGTH` | const | Maximum length of the hexadecimal string carrying one artifact's bytes. |
@@ -124,6 +126,7 @@ Exported from `@orkestrel/scaffold`, and reachable from
124
126
  | `ORCHESTRATION_PATH_NAMES` | const | The exact root filenames that wire an agent bench rather than the toolchain, frozen. |
125
127
  | `ORCHESTRATION_PATH_PREFIXES` | const | The path prefixes whose contents instruct or wire an agent, frozen. |
126
128
  | `ORKESTREL_RANGE_PATTERN` | const | The exact caret-pinned pre-1.0 range accepted for an `@orkestrel/*` runtime dependency. |
129
+ | `PRINT_WIDTH` | const | Columns one emitted line may occupy, matching `printWidth` in `.oxfmtrc.json`. |
127
130
  | `SERVICE_SCRIPT_PATH` | const | The provisioner skeleton a workspace with declared service vendors is given once. |
128
131
  | `SERVICE_SETUP_PATH` | const | The live-service readiness module whose presence makes a workspace `service`. |
129
132
  | `SERVICE_TEST_INCLUDE` | const | The include the live-service project covers, which is a directory rather than one proof. |
@@ -131,7 +134,9 @@ Exported from `@orkestrel/scaffold`, and reachable from
131
134
  | `SHOWCASE_DEV_DEPENDENCIES` | const | The development dependency used only by the optional single-file showcase build. |
132
135
  | `SOURCE_BROWSER_DEV_DEPENDENCIES` | const | The development dependencies a published browser `src` environment adds. |
133
136
  | `SRC_MATRIX` | const | The build and export settings each published `src` environment contributes, frozen. |
137
+ | `TAB_WIDTH` | const | Columns one tab occupies when the formatter measures a line, matching `tabWidth`. |
134
138
  | `VERSION_PATTERN` | const | The exact three-component version syntax a blueprint declares. |
139
+ | `WORKSPACE_OWNED_PATHS` | const | The vendored paths whose present bytes belong to each workspace, frozen. |
135
140
 
136
141
  #### Guards
137
142
 
@@ -145,8 +150,8 @@ Exported from `@orkestrel/scaffold`, and reachable from
145
150
  | `isCompilerHooks` | const | Narrow a value to the compiler's initial listener record. |
146
151
  | `isCompilerOptions` | const | Narrow a value to `CompilerOptions`. |
147
152
  | `isContent` | const | Narrow a value to text this package will accept as one artifact's content. |
148
- | `isDependencyName` | const | Narrow a value to the scoped package name a runtime dependency carries. |
149
153
  | `isDependency` | const | Narrow a value to a `Dependency`. |
154
+ | `isDependencyName` | const | Narrow a value to the scoped package name a runtime dependency carries. |
150
155
  | `isEnvironment` | const | Narrow a value to one `Environment` a workspace may select. |
151
156
  | `isFinding` | const | Narrow a value to a `Finding`. |
152
157
  | `isGroup` | const | Narrow a value to one `Group` a plan selects over. |
@@ -189,9 +194,10 @@ Exported from `@orkestrel/scaffold`, and reachable from
189
194
  | `matchesDriftReachability` | function | Test whether `inferDrift` could have produced a finding for an ownership. |
190
195
  | `matchesEngines` | function | Test whether a declared engines floor is at or above the supported minimum. |
191
196
  | `matchesOrchestrationPath` | function | Test whether a path instructs or wires an agent rather than the toolchain. |
197
+ | `matchesPrintWidth` | function | Test whether one emitted line fits the vendored formatter width. |
192
198
  | `matchesRange` | function | Test whether a declared range already admits a published version. |
193
199
  | `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. |
200
+ | `nameToRewrite` | function | Derive the declaration rewrite a published face's `beforeWriteFile` applies. |
195
201
  | `planToSummary` | function | Project a plan into its tally by artifact origin. |
196
202
  | `selectGroups` | function | Select the groups a compile covers, in plan order. |
197
203
  | `selectHostPaths` | function | Select the host paths a named workspace vendors. |
@@ -458,9 +464,10 @@ An option a verb does not list is refused by name rather than parsed and ignored
458
464
  the registry, so `new` fails when the registry names no release for a package it was given: the
459
465
  workspace would otherwise declare a dependency that does not resolve.
460
466
 
461
- `new --bin` creates the executable entry, its test, and its scoped Vite and TypeScript wrappers.
462
- The other structural facts do not need creation flags. Add `tests/integration.test.ts` for
463
- `integration`, `tests/conformance.test.ts` for `conformance`, `tests/setupService.ts` for `service`,
467
+ `new --bin` creates the executable entry, its test, and its scoped Vite and TypeScript wrappers. The
468
+ other structural facts do not need creation flags. Add `tests/guides.test.ts` for `guides`,
469
+ `tests/distribution.test.ts` for `distribution`, `tests/integration.test.ts` for `integration`,
470
+ `tests/conformance.test.ts` for `conformance`, `tests/setupService.ts` for `service`,
464
471
  `tests/setupGlobal.ts` for `global`, and `configs/app/vite.showcase.config.ts` for `showcase`;
465
472
  reading verbs detect each exact-case file and register its fixed machinery. Add `scripts/service.sh`
466
473
  for `vendors`. Reading verbs preserve and protect that birth-owned script, but do not infer its
@@ -471,8 +478,9 @@ vendor list from edited text.
471
478
  `audit`, `repair`, `catalog`, and `overwrite` derive the blueprint from the target itself. The name
472
479
  and the declared `@orkestrel/*` packages come from `package.json`. The two environment axes come
473
480
  from the directories the target actually ships, because a directory is the fact and a declaration
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
481
+ beside it could disagree. Eight more facts come from exact-case files: `src/bin/main.ts` selects
482
+ `bin`, `tests/guides.test.ts` selects `guides`, `tests/distribution.test.ts` selects `distribution`,
483
+ `tests/integration.test.ts` selects `integration`, `tests/conformance.test.ts` selects
476
484
  `conformance`, `tests/setupService.ts` selects `service`, `tests/setupGlobal.ts` selects `global`,
477
485
  and `configs/app/vite.showcase.config.ts` selects `showcase`. A containing directory does not select
478
486
  the fact by itself.
@@ -488,27 +496,43 @@ cannot recover would leave every such workspace unplannable. `tests/setupService
488
496
  is the module the root configuration names by path, and is what a live proof needs anyway, so it
489
497
  carries the fact and the vendor list keeps its own separate job.
490
498
 
491
- The root Vite configuration always defines the fixed `guides` project and selects it at
492
- configuration load only when `tests/guides.test.ts` is a physical file with that exact path case. A
493
- directory or a case-folded spelling does not select it. A fresh workspace therefore carries no
494
- guides project. A workspace that later adds the proof keeps the project through `repair`. The
495
- birth-owned manifest still gains no `test:guides` script automatically; the developer who adds the
496
- proof adds that script too.
497
-
498
- The three plan-reading verbs compare every Vitest project named by the target manifest's scripts
499
- with the project set the planned root configuration will load. One shell-token pass reads quoted and
500
- unquoted `--project value` and `--project=value` forms. A shell expansion or malformed quote that
501
- prevents a project value from being resolved statically produces a question instead of licensing a
502
- write. The classifier is deliberately bounded to manifest script text that names `vitest`; an
503
- external wrapper whose name does not identify its runner supplies no static Vitest fact to infer.
499
+ The root Vite configuration defines and registers the fixed `guides` project only when the derived
500
+ blueprint carries `guides`. Reading verbs set that fact only when `tests/guides.test.ts` is a
501
+ physical file with that exact path case. A directory or a case-folded spelling does not select it. A
502
+ fresh workspace therefore carries no guides project or script. A developer who adds the proof must
503
+ also add the exact `test:guides` script line that the plan reports; the manifest remains
504
+ birth-owned.
505
+
506
+ The three plan-reading verbs compare the Vitest project set named by the target manifest with the
507
+ project set the planned root configuration registers. Every planned proof project must also be
508
+ reachable from the manifest's `test` chain. A target whose manifest does not set `private: true` may
509
+ also reach it from `prepublishOnly`. A private target cannot use that chain, because npm refuses the
510
+ package before a publish lifecycle script runs, so crediting it would report a dead gate as a live
511
+ one. Generated integration runs from `test`. One shell-token pass reads quoted and unquoted
512
+ `--project value` and `--project=value` forms and follows literal `npm run` calls. A shell expansion
513
+ or malformed quote that prevents a project or script name from being resolved statically produces a
514
+ question instead of licensing a write. The classifier is deliberately bounded to manifest script
515
+ text that names `vitest`; an external wrapper whose name does not identify its runner supplies no
516
+ static Vitest fact to infer.
504
517
 
505
518
  `audit` still completes the comparison and reports one non-blocking `projects` question. For a
506
519
  literal absent project, its advisory tells the developer to register the project or remove the
507
- script. `repair` and `overwrite` refuse instead. Their refusal tells the developer to remove the
520
+ script. For a planned project absent from both gate chains, the advisory gives the exact direct
521
+ script line to add to `package.json`. `repair` and `overwrite` refuse either mismatch and do not
522
+ write the manifest or configuration. Their absent-project refusal tells the developer to remove the
508
523
  script or not use scaffold writing verbs for a workspace that needs custom Vitest projects. It does
509
524
  not recommend editing the content-owned configuration that the refusing verb would restore. An
510
525
  advisory alone does not make an aligned target drift.
511
526
 
527
+ The same three plan-reading verbs compare the tooling set the derived blueprint plans against
528
+ `dependencies` and `devDependencies` together. A missing planned package produces one non-blocking
529
+ `dependencies` question naming every missing package and the exact manifest lines to add, in stable
530
+ order. The comparison measures membership only: range differences and workspace-owned extras are
531
+ outside it, and a planned tool may live in either section. A present section that is not an object
532
+ produces a question instead of a crash. `audit` reports the question without changing its exit
533
+ semantics. `repair` and `overwrite` refuse before writing configuration, and no verb edits the
534
+ birth-owned `package.json`.
535
+
512
536
  ### Exit codes
513
537
 
514
538
  `0` means the target matched its plan and every step completed. `1` means the target drifted or a
@@ -572,22 +596,50 @@ because the shape is chosen once and read afterwards: `new` refuses the advisory
572
596
  `repair` need the plan to describe and restore a target that already has that shape. A library
573
597
  caller creating a workspace holds the same refusal, and the Compile section below states it.
574
598
 
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.
599
+ `bin`, `guides`, `distribution`, `integration`, `conformance`, `service`, `vendors`, `global`, and
600
+ `showcase` are structural facts. Each is set only when the workspace physically ships the directory
601
+ or exact-case file that defines it, never because of the workspace's name and never because a
602
+ sibling fact is set.
603
+
604
+ A structural fact is read when a verb runs, not when the file appears. Writing
605
+ `tests/integration.test.ts` into a workspace sets the fact, but the root configuration on disk was
606
+ generated before that file existed and still registers no `integration` project, so `test:config`
607
+ fails with `integration has no project factory or configuration` until a plan-writing verb
608
+ regenerates it.
609
+
610
+ `repair` alone does not close that, and refusing is correct rather than a gap. `package.json` is
611
+ birth-owned, so the verb cannot add the project's script, and it will not register a project the
612
+ manifest reaches from no gate. It exits 1 naming the target and writes nothing.
613
+
614
+ Adding a structural proof is therefore three steps, in order: write the file; declare its
615
+ `test:<project>` script and invoke that script from a gate chain; then run `repair`, which
616
+ regenerates the root configuration and registers the project. `audit` reports whichever piece is
617
+ still outstanding at each step.
618
+
619
+ `distribution` projects only when the workspace also publishes at least one `src` environment. It
620
+ packs and installs the published artifact, so without that axis there is nothing to pack, and the
621
+ declared flag alone adds no project, no `test:distribution` script, and no gate entry.
578
622
 
579
623
  `service` says the workspace runs a live-service Vitest project over `tests/service`, and it alone
580
624
  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.
585
-
586
- An axis-dependent structural fact projects only when its required axis exists. `integration`
587
- projects a published `src`, and `showcase` projects the browser `app` environment. When that axis is
588
- absent the flag adds no artifact, configuration, script, or dependency, and the gate reports a
589
- non-blocking question on that field so the caller who set it learns it emitted nothing. The same
590
- rule applies to both facts.
625
+ the project names. A publishing workspace invokes it from `prepublishOnly`; a `private: true`
626
+ workspace invokes it from `test`, which is the only gate it has. Its longer timeouts and disabled
627
+ file parallelism are the same in both. `vendors` names each external service the workspace drives
628
+ and emits `scripts/service.sh`, the provisioner that starts them. Neither is derivable from the
629
+ other: a workspace may declare vendors before it writes a suite, and a suite may drive a service the
630
+ skeleton does not start.
631
+
632
+ `integration` projects a cross-environment composition proof for any workspace, independently of
633
+ whether it has a published `src`. Its generated seed imports every selected `src` and `app`
634
+ environment through its public barrel. It starts no process and does not pack or install the
635
+ workspace. The proof composes across environments, so when `src` and `app` together declare fewer
636
+ than two the gate reports a non-blocking `integration` question: the project, the script, and the
637
+ `test` entry are all still registered, and the advisory reports that the seed composes nothing
638
+ rather than withholding it.
639
+
640
+ `showcase` projects only when the browser `app` environment exists. Without that axis the flag adds
641
+ no artifact, configuration, script, or dependency, and the gate reports a non-blocking question on
642
+ that field so the caller who set it learns it emitted nothing.
591
643
 
592
644
  `createBlueprint` enforces shape only. Whether the name is a name, the version a version, and the
593
645
  axis combination one this package can generate are the gate's laws, and the gate answers them with
@@ -688,15 +740,21 @@ content is produced. `Ownership` says what scaffold claims at the path.
688
740
  | `presence` | Existence only | Restore an absent file, never touch present bytes |
689
741
  | `birth` | Nothing | Create the file only while it is absent |
690
742
 
743
+ Presence ownership has two separate mechanisms, and a reader needs to know which applies:
744
+
745
+ | Mechanism | Paths | Bytes belong to | Cost |
746
+ | --------------- | ------------------------------------------------- | --------------------- | ------------------------------------------------------- |
747
+ | Verb-owned | `CATALOG_AGENT_PATH` and dependency guide mirrors | `catalog` or `mirror` | The owning verb is the only route for a later update. |
748
+ | Workspace-owned | `WORKSPACE_OWNED_PATHS`, currently `.gitignore` | The target workspace | Present bytes receive no later canonical ignore update. |
749
+
691
750
  Birth ownership is what makes a generated workspace the consumer's. `package.json`, the source
692
751
  barrels, the tests, `README.md`, and `guides/README.md` are written once and are never rewritten by
693
752
  a later verb.
694
753
 
695
754
  Content ownership does not preserve an arbitrary custom Vitest project. Fixed optional proofs are
696
- selected by their defining paths, as `guides`, `conformance`, and `service` are. A workspace that
697
- needs other local configuration
698
- must keep those edits outside a content-owned file; `repair` restores that file to the canonical
699
- project set.
755
+ selected by their defining paths, as `guides`, `distribution`, `integration`, `conformance`, and
756
+ `service` are. A workspace that needs other local configuration must keep those edits outside a
757
+ content-owned file; `repair` restores that file to the canonical project set.
700
758
 
701
759
  An audit reports one `Finding` per planned path, followed by any foreign path beneath the groups
702
760
  the plan covers. Every planned finding carries its artifact's `ownership`. A foreign finding has
@@ -840,7 +898,10 @@ except the manifest.
840
898
  selection declares the `playwright` this module imports.
841
899
  - One template artifact per source and test file the selection needs: an `index.ts` barrel per
842
900
  selected environment, `main.ts` and `index.html` for an application browser, `tests/setup.ts`
843
- plus the host setup modules the selection reaches, and one entry test per axis project.
901
+ plus the host setup modules the selection reaches, and one entry test per axis project. An
902
+ integration selection also emits a birth-owned `tests/integration.test.ts` seed that imports each
903
+ selected public barrel and records its initial empty exports for the consumer to replace with an
904
+ observable cross-environment flow.
844
905
  - One template artifact each for `README.md` and `guides/README.md`.
845
906
  - One host artifact per vendored path the workspace selects. A vendored directory is one planned
846
907
  path that expands into the files the data root stores beneath it.
@@ -998,7 +1059,7 @@ proves that every fence imports only real exports of the two barrels, and that e
998
1059
  in this file resolves to one. It neither runs a fence nor typechecks one, so a trailing `// value`
999
1060
  comment inside a fence states what this guide claims rather than what the build answered. The
1000
1061
  verdicts that are measured are the ones a consumer hovers:
1001
- [`tests/integration.test.ts`](../tests/integration.test.ts) drives every `@example` the built
1062
+ [`tests/distribution.test.ts`](../tests/distribution.test.ts) drives every `@example` the built
1002
1063
  declarations print against the installed package, scores each verdict it can read as a value, and
1003
1064
  names exactly the ones it cannot. Fences are not added to that instrument, because most of them
1004
1065
  cannot be run: several declare an ambient value that has no runtime, and several write to a
@@ -1043,29 +1104,43 @@ nothing. This is deliberate: a generated sample entity is repeatedly mistaken fo
1043
1104
  implementation. What a consumer does first is write the module's `types.ts`, then the
1044
1105
  implementation that conforms to it, then export both from the barrel — the order `AGENTS.md` fixes.
1045
1106
 
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`.
1107
+ **A selected distribution, conformance, or live-service proof is registered, but none is written for
1108
+ you.** Scaffold registers `conformance` and `service` when their structural facts are set, and
1109
+ registers `distribution` only when the workspace also publishes `src`. In a publishing workspace,
1110
+ `distribution` and `service` run from `prepublishOnly` and `conformance` stays in `test`. In a
1111
+ `private: true` workspace, `distribution` is absent, `service` runs from `test`, and there is no
1112
+ `prepublishOnly` at all. Scaffold emits no proof into any registered project, because each names
1113
+ something only the package knows: the behavior its own packed artifact must hold once installed, the
1114
+ official artifact a conformance check measures against, and the service a live proof drives. A
1115
+ generated placeholder would read as a proof while measuring nothing, so the file a consumer writes
1116
+ is the file that selects the project.
1117
+
1118
+ A distribution proof carries one contract scaffold does enforce from the outside. The generated
1119
+ `prepublishOnly` invokes it as `npm run test:distribution -- --mode release`, and a proof that reads
1120
+ `import.meta.env.MODE === 'release'` must **fail** on an unreachable registry rather than skip. An
1121
+ ordinary local run may skip that case, because a developer offline is not a defect; a release run
1122
+ may not, because skipping there passes the publish gate without ever proving the artifact installs.
1123
+ Scaffold writes no proof, so honouring the flag is the consumer's, and a proof that ignores it
1124
+ reports green on exactly the runs that matter.
1125
+
1126
+ The consequence is one empty-project case per registered proof. A publishing blueprint carrying
1127
+ `distribution` with no `tests/distribution.test.ts`, or any blueprint carrying `conformance` with no
1128
+ `tests/conformance.test.ts`, registers a project whose include resolves to nothing, and Vitest exits
1129
+ non-zero on it. A blueprint carrying `service` gets `tests/setupService.ts` — the root configuration
1130
+ names that module by path, so an absent one fails the project's load rather than its run — and still
1131
+ no suite beneath `tests/service`, so `test:service` reports no test files until the consumer writes
1132
+ the first one. Every case is visible the first time the script runs, which is why none is silent.
1133
+
1134
+ None of the three folds into `integration`, which measures a different axis rather than a smaller
1135
+ one: the workspace's selected environments compose through their public barrels. The generated seed
1136
+ proves only that those barrels load together and expose the initial empty surfaces; the consumer
1137
+ replaces it with an observable cross-environment flow. The seed starts no process and does not pack
1138
+ or install the workspace, so the project stays in `test`. Two fleet packages hold the distinction.
1139
+ `@orkestrel/ollama` drives a real Ollama daemon through a `service` project, so a real service
1140
+ answers it and it runs from `prepublishOnly`. `@orkestrel/mcp` measures its server against the
1141
+ specification's own runner, `@modelcontextprotocol/conformance`, through a `conformance` project. It pins that runner as a development dependency and resolves it out
1142
+ of `node_modules`, and the server the runner drives is one the fixture starts itself on a loopback
1143
+ port, so the run drives nothing external and stays in `test`.
1069
1144
 
1070
1145
  ## Tests
1071
1146
 
@@ -42,7 +42,7 @@ describe('root configuration', () => {
42
42
  for (const key of required.keys()) {
43
43
  if (!absent.has(key)) throw new Error('The alias population carries no required entry')
44
44
  }
45
- }).toThrowError('The alias population carries no required entry')
45
+ }).toThrow('The alias population carries no required entry')
46
46
  for (const [key, expected] of required) {
47
47
  const values = declared.get(key)
48
48
  if (values === undefined) throw new Error(`${key} is not declared`)
@@ -102,7 +102,14 @@ describe('root configuration', () => {
102
102
  setup: ['./tests/setup.ts', './tests/setupServer.ts'],
103
103
  })
104
104
  }
105
- for (const label of ['policy', 'config', 'guides', 'conformance', 'integration']) {
105
+ for (const label of [
106
+ 'policy',
107
+ 'config',
108
+ 'guides',
109
+ 'conformance',
110
+ 'distribution',
111
+ 'integration',
112
+ ]) {
106
113
  if (!existsSync(resolve(root, `tests/${label}.test.ts`))) continue
107
114
  expected.set(label, {
108
115
  include: `tests/${label}.test.ts`,
@@ -218,7 +225,7 @@ describe('root configuration', () => {
218
225
  expect(missing.delete('probe')).toBe(true)
219
226
  expect(() => {
220
227
  for (const [label, project] of expected) expect(missing.get(label)).toStrictEqual(project)
221
- }).toThrowError(/strictly equal/u)
228
+ }).toThrow(/strictly equal/u)
222
229
 
223
230
  const misconfigured = new Map(configured)
224
231
  const probe = misconfigured.get('probe')
@@ -227,7 +234,7 @@ describe('root configuration', () => {
227
234
  expect(() => {
228
235
  for (const [label, project] of expected)
229
236
  expect(misconfigured.get(label)).toStrictEqual(project)
230
- }).toThrowError(/strictly equal/u)
237
+ }).toThrow(/strictly equal/u)
231
238
  })
232
239
 
233
240
  it('requires and validates every selected target wrapper', async () => {
@@ -344,10 +351,10 @@ describe('root configuration', () => {
344
351
  const controlFound = ['configs/app/vite.server.config.ts']
345
352
  expect(() => {
346
353
  for (const wrapper of controlRequired) expect(controlFound).toContain(wrapper)
347
- }).toThrowError(/vite\.browser/u)
348
- expect(() =>
349
- expect(resolve(root, 'dist/actual')).toBe(resolve(root, 'dist/control')),
350
- ).toThrowError(/expected/u)
354
+ }).toThrow(/vite\.browser/u)
355
+ expect(() => expect(resolve(root, 'dist/actual')).toBe(resolve(root, 'dist/control'))).toThrow(
356
+ /expected/u,
357
+ )
351
358
  })
352
359
 
353
360
  it('registers proof scripts in the correct gate', () => {
@@ -359,8 +366,10 @@ describe('root configuration', () => {
359
366
  if (typeof scripts !== 'object' || scripts === null) {
360
367
  throw new Error('The package manifest carries no scripts')
361
368
  }
369
+ const publishes = Object.getOwnPropertyDescriptor(manifest, 'private')?.value !== true
362
370
  const test = Object.getOwnPropertyDescriptor(scripts, 'test')?.value
363
371
  const config = Object.getOwnPropertyDescriptor(scripts, 'test:config')?.value
372
+ const distribution = Object.getOwnPropertyDescriptor(scripts, 'test:distribution')?.value
364
373
  const integration = Object.getOwnPropertyDescriptor(scripts, 'test:integration')?.value
365
374
  const conformance = Object.getOwnPropertyDescriptor(scripts, 'test:conformance')?.value
366
375
  const service = Object.getOwnPropertyDescriptor(scripts, 'test:service')?.value
@@ -390,24 +399,39 @@ describe('root configuration', () => {
390
399
  expect(registered.has('config')).toBe(true)
391
400
  expect(registered.has('control')).toBe(false)
392
401
  const hasConformance = registered.has('conformance')
402
+ const hasDistribution = registered.has('distribution')
393
403
  const hasService = registered.has('service')
394
404
  expect(config).toBe(
395
405
  'vitest run --config vite.config.ts --no-cache --reporter=dot --project config',
396
406
  )
397
407
  expect(typeof test === 'string' && test.includes('npm run test:config')).toBe(true)
408
+ expect(distribution).toBe(
409
+ hasDistribution
410
+ ? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project distribution'
411
+ : undefined,
412
+ )
413
+ expect(typeof test === 'string' && test.includes('test:distribution')).toBe(false)
414
+ expect(typeof publish === 'string' && publish.includes('npm run test:distribution')).toBe(
415
+ hasDistribution && publishes,
416
+ )
417
+ expect(typeof publish === 'string').toBe(publishes)
398
418
  expect(integration).toBe(
399
419
  hasIntegration
400
420
  ? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project integration'
401
421
  : undefined,
402
422
  )
403
- expect(typeof test === 'string' && test.includes('test:integration')).toBe(false)
404
- expect(typeof publish === 'string' && publish.includes('npm run test:integration')).toBe(
423
+ // The integration seed composes barrels and starts no process, so it runs in
424
+ // `test` like any other hermetic proof. `prepublishOnly` reaches it through
425
+ // `npm test` rather than through a second direct invocation.
426
+ expect(typeof test === 'string' && test.includes('npm run test:integration')).toBe(
405
427
  hasIntegration,
406
428
  )
429
+ expect(typeof publish === 'string' && publish.includes('npm run test:integration')).toBe(false)
407
430
  // A registered project no gate runs is a proof that never executes, and it
408
431
  // 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.
432
+ // hermetic and belongs to `test`. A publishing workspace isolates the
433
+ // live-service project in `prepublishOnly`; a private workspace reaches it
434
+ // from `test`, because npm never runs a private package's publish lifecycle.
411
435
  expect(conformance).toBe(
412
436
  hasConformance
413
437
  ? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project conformance'
@@ -421,8 +445,12 @@ describe('root configuration', () => {
421
445
  ? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project service'
422
446
  : undefined,
423
447
  )
424
- expect(typeof test === 'string' && test.includes('test:service')).toBe(false)
425
- expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(hasService)
448
+ expect(typeof test === 'string' && test.includes('npm run test:service')).toBe(
449
+ hasService && !publishes,
450
+ )
451
+ expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(
452
+ hasService && publishes,
453
+ )
426
454
  })
427
455
  })
428
456
 
@@ -562,9 +590,9 @@ describe('configuration helpers', () => {
562
590
  expect(() => configHelpers.enforceOutputPath(expected, expected)).not.toThrow()
563
591
  expect(() =>
564
592
  configHelpers.enforceOutputPath(resolve(root, 'dist/outside-control'), expected),
565
- ).toThrowError('Build output must use its exact configured workspace directory')
593
+ ).toThrow('Build output must use its exact configured workspace directory')
566
594
  const outside = resolve(dirname(root), 'outside-config-control')
567
- expect(() => configHelpers.enforceOutputPath(outside, outside)).toThrowError(
595
+ expect(() => configHelpers.enforceOutputPath(outside, outside)).toThrow(
568
596
  'Build output must remain inside the workspace',
569
597
  )
570
598
  })
@@ -611,7 +639,7 @@ describe('configuration helpers', () => {
611
639
  error: expect.unreachable,
612
640
  resolve: Promise.resolve.bind(Promise, { id: source }),
613
641
  }
614
- await expect(Reflect.apply(hook, context, ['@src/server', source])).rejects.toThrowError(
642
+ await expect(Reflect.apply(hook, context, ['@src/server', source])).rejects.toThrow(
615
643
  'Browser modules cannot depend on Node or server-only modules',
616
644
  )
617
645
  await expect(Reflect.apply(hook, context, ['@src/core', source])).resolves.toBeNull()