@orkestrel/scaffold 0.0.20 → 0.0.22

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.
Files changed (69) hide show
  1. package/dist/bin/scaffold.js +385 -148
  2. package/dist/bin/scaffold.js.map +1 -1
  3. package/dist/host/AGENTS.md +61 -42
  4. package/dist/host/CLAUDE.md +39 -387
  5. package/dist/host/agents/orchestration.md +454 -0
  6. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +25 -7
  7. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +2 -2
  8. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +42 -31
  9. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
  10. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +3 -3
  11. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +4 -4
  12. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +12 -0
  13. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +167 -0
  14. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +98 -0
  15. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +148 -0
  16. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +4 -2
  17. package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +5 -5
  18. package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +2 -0
  19. package/dist/host/agents/skills/orkestrel-polish-surface/SKILL.md +11 -6
  20. package/dist/host/claude/agents/analyst.md +59 -0
  21. package/dist/host/claude/agents/application.md +1 -1
  22. package/dist/host/claude/agents/builder.md +1 -1
  23. package/dist/host/claude/agents/checker.md +5 -1
  24. package/dist/host/claude/agents/codex.md +120 -87
  25. package/dist/host/claude/agents/grok.md +44 -32
  26. package/dist/host/claude/agents/implementer.md +1 -1
  27. package/dist/host/claude/agents/orkestrel.md +3 -3
  28. package/dist/host/claude/agents/planner.md +9 -3
  29. package/dist/host/claude/agents/researcher.md +6 -5
  30. package/dist/host/claude/agents/reviewer.md +17 -6
  31. package/dist/host/claude/agents/scout.md +7 -4
  32. package/dist/host/claude/agents/verifier.md +1 -1
  33. package/dist/host/claude/rules/application.md +17 -16
  34. package/dist/host/claude/rules/architecture.md +41 -22
  35. package/dist/host/claude/rules/documentation.md +2 -1
  36. package/dist/host/claude/rules/names.md +7 -6
  37. package/dist/host/claude/rules/quality.md +71 -14
  38. package/dist/host/claude/rules/styles.md +1 -1
  39. package/dist/host/claude/rules/tests.md +18 -2
  40. package/dist/host/claude/rules/typescript.md +1 -1
  41. package/dist/host/claude/rules/workspace.md +6 -3
  42. package/dist/host/claude/settings.json +2 -0
  43. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +10 -0
  44. package/dist/host/codex/agents/analyst.toml +15 -2
  45. package/dist/host/codex/agents/checker.toml +1 -1
  46. package/dist/host/codex/agents/grok.toml +2 -1
  47. package/dist/host/codex/agents/opus.toml +1 -1
  48. package/dist/host/codex/agents/planner.toml +1 -1
  49. package/dist/host/codex/agents/researcher.toml +2 -2
  50. package/dist/host/codex/agents/reviewer.toml +4 -2
  51. package/dist/host/codex/agents/scout.toml +2 -2
  52. package/dist/host/codex/config.toml +23 -64
  53. package/dist/host/cursor/rules/orchestration.mdc +33 -0
  54. package/dist/host/guides/src/scaffold.md +419 -150
  55. package/dist/host/manifest.json +47 -1
  56. package/dist/host/tests/setupPolicy.ts +130 -12
  57. package/dist/src/core/index.cjs +2059 -913
  58. package/dist/src/core/index.cjs.map +1 -1
  59. package/dist/src/core/index.d.cts +202 -47
  60. package/dist/src/core/index.d.ts +202 -47
  61. package/dist/src/core/index.js +2050 -914
  62. package/dist/src/core/index.js.map +1 -1
  63. package/dist/src/server/index.cjs +83 -33
  64. package/dist/src/server/index.cjs.map +1 -1
  65. package/dist/src/server/index.d.cts +32 -16
  66. package/dist/src/server/index.d.ts +32 -16
  67. package/dist/src/server/index.js +84 -35
  68. package/dist/src/server/index.js.map +1 -1
  69. package/package.json +7 -5
@@ -103,20 +103,29 @@ var APP_MATRIX = Object.freeze({
103
103
  * The byte-copied host artifact paths, frozen.
104
104
  *
105
105
  * @remarks
106
- * The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`, `.agents`, `.claude`, `.codex`,
107
- * the four SessionStart hook scripts (`scripts/deps.sh` / `scripts/cursor.sh` /
108
- * `scripts/codex.sh` / `scripts/ollama.sh`), the repository coding-law policy module,
109
- * the line's seven byte-identical root dotfiles, and the two guides-grouped
110
- * mirror candidates: the line-wide dev-tooling guide
111
- * (`guides/src/guide.md`) and the scaffold bin's own self-guide
112
- * (`guides/src/scaffold.md`). `stageHost` vendors both; each plan carries the
113
- * subset selected by `selectHostPaths`, omitting the target blueprint's own
114
- * guide.
106
+ * The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`, the canonical
107
+ * orchestration contract (`.agents/orchestration.md`) every harness bridge
108
+ * points at, `.agents`, `.claude`, `.codex`, `.cursor`, the four SessionStart
109
+ * hook scripts (`scripts/deps.sh` / `scripts/cursor.sh` / `scripts/codex.sh` /
110
+ * `scripts/ollama.sh`), the repository coding-law policy module, the line's
111
+ * seven byte-identical root dotfiles, and the two guides-grouped mirror
112
+ * candidates: the line-wide dev-tooling guide (`guides/src/guide.md`) and the
113
+ * scaffold bin's own self-guide (`guides/src/scaffold.md`). `stageHost` vendors
114
+ * both; each plan carries the subset selected by `selectHostPaths`, omitting the
115
+ * target blueprint's own guide.
116
+ *
117
+ * Three harness bridges point at `.agents/orchestration.md` and carry only their
118
+ * own harness's specifics: `CLAUDE.md`, `.codex/config.toml`, and
119
+ * `.cursor/rules`. They are meaningless without the contract they reference, but
120
+ * they do not share a `Group` — `hostGroup` keeps `CLAUDE.md` in `docs` with the
121
+ * other root documents, so a plan selecting `orchestration` carries two of the
122
+ * three and a plan selecting `docs` carries the third.
115
123
  */
116
124
  var HOST_PATHS = Object.freeze([
117
125
  "AGENTS.md",
118
126
  "CLAUDE.md",
119
127
  "LICENSE",
128
+ ".agents/orchestration.md",
120
129
  ".agents/skills",
121
130
  ".claude/agents",
122
131
  ".claude/rules",
@@ -125,6 +134,7 @@ var HOST_PATHS = Object.freeze([
125
134
  ".codex/agents",
126
135
  ".codex/config.toml",
127
136
  ".cursor/mcp.json",
137
+ ".cursor/rules",
128
138
  ".mcp.json",
129
139
  "scripts/deps.sh",
130
140
  "scripts/cursor.sh",
@@ -141,12 +151,48 @@ var HOST_PATHS = Object.freeze([
141
151
  "guides/src/guide.md",
142
152
  "guides/src/scaffold.md"
143
153
  ]);
144
- /** The consumer-owned live-service provisioner expected only by service workspaces. */
154
+ /**
155
+ * The path prefixes whose contents instruct or wire an agent, frozen.
156
+ *
157
+ * @remarks
158
+ * Group classification splits by what a path governs, not by where it sits:
159
+ * anything under these prefixes is `orchestration`, and everything else that is
160
+ * not source, tests, guides, docs, or a manifest is `configs`. Both classifiers
161
+ * — `inferGroup` for a foreign target path and `hostGroup` for a `HOST_PATHS`
162
+ * entry — read this one list, so a new harness directory is admitted once.
163
+ */
164
+ var ORCHESTRATION_PATH_PREFIXES = Object.freeze([
165
+ ".agents/",
166
+ ".claude/",
167
+ ".codex/",
168
+ ".cursor/",
169
+ ".github/",
170
+ "scripts/"
171
+ ]);
172
+ /**
173
+ * The exact root filenames that wire an agent bench rather than the toolchain, frozen.
174
+ *
175
+ * @remarks
176
+ * `.mcp.json` registers MCP servers for the harness. It sits among the root
177
+ * dotfiles but governs agents, so it groups with the harness bridges.
178
+ */
179
+ var ORCHESTRATION_PATH_NAMES = Object.freeze([".mcp.json"]);
180
+ /** The birth-only provisioner skeleton retained by workspaces with declared service vendors. */
145
181
  var SERVICE_SCRIPT_PATH = "scripts/service.sh";
146
182
  /** The consumer-owned Vitest global-setup module shared by its independently selected projects. */
147
183
  var GLOBAL_SETUP_PATH = "tests/setupGlobal.ts";
148
184
  /** The consumer-owned Vite wrapper whose physical presence enables the optional app showcase. */
149
185
  var SHOWCASE_CONFIG_PATH = "configs/app/vite.showcase.config.ts";
186
+ /**
187
+ * The catalog agent file whose bounded marker region the catalog operation alone owns.
188
+ *
189
+ * @remarks
190
+ * Vendored like every other host artifact, but presence-owned after hydration:
191
+ * `diffPlan` compares this one path by presence, so a consumer restores it while
192
+ * absent and never replaces its bytes — not from an audit, not from a repair, and
193
+ * not under `replace`.
194
+ */
195
+ var CATALOG_AGENT_PATH = ".claude/agents/orkestrel.md";
150
196
  /** The package-name RegExp — lowercase alphanumeric-with-hyphens, letter-first. */
151
197
  var NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
152
198
  /** Maximum bare workspace name length beneath the generated `@orkestrel/` scope. */
@@ -229,15 +275,15 @@ var DEFAULT_VERSION = "0.0.1";
229
275
  /** The `engines.node` range the `blueprint` builder fills. */
230
276
  var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
231
277
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
232
- var SCAFFOLD_RANGE = "^0.0.20";
278
+ var SCAFFOLD_RANGE = "^0.0.22";
233
279
  /** Tooling versions shared by scaffold and every generated workspace. */
234
280
  var BASE_DEV_DEPENDENCIES = Object.freeze({
235
281
  "@microsoft/api-extractor": "^7.58.12",
236
282
  "@orkestrel/guide": "^0.0.8",
237
283
  "@orkestrel/scaffold": SCAFFOLD_RANGE,
238
284
  "@types/node": "^26.1.2",
239
- oxfmt: "^0.61.0",
240
- oxlint: "^1.76.0",
285
+ oxfmt: "^0.62.0",
286
+ oxlint: "^1.77.0",
241
287
  typescript: "^6.0.3",
242
288
  vite: "~8.2.0",
243
289
  "vite-plugin-dts": "^5.0.3",
@@ -248,13 +294,23 @@ var SOURCE_BROWSER_DEV_DEPENDENCIES = Object.freeze({
248
294
  "@vitest/browser-playwright": "^4.1.10",
249
295
  playwright: "^1.62.1"
250
296
  });
297
+ /** Baseline development dependency required by every private application environment. */
298
+ var APP_DEV_DEPENDENCIES = Object.freeze({ "@orkestrel/contract": "^0.0.9" });
251
299
  /** Additional development dependencies required by a private Vue browser application. */
252
300
  var APP_BROWSER_DEV_DEPENDENCIES = Object.freeze({
253
301
  ...SOURCE_BROWSER_DEV_DEPENDENCIES,
302
+ "@orkestrel/html": "^0.0.2",
254
303
  "@vitejs/plugin-vue": "^6.0.8",
255
304
  vue: "^3.5.40",
256
305
  "vue-tsc": "^3.3.7"
257
306
  });
307
+ /** Additional development dependencies required by a private server application. */
308
+ var APP_SERVER_DEV_DEPENDENCIES = Object.freeze({
309
+ "@orkestrel/emitter": "^0.0.5",
310
+ "@orkestrel/middleware": "^0.0.9",
311
+ "@orkestrel/router": "^0.0.8",
312
+ "@orkestrel/server": "^0.0.10"
313
+ });
258
314
  /** Immutable official actions/checkout v6.0.2 commit used by generated CI. */
259
315
  var CHECKOUT_ACTION_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
260
316
  /** Immutable official actions/setup-node v6.4.0 commit used by generated CI. */
@@ -428,8 +484,8 @@ function member(name, category, summary, environment = "core") {
428
484
  * @remarks
429
485
  * `version` / `engines` default `DEFAULT_VERSION` / `DEFAULT_ENGINES`,
430
486
  * `src` defaults `['core']`, and `app` / `keywords` / `dependencies` /
431
- * `peers` / `extras` / `overrides` default `[]`, and `bin` / `integration` /
432
- * `service` / `global` / `showcase` default `false`. `description` is OMITTED entirely
487
+ * `peers` / `extras` / `overrides` / `services` default `[]`, and `bin` /
488
+ * `integration` / `global` / `showcase` default `false`. `description` is OMITTED entirely
433
489
  * when absent, so the result round-trips the exact-record `Blueprint` guard.
434
490
  * @returns A complete `Blueprint`.
435
491
  *
@@ -454,7 +510,7 @@ function blueprint(name, options) {
454
510
  overrides: options?.overrides ?? [],
455
511
  bin: options?.bin ?? false,
456
512
  integration: options?.integration ?? false,
457
- service: options?.service ?? false,
513
+ services: [...options?.services ?? []].sort(),
458
514
  global: options?.global ?? false,
459
515
  showcase: options?.showcase ?? false
460
516
  };
@@ -528,13 +584,45 @@ function serializeTypeScriptString(value) {
528
584
  return `${output}'`;
529
585
  }
530
586
  /**
587
+ * Determine whether an application blueprint spans the shared browser/server boundary.
588
+ *
589
+ * @param spec - The blueprint to inspect.
590
+ * @returns True only when app/core, app/browser, and app/server are all selected.
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * hasApplicationBoundary(blueprint('application', { app: ['core', 'browser', 'server'] }))
595
+ * ```
596
+ */
597
+ function hasApplicationBoundary(spec) {
598
+ return spec.app.includes("core") && spec.app.includes("browser") && spec.app.includes("server");
599
+ }
600
+ /**
601
+ * Determine whether an application blueprint emits its browser showcase.
602
+ *
603
+ * @param spec - The blueprint to inspect.
604
+ * @returns True only when showcase intent accompanies app/browser.
605
+ *
606
+ * @example
607
+ * ```ts
608
+ * hasApplicationShowcase(blueprint('application', { app: ['browser'], showcase: true }))
609
+ * ```
610
+ */
611
+ function hasApplicationShowcase(spec) {
612
+ return spec.showcase && spec.app.includes("browser");
613
+ }
614
+ /**
531
615
  * Derive the declared public `Member[]` from a blueprint.
532
616
  *
533
617
  * @param spec - The blueprint to derive members from.
534
618
  * @remarks
535
619
  * Published source environments receive the canonical entity/type/factory/constant
536
620
  * inventory. Application environments receive their exact public declaration kinds,
537
- * including parsers, guards, handlers, errors, and runners where present.
621
+ * including parsers, guards, handlers, errors, and runners where present. Two groups
622
+ * move rather than duplicate: the health contract is declared against `app/server`
623
+ * while the server alone reads it and against `app/core` once the browser reads it
624
+ * too, and the showcase seed, factory, and root-view identity appear only for a
625
+ * blueprint whose showcase accompanies `app/browser`.
538
626
  * @returns The declared `Member[]`, one set per environment.
539
627
  *
540
628
  * @example
@@ -547,6 +635,8 @@ function serializeTypeScriptString(value) {
547
635
  function blueprintToMembers(spec) {
548
636
  const pascal = pascalCase(spec.name);
549
637
  const members = [];
638
+ const hasBoundary = hasApplicationBoundary(spec);
639
+ const hasShowcase = hasApplicationShowcase(spec);
550
640
  for (const environment of spec.src) {
551
641
  members.push(member(pascal, "entity", `The ${pascal} entity.`, environment));
552
642
  members.push(member(`${pascal}Options`, "type", `Options for creating a ${pascal}.`, environment));
@@ -564,15 +654,21 @@ function blueprintToMembers(spec) {
564
654
  members.push(member("isApplicationError", "guard", "Narrow a caught value to ApplicationError.", "core"));
565
655
  members.push(member("parseApplicationName", "parser", "Parse an application name.", "core"));
566
656
  members.push(member("createApplication", "factory", "Create an application identity.", "core"));
657
+ if (hasBoundary) members.push(member("ApplicationRecord", "type", "The shared application health record.", "core"), member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "core"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "core"), member("APP_HEALTH_TIMEOUT", "constant", "The shared health read timeout.", "core"), member("isApplicationRecord", "guard", "Narrow a transport value to the shared record.", "core"), member("readApplicationHealth", "handler", "Read the shared health boundary as the application identity.", "core"));
567
658
  }
568
659
  if (spec.app.includes("browser")) {
569
660
  members.push(member("BrowserApplicationErrorCode", "alias", "A browser application configuration error reason.", "browser"), member("BrowserApplicationErrorContext", "type", "Browser application boundary-failure context.", "browser"), member("BrowserApplicationOptions", "type", "Options for creating the browser application.", "browser"));
570
661
  members.push(member("MAX_BROWSER_APPLICATION_NAME_LENGTH", "constant", "The maximum browser application-name length.", "browser"), member("MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH", "constant", "The maximum raw browser application-name input length.", "browser"), member("BrowserApplicationError", "error", "A browser application configuration error.", "browser"), member("isBrowserApplicationError", "guard", "Narrow a caught value to BrowserApplicationError.", "browser"), member("parseBrowserApplicationOptions", "parser", "Parse browser application options.", "browser"));
571
662
  if (!spec.app.includes("core")) members.push(member("APP_NAME", "constant", "The browser application name.", "browser"));
663
+ if (hasShowcase && !spec.app.includes("core")) members.push(member("Application", "type", "The identity the root view renders.", "browser"));
664
+ if (hasShowcase) members.push(member("seedApplication", "factory", "Seed the inert showcase identity.", "browser"));
572
665
  members.push(member("createBrowserApplication", "factory", "Create an unmounted Vue application.", "browser"));
666
+ if (hasShowcase) members.push(member("mountShowcaseApplication", "factory", "Mount the seeded showcase.", "browser"));
667
+ if (hasBoundary) members.push(member("mountBrowserApplication", "factory", "Mount the application over its server boundary.", "browser"));
573
668
  }
574
669
  if (spec.app.includes("server")) {
575
- members.push(member("ApplicationServerErrorCode", "alias", "An application server error reason.", "server"), member("ApplicationServerErrorContext", "type", "Application server boundary-failure context.", "server"), member("ApplicationServerOptions", "type", "Options for creating an application server.", "server"), member("ApplicationServerInterface", "type", "The application server lifecycle contract.", "server"), member("ApplicationServerRunnerInterface", "type", "The application server process lifecycle contract.", "server"), member("DEFAULT_APP_HOST", "constant", "The loopback host default.", "server"), member("DEFAULT_APP_PORT", "constant", "The application port default.", "server"), member("DEFAULT_APP_START_TIMEOUT", "constant", "The application startup timeout default.", "server"), member("MAX_APP_START_TIMEOUT", "constant", "The maximum application startup timeout.", "server"), member("MAX_APP_HOST_INPUT_LENGTH", "constant", "The maximum raw application-host input length.", "server"), member("MAX_APP_NUMBER_INPUT_LENGTH", "constant", "The maximum raw application numeric input length.", "server"), member("APP_MAX_CONNECTIONS", "constant", "The simultaneous connection limit.", "server"), member("APP_MAX_HEADERS", "constant", "The request-header count limit.", "server"), member("APP_HEADERS_TIMEOUT", "constant", "The request-header timeout.", "server"), member("APP_REQUEST_TIMEOUT", "constant", "The complete-request timeout.", "server"), member("APP_KEEP_ALIVE_TIMEOUT", "constant", "The idle keep-alive timeout.", "server"), member("APP_MAX_REQUESTS_PER_SOCKET", "constant", "The keep-alive request limit.", "server"), member("APP_PORT_PATTERN", "constant", "The decimal application-port syntax.", "server"), member("APP_HOST_LABEL_PATTERN", "constant", "The DNS application-host label syntax.", "server"), member("APP_NUMERIC_HOST_PATTERN", "constant", "The ambiguous numeric-host rejection syntax.", "server"), member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "server"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "server"), member("ApplicationServer", "entity", "The Node HTTP application server.", "server"), member("ApplicationServerRunner", "entity", "The application server process lifecycle owner.", "server"), member("ApplicationServerError", "error", "A server configuration or lifecycle error.", "server"), member("isApplicationServerError", "guard", "Narrow a caught value to ApplicationServerError.", "server"), member("parseApplicationHost", "parser", "Parse an application host.", "server"), member("parseApplicationPort", "parser", "Parse an application port.", "server"), member("parseApplicationStartTimeout", "parser", "Parse an application startup timeout.", "server"), member("parseApplicationServerOptions", "parser", "Parse application server options.", "server"), member("handleApplicationRequest", "handler", "Handle an application HTTP request.", "server"), member("reportApplicationServerError", "handler", "Report a process-owned failure without exposing diagnostic context.", "server"), member("createApplicationServer", "factory", "Create a stopped application server.", "server"), member("startApplicationServer", "factory", "Start the process-owned application server.", "server"));
670
+ if (!hasBoundary) members.push(member("ApplicationRecord", "type", "The application health record.", "server"));
671
+ members.push(member("ApplicationState", "type", "Per-request application state.", "server"), member("ApplicationServerErrorCode", "alias", "An application server error reason.", "server"), member("ApplicationServerErrorContext", "type", "Application server boundary-failure context.", "server"), member("ApplicationServerOptions", "type", "Options for creating an application server.", "server"), member("ApplicationServerInterface", "type", "The application server lifecycle contract.", "server"), member("ApplicationServerRunnerInterface", "type", "The application server process lifecycle contract.", "server"), member("ApplicationServerRunnerEventMap", "alias", "Observable application server runner outcomes.", "server"), member("ApplicationServerRunnerOptions", "type", "Options for observing an application server runner.", "server"), member("DEFAULT_APP_HOST", "constant", "The loopback host default.", "server"), member("DEFAULT_APP_PORT", "constant", "The application port default.", "server"), member("DEFAULT_APP_START_TIMEOUT", "constant", "The application startup timeout default.", "server"), member("MAX_APP_START_TIMEOUT", "constant", "The maximum application startup timeout.", "server"), member("MAX_APP_HOST_INPUT_LENGTH", "constant", "The maximum raw application-host input length.", "server"), member("MAX_APP_NUMBER_INPUT_LENGTH", "constant", "The maximum raw application numeric input length.", "server"), member("APP_PORT_PATTERN", "constant", "The decimal application-port syntax.", "server"), member("APP_HOST_LABEL_PATTERN", "constant", "The DNS application-host label syntax.", "server"), member("APP_NUMERIC_HOST_PATTERN", "constant", "The ambiguous numeric-host rejection syntax.", "server"), ...hasBoundary ? [] : [member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "server"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "server")], member("createApplicationDispatcher", "factory", "Create a standalone application route dispatcher.", "server"), member("ApplicationServer", "entity", "The composed application server.", "server"), member("ApplicationServerRunner", "entity", "The application server process lifecycle owner.", "server"), member("ApplicationServerError", "error", "A server configuration or lifecycle error.", "server"), member("isApplicationServerError", "guard", "Narrow a caught value to ApplicationServerError.", "server"), member("parseApplicationHost", "parser", "Parse an application host.", "server"), member("parseApplicationPort", "parser", "Parse an application port.", "server"), member("parseApplicationStartTimeout", "parser", "Parse an application startup timeout.", "server"), member("parseApplicationServerOptions", "parser", "Parse application server options.", "server"), member("handleApplicationHealth", "handler", "Return the application health record.", "server"), member("reportApplicationServerError", "handler", "Report a process-owned failure without exposing diagnostic context.", "server"), member("createApplicationServer", "factory", "Create a stopped application server.", "server"), member("startApplicationServer", "factory", "Start the process-owned application server.", "server"));
576
672
  if (!spec.app.includes("core")) members.push(member("APP_NAME", "constant", "The server application name.", "server"));
577
673
  }
578
674
  return members;
@@ -903,7 +999,7 @@ function syncToReview(report) {
903
999
  }
904
1000
  /**
905
1001
  * Project a fleet package catalog into a markdown table — the block
906
- * `.claude/agents/orkestrel.md`'s catalog markers wrap.
1002
+ * `CATALOG_AGENT_PATH`'s catalog markers wrap.
907
1003
  *
908
1004
  * @param entries - The catalog rows to render.
909
1005
  * @remarks
@@ -935,10 +1031,12 @@ function catalogToBlock(entries) {
935
1031
  *
936
1032
  * @param path - The target-relative path to classify.
937
1033
  * @remarks
938
- * Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`,
939
- * then `.agents/`, `.claude/`, `.codex/`, `.github/`, and `scripts/` as
940
- * `'orchestration'`, then the two manifest files by exact name. Anything else
941
- * (a root-level, prefix-less file) falls through to `'configs'`.
1034
+ * Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`, then
1035
+ * `matchesOrchestrationPath`, which owns the orchestration membership rule for
1036
+ * this function and for `hostGroup` alike, then the two manifest files by exact
1037
+ * name. Anything left falls through to `'configs'`. Read the rule at
1038
+ * `matchesOrchestrationPath` rather than here; one rule with two descriptions
1039
+ * drifts exactly as fast as one rule with two implementations.
942
1040
  * @returns The inferred `Group` for `path`.
943
1041
  *
944
1042
  * @example
@@ -956,23 +1054,43 @@ function inferGroup(path) {
956
1054
  if (path.startsWith("guides/")) return "guides";
957
1055
  if (path.startsWith("docs/")) return "docs";
958
1056
  if (path.startsWith("configs/")) return "configs";
959
- if (path.startsWith(".agents/") || path.startsWith(".claude/") || path.startsWith(".codex/") || path.startsWith(".github/") || path.startsWith("scripts/")) return "orchestration";
1057
+ if (matchesOrchestrationPath(path)) return "orchestration";
960
1058
  if (path === "package.json" || path === "package-lock.json") return "manifest";
961
1059
  return "configs";
962
1060
  }
963
1061
  /**
1062
+ * Test whether a path instructs or wires an agent rather than the toolchain.
1063
+ *
1064
+ * @param path - The portable path to classify.
1065
+ * @returns `true` when the path is agent orchestration.
1066
+ *
1067
+ * @example
1068
+ * ```ts
1069
+ * import { matchesOrchestrationPath } from '@orkestrel/scaffold'
1070
+ *
1071
+ * matchesOrchestrationPath('.cursor/rules/orchestration.mdc') // true
1072
+ * matchesOrchestrationPath('.mcp.json') // true
1073
+ * matchesOrchestrationPath('.oxlintrc.json') // false
1074
+ * ```
1075
+ */
1076
+ function matchesOrchestrationPath(path) {
1077
+ if (ORCHESTRATION_PATH_NAMES.includes(path)) return true;
1078
+ return ORCHESTRATION_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
1079
+ }
1080
+ /**
964
1081
  * Diff a plan's artifacts against a target's current content.
965
1082
  *
966
1083
  * @param plan - The plan whose artifacts are the source of truth.
967
1084
  * @param current - The target's current content, keyed by artifact-relative path.
968
1085
  * @remarks
969
- * Audit semantics are per-origin. A `host`-origin artifact is audited by
970
- * PRESENCE only `missing` or `aligned`, never `stale` UNLESS it has been
971
- * hydrated with its real host bytes (`hydratePlan`'s `content`), in which case
972
- * it is content-compared exactly like a `computed` artifact and CAN be
973
- * `stale`. `hydratePlan` expands directory-shaped host artifacts into
974
- * content-bearing file artifacts; only an unresolved degrade-path host
975
- * artifact stays presence-only. A `computed` artifact is content-aware canon —
1086
+ * Audit semantics are per-origin. A `host`-origin artifact with canonical
1087
+ * `hex` is content-compared exactly like a `computed` artifact and CAN be
1088
+ * `stale`. A host artifact without canonical bytes is presence-owned:
1089
+ * present is `aligned`, absent is `missing`. `CATALOG_AGENT_PATH` remains
1090
+ * presence-owned after hydration because the catalog operation alone owns its
1091
+ * bounded marker region. `hydratePlan` expands directory-shaped host artifacts
1092
+ * into byte-aware file artifacts; only an intentional dependency-guide pointer
1093
+ * may remain without `hex`. A `computed` artifact is content-aware canon —
976
1094
  * `missing` / `aligned` / `stale` — and gates the audit like any drifted
977
1095
  * finding. A `template`-origin artifact is BIRTH-ONLY and AUDIT-EXEMPT: it is
978
1096
  * always reported `aligned`, regardless of whether the target has it at all
@@ -1012,7 +1130,7 @@ function diffPlan(plan, current) {
1012
1130
  if (artifact.origin === "host") {
1013
1131
  let drift;
1014
1132
  if (seen === void 0) drift = "missing";
1015
- else if (artifact.hex === void 0) drift = "aligned";
1133
+ else if (artifact.path === ".claude/agents/orkestrel.md" || artifact.hex === void 0) drift = "aligned";
1016
1134
  else drift = seen === artifact.hex ? "aligned" : "stale";
1017
1135
  findings.push({
1018
1136
  path: artifact.path,
@@ -1369,6 +1487,35 @@ function validateBlueprint(spec) {
1369
1487
  text: "Showcase requires the app browser environment",
1370
1488
  blocking: true
1371
1489
  });
1490
+ const seenServices = /* @__PURE__ */ new Set();
1491
+ const serviceProjects = /* @__PURE__ */ new Set();
1492
+ let previousService;
1493
+ for (const service of spec.services) {
1494
+ if (!NAME_PATTERN.test(service) || service.length > 203) questions.push({
1495
+ field: "services",
1496
+ text: `Service name "${service}" must be a bounded lowercase directory name matching ${NAME_PATTERN.source}`,
1497
+ blocking: true
1498
+ });
1499
+ if (seenServices.has(service)) questions.push({
1500
+ field: "services",
1501
+ text: `Service "${service}" is declared more than once`,
1502
+ blocking: true
1503
+ });
1504
+ if (previousService !== void 0 && previousService > service) questions.push({
1505
+ field: "services",
1506
+ text: "Services must be sorted by directory name",
1507
+ blocking: true
1508
+ });
1509
+ const project = pascalCase(service);
1510
+ if (serviceProjects.has(project)) questions.push({
1511
+ field: "services",
1512
+ text: `Service "${service}" collides with another generated project name`,
1513
+ blocking: true
1514
+ });
1515
+ seenServices.add(service);
1516
+ serviceProjects.add(project);
1517
+ previousService = service;
1518
+ }
1372
1519
  if (spec.src.length > 0) {
1373
1520
  for (const environment of spec.src) if (!ENVIRONMENTS.includes(environment)) questions.push({
1374
1521
  field: "src",
@@ -1653,6 +1800,34 @@ function renderArray(entries, indent, prefix, suffix) {
1653
1800
  return `[\n${items.map((item) => `${childIndent}${item}`).join(",\n")}\n${indent}]`;
1654
1801
  }
1655
1802
  /**
1803
+ * Render a single-quoted TypeScript string array literal through `oxfmt`'s
1804
+ * inline-or-broken rule — inline when the rendered width fits
1805
+ * `JSON_PRINT_WIDTH`, one item per line with a trailing comma on every line
1806
+ * (including the last) otherwise, matching `.oxfmtrc.json`'s
1807
+ * `trailingComma: "all"` for non-JSON files.
1808
+ *
1809
+ * @param entries - The array's string elements, in order.
1810
+ * @param indent - The current indentation prefix.
1811
+ * @param prefix - The text already emitted on this line before the array.
1812
+ * @param suffix - The text that will follow the array on this line.
1813
+ * @returns The rendered array fragment (no trailing newline).
1814
+ *
1815
+ * @example
1816
+ * ```ts
1817
+ * import { renderStringArray } from '@orkestrel/scaffold'
1818
+ *
1819
+ * renderStringArray(['app', 'guides', 'tests'], '', '', '') // "['app', 'guides', 'tests']"
1820
+ * ```
1821
+ */
1822
+ function renderStringArray(entries, indent, prefix, suffix) {
1823
+ if (entries.length === 0) return "[]";
1824
+ const items = entries.map((entry) => serializeTypeScriptString(entry));
1825
+ const inline = `[${items.join(", ")}]`;
1826
+ if (fitsPrintWidth(`${prefix}${inline}${suffix}`)) return inline;
1827
+ const childIndent = `${indent}\t`;
1828
+ return `[\n${items.map((item) => `${childIndent}${item},`).join("\n")}\n${indent}]`;
1829
+ }
1830
+ /**
1656
1831
  * Render a JSON object through `formatJson`'s one-key-per-line rule.
1657
1832
  *
1658
1833
  * @param entry - The object to render.
@@ -1831,7 +2006,10 @@ function blueprintShape() {
1831
2006
  overrides: (0, _orkestrel_contract.arrayShape)(overrideShape(), { max: MAX_COLLECTION_ITEMS }),
1832
2007
  bin: (0, _orkestrel_contract.booleanShape)(),
1833
2008
  integration: (0, _orkestrel_contract.booleanShape)(),
1834
- service: (0, _orkestrel_contract.booleanShape)(),
2009
+ services: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)({
2010
+ min: 1,
2011
+ max: 203
2012
+ }), { max: MAX_COLLECTION_ITEMS }),
1835
2013
  global: (0, _orkestrel_contract.booleanShape)(),
1836
2014
  showcase: (0, _orkestrel_contract.booleanShape)()
1837
2015
  });
@@ -2895,7 +3073,10 @@ export * from './factories.js'
2895
3073
  name: "appCoreTypes",
2896
3074
  summary: "The host-independent application contract.",
2897
3075
  category: "source",
2898
- placeholders: Object.freeze([]),
3076
+ placeholders: Object.freeze([Object.freeze({
3077
+ name: "record",
3078
+ description: "The shared health record declared once both hosts read it."
3079
+ })]),
2899
3080
  content: `/** A rejected shared application boundary. */
2900
3081
  ${EXPORT_KEYWORD} type ApplicationErrorCode = 'CONFIG'
2901
3082
 
@@ -2909,7 +3090,7 @@ ${EXPORT_KEYWORD} interface ApplicationErrorContext {
2909
3090
  ${EXPORT_KEYWORD} interface Application {
2910
3091
  readonly name: string
2911
3092
  }
2912
- `
3093
+ {{record}}`
2913
3094
  }),
2914
3095
  appCoreConstants: Object.freeze({
2915
3096
  id: "appCoreConstants",
@@ -2919,6 +3100,9 @@ ${EXPORT_KEYWORD} interface Application {
2919
3100
  placeholders: Object.freeze([Object.freeze({
2920
3101
  name: "nameLiteral",
2921
3102
  description: "The JSON-serialized application name."
3103
+ }), Object.freeze({
3104
+ name: "health",
3105
+ description: "The shared health route constants declared once both hosts read them."
2922
3106
  })]),
2923
3107
  content: `/** The application name shared by every host environment. */
2924
3108
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = {{nameLiteral}}
@@ -2928,7 +3112,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_LENGTH = 203
2928
3112
 
2929
3113
  /** Maximum raw Unicode code units inspected before trimming an application name. */
2930
3114
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_INPUT_LENGTH = 255
2931
- `
3115
+ {{health}}`
2932
3116
  }),
2933
3117
  appCoreErrors: Object.freeze({
2934
3118
  id: "appCoreErrors",
@@ -2937,6 +3121,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_INPUT_LENGTH = 255
2937
3121
  category: "source",
2938
3122
  placeholders: Object.freeze([]),
2939
3123
  content: `import type { ApplicationErrorCode, ApplicationErrorContext } from './types.js'
3124
+ import { holds } from '@orkestrel/contract'
2940
3125
 
2941
3126
  /** A rejected shared application configuration value. */
2942
3127
  ${EXPORT_KEYWORD} class ApplicationError extends Error {
@@ -2965,11 +3150,34 @@ ${EXPORT_KEYWORD} class ApplicationError extends Error {
2965
3150
  * \`\`\`
2966
3151
  */
2967
3152
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationError(value: unknown): value is ApplicationError {
2968
- try {
2969
- return value instanceof ApplicationError
2970
- } catch {
2971
- return false
2972
- }
3153
+ return holds(() => value instanceof ApplicationError)
3154
+ }
3155
+ `
3156
+ }),
3157
+ appCoreValidators: Object.freeze({
3158
+ id: "appCoreValidators",
3159
+ name: "appCoreValidators",
3160
+ summary: "The host-independent guard over the shared health record.",
3161
+ category: "source",
3162
+ placeholders: Object.freeze([]),
3163
+ content: `import type { ApplicationRecord } from './types.js'
3164
+ import { holds, isNonEmptyString, isRecord } from '@orkestrel/contract'
3165
+
3166
+ /**
3167
+ * Narrow one unvalidated transport value to the shared application record.
3168
+ *
3169
+ * @param value - The value read from the health route, before validation.
3170
+ * @returns True only for the exact record both hosts agreed on.
3171
+ *
3172
+ * @example
3173
+ * \`\`\`ts
3174
+ * import { isApplicationRecord } from '@app/core'
3175
+ *
3176
+ * isApplicationRecord({ name: 'example', status: 'ok' }) // true
3177
+ * \`\`\`
3178
+ */
3179
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationRecord(value: unknown): value is ApplicationRecord {
3180
+ return holds(() => isRecord(value) && isNonEmptyString(value.name) && value.status === 'ok')
2973
3181
  }
2974
3182
  `
2975
3183
  }),
@@ -2979,7 +3187,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationError(value: unknown): value
2979
3187
  summary: "The host-independent application value parsers.",
2980
3188
  category: "source",
2981
3189
  placeholders: Object.freeze([]),
2982
- content: `import { MAX_APPLICATION_NAME_INPUT_LENGTH, MAX_APPLICATION_NAME_LENGTH } from './constants.js'
3190
+ content: `import { isNonEmptyString } from '@orkestrel/contract'
3191
+ import { MAX_APPLICATION_NAME_INPUT_LENGTH, MAX_APPLICATION_NAME_LENGTH } from './constants.js'
2983
3192
  import { ApplicationError } from './errors.js'
2984
3193
 
2985
3194
  /**
@@ -3001,7 +3210,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationName(value: unknown): stri
3001
3210
  throw new ApplicationError('CONFIG', 'Application name must be a string', { value })
3002
3211
  }
3003
3212
  const name = value.trim()
3004
- if (name.length === 0 || name.length > MAX_APPLICATION_NAME_LENGTH) {
3213
+ if (!isNonEmptyString(name) || name.length > MAX_APPLICATION_NAME_LENGTH) {
3005
3214
  throw new ApplicationError(
3006
3215
  'CONFIG',
3007
3216
  \`Application name must contain 1 through \${MAX_APPLICATION_NAME_LENGTH} characters\`,
@@ -3010,6 +3219,52 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationName(value: unknown): stri
3010
3219
  }
3011
3220
  return name
3012
3221
  }
3222
+ `
3223
+ }),
3224
+ appCoreHandlers: Object.freeze({
3225
+ id: "appCoreHandlers",
3226
+ name: "appCoreHandlers",
3227
+ summary: "The host-independent read of the shared health boundary.",
3228
+ category: "source",
3229
+ placeholders: Object.freeze([]),
3230
+ content: `import type { Application } from './types.js'
3231
+ import { APP_HEALTH_PATH, APP_HEALTH_TIMEOUT } from './constants.js'
3232
+ import { isApplicationRecord } from './validators.js'
3233
+
3234
+ /**
3235
+ * Read the application server's health route and translate its unvalidated JSON
3236
+ * into the shared application identity.
3237
+ *
3238
+ * @param origin - The absolute origin serving the application health route.
3239
+ * @returns The identity the running server reported, or undefined when the boundary
3240
+ * is unreachable, too slow, or off-contract.
3241
+ *
3242
+ * @remarks
3243
+ * The single translation point between the two hosts: the response body is read as
3244
+ * \`unknown\` and narrowed by {@link isApplicationRecord} before any field is consumed,
3245
+ * so a missing, slow, or foreign server degrades to \`undefined\` instead of leaking an
3246
+ * unvalidated value into the application. The record's \`status\` proves liveness; the
3247
+ * identity is what the caller renders.
3248
+ *
3249
+ * @example
3250
+ * \`\`\`ts
3251
+ * import { readApplicationHealth } from '@app/core'
3252
+ *
3253
+ * await readApplicationHealth('http://127.0.0.1:3000') // { name: 'example' }
3254
+ * \`\`\`
3255
+ */
3256
+ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} readApplicationHealth(origin: string): Promise<Application | undefined> {
3257
+ try {
3258
+ const response = await fetch(new URL(APP_HEALTH_PATH, origin), {
3259
+ signal: AbortSignal.timeout(APP_HEALTH_TIMEOUT),
3260
+ })
3261
+ if (!response.ok) return undefined
3262
+ const record: unknown = await response.json()
3263
+ return isApplicationRecord(record) ? Object.freeze({ name: record.name }) : undefined
3264
+ } catch {
3265
+ return undefined
3266
+ }
3267
+ }
3013
3268
  `
3014
3269
  }),
3015
3270
  appCoreFactories: Object.freeze({
@@ -3045,12 +3300,18 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplication(name: string = APP_NAME)
3045
3300
  name: "appCoreIndex",
3046
3301
  summary: "The application core barrel.",
3047
3302
  category: "source",
3048
- placeholders: Object.freeze([]),
3303
+ placeholders: Object.freeze([Object.freeze({
3304
+ name: "validators",
3305
+ description: "The optional shared-record guard barrel row."
3306
+ }), Object.freeze({
3307
+ name: "handlers",
3308
+ description: "The optional shared health-boundary barrel row."
3309
+ })]),
3049
3310
  content: `export * from './types.js'
3050
3311
  export * from './constants.js'
3051
3312
  export * from './errors.js'
3052
- export * from './parsers.js'
3053
- export * from './factories.js'
3313
+ {{validators}}export * from './parsers.js'
3314
+ {{handlers}}export * from './factories.js'
3054
3315
  `
3055
3316
  }),
3056
3317
  appBrowserTypes: Object.freeze({
@@ -3058,7 +3319,10 @@ export * from './factories.js'
3058
3319
  name: "appBrowserTypes",
3059
3320
  summary: "The browser application options.",
3060
3321
  category: "source",
3061
- placeholders: Object.freeze([]),
3322
+ placeholders: Object.freeze([Object.freeze({
3323
+ name: "application",
3324
+ description: "The browser-owned root-view identity declared without application core."
3325
+ })]),
3062
3326
  content: `/** A rejected browser application boundary. */
3063
3327
  ${EXPORT_KEYWORD} type BrowserApplicationErrorCode = 'CONFIG'
3064
3328
 
@@ -3072,7 +3336,7 @@ ${EXPORT_KEYWORD} interface BrowserApplicationErrorContext {
3072
3336
  ${EXPORT_KEYWORD} interface BrowserApplicationOptions {
3073
3337
  readonly name?: string
3074
3338
  }
3075
- `
3339
+ {{application}}`
3076
3340
  }),
3077
3341
  appBrowserConstants: Object.freeze({
3078
3342
  id: "appBrowserConstants",
@@ -3097,6 +3361,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH = 2
3097
3361
  category: "source",
3098
3362
  placeholders: Object.freeze([]),
3099
3363
  content: `import type { BrowserApplicationErrorCode, BrowserApplicationErrorContext } from './types.js'
3364
+ import { holds } from '@orkestrel/contract'
3100
3365
 
3101
3366
  /** A rejected browser application configuration value. */
3102
3367
  ${EXPORT_KEYWORD} class BrowserApplicationError extends Error {
@@ -3129,11 +3394,7 @@ ${EXPORT_KEYWORD} class BrowserApplicationError extends Error {
3129
3394
  * \`\`\`
3130
3395
  */
3131
3396
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserApplicationError(value: unknown): value is BrowserApplicationError {
3132
- try {
3133
- return value instanceof BrowserApplicationError
3134
- } catch {
3135
- return false
3136
- }
3397
+ return holds(() => value instanceof BrowserApplicationError)
3137
3398
  }
3138
3399
  `
3139
3400
  }),
@@ -3144,6 +3405,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserApplicationError(value: unknown):
3144
3405
  category: "source",
3145
3406
  placeholders: Object.freeze([]),
3146
3407
  content: `import type { BrowserApplicationOptions } from './types.js'
3408
+ import { isNonEmptyString } from '@orkestrel/contract'
3147
3409
  import {
3148
3410
  MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH,
3149
3411
  MAX_BROWSER_APPLICATION_NAME_LENGTH,
@@ -3180,6 +3442,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
3180
3442
  { value },
3181
3443
  )
3182
3444
  }
3445
+ // Walk own descriptors directly: reading values through a getter would run caller code.
3183
3446
  const keys = Reflect.ownKeys(value)
3184
3447
  if (keys.some((key) => key !== 'name')) {
3185
3448
  throw new BrowserApplicationError('CONFIG', 'Unknown browser application option', {
@@ -3204,7 +3467,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
3204
3467
  })
3205
3468
  }
3206
3469
  const name = descriptor.value.trim()
3207
- if (name.length === 0 || name.length > MAX_BROWSER_APPLICATION_NAME_LENGTH) {
3470
+ if (!isNonEmptyString(name) || name.length > MAX_BROWSER_APPLICATION_NAME_LENGTH) {
3208
3471
  throw new BrowserApplicationError(
3209
3472
  'CONFIG',
3210
3473
  \`Browser application name must contain 1 through \${MAX_BROWSER_APPLICATION_NAME_LENGTH} characters\`,
@@ -3221,22 +3484,73 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
3221
3484
  }
3222
3485
  `
3223
3486
  }),
3224
- appBrowserFactories: Object.freeze({
3225
- id: "appBrowserFactories",
3226
- name: "appBrowserFactories",
3227
- summary: "The Vue browser application factory.",
3487
+ appBrowserSeeders: Object.freeze({
3488
+ id: "appBrowserSeeders",
3489
+ name: "appBrowserSeeders",
3490
+ summary: "The frozen, inert identity the showcase renders.",
3228
3491
  category: "source",
3229
3492
  placeholders: Object.freeze([Object.freeze({
3493
+ name: "applicationImport",
3494
+ description: "The selected layer type import for the root-view identity."
3495
+ }), Object.freeze({
3230
3496
  name: "nameImport",
3231
3497
  description: "The selected layer import for APP_NAME."
3232
3498
  })]),
3499
+ content: `{{applicationImport}}
3500
+ {{nameImport}}
3501
+
3502
+ /**
3503
+ * Seed the inert identity the showcase renders.
3504
+ *
3505
+ * @returns A fresh frozen identity, identical on every call.
3506
+ *
3507
+ * @remarks
3508
+ * The showcase's only data. It is exactly the value the shipped root view receives from
3509
+ * the running application, so the showcase exercises the shipped view rather than a
3510
+ * parallel copy of it.
3511
+ *
3512
+ * @example
3513
+ * \`\`\`ts
3514
+ * import { seedApplication } from '@app/browser'
3515
+ *
3516
+ * seedApplication().name // the seeded showcase identity
3517
+ * \`\`\`
3518
+ */
3519
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} seedApplication(): Application {
3520
+ return Object.freeze({ name: \`\${APP_NAME} showcase\` })
3521
+ }
3522
+ `
3523
+ }),
3524
+ appBrowserFactories: Object.freeze({
3525
+ id: "appBrowserFactories",
3526
+ name: "appBrowserFactories",
3527
+ summary: "The Vue browser application factory.",
3528
+ category: "source",
3529
+ placeholders: Object.freeze([
3530
+ Object.freeze({
3531
+ name: "nameImport",
3532
+ description: "The selected layer import for APP_NAME."
3533
+ }),
3534
+ Object.freeze({
3535
+ name: "seedImport",
3536
+ description: "The optional showcase seeder import."
3537
+ }),
3538
+ Object.freeze({
3539
+ name: "showcase",
3540
+ description: "The optional seeded showcase factory."
3541
+ }),
3542
+ Object.freeze({
3543
+ name: "boundary",
3544
+ description: "The optional server-boundary startup factory."
3545
+ })
3546
+ ]),
3233
3547
  content: `import type { App } from 'vue'
3234
3548
  import type { BrowserApplicationOptions } from './types.js'
3235
3549
  import { createApp } from 'vue'
3236
3550
  import ApplicationView from './ApplicationView.vue'
3237
3551
  {{nameImport}}
3238
3552
  import { parseBrowserApplicationOptions } from './parsers.js'
3239
-
3553
+ {{seedImport}}
3240
3554
  /**
3241
3555
  * Create an unmounted Vue application.
3242
3556
  *
@@ -3254,19 +3568,22 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createBrowserApplication(options: BrowserA
3254
3568
  const parsed = parseBrowserApplicationOptions(options)
3255
3569
  return createApp(ApplicationView, { name: parsed.name ?? APP_NAME })
3256
3570
  }
3257
- `
3571
+ {{showcase}}{{boundary}}`
3258
3572
  }),
3259
3573
  appBrowserIndex: Object.freeze({
3260
3574
  id: "appBrowserIndex",
3261
3575
  name: "appBrowserIndex",
3262
3576
  summary: "The browser application barrel.",
3263
3577
  category: "source",
3264
- placeholders: Object.freeze([]),
3578
+ placeholders: Object.freeze([Object.freeze({
3579
+ name: "seeders",
3580
+ description: "The optional showcase seeder barrel row."
3581
+ })]),
3265
3582
  content: `export * from './types.js'
3266
3583
  export * from './constants.js'
3267
3584
  export * from './errors.js'
3268
3585
  export * from './parsers.js'
3269
- export * from './factories.js'
3586
+ {{seeders}}export * from './factories.js'
3270
3587
  `
3271
3588
  }),
3272
3589
  appBrowserMain: Object.freeze({
@@ -3274,10 +3591,27 @@ export * from './factories.js'
3274
3591
  name: "appBrowserMain",
3275
3592
  summary: "The browser executable entry.",
3276
3593
  category: "source",
3594
+ placeholders: Object.freeze([Object.freeze({
3595
+ name: "factory",
3596
+ description: "The factory the shipped entry mounts through."
3597
+ }), Object.freeze({
3598
+ name: "mount",
3599
+ description: "The mounting statement."
3600
+ })]),
3601
+ content: `import { {{factory}} } from './index.js'
3602
+
3603
+ {{mount}}
3604
+ `
3605
+ }),
3606
+ appBrowserShowcase: Object.freeze({
3607
+ id: "appBrowserShowcase",
3608
+ name: "appBrowserShowcase",
3609
+ summary: "The showcase executable entry.",
3610
+ category: "source",
3277
3611
  placeholders: Object.freeze([]),
3278
- content: `import { createBrowserApplication } from './index.js'
3612
+ content: `import { mountShowcaseApplication } from './index.js'
3279
3613
 
3280
- createBrowserApplication().mount('#app')
3614
+ mountShowcaseApplication('#app')
3281
3615
  `
3282
3616
  }),
3283
3617
  appBrowserView: Object.freeze({
@@ -3322,6 +3656,33 @@ defineProps<{ readonly name: string }>()
3322
3656
  <script type="module" src="/main.ts"><\/script>
3323
3657
  </body>
3324
3658
  </html>
3659
+ `
3660
+ }),
3661
+ appBrowserShowcaseHtml: Object.freeze({
3662
+ id: "appBrowserShowcaseHtml",
3663
+ name: "appBrowserShowcaseHtml",
3664
+ summary: "The showcase HTML entry with its development security policy.",
3665
+ category: "source",
3666
+ placeholders: Object.freeze([Object.freeze({
3667
+ name: "name",
3668
+ description: "The application name."
3669
+ })]),
3670
+ content: `<!doctype html>
3671
+ <html lang="en">
3672
+ <head>
3673
+ <meta
3674
+ http-equiv="Content-Security-Policy"
3675
+ content="default-src 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src data:; font-src data:; script-src-attr 'none'"
3676
+ />
3677
+ <meta charset="UTF-8" />
3678
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3679
+ <title>{{name}} showcase</title>
3680
+ </head>
3681
+ <body>
3682
+ <div id="app"></div>
3683
+ <script type="module" src="/showcase.ts"><\/script>
3684
+ </body>
3685
+ </html>
3325
3686
  `
3326
3687
  }),
3327
3688
  appBrowserEnv: Object.freeze({
@@ -3345,8 +3706,20 @@ declare module '*.vue' {
3345
3706
  name: "appServerTypes",
3346
3707
  summary: "The application server contract.",
3347
3708
  category: "source",
3348
- placeholders: Object.freeze([]),
3349
- content: `/** A rejected application server boundary. */
3709
+ placeholders: Object.freeze([Object.freeze({
3710
+ name: "record",
3711
+ description: "The health record, declared here only while the server alone reads it."
3712
+ })]),
3713
+ content: `import type { EmitterErrorHandler, EmitterHooks, EmitterInterface } from '@orkestrel/emitter'
3714
+ import type { IdentifierState } from '@orkestrel/middleware'
3715
+ import type { ConnectionInfo, ServerStatus } from '@orkestrel/server'
3716
+
3717
+ {{record}}/** Per-request application state derived from connection facts. */
3718
+ ${EXPORT_KEYWORD} interface ApplicationState extends IdentifierState {
3719
+ readonly connection: ConnectionInfo
3720
+ }
3721
+
3722
+ /** A rejected application server boundary. */
3350
3723
  ${EXPORT_KEYWORD} type ApplicationServerErrorCode = 'CONFIG' | 'LIFECYCLE'
3351
3724
 
3352
3725
  /** Diagnostic context attached to an application server boundary error. */
@@ -3357,26 +3730,49 @@ ${EXPORT_KEYWORD} interface ApplicationServerErrorContext {
3357
3730
 
3358
3731
  /** Options for creating an application server. */
3359
3732
  ${EXPORT_KEYWORD} interface ApplicationServerOptions {
3360
- readonly host?: string
3361
- readonly port?: number
3362
- readonly timeout?: number
3733
+ readonly server?: {
3734
+ readonly host?: string
3735
+ readonly port?: number
3736
+ readonly timeout?: number
3737
+ }
3363
3738
  }
3364
3739
 
3365
3740
  /** A lifecycle-safe application server. */
3366
3741
  ${EXPORT_KEYWORD} interface ApplicationServerInterface {
3367
3742
  readonly host: string
3368
- readonly port: number
3369
- readonly listening: boolean
3370
- readonly url: string
3743
+ readonly port: number | undefined
3744
+ readonly status: ServerStatus
3745
+ readonly url: string | undefined
3371
3746
  start(signal?: AbortSignal): Promise<void>
3372
3747
  stop(): Promise<void>
3748
+ destroy(): Promise<void>
3749
+ }
3750
+
3751
+ /** Observable process-lifecycle outcomes for an application server runner. */
3752
+ ${EXPORT_KEYWORD} type ApplicationServerRunnerEventMap = {
3753
+ readonly ready: readonly [url: string]
3754
+ readonly fail: readonly [error: unknown]
3373
3755
  }
3374
3756
 
3375
3757
  /** The process lifecycle owner for an application server. */
3376
3758
  ${EXPORT_KEYWORD} interface ApplicationServerRunnerInterface {
3759
+ readonly emitter: EmitterInterface<ApplicationServerRunnerEventMap>
3377
3760
  start(): void
3378
3761
  stop(): Promise<void>
3379
3762
  }
3763
+
3764
+ /**
3765
+ * Options for observing an application server runner.
3766
+ *
3767
+ * @remarks
3768
+ * Initial \`on\` hooks run before the runner's own readiness and failure effects. When no earlier
3769
+ * failure set an exit code, a synchronous fail hook therefore observes \`process.exitCode\` as
3770
+ * \`undefined\` before the default reporter sets it to \`1\`.
3771
+ */
3772
+ ${EXPORT_KEYWORD} interface ApplicationServerRunnerOptions {
3773
+ readonly on?: EmitterHooks<ApplicationServerRunnerEventMap>
3774
+ readonly error?: EmitterErrorHandler
3775
+ }
3380
3776
  `
3381
3777
  }),
3382
3778
  appServerConstants: Object.freeze({
@@ -3387,6 +3783,9 @@ ${EXPORT_KEYWORD} interface ApplicationServerRunnerInterface {
3387
3783
  placeholders: Object.freeze([Object.freeze({
3388
3784
  name: "nameConstant",
3389
3785
  description: "The optional server-only APP_NAME declaration."
3786
+ }), Object.freeze({
3787
+ name: "health",
3788
+ description: "The health route constants, declared here only while the server alone reads them."
3390
3789
  })]),
3391
3790
  content: `{{nameConstant}}/** The fail-closed loopback host default. */
3392
3791
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} DEFAULT_APP_HOST = '127.0.0.1'
@@ -3406,24 +3805,6 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APP_HOST_INPUT_LENGTH = 255
3406
3805
  /** Maximum raw characters inspected at an application numeric boundary. */
3407
3806
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APP_NUMBER_INPUT_LENGTH = 32
3408
3807
 
3409
- /** Maximum simultaneous connections accepted by the generated server. */
3410
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_CONNECTIONS = 16
3411
-
3412
- /** Maximum request headers accepted before Node rejects the request. */
3413
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_HEADERS = 100
3414
-
3415
- /** Maximum milliseconds allowed to receive complete request headers. */
3416
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEADERS_TIMEOUT = 10_000
3417
-
3418
- /** Maximum milliseconds allowed for one complete request. */
3419
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_REQUEST_TIMEOUT = 30_000
3420
-
3421
- /** Idle keep-alive milliseconds before a connection is closed. */
3422
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_KEEP_ALIVE_TIMEOUT = 5_000
3423
-
3424
- /** Maximum requests served through one keep-alive connection. */
3425
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_REQUESTS_PER_SOCKET = 100
3426
-
3427
3808
  /** The decimal-only syntax accepted at the APP_PORT string boundary. */
3428
3809
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_PORT_PATTERN = /^\\d+$/
3429
3810
 
@@ -3432,13 +3813,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HOST_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Z
3432
3813
 
3433
3814
  /** Numeric-looking non-IP hosts rejected before platform DNS interpretation. */
3434
3815
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NUMERIC_HOST_PATTERN = /^[0-9.]+$/
3435
-
3436
- /** The only HTTP method owned by the application health route. */
3437
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_METHOD = 'GET'
3438
-
3439
- /** The only HTTP path owned by the generated application server. */
3440
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/'
3441
- `
3816
+ {{health}}`
3442
3817
  }),
3443
3818
  appServerErrors: Object.freeze({
3444
3819
  id: "appServerErrors",
@@ -3447,6 +3822,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/'
3447
3822
  category: "source",
3448
3823
  placeholders: Object.freeze([]),
3449
3824
  content: `import type { ApplicationServerErrorCode, ApplicationServerErrorContext } from './types.js'
3825
+ import { holds } from '@orkestrel/contract'
3450
3826
 
3451
3827
  /** A rejected application server configuration or lifecycle operation. */
3452
3828
  ${EXPORT_KEYWORD} class ApplicationServerError extends Error {
@@ -3479,11 +3855,7 @@ ${EXPORT_KEYWORD} class ApplicationServerError extends Error {
3479
3855
  * \`\`\`
3480
3856
  */
3481
3857
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationServerError(value: unknown): value is ApplicationServerError {
3482
- try {
3483
- return value instanceof ApplicationServerError
3484
- } catch {
3485
- return false
3486
- }
3858
+ return holds(() => value instanceof ApplicationServerError)
3487
3859
  }
3488
3860
  `
3489
3861
  }),
@@ -3494,6 +3866,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationServerError(value: unknown):
3494
3866
  category: "source",
3495
3867
  placeholders: Object.freeze([]),
3496
3868
  content: `import type { ApplicationServerOptions } from './types.js'
3869
+ import { isNonEmptyString, parseString } from '@orkestrel/contract'
3497
3870
  import { isIP } from 'node:net'
3498
3871
  import {
3499
3872
  APP_HOST_LABEL_PATTERN,
@@ -3523,7 +3896,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationHost(value: unknown): stri
3523
3896
  const host = value.trim()
3524
3897
  const family = isIP(host)
3525
3898
  if (
3526
- host.length === 0 ||
3899
+ !isNonEmptyString(host) ||
3527
3900
  host.length > 253 ||
3528
3901
  (family === 0 &&
3529
3902
  (APP_NUMERIC_HOST_PATTERN.test(host) ||
@@ -3545,18 +3918,14 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationHost(value: unknown): stri
3545
3918
  */
3546
3919
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationPort(value: unknown): number {
3547
3920
  if (value === undefined) return DEFAULT_APP_PORT
3548
- if (typeof value !== 'string' && typeof value !== 'number') {
3549
- throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
3550
- value,
3551
- })
3552
- }
3553
- if (typeof value === 'string' && value.length > MAX_APP_NUMBER_INPUT_LENGTH) {
3921
+ const text = parseString(value)
3922
+ if (text === undefined || text.length > MAX_APP_NUMBER_INPUT_LENGTH) {
3554
3923
  throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
3555
3924
  value,
3556
3925
  })
3557
3926
  }
3558
- const text = typeof value === 'string' ? value.trim() : String(value)
3559
- const port = APP_PORT_PATTERN.test(text) ? Number(text) : Number.NaN
3927
+ const normalized = text.trim()
3928
+ const port = APP_PORT_PATTERN.test(normalized) ? Number(normalized) : Number.NaN
3560
3929
  if (!Number.isInteger(port) || port < 0 || port > 65_535) {
3561
3930
  throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
3562
3931
  value,
@@ -3574,22 +3943,16 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationPort(value: unknown): numb
3574
3943
  */
3575
3944
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationStartTimeout(value: unknown): number {
3576
3945
  if (value === undefined) return DEFAULT_APP_START_TIMEOUT
3577
- if (typeof value !== 'string' && typeof value !== 'number') {
3578
- throw new ApplicationServerError(
3579
- 'CONFIG',
3580
- \`APP_START_TIMEOUT must be an integer from 1 through \${MAX_APP_START_TIMEOUT}\`,
3581
- { value },
3582
- )
3583
- }
3584
- if (typeof value === 'string' && value.length > MAX_APP_NUMBER_INPUT_LENGTH) {
3946
+ const text = parseString(value)
3947
+ if (text === undefined || text.length > MAX_APP_NUMBER_INPUT_LENGTH) {
3585
3948
  throw new ApplicationServerError(
3586
3949
  'CONFIG',
3587
3950
  \`APP_START_TIMEOUT must be an integer from 1 through \${MAX_APP_START_TIMEOUT}\`,
3588
3951
  { value },
3589
3952
  )
3590
3953
  }
3591
- const text = typeof value === 'string' ? value.trim() : String(value)
3592
- const timeout = APP_PORT_PATTERN.test(text) ? Number(text) : Number.NaN
3954
+ const normalized = text.trim()
3955
+ const timeout = APP_PORT_PATTERN.test(normalized) ? Number(normalized) : Number.NaN
3593
3956
  if (!Number.isInteger(timeout) || timeout < 1 || timeout > MAX_APP_START_TIMEOUT) {
3594
3957
  throw new ApplicationServerError(
3595
3958
  'CONFIG',
@@ -3625,24 +3988,58 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
3625
3988
  )
3626
3989
  }
3627
3990
  const keys = Reflect.ownKeys(value)
3628
- const unknown = keys.filter((key) => key !== 'host' && key !== 'port' && key !== 'timeout')
3991
+ const unknown = keys.filter((key) => key !== 'server')
3629
3992
  if (unknown.length > 0) {
3630
3993
  throw new ApplicationServerError('CONFIG', 'Unknown application server option', { value })
3631
3994
  }
3632
- const hostDescriptor = Reflect.getOwnPropertyDescriptor(value, 'host')
3633
- const portDescriptor = Reflect.getOwnPropertyDescriptor(value, 'port')
3634
- const timeoutDescriptor = Reflect.getOwnPropertyDescriptor(value, 'timeout')
3995
+ const serverDescriptor = Reflect.getOwnPropertyDescriptor(value, 'server')
3996
+ if (
3997
+ keys.includes('server') &&
3998
+ (serverDescriptor === undefined || !Reflect.has(serverDescriptor, 'value'))
3999
+ ) {
4000
+ throw new ApplicationServerError(
4001
+ 'CONFIG',
4002
+ 'Application server options must use data properties',
4003
+ { value },
4004
+ )
4005
+ }
4006
+ const server = serverDescriptor?.value
4007
+ if (server === undefined) return {}
4008
+ if (typeof server !== 'object' || server === null || Array.isArray(server)) {
4009
+ throw new ApplicationServerError('CONFIG', 'Application server settings must be an object', {
4010
+ value,
4011
+ })
4012
+ }
4013
+ const serverPrototype = Reflect.getPrototypeOf(server)
4014
+ if (serverPrototype !== Object.prototype && serverPrototype !== null) {
4015
+ throw new ApplicationServerError(
4016
+ 'CONFIG',
4017
+ 'Application server settings must be a plain record',
4018
+ { value },
4019
+ )
4020
+ }
4021
+ // Walk own descriptors directly: reading values through a getter would run caller code.
4022
+ const serverKeys = Reflect.ownKeys(server)
4023
+ const serverUnknown = serverKeys.filter(
4024
+ (key) => key !== 'host' && key !== 'port' && key !== 'timeout',
4025
+ )
4026
+ if (serverUnknown.length > 0) {
4027
+ throw new ApplicationServerError('CONFIG', 'Unknown application server setting', { value })
4028
+ }
4029
+ const hostDescriptor = Reflect.getOwnPropertyDescriptor(server, 'host')
4030
+ const portDescriptor = Reflect.getOwnPropertyDescriptor(server, 'port')
4031
+ const timeoutDescriptor = Reflect.getOwnPropertyDescriptor(server, 'timeout')
3635
4032
  if (
3636
- (keys.includes('host') &&
4033
+ (serverKeys.includes('host') &&
3637
4034
  (hostDescriptor === undefined || !Reflect.has(hostDescriptor, 'value'))) ||
3638
- (keys.includes('port') &&
4035
+ (serverKeys.includes('port') &&
3639
4036
  (portDescriptor === undefined || !Reflect.has(portDescriptor, 'value'))) ||
3640
- (keys.includes('timeout') &&
4037
+ (serverKeys.includes('timeout') &&
3641
4038
  (timeoutDescriptor === undefined || !Reflect.has(timeoutDescriptor, 'value')))
3642
4039
  ) {
3643
4040
  throw new ApplicationServerError(
3644
4041
  'CONFIG',
3645
- 'Application server options must use data properties',
4042
+ 'Application server settings must use data properties',
3646
4043
  { value },
3647
4044
  )
3648
4045
  }
@@ -3650,9 +4047,11 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
3650
4047
  const port = portDescriptor?.value
3651
4048
  const timeout = timeoutDescriptor?.value
3652
4049
  return {
3653
- ...(host === undefined ? {} : { host: parseApplicationHost(host) }),
3654
- ...(port === undefined ? {} : { port: parseApplicationPort(port) }),
3655
- ...(timeout === undefined ? {} : { timeout: parseApplicationStartTimeout(timeout) }),
4050
+ server: {
4051
+ ...(host === undefined ? {} : { host: parseApplicationHost(host) }),
4052
+ ...(port === undefined ? {} : { port: parseApplicationPort(port) }),
4053
+ ...(timeout === undefined ? {} : { timeout: parseApplicationStartTimeout(timeout) }),
4054
+ },
3656
4055
  }
3657
4056
  } catch (error) {
3658
4057
  if (isApplicationServerError(error)) throw error
@@ -3661,47 +4060,56 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
3661
4060
  })
3662
4061
  }
3663
4062
  }
4063
+ `
4064
+ }),
4065
+ appServerRoutes: Object.freeze({
4066
+ id: "appServerRoutes",
4067
+ name: "appServerRoutes",
4068
+ summary: "The standalone application route dispatcher factory.",
4069
+ category: "source",
4070
+ placeholders: Object.freeze([Object.freeze({
4071
+ name: "healthImport",
4072
+ description: "The selected layer import for the health route constants."
4073
+ })]),
4074
+ content: `import type { DispatcherInterface } from '@orkestrel/router'
4075
+ import type { ApplicationState } from './types.js'
4076
+ import { createDispatcher } from '@orkestrel/router'
4077
+ {{healthImport}}
4078
+ import { handleApplicationHealth } from './handlers.js'
4079
+
4080
+ /** Create a fresh dispatcher for the generated application's fetch-standard health boundary. */
4081
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationDispatcher(): DispatcherInterface<ApplicationState> {
4082
+ return createDispatcher<ApplicationState>({
4083
+ routes: [
4084
+ {
4085
+ method: APP_HEALTH_METHOD,
4086
+ path: APP_HEALTH_PATH,
4087
+ handler: handleApplicationHealth,
4088
+ },
4089
+ ],
4090
+ })
4091
+ }
3664
4092
  `
3665
4093
  }),
3666
4094
  appServerHandlers: Object.freeze({
3667
4095
  id: "appServerHandlers",
3668
4096
  name: "appServerHandlers",
3669
- summary: "The server HTTP request handler.",
4097
+ summary: "The application server diagnostic reporter.",
3670
4098
  category: "source",
3671
4099
  placeholders: Object.freeze([Object.freeze({
4100
+ name: "recordImport",
4101
+ description: "The selected layer type import for the health record."
4102
+ }), Object.freeze({
3672
4103
  name: "nameImport",
3673
4104
  description: "The selected layer import for APP_NAME."
3674
4105
  })]),
3675
- content: `import type { IncomingMessage, ServerResponse } from 'node:http'
4106
+ content: `{{recordImport}}
3676
4107
  {{nameImport}}
3677
- import { APP_HEALTH_METHOD, APP_HEALTH_PATH } from './constants.js'
3678
4108
  import { isApplicationServerError } from './errors.js'
3679
4109
 
3680
- /**
3681
- * Respond to the application health endpoint and reject every other route.
3682
- *
3683
- * @param request - The incoming Node request.
3684
- * @param response - The Node response to complete exactly once.
3685
- */
3686
- ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} handleApplicationRequest(request: IncomingMessage, response: ServerResponse): void {
3687
- if (request.method !== APP_HEALTH_METHOD) {
3688
- response.writeHead(405, {
3689
- allow: APP_HEALTH_METHOD,
3690
- 'content-type': 'text/plain; charset=utf-8',
3691
- })
3692
- response.end('Method Not Allowed')
3693
- return
3694
- }
3695
- if (request.url !== APP_HEALTH_PATH) {
3696
- response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
3697
- response.end('Not Found')
3698
- return
3699
- }
3700
- response.writeHead(200, {
3701
- 'cache-control': 'no-store',
3702
- 'content-type': 'application/json; charset=utf-8',
3703
- })
3704
- response.end(JSON.stringify({ name: APP_NAME, status: 'ok' }))
4110
+ /** Return the generated application's shared health record. */
4111
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} handleApplicationHealth(): Response {
4112
+ return Response.json({ name: APP_NAME, status: 'ok' } satisfies ApplicationRecord)
3705
4113
  }
3706
4114
 
3707
4115
  /**
@@ -3729,8 +4137,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} reportApplicationServerError(error: unknow
3729
4137
  } catch {
3730
4138
  message = '[ERROR] Application server failed'
3731
4139
  }
3732
- process.stderr.write(\`\${message}\\n\`)
3733
4140
  process.exitCode = 1
4141
+ process.stderr.write(\`\${message}\\n\`)
3734
4142
  }
3735
4143
  `
3736
4144
  }),
@@ -3740,176 +4148,106 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} reportApplicationServerError(error: unknow
3740
4148
  summary: "The application server implementation.",
3741
4149
  category: "source",
3742
4150
  placeholders: Object.freeze([]),
3743
- content: `import type { Server } from 'node:http'
3744
- import type { ApplicationServerInterface, ApplicationServerOptions } from './types.js'
3745
- import { once } from 'node:events'
3746
- import { createServer } from 'node:http'
3747
- import { promisify } from 'node:util'
3748
- import {
3749
- APP_HEADERS_TIMEOUT,
3750
- APP_KEEP_ALIVE_TIMEOUT,
3751
- APP_MAX_CONNECTIONS,
3752
- APP_MAX_HEADERS,
3753
- APP_MAX_REQUESTS_PER_SOCKET,
3754
- APP_REQUEST_TIMEOUT,
3755
- } from './constants.js'
3756
- import { ApplicationServerError, isApplicationServerError } from './errors.js'
3757
- import { handleApplicationRequest } from './handlers.js'
4151
+ content: `import type { DispatcherInterface } from '@orkestrel/router'
4152
+ import type { ConnectionInfo, ServerInterface } from '@orkestrel/server'
4153
+ import type {
4154
+ ApplicationServerInterface,
4155
+ ApplicationServerOptions,
4156
+ ApplicationState,
4157
+ } from './types.js'
4158
+ import { createBoundary, createDeadline, createSecurity } from '@orkestrel/middleware'
4159
+ import { createServer } from '@orkestrel/server'
4160
+ import { ApplicationServerError } from './errors.js'
3758
4161
  import {
3759
4162
  parseApplicationHost,
3760
4163
  parseApplicationPort,
3761
4164
  parseApplicationServerOptions,
3762
4165
  parseApplicationStartTimeout,
3763
4166
  } from './parsers.js'
4167
+ import { createApplicationDispatcher } from './routes.js'
3764
4168
 
3765
- /** A repeat-safe Node HTTP application server. */
4169
+ /** A repeat-safe application server composed from the installed server substrate. */
3766
4170
  ${EXPORT_KEYWORD} class ApplicationServer implements ApplicationServerInterface {
3767
4171
  readonly host: string
3768
- readonly #requestedPort: number
3769
- readonly #timeout: number
3770
- #port: number
3771
- readonly #server: Server
3772
- #transition: Promise<void> = Promise.resolve()
3773
- readonly #starts = new Set<AbortController>()
3774
- readonly #stopped = new WeakSet<AbortController>()
4172
+ readonly #dispatcher: DispatcherInterface<ApplicationState>
4173
+ readonly #server: ServerInterface<ApplicationState>
3775
4174
 
3776
4175
  constructor(options: ApplicationServerOptions = {}) {
3777
4176
  const parsed = parseApplicationServerOptions(options)
3778
- this.host = parseApplicationHost(parsed.host === undefined ? process.env.APP_HOST : parsed.host)
3779
- this.#requestedPort = parseApplicationPort(
3780
- parsed.port === undefined ? process.env.APP_PORT : parsed.port,
4177
+ const server = parsed.server
4178
+ const host = parseApplicationHost(
4179
+ server?.host === undefined ? process.env.APP_HOST : server.host,
3781
4180
  )
3782
- this.#port = this.#requestedPort
3783
- this.#timeout = parseApplicationStartTimeout(
3784
- parsed.timeout === undefined ? process.env.APP_START_TIMEOUT : parsed.timeout,
4181
+ const port = parseApplicationPort(
4182
+ server?.port === undefined ? process.env.APP_PORT : server.port,
3785
4183
  )
3786
- this.#server = createServer(handleApplicationRequest)
3787
- this.#server.maxConnections = APP_MAX_CONNECTIONS
3788
- this.#server.maxHeadersCount = APP_MAX_HEADERS
3789
- this.#server.headersTimeout = APP_HEADERS_TIMEOUT
3790
- this.#server.requestTimeout = APP_REQUEST_TIMEOUT
3791
- this.#server.keepAliveTimeout = APP_KEEP_ALIVE_TIMEOUT
3792
- this.#server.maxRequestsPerSocket = APP_MAX_REQUESTS_PER_SOCKET
3793
- }
3794
-
3795
- get port(): number {
3796
- return this.#port
3797
- }
3798
-
3799
- get listening(): boolean {
3800
- return this.#server.listening
4184
+ const timeout = parseApplicationStartTimeout(
4185
+ server?.timeout === undefined ? process.env.APP_START_TIMEOUT : server.timeout,
4186
+ )
4187
+ this.host = host
4188
+ this.#dispatcher = createApplicationDispatcher()
4189
+ this.#server = createServer<ApplicationState>({
4190
+ dispatcher: this.#dispatcher,
4191
+ state: ApplicationServer.#state,
4192
+ middleware: [
4193
+ createBoundary<ApplicationState>(),
4194
+ createSecurity<ApplicationState>(),
4195
+ createDeadline<ApplicationState>({ ms: timeout }),
4196
+ ],
4197
+ host,
4198
+ port,
4199
+ timeouts: { start: timeout },
4200
+ })
3801
4201
  }
3802
4202
 
3803
- get url(): string {
3804
- const hostname = this.host.includes(':') ? \`[\${this.host}]\` : this.host
3805
- return \`http://\${hostname}:\${this.port}\`
4203
+ static #state(connection: ConnectionInfo): ApplicationState {
4204
+ return { connection }
3806
4205
  }
3807
4206
 
3808
- start(signal?: AbortSignal): Promise<void> {
3809
- const controller = new AbortController()
3810
- this.#starts.add(controller)
3811
- const queued = this.#queue(this.#start.bind(this, controller, signal))
3812
- void queued.then(this.#settle.bind(this, controller), this.#settle.bind(this, controller))
3813
- return queued
4207
+ get port(): number | undefined {
4208
+ return this.#server.port
3814
4209
  }
3815
4210
 
3816
- stop(): Promise<void> {
3817
- for (const controller of this.#starts) {
3818
- this.#stopped.add(controller)
3819
- controller.abort()
3820
- }
3821
- return this.#queue(this.#stop.bind(this))
4211
+ get status(): ApplicationServerInterface['status'] {
4212
+ return this.#server.status
3822
4213
  }
3823
4214
 
3824
- #queue(operation: () => Promise<void>): Promise<void> {
3825
- const queued = this.#transition.then(operation, operation)
3826
- this.#transition = queued.then(
3827
- () => undefined,
3828
- () => undefined,
3829
- )
3830
- return queued
4215
+ get url(): string | undefined {
4216
+ const port = this.port
4217
+ if (port === undefined) return undefined
4218
+ const hostname = this.host.includes(':') ? \`[\${this.host}]\` : this.host
4219
+ return \`http://\${hostname}:\${port}\`
3831
4220
  }
3832
4221
 
3833
- async #start(controller: AbortController, signal?: AbortSignal): Promise<void> {
3834
- if (this.listening) return
3835
- if (controller.signal.aborted) {
3836
- if (this.#stopped.has(controller)) return
3837
- throw new ApplicationServerError('LIFECYCLE', 'Application server startup was cancelled')
3838
- }
4222
+ async start(signal?: AbortSignal): Promise<void> {
3839
4223
  try {
3840
- if (signal?.aborted === true) {
3841
- throw new ApplicationServerError('LIFECYCLE', 'Application server startup was cancelled', {
3842
- cause: signal.reason,
3843
- })
3844
- }
3845
- await this.#listen(controller, signal)
4224
+ await this.#server.start(signal)
3846
4225
  } catch (error) {
3847
- if (this.#stopped.has(controller)) return
3848
- if (isApplicationServerError(error)) throw error
3849
- throw new ApplicationServerError('LIFECYCLE', 'Failed to inspect startup signal', {
4226
+ throw new ApplicationServerError('LIFECYCLE', 'Failed to start application server', {
3850
4227
  cause: error,
3851
4228
  })
3852
4229
  }
3853
4230
  }
3854
4231
 
3855
- async #stop(): Promise<void> {
3856
- if (!this.listening) return
3857
- await this.#close()
3858
- }
3859
-
3860
- async #listen(controller: AbortController, signal?: AbortSignal): Promise<void> {
3861
- const relay = signal === undefined ? undefined : this.#abort.bind(this, controller, signal)
3862
- const timer = setTimeout(this.#expire.bind(this, controller), this.#timeout)
4232
+ async stop(): Promise<void> {
3863
4233
  try {
3864
- if (signal !== undefined && relay !== undefined) {
3865
- signal.addEventListener('abort', relay, { once: true })
3866
- }
3867
- this.#server.listen({
3868
- port: this.#requestedPort,
3869
- host: this.host,
3870
- signal: controller.signal,
3871
- })
3872
- await once(this.#server, 'listening', { signal: controller.signal })
4234
+ await this.#server.stop()
3873
4235
  } catch (error) {
3874
- throw new ApplicationServerError('LIFECYCLE', 'Failed to start application server', {
4236
+ throw new ApplicationServerError('LIFECYCLE', 'Failed to stop application server', {
3875
4237
  cause: error,
3876
4238
  })
3877
- } finally {
3878
- clearTimeout(timer)
3879
- if (relay !== undefined && signal !== undefined) {
3880
- signal.removeEventListener('abort', relay)
3881
- }
3882
4239
  }
3883
- const address = this.#server.address()
3884
- if (address === null || typeof address === 'string') {
3885
- await this.#close()
3886
- throw new ApplicationServerError('LIFECYCLE', 'Server did not expose a TCP address')
3887
- }
3888
- this.#port = address.port
3889
- }
3890
-
3891
- #abort(controller: AbortController, signal: AbortSignal): void {
3892
- controller.abort(signal.reason)
3893
- }
3894
-
3895
- #expire(controller: AbortController): void {
3896
- controller.abort(new Error(\`Application server startup exceeded \${this.#timeout} milliseconds\`))
3897
- }
3898
-
3899
- #settle(controller: AbortController): void {
3900
- this.#starts.delete(controller)
3901
4240
  }
3902
4241
 
3903
- async #close(): Promise<void> {
4242
+ async destroy(): Promise<void> {
3904
4243
  try {
3905
- const closed = promisify(this.#server.close.bind(this.#server))()
3906
- this.#server.closeIdleConnections()
3907
- this.#server.closeAllConnections()
3908
- await closed
4244
+ await this.#server.destroy()
3909
4245
  } catch (error) {
3910
- throw new ApplicationServerError('LIFECYCLE', 'Failed to stop application server', {
4246
+ throw new ApplicationServerError('LIFECYCLE', 'Failed to destroy application server', {
3911
4247
  cause: error,
3912
4248
  })
4249
+ } finally {
4250
+ this.#dispatcher.destroy()
3913
4251
  }
3914
4252
  }
3915
4253
  }
@@ -3932,16 +4270,17 @@ import { ApplicationServerRunner } from './ApplicationServerRunner.js'
3932
4270
  /**
3933
4271
  * Create a stopped application server.
3934
4272
  *
3935
- * @param options - Optional host and port overrides.
4273
+ * @param options - Optional grouped server overrides.
3936
4274
  * @returns A lifecycle-safe application server.
3937
4275
  *
3938
4276
  * @example
3939
4277
  * \`\`\`ts
3940
4278
  * import { createApplicationServer } from '@app/server'
3941
4279
  *
3942
- * ${CONST_KEYWORD} server = createApplicationServer({ port: 0 })
4280
+ * ${CONST_KEYWORD} server = createApplicationServer({ server: { port: 0 } })
3943
4281
  * await server.start()
3944
4282
  * await server.stop()
4283
+ * await server.destroy()
3945
4284
  * \`\`\`
3946
4285
  */
3947
4286
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationServer(
@@ -3953,21 +4292,21 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationServer(
3953
4292
  /**
3954
4293
  * Start a process-owned application server.
3955
4294
  *
3956
- * @param options - Optional host and port overrides.
4295
+ * @param options - Optional grouped server overrides.
3957
4296
  * @returns The runner that owns signals and provides explicit asynchronous cleanup.
3958
4297
  *
3959
4298
  * @example
3960
4299
  * \`\`\`ts
3961
4300
  * import { startApplicationServer } from '@app/server'
3962
4301
  *
3963
- * ${CONST_KEYWORD} runner = startApplicationServer({ port: 0 })
4302
+ * ${CONST_KEYWORD} runner = startApplicationServer({ server: { port: 0 } })
3964
4303
  * await runner.stop()
3965
4304
  * \`\`\`
3966
4305
  */
3967
4306
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationServer(
3968
4307
  options: ApplicationServerOptions = {},
3969
4308
  ): ApplicationServerRunnerInterface {
3970
- const runner = new ApplicationServerRunner(options)
4309
+ const runner = new ApplicationServerRunner(new ApplicationServer(options))
3971
4310
  runner.start()
3972
4311
  return runner
3973
4312
  }
@@ -3978,57 +4317,144 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationServer(
3978
4317
  name: "appServerRunner",
3979
4318
  summary: "The application server process lifecycle owner.",
3980
4319
  category: "source",
3981
- placeholders: Object.freeze([]),
3982
- content: `import type {
4320
+ placeholders: Object.freeze([Object.freeze({
4321
+ name: "nameImport",
4322
+ description: "The selected layer import for APP_NAME."
4323
+ })]),
4324
+ content: `import type { EmitterInterface } from '@orkestrel/emitter'
4325
+ import type {
3983
4326
  ApplicationServerInterface,
3984
- ApplicationServerOptions,
4327
+ ApplicationServerRunnerEventMap,
3985
4328
  ApplicationServerRunnerInterface,
4329
+ ApplicationServerRunnerOptions,
3986
4330
  } from './types.js'
3987
- import { ApplicationServer } from './ApplicationServer.js'
4331
+ import { Emitter } from '@orkestrel/emitter'
4332
+ {{nameImport}}
4333
+ import { ApplicationServerError } from './errors.js'
3988
4334
  import { reportApplicationServerError } from './handlers.js'
3989
4335
 
3990
4336
  /** Own process signals and startup failure handling for one application server. */
3991
4337
  ${EXPORT_KEYWORD} class ApplicationServerRunner implements ApplicationServerRunnerInterface {
4338
+ readonly #emitter: Emitter<ApplicationServerRunnerEventMap>
3992
4339
  readonly #server: ApplicationServerInterface
3993
- readonly #signal: () => void
4340
+ readonly #handler: () => void
4341
+ #controller: AbortController | undefined
4342
+ #queue: Promise<void> = Promise.resolve()
4343
+ #stopping: Promise<void> | undefined
4344
+ #announcement: number | undefined
3994
4345
  #generation = 0
3995
4346
  #started = false
3996
4347
 
3997
- constructor(options: ApplicationServerOptions = {}) {
3998
- this.#server = new ApplicationServer(options)
3999
- this.#signal = this.#shutdown.bind(this)
4348
+ constructor(server: ApplicationServerInterface, options: ApplicationServerRunnerOptions = {}) {
4349
+ this.#emitter = new Emitter({
4350
+ ...(options.on === undefined ? {} : { on: options.on }),
4351
+ ...(options.error === undefined ? {} : { error: options.error }),
4352
+ })
4353
+ this.#server = server
4354
+ this.#handler = this.#shutdown.bind(this)
4355
+ this.#emitter.on('ready', this.#announce.bind(this))
4356
+ this.#emitter.on('fail', reportApplicationServerError)
4357
+ }
4358
+
4359
+ get emitter(): EmitterInterface<ApplicationServerRunnerEventMap> {
4360
+ return this.#emitter
4000
4361
  }
4001
4362
 
4002
4363
  start(): void {
4003
4364
  if (this.#started) return
4004
4365
  this.#started = true
4366
+ this.#stopping = undefined
4005
4367
  const generation = ++this.#generation
4006
- process.once('SIGINT', this.#signal)
4007
- process.once('SIGTERM', this.#signal)
4008
- void this.#server.start().catch(this.#fail.bind(this, generation))
4368
+ const controller = new AbortController()
4369
+ this.#controller = controller
4370
+ process.once('SIGINT', this.#handler)
4371
+ process.once('SIGTERM', this.#handler)
4372
+ this.#queue = this.#queue.then(this.#begin.bind(this, generation, controller))
4009
4373
  }
4010
4374
 
4011
4375
  stop(): Promise<void> {
4012
- this.#generation += 1
4376
+ if (this.#stopping !== undefined) return this.#stopping
4377
+ // Aborting first lets an in-flight substrate startup settle before stop inspects its state,
4378
+ // and the generation bump is what discards that abort's rejection while leaving a genuine
4379
+ // stop failure reportable. The shared queue then keeps a subsequent restart behind this complete
4380
+ // shutdown; concurrent callers join one substrate stop through #stopping, and the trailing catch
4381
+ // discards nothing — it only keeps one failed stop from wedging every later one.
4382
+ ++this.#generation
4383
+ this.#controller?.abort()
4013
4384
  this.#release()
4014
- return this.#server.stop()
4385
+ const stopping = this.#queue.then(() => this.#server.stop())
4386
+ this.#stopping = stopping
4387
+ this.#queue = stopping.catch(() => undefined)
4388
+ void stopping.then(this.#finishStop.bind(this, stopping), this.#rejectStop.bind(this, stopping))
4389
+ return stopping
4390
+ }
4391
+
4392
+ async #begin(generation: number, controller: AbortController): Promise<void> {
4393
+ if (generation !== this.#generation || !this.#started) return
4394
+ try {
4395
+ await this.#server.start(controller.signal)
4396
+ await this.#ready(generation)
4397
+ } catch (error) {
4398
+ this.#fail(generation, error)
4399
+ }
4015
4400
  }
4016
4401
 
4017
4402
  #fail(generation: number, error: unknown): void {
4018
4403
  if (generation !== this.#generation) return
4019
4404
  this.#release()
4020
- reportApplicationServerError(error)
4405
+ this.#emitter.emit('fail', error)
4406
+ }
4407
+
4408
+ #finishStop(stopping: Promise<void>): void {
4409
+ if (this.#stopping === stopping) this.#stopping = undefined
4410
+ }
4411
+
4412
+ #rejectStop(stopping: Promise<void>, error: unknown): void {
4413
+ this.#finishStop(stopping)
4414
+ this.#emitter.emit('fail', error)
4415
+ }
4416
+
4417
+ async #ready(generation: number): Promise<void> {
4418
+ if (generation !== this.#generation || !this.#started) return
4419
+ const url = this.#server.url
4420
+ if (url === undefined) {
4421
+ const failure = new ApplicationServerError(
4422
+ 'LIFECYCLE',
4423
+ 'Application server did not expose a URL after successful startup',
4424
+ )
4425
+ try {
4426
+ await this.#server.stop()
4427
+ } catch (error) {
4428
+ this.#fail(generation, error)
4429
+ return
4430
+ }
4431
+ this.#fail(generation, failure)
4432
+ return
4433
+ }
4434
+ this.#announcement = generation
4435
+ this.#emitter.emit('ready', url)
4436
+ this.#announcement = undefined
4437
+ }
4438
+
4439
+ #announce(url: string): void {
4440
+ if (this.#announcement !== this.#generation) return
4441
+ try {
4442
+ process.stderr.write(\`[READY] \${APP_NAME} \${url}\\n\`)
4443
+ } catch (error) {
4444
+ const stopped = this.stop()
4445
+ const generation = this.#generation
4446
+ void stopped.then(this.#fail.bind(this, generation, error), () => undefined)
4447
+ }
4021
4448
  }
4022
4449
 
4023
4450
  #shutdown(): void {
4024
4451
  const stopped = this.stop()
4025
- const generation = this.#generation
4026
- void stopped.catch(this.#fail.bind(this, generation))
4452
+ void stopped.catch(() => undefined)
4027
4453
  }
4028
4454
 
4029
4455
  #release(): void {
4030
- process.off('SIGINT', this.#signal)
4031
- process.off('SIGTERM', this.#signal)
4456
+ process.off('SIGINT', this.#handler)
4457
+ process.off('SIGTERM', this.#handler)
4032
4458
  this.#started = false
4033
4459
  }
4034
4460
  }
@@ -4044,6 +4470,7 @@ ${EXPORT_KEYWORD} class ApplicationServerRunner implements ApplicationServerRunn
4044
4470
  export * from './constants.js'
4045
4471
  export * from './errors.js'
4046
4472
  export * from './parsers.js'
4473
+ export * from './routes.js'
4047
4474
  export * from './handlers.js'
4048
4475
  export * from './ApplicationServer.js'
4049
4476
  export * from './ApplicationServerRunner.js'
@@ -4068,10 +4495,16 @@ try {
4068
4495
  setup: Object.freeze({
4069
4496
  id: "setup",
4070
4497
  name: "setup",
4071
- summary: "The generated-minimal `tests/setup.ts` recorder helper — no placeholders.",
4498
+ summary: "The generated-minimal `tests/setup.ts` shared test helpers.",
4072
4499
  category: "tests",
4073
- placeholders: Object.freeze([]),
4074
- content: `// ── Call recorder (a real callback, not a mock) ──────────────────────────────
4500
+ placeholders: Object.freeze([Object.freeze({
4501
+ name: "eventImport",
4502
+ description: "The optional emitter types used by application server tests."
4503
+ }), Object.freeze({
4504
+ name: "eventHelper",
4505
+ description: "The optional typed event waiter used by application server tests."
4506
+ })]),
4507
+ content: `{{eventImport}}// ── Call recorder (a real callback, not a mock) ──────────────────────────────
4075
4508
  //
4076
4509
  // The test rules require a recording callback when a test only needs to count calls or inspect arguments:
4077
4510
  // recorder — a real listener that records every invocation — rather than a test-
@@ -4111,12 +4544,145 @@ ${EXPORT_KEYWORD} function createRecorder<TArgs extends readonly unknown[]>(): T
4111
4544
  },
4112
4545
  }
4113
4546
  }
4114
-
4547
+ {{eventHelper}}
4115
4548
  /** Whether a repository-relative Vue SFC belongs to the private browser application. */
4116
4549
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserVuePath(path: string): boolean {
4117
4550
  const normalized = path.replaceAll('\\\\', '/')
4118
4551
  return normalized.startsWith('app/browser/')
4119
4552
  }
4553
+ `
4554
+ }),
4555
+ configTest: Object.freeze({
4556
+ id: "configTest",
4557
+ name: "configTest",
4558
+ summary: "The generated root Vite configuration behavior test.",
4559
+ category: "tests",
4560
+ placeholders: Object.freeze([Object.freeze({
4561
+ name: "imports",
4562
+ description: "The machinery-aware root configuration imports."
4563
+ }), Object.freeze({
4564
+ name: "cases",
4565
+ description: "The machinery-aware root configuration cases."
4566
+ })]),
4567
+ content: `{{imports}}
4568
+
4569
+ describe('root Vite configuration', () => {
4570
+ it('keeps workspace paths physically contained', () => {
4571
+ const root = resolveWorkspacePath('.')
4572
+ const source = resolveWorkspacePath('src')
4573
+ const parent = resolveWorkspacePath('..')
4574
+
4575
+ expect(workspacePath(root)).toBe('')
4576
+ expect(workspacePath(source)).toBe('src')
4577
+ expect(workspacePath(parent)).toBeUndefined()
4578
+ expect(containedPath(root, source)).toBe(true)
4579
+ expect(containedPath(root, parent)).toBe(false)
4580
+ })
4581
+
4582
+ it('enforces environment direction for paths and module sources', () => {
4583
+ expect(environmentPathError('src/core', 'app/core/index.ts')).toBe(
4584
+ 'Published modules cannot depend on private application modules',
4585
+ )
4586
+ expect(environmentPathError('app/core', 'src/browser/index.ts')).toBe(
4587
+ 'Core modules must remain host-independent',
4588
+ )
4589
+ expect(environmentPathError('app/browser', 'src/server/index.ts')).toBe(
4590
+ 'Browser modules cannot depend on Node or server-only modules',
4591
+ )
4592
+ expect(environmentPathError('app/server', 'src/browser/index.ts')).toBe(
4593
+ 'Server modules cannot depend on Vue or browser-only modules',
4594
+ )
4595
+ expect(environmentPathError('app/browser', 'src/core/index.ts')).toBeUndefined()
4596
+ expect(environmentSourceError('src/core', 'node:path')).toBe(
4597
+ 'Core modules must remain host-independent',
4598
+ )
4599
+ expect(environmentSourceError('app/server', 'vue')).toBe(
4600
+ 'Server modules cannot depend on Vue or browser-only modules',
4601
+ )
4602
+ expect(environmentSourceError('app/browser', '@app/core')).toBeUndefined()
4603
+ }){{cases}}
4604
+ })
4605
+ `
4606
+ }),
4607
+ serviceConformance: Object.freeze({
4608
+ id: "serviceConformance",
4609
+ name: "serviceConformance",
4610
+ summary: "The generated service-vendor structure and configuration conformance test.",
4611
+ category: "tests",
4612
+ placeholders: Object.freeze([Object.freeze({
4613
+ name: "services",
4614
+ description: "The sorted service vendor directory names."
4615
+ })]),
4616
+ content: `import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
4617
+ import { join } from 'node:path'
4618
+ import { isRecord, parseJSON } from '@orkestrel/contract'
4619
+ import { expect, it } from 'vitest'
4620
+
4621
+ it('keeps every service vendor structurally complete and exactly configured', () => {
4622
+ const declared = {{services}}
4623
+ const root = join(process.cwd(), 'tests', 'service')
4624
+ const directories = readdirSync(root, { withFileTypes: true })
4625
+ .filter((entry) => entry.isDirectory())
4626
+ .map((entry) => entry.name)
4627
+ .sort()
4628
+ const manifest = parseJSON(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
4629
+ const scripts = isRecord(manifest) && isRecord(manifest.scripts) ? manifest.scripts : {}
4630
+ const config = readFileSync(join(process.cwd(), 'vite.config.ts'), 'utf8')
4631
+ const provisioner = join(process.cwd(), 'scripts', 'service.sh')
4632
+
4633
+ expect(directories).toEqual(declared)
4634
+ if (!existsSync(provisioner) || !lstatSync(provisioner).isFile()) {
4635
+ throw new Error(
4636
+ 'Service vendors require scripts/service.sh to provision every declared vendor idempotently or exit nonzero',
4637
+ )
4638
+ }
4639
+ for (const service of declared) {
4640
+ const setup = join(root, service, 'setup.ts')
4641
+ if (!existsSync(setup) || !lstatSync(setup).isFile()) {
4642
+ throw new Error(
4643
+ \`Service vendor "\${service}" is missing tests/service/\${service}/setup.ts; add a readiness module that probes and warms the vendor and throws when unavailable\`,
4644
+ )
4645
+ }
4646
+ const label = \`name: { label: 'service:\${service}', color: 'red' }\`
4647
+ const include = \`include: ['tests/service/\${service}/**/*.test.ts']\`
4648
+ const setupFiles = \`setupFiles: ['./tests/setup.ts', './tests/setupServer.ts', './tests/service/\${service}/setup.ts']\`
4649
+ if (!config.includes(label) || !config.includes(include) || !config.includes(setupFiles)) {
4650
+ throw new Error(\`Service vendor "\${service}" is missing its exact Vite project\`)
4651
+ }
4652
+ const command = \`vitest run --config vite.config.ts --no-cache --reporter=dot --project service:\${service}\`
4653
+ if (scripts[\`test:service:\${service}\`] !== command) {
4654
+ throw new Error(\`Service vendor "\${service}" is missing its exact npm script\`)
4655
+ }
4656
+ }
4657
+ const aggregate =
4658
+ 'vitest run --config vite.config.ts --no-cache --reporter=dot ' +
4659
+ declared.map((service) => \`--project service:\${service}\`).join(' ')
4660
+ if (scripts['test:service'] !== aggregate) {
4661
+ throw new Error('test:service must name every declared service exactly once')
4662
+ }
4663
+ if (typeof scripts.test !== 'string' || scripts.test.includes('test:service')) {
4664
+ throw new Error('The default test script must omit live service proofs')
4665
+ }
4666
+ if (
4667
+ typeof scripts.prepublishOnly !== 'string' ||
4668
+ !scripts.prepublishOnly.endsWith(' && npm run test:service')
4669
+ ) {
4670
+ throw new Error('prepublishOnly must end with the aggregate live service proof')
4671
+ }
4672
+ })
4673
+ `
4674
+ }),
4675
+ serviceProvisioner: Object.freeze({
4676
+ id: "serviceProvisioner",
4677
+ name: "serviceProvisioner",
4678
+ summary: "The birth-only workspace-owned service-vendor provisioner skeleton.",
4679
+ category: "orchestration",
4680
+ placeholders: Object.freeze([]),
4681
+ content: `#!/bin/sh
4682
+ set -eu
4683
+
4684
+ printf '%s\\n' 'Service provisioning is not configured. Implement scripts/service.sh to provision every declared vendor idempotently, then remove this failure.' >&2
4685
+ exit 1
4120
4686
  `
4121
4687
  }),
4122
4688
  policyTest: Object.freeze({
@@ -4124,32 +4690,25 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserVuePath(path: string): boolean {
4124
4690
  name: "policyTest",
4125
4691
  summary: "The generated repository filename-policy test.",
4126
4692
  category: "tests",
4127
- placeholders: Object.freeze([
4128
- Object.freeze({
4129
- name: "browserPolicySpecifier",
4130
- description: "The optional real Chromium filesystem-probe import."
4131
- }),
4132
- Object.freeze({
4133
- name: "browserPolicyImport",
4134
- description: "The optional real Chromium package import."
4135
- }),
4136
- Object.freeze({
4137
- name: "browserPolicyTest",
4138
- description: "The optional capability-gated Chromium policy test."
4139
- }),
4140
- Object.freeze({
4141
- name: "vuePolicyImport",
4142
- description: "The optional official Vue SFC compiler import."
4143
- }),
4144
- Object.freeze({
4145
- name: "workspacePolicyAssertion",
4146
- description: "The formatter-stable workspace policy assertion."
4147
- })
4148
- ]),
4149
- content: `import { globSync{{browserPolicySpecifier}} } from 'node:fs'
4693
+ placeholders: Object.freeze([Object.freeze({
4694
+ name: "vuePolicyImport",
4695
+ description: "The Vue SFC parser import, when the workspace declares app/browser."
4696
+ }), Object.freeze({
4697
+ name: "workspacePolicyAssertion",
4698
+ description: "The workspace coding-law assertion, carrying a Vue script extractor when the workspace declares app/browser."
4699
+ })]),
4700
+ content: `import { globSync } from 'node:fs'
4150
4701
  import { describe, expect, it } from 'vitest'
4151
4702
  import { isBrowserVuePath } from './setup.js'
4152
- import { inspectCodingWorkspace } from './setupPolicy.js'{{browserPolicyImport}}{{vuePolicyImport}}
4703
+ import { inspectCodingLaw, inspectCodingWorkspace, isFunctionDomainPath } from './setupPolicy.js'{{vuePolicyImport}}
4704
+
4705
+ ${CONST_KEYWORD} FUNCTION_MODULE_PATH = 'app/browser/composables/useTheme.ts'
4706
+ ${CONST_KEYWORD} FUNCTION_MODULE_VIOLATION =
4707
+ FUNCTION_MODULE_PATH + ' declarations do not form one matching exported function implementation'
4708
+ ${CONST_KEYWORD} FUNCTION_DOMAIN_FILE_PATH = 'app/server/composables.ts'
4709
+ ${CONST_KEYWORD} FUNCTION_DOMAIN_FILE_VIOLATION =
4710
+ FUNCTION_DOMAIN_FILE_PATH +
4711
+ ' names a function domain, which belongs in a folder rather than a file'
4153
4712
 
4154
4713
  describe('repository coding law', () => {
4155
4714
  it('keeps Vue single-file components exclusively in browser environments', () => {
@@ -4160,7 +4719,126 @@ describe('repository coding law', () => {
4160
4719
 
4161
4720
  it('enforces source placement, exports, readonly contracts, and syntax law', () => {
4162
4721
  {{workspacePolicyAssertion}}
4163
- }){{browserPolicyTest}}
4722
+ })
4723
+
4724
+ it('accepts one matching exported function in a registered domain', () => {
4725
+ expect(
4726
+ inspectCodingLaw(
4727
+ FUNCTION_MODULE_PATH,
4728
+ "import type { Ref } from 'vue'\\nexport function useTheme(): Ref<undefined> { throw new Error() }",
4729
+ ),
4730
+ ).toEqual([])
4731
+ })
4732
+
4733
+ it('accepts one matching exported generator in a registered domain', () => {
4734
+ expect(
4735
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export function* useTheme(): Generator<void> {}'),
4736
+ ).toEqual([])
4737
+ })
4738
+
4739
+ it('accepts overload signatures beside one matching implementation', () => {
4740
+ expect(
4741
+ inspectCodingLaw(
4742
+ FUNCTION_MODULE_PATH,
4743
+ 'export function useTheme(): void\\nexport function useTheme(mode: string): void\\nexport function useTheme(_mode?: string): void {}',
4744
+ ),
4745
+ ).toEqual([])
4746
+ })
4747
+
4748
+ it('rejects a bodyless function-domain declaration', () => {
4749
+ expect(
4750
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export declare function useTheme(): void'),
4751
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4752
+ })
4753
+
4754
+ it('rejects a mismatched bodyless declaration beside one implementation', () => {
4755
+ expect(
4756
+ inspectCodingLaw(
4757
+ FUNCTION_MODULE_PATH,
4758
+ 'declare function smuggled(secret: string): void\\nexport function useTheme(): void {}',
4759
+ ),
4760
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4761
+ })
4762
+
4763
+ it('rejects two exported functions in a function module', () => {
4764
+ expect(
4765
+ inspectCodingLaw(
4766
+ FUNCTION_MODULE_PATH,
4767
+ 'export function useTheme(): void {}\\nexport function useMode(): void {}',
4768
+ ),
4769
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4770
+ })
4771
+
4772
+ it('rejects module data beside a function-domain export', () => {
4773
+ expect(
4774
+ inspectCodingLaw(
4775
+ FUNCTION_MODULE_PATH,
4776
+ "const THEME = 'dark'\\nexport function useTheme(): string { return THEME }",
4777
+ ),
4778
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4779
+ })
4780
+
4781
+ it('rejects a function whose name differs from its filename', () => {
4782
+ expect(inspectCodingLaw(FUNCTION_MODULE_PATH, 'export function useMode(): void {}')).toEqual([
4783
+ FUNCTION_MODULE_VIOLATION,
4784
+ ])
4785
+ })
4786
+
4787
+ it('rejects a non-exported function-domain declaration', () => {
4788
+ expect(inspectCodingLaw(FUNCTION_MODULE_PATH, 'function useTheme(): void {}')).toEqual([
4789
+ FUNCTION_MODULE_VIOLATION,
4790
+ ])
4791
+ })
4792
+
4793
+ it('rejects a default function-domain export', () => {
4794
+ expect(
4795
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export default function useTheme(): void {}'),
4796
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4797
+ })
4798
+
4799
+ it('keeps index modules outside the function-domain shape', () => {
4800
+ const path = 'app/browser/composables/index.ts'
4801
+
4802
+ expect(isFunctionDomainPath(path)).toBe(false)
4803
+ expect(inspectCodingLaw(path, 'export function index(): void {}')).toContain(
4804
+ path + ' places module functions in their centralized kind file',
4805
+ )
4806
+ })
4807
+
4808
+ it('keeps main modules outside the function-domain shape', () => {
4809
+ const path = 'app/browser/composables/main.ts'
4810
+
4811
+ expect(isFunctionDomainPath(path)).toBe(false)
4812
+ expect(inspectCodingLaw(path, 'export function main(): void {}')).toContain(
4813
+ path + ' places module functions in their centralized kind file',
4814
+ )
4815
+ })
4816
+
4817
+ it('rejects a file named for a registered function domain', () => {
4818
+ const content =
4819
+ "import { parentPort } from 'node:worker_threads'\\nexport function start(): void { parentPort?.close() }"
4820
+
4821
+ expect(inspectCodingLaw(FUNCTION_DOMAIN_FILE_PATH, content)).toEqual([
4822
+ FUNCTION_DOMAIN_FILE_VIOLATION,
4823
+ ])
4824
+ })
4825
+
4826
+ it('keeps camelCase modules in unregistered domains under centralized placement', () => {
4827
+ const path = 'app/browser/services/normalizePath.ts'
4828
+
4829
+ expect(isFunctionDomainPath(path)).toBe(false)
4830
+ expect(inspectCodingLaw(path, 'export function normalizePath(): void {}')).toEqual([
4831
+ path + ' places module functions in their centralized kind file',
4832
+ ])
4833
+ })
4834
+
4835
+ it('preserves the self-contained Node runtime exemption', () => {
4836
+ const path = 'app/server/worker.ts'
4837
+ const content =
4838
+ "import { parentPort } from 'node:worker_threads'\\nconst port = parentPort\\nexport function start(): void { port?.close() }"
4839
+
4840
+ expect(inspectCodingLaw(path, content)).toEqual([])
4841
+ })
4164
4842
  })
4165
4843
  `
4166
4844
  }),
@@ -4180,6 +4858,7 @@ ${IMPORT_KEYWORD} { createServer } from 'node:http'
4180
4858
  /** One real application child process plus its captured diagnostic output. */
4181
4859
  ${EXPORT_KEYWORD} interface ApplicationProcessInterface {
4182
4860
  readonly child: ChildProcess
4861
+ readonly ready: Promise<void>
4183
4862
  output(): string
4184
4863
  }
4185
4864
 
@@ -4219,11 +4898,17 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} buildApplicationServer(): void {
4219
4898
  }
4220
4899
  }
4221
4900
 
4222
- /** Reserve and release a real loopback port for an immediate child-process bind. */
4223
- ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<number> {
4901
+ /** Start a real Node server bound to one loopback port, or 0 for an ephemeral one. */
4902
+ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} startLoopbackServer(port: number): Promise<Server> {
4224
4903
  const server = createServer()
4225
- server.listen(0, '127.0.0.1')
4904
+ server.listen(port, '127.0.0.1')
4226
4905
  await once(server, 'listening')
4906
+ return server
4907
+ }
4908
+
4909
+ /** Reserve and release a real loopback port for an immediate child-process bind. */
4910
+ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<number> {
4911
+ const server = await startLoopbackServer(0)
4227
4912
  const address = server.address()
4228
4913
  if (address === null || typeof address === 'string') {
4229
4914
  await stopNodeServer(server)
@@ -4233,23 +4918,6 @@ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<numbe
4233
4918
  return address.port
4234
4919
  }
4235
4920
 
4236
- /** Wait until one in-process application server responds on loopback. */
4237
- ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} waitForLoopbackResponse(port: number): Promise<Response> {
4238
- const deadline = Date.now() + 10_000
4239
- let failure: unknown
4240
- while (Date.now() < deadline) {
4241
- try {
4242
- return await fetch(\`http://127.0.0.1:\${port}\`, {
4243
- signal: AbortSignal.timeout(250),
4244
- })
4245
- } catch (error) {
4246
- failure = error
4247
- await new Promise<void>((resolvePromise) => setTimeout(resolvePromise, 25))
4248
- }
4249
- }
4250
- throw new Error(\`application server did not become ready: \${String(failure)}\`)
4251
- }
4252
-
4253
4921
  /** Start the built application entry as a real child process. */
4254
4922
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationProcess(
4255
4923
  port: number,
@@ -4266,16 +4934,20 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationProcess(
4266
4934
  stdio: ['ignore', 'pipe', 'pipe'],
4267
4935
  })
4268
4936
  const output: string[] = []
4937
+ const readiness = Promise.withResolvers<void>()
4269
4938
  child.stdout?.setEncoding('utf8')
4270
4939
  child.stderr?.setEncoding('utf8')
4271
4940
  child.stdout?.on('data', (chunk: unknown) => {
4272
4941
  if (typeof chunk === 'string') output.push(chunk)
4273
4942
  })
4274
4943
  child.stderr?.on('data', (chunk: unknown) => {
4275
- if (typeof chunk === 'string') output.push(chunk)
4944
+ if (typeof chunk !== 'string') return
4945
+ output.push(chunk)
4946
+ if (output.join('').includes('[READY] ')) readiness.resolve()
4276
4947
  })
4277
4948
  return {
4278
4949
  child,
4950
+ ready: readiness.promise,
4279
4951
  output() {
4280
4952
  return output.join('')
4281
4953
  },
@@ -4440,15 +5112,28 @@ describe('create{{pascal}}', () => {
4440
5112
  name: "appCoreTest",
4441
5113
  summary: "The host-independent application test.",
4442
5114
  category: "tests",
4443
- placeholders: Object.freeze([]),
5115
+ placeholders: Object.freeze([
5116
+ Object.freeze({
5117
+ name: "guardImport",
5118
+ description: "The optional shared-record guard test import."
5119
+ }),
5120
+ Object.freeze({
5121
+ name: "readImport",
5122
+ description: "The optional shared health-boundary read test import."
5123
+ }),
5124
+ Object.freeze({
5125
+ name: "boundary",
5126
+ description: "The optional shared health-boundary contract test."
5127
+ })
5128
+ ]),
4444
5129
  content: `import {
4445
5130
  APP_NAME,
4446
5131
  ApplicationError,
4447
5132
  createApplication,
4448
5133
  isApplicationError,
4449
- MAX_APPLICATION_NAME_INPUT_LENGTH,
5134
+ {{guardImport}} MAX_APPLICATION_NAME_INPUT_LENGTH,
4450
5135
  parseApplicationName,
4451
- } from '@app/core'
5136
+ {{readImport}}} from '@app/core'
4452
5137
  import { describe, expect, it } from 'vitest'
4453
5138
 
4454
5139
  describe('createApplication', () => {
@@ -4485,7 +5170,14 @@ describe('createApplication', () => {
4485
5170
  revocable.revoke()
4486
5171
  expect(isApplicationError(revocable.proxy)).toBe(false)
4487
5172
  })
4488
- })
5173
+
5174
+ it('refuses a foreign application error', () => {
5175
+ const foreign = new Error('foreign')
5176
+ foreign.name = 'ApplicationError'
5177
+
5178
+ expect(isApplicationError(foreign)).toBe(false)
5179
+ })
5180
+ }){{boundary}}
4489
5181
  `
4490
5182
  }),
4491
5183
  appBrowserTest: Object.freeze({
@@ -4493,18 +5185,36 @@ describe('createApplication', () => {
4493
5185
  name: "appBrowserTest",
4494
5186
  summary: "The real-browser application mount test.",
4495
5187
  category: "tests",
4496
- placeholders: Object.freeze([Object.freeze({
4497
- name: "browserTestNameImport",
4498
- description: "The layer-correct browser APP_NAME test import."
4499
- })]),
5188
+ placeholders: Object.freeze([
5189
+ Object.freeze({
5190
+ name: "browserTestNameImport",
5191
+ description: "The layer-correct browser APP_NAME test import."
5192
+ }),
5193
+ Object.freeze({
5194
+ name: "showcaseImport",
5195
+ description: "The optional showcase factory test import."
5196
+ }),
5197
+ Object.freeze({
5198
+ name: "entryImport",
5199
+ description: "The optional seeder and boundary-entry test imports."
5200
+ }),
5201
+ Object.freeze({
5202
+ name: "showcase",
5203
+ description: "The optional real-browser showcase mount test."
5204
+ }),
5205
+ Object.freeze({
5206
+ name: "boundary",
5207
+ description: "The optional real-browser boundary degradation test."
5208
+ })
5209
+ ]),
4500
5210
  content: `{{browserTestNameImport}}
4501
5211
  import {
4502
5212
  BrowserApplicationError,
4503
5213
  createBrowserApplication,
4504
- isBrowserApplicationError,
5214
+ {{showcaseImport}} isBrowserApplicationError,
4505
5215
  MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH,
4506
5216
  parseBrowserApplicationOptions,
4507
- } from '@app/browser'
5217
+ {{entryImport}}} from '@app/browser'
4508
5218
  import { buildElement } from '../../setupBrowser.js'
4509
5219
  import { describe, expect, it } from 'vitest'
4510
5220
 
@@ -4588,313 +5298,205 @@ describe('createBrowserApplication', () => {
4588
5298
  expect(isBrowserApplicationError(new Error('plain'))).toBe(false)
4589
5299
  expect(isBrowserApplicationError(revocable.proxy)).toBe(false)
4590
5300
  })
4591
- })
5301
+ }){{showcase}}{{boundary}}
4592
5302
  `
4593
5303
  }),
4594
5304
  appServerTest: Object.freeze({
4595
5305
  id: "appServerTest",
4596
5306
  name: "appServerTest",
4597
- summary: "The real loopback application server lifecycle test.",
5307
+ summary: "The real dispatcher, server-substrate, and runner integration test.",
4598
5308
  category: "tests",
4599
- placeholders: Object.freeze([Object.freeze({
4600
- name: "testNameImport",
4601
- description: "The layer-correct APP_NAME test import."
4602
- })]),
5309
+ placeholders: Object.freeze([
5310
+ Object.freeze({
5311
+ name: "testNameImport",
5312
+ description: "The layer-correct APP_NAME and health-contract test import."
5313
+ }),
5314
+ Object.freeze({
5315
+ name: "serverImport",
5316
+ description: "The application server test import, minus any relocated health contract."
5317
+ }),
5318
+ Object.freeze({
5319
+ name: "boundary",
5320
+ description: "The optional real-server shared-boundary test."
5321
+ })
5322
+ ]),
4603
5323
  content: `{{testNameImport}}
4604
- import {
4605
- APP_HEALTH_METHOD,
4606
- APP_HEALTH_PATH,
4607
- APP_MAX_CONNECTIONS,
4608
- ApplicationServerRunner,
4609
- createApplicationServer,
4610
- startApplicationServer,
4611
- } from '@app/server'
4612
- import { once } from 'node:events'
4613
- import { createServer } from 'node:http'
4614
- import { connect } from 'node:net'
5324
+ {{serverImport}}
4615
5325
  import { beforeAll, describe, expect, it } from 'vitest'
4616
5326
  import {
4617
5327
  buildApplicationServer,
4618
5328
  reserveLoopbackPort,
4619
5329
  startApplicationProcess,
5330
+ startLoopbackServer,
4620
5331
  stopNodeServer,
4621
5332
  waitForApplicationProcess,
4622
- waitForApplicationResponse,
4623
- waitForLoopbackResponse,
4624
- waitForSocketClose,
4625
5333
  } from '../../setupServer.js'
5334
+ import { createRecorder, waitForEvent } from '../../setup.js'
4626
5335
 
4627
5336
  beforeAll(() => buildApplicationServer(), 60_000)
4628
5337
 
4629
- describe('ApplicationServer', () => {
4630
- it('serves a real loopback request and tolerates repeated lifecycle calls', async () => {
4631
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
5338
+ describe('application dispatcher', () => {
5339
+ const state = { connection: { encrypted: false } }
5340
+
5341
+ it('returns the typed shared application record for the health route', async () => {
5342
+ const dispatcher = createApplicationDispatcher()
4632
5343
  try {
4633
- await Promise.all([server.start(), server.start(), server.start()])
4634
- await server.start()
5344
+ const response = await dispatcher.handle(
5345
+ new Request(\`http://application.test\${APP_HEALTH_PATH}\`),
5346
+ state,
5347
+ )
4635
5348
 
4636
- const response = await fetch(server.url)
4637
5349
  expect(response.status).toBe(200)
4638
- expect(response.headers.get('cache-control')).toBe('no-store')
4639
- expect(response.headers.get('content-type')).toBe('application/json; charset=utf-8')
4640
5350
  expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
5351
+ expect(APP_HEALTH_PATH).toBe('/health')
4641
5352
  } finally {
4642
- await Promise.all([server.stop(), server.stop(), server.stop()])
4643
- await server.stop()
4644
- }
4645
- })
4646
-
4647
- it('rejects unsupported methods and unknown routes', async () => {
4648
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4649
- try {
4650
- await server.start()
4651
- const method = await fetch(server.url, { method: 'POST' })
4652
- expect(method.status).toBe(405)
4653
- expect(method.headers.get('allow')).toBe(APP_HEALTH_METHOD)
4654
- expect(method.headers.get('content-type')).toBe('text/plain; charset=utf-8')
4655
- expect(await method.text()).toBe('Method Not Allowed')
4656
- const route = await fetch(\`\${server.url}/missing\`)
4657
- expect(route.status).toBe(404)
4658
- expect(route.headers.get('content-type')).toBe('text/plain; charset=utf-8')
4659
- expect(await route.text()).toBe('Not Found')
4660
- expect(APP_HEALTH_PATH).toBe('/')
4661
- } finally {
4662
- await server.stop()
5353
+ dispatcher.destroy()
4663
5354
  }
4664
5355
  })
4665
5356
 
4666
- it('serves concurrent loopback requests without cross-request state', async () => {
4667
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
5357
+ it('distinguishes a wrong method from a missing route', async () => {
5358
+ const dispatcher = createApplicationDispatcher()
4668
5359
  try {
4669
- await server.start()
4670
- const responses = await Promise.all(
4671
- Array.from({ length: 8 }, async () => {
4672
- const response = await fetch(server.url)
4673
- return { status: response.status, body: await response.json() }
4674
- }),
4675
- )
4676
- expect(responses).toHaveLength(8)
4677
- for (const response of responses) {
4678
- expect(response).toEqual({
4679
- status: 200,
4680
- body: { name: APP_NAME, status: 'ok' },
4681
- })
4682
- }
4683
- } finally {
4684
- await server.stop()
4685
- }
4686
- })
4687
-
4688
- it('validates direct options before allocating a listener', () => {
4689
- expect(() => createApplicationServer({ host: ' ' })).toThrow(
4690
- expect.objectContaining({ code: 'CONFIG' }),
4691
- )
4692
- expect(() => createApplicationServer({ port: Number.NaN })).toThrow(
4693
- expect.objectContaining({ code: 'CONFIG' }),
4694
- )
4695
- expect(() => createApplicationServer({ port: -1 })).toThrow(
4696
- expect.objectContaining({ code: 'CONFIG' }),
4697
- )
4698
- expect(() => createApplicationServer({ timeout: 0 })).toThrow(
4699
- expect.objectContaining({ code: 'CONFIG' }),
4700
- )
4701
- for (const value of [null, 42, [], { host: 42 }, { port: [42] }]) {
4702
- expect(() => Reflect.apply(createApplicationServer, undefined, [value])).toThrow(
4703
- expect.objectContaining({ code: 'CONFIG' }),
5360
+ const method = await dispatcher.handle(
5361
+ new Request(\`http://application.test\${APP_HEALTH_PATH}\`, { method: 'POST' }),
5362
+ state,
4704
5363
  )
4705
- }
4706
- expect(createApplicationServer({ host: '::1' }).url).toBe('http://[::1]:3000')
4707
- })
5364
+ const missing = await dispatcher.handle(new Request('http://application.test/missing'), state)
4708
5365
 
4709
- it('parses the real APP_HOST, APP_PORT, and APP_START_TIMEOUT environment boundary and restores it', () => {
4710
- const previousHost = process.env.APP_HOST
4711
- const previousPort = process.env.APP_PORT
4712
- const previousTimeout = process.env.APP_START_TIMEOUT
4713
- try {
4714
- process.env.APP_HOST = ' 127.0.0.1 '
4715
- process.env.APP_PORT = '0'
4716
- process.env.APP_START_TIMEOUT = '250'
4717
- const server = createApplicationServer()
4718
- expect(server.host).toBe('127.0.0.1')
4719
- expect(server.port).toBe(0)
4720
-
4721
- process.env.APP_PORT = '1e3'
4722
- expect(() => createApplicationServer()).toThrow(expect.objectContaining({ code: 'CONFIG' }))
4723
- process.env.APP_PORT = '0'
4724
- process.env.APP_START_TIMEOUT = '0'
4725
- expect(() => createApplicationServer()).toThrow(expect.objectContaining({ code: 'CONFIG' }))
5366
+ expect(method.status).toBe(405)
5367
+ expect(method.headers.get('allow')).toContain(APP_HEALTH_METHOD)
5368
+ expect(missing.status).toBe(404)
4726
5369
  } finally {
4727
- if (previousHost === undefined) delete process.env.APP_HOST
4728
- else process.env.APP_HOST = previousHost
4729
- if (previousPort === undefined) delete process.env.APP_PORT
4730
- else process.env.APP_PORT = previousPort
4731
- if (previousTimeout === undefined) delete process.env.APP_START_TIMEOUT
4732
- else process.env.APP_START_TIMEOUT = previousTimeout
5370
+ dispatcher.destroy()
4733
5371
  }
4734
5372
  })
5373
+ })
4735
5374
 
4736
- it('rejects an aborted startup promptly, cleans up, and remains restartable', async () => {
5375
+ describe('ApplicationServer', () => {
5376
+ it('composes the real substrate on loopback with security and repeatable lifecycle', async () => {
4737
5377
  const server = createApplicationServer({
4738
- host: '127.0.0.1',
4739
- port: 0,
4740
- timeout: 1_000,
5378
+ server: { host: '127.0.0.1', port: 0, timeout: 1_000 },
4741
5379
  })
4742
- const controller = new AbortController()
4743
- controller.abort(new Error('cancelled by test'))
4744
5380
  try {
4745
- const started = Date.now()
4746
- await expect(server.start(controller.signal)).rejects.toMatchObject({ code: 'LIFECYCLE' })
4747
- expect(Date.now() - started).toBeLessThan(5_000)
4748
- expect(server.listening).toBe(false)
4749
-
4750
- await server.stop()
5381
+ expect(server.host).toBe('127.0.0.1')
5382
+ expect(server.port).toBeUndefined()
5383
+ expect(server.url).toBeUndefined()
5384
+ expect(server.status).toBe('idle')
4751
5385
  await server.start()
4752
- expect((await fetch(server.url)).status).toBe(200)
4753
- } finally {
4754
- await server.stop()
4755
- }
4756
- })
5386
+ expect(server.status).toBe('listening')
5387
+ expect(server.port).toEqual(expect.any(Number))
5388
+ const firstUrl = server.url
5389
+ if (firstUrl === undefined) throw new Error('Expected a bound application URL')
5390
+
5391
+ const first = await fetch(\`\${firstUrl}\${APP_HEALTH_PATH}\`)
5392
+ expect(first.status).toBe(200)
5393
+ expect(first.headers.get('x-content-type-options')).toBe('nosniff')
5394
+ expect(first.headers.get('x-frame-options')).toBe('DENY')
5395
+ expect(first.headers.get('content-security-policy')).not.toBeNull()
5396
+ expect(first.headers.get('x-request-id')).not.toBeNull()
5397
+ expect(await first.json()).toEqual({ name: APP_NAME, status: 'ok' })
4757
5398
 
4758
- it('contains a revoked startup signal without listening or leaking transition state', async () => {
4759
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4760
- const revocable = Proxy.revocable(new AbortController().signal, {})
4761
- revocable.revoke()
4762
- try {
4763
- await expect(Reflect.apply(server.start, server, [revocable.proxy])).rejects.toMatchObject({
4764
- code: 'LIFECYCLE',
4765
- })
4766
- expect(server.listening).toBe(false)
4767
-
4768
- await server.start()
4769
- expect((await fetch(server.url)).status).toBe(200)
4770
- } finally {
4771
5399
  await server.stop()
4772
- }
4773
- })
4774
-
4775
- it('fails closed on a port collision and preserves the owning server', async () => {
4776
- const owner = createApplicationServer({ host: '127.0.0.1', port: 0 })
4777
- let blocked: ReturnType<typeof createApplicationServer> | undefined
4778
- try {
4779
- await owner.start()
4780
- blocked = createApplicationServer({ host: '127.0.0.1', port: owner.port })
4781
- await expect(blocked.start()).rejects.toMatchObject({ code: 'LIFECYCLE' })
4782
- expect(blocked.listening).toBe(false)
4783
- expect((await fetch(owner.url)).status).toBe(200)
4784
- } finally {
4785
- await blocked?.stop()
4786
- await owner.stop()
4787
- }
4788
- })
4789
-
4790
- it('restarts cleanly after a completed stop', async () => {
4791
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4792
- try {
4793
- await server.start()
4794
- await server.stop()
4795
- await Promise.all([server.start(), server.start()])
4796
- expect((await fetch(server.url)).status).toBe(200)
4797
- } finally {
4798
5400
  await server.stop()
4799
- }
4800
- })
5401
+ expect(server.port).toBeUndefined()
5402
+ expect(server.url).toBeUndefined()
5403
+ expect(server.status).toBe('stopped')
4801
5404
 
4802
- it('requests a fresh ephemeral port when the previous port is occupied', async () => {
4803
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4804
- const occupant = createServer()
4805
- try {
4806
5405
  await server.start()
4807
- const previousPort = server.port
5406
+ const secondUrl = server.url
5407
+ if (secondUrl === undefined) throw new Error('Expected a rebound application URL')
5408
+ const second = await fetch(\`\${secondUrl}\${APP_HEALTH_PATH}\`)
5409
+ expect(second.status).toBe(200)
4808
5410
  await server.stop()
4809
-
4810
- occupant.listen(previousPort, server.host)
4811
- await once(occupant, 'listening')
4812
- await server.start()
4813
-
4814
- expect(server.port).not.toBe(previousPort)
4815
- expect((await fetch(server.url)).status).toBe(200)
5411
+ await server.destroy()
5412
+ await server.destroy()
5413
+ expect(server.status).toBe('stopped')
4816
5414
  } finally {
4817
- await server.stop()
4818
- await stopNodeServer(occupant)
5415
+ await server.destroy()
4819
5416
  }
4820
5417
  })
4821
5418
 
4822
- it('honors the latest requested state across opposing concurrent transitions', async () => {
4823
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4824
- try {
4825
- const firstStart = server.start()
4826
- const stopping = server.stop()
4827
- const latestStart = server.start()
4828
- await Promise.all([firstStart, stopping, latestStart])
4829
-
4830
- expect(server.listening).toBe(true)
4831
- expect((await fetch(server.url)).status).toBe(200)
4832
- } finally {
4833
- await server.stop()
5419
+ it('rejects invalid grouped host and port options before binding', () => {
5420
+ for (const value of [
5421
+ { server: { host: ' ' } },
5422
+ { server: { port: Number.NaN } },
5423
+ { server: { port: -1 } },
5424
+ { server: { timeout: 0 } },
5425
+ ]) {
5426
+ expect(() => Reflect.apply(createApplicationServer, undefined, [value])).toThrow(
5427
+ expect.objectContaining({ code: 'CONFIG' }),
5428
+ )
4834
5429
  }
4835
5430
  })
5431
+ })
4836
5432
 
4837
- it('forces a hostile partial-header connection closed during stop', async () => {
4838
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
5433
+ describe('ApplicationServerRunner', () => {
5434
+ it('serves a real request after a restart queued during shutdown', async () => {
5435
+ const port = await reserveLoopbackPort()
5436
+ const code = process.exitCode
5437
+ const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
5438
+ const runner = new ApplicationServerRunner(server)
4839
5439
  try {
4840
- await server.start()
4841
- const socket = connect({ host: server.host, port: server.port })
4842
- try {
4843
- await once(socket, 'connect')
4844
- socket.write('GET / HTTP/1.1\\r\\nHost: localhost')
4845
- const closed = waitForSocketClose(socket)
5440
+ process.exitCode = undefined
5441
+ const first = waitForEvent(runner.emitter, 'ready')
5442
+ runner.start()
5443
+ await first
5444
+ const stopped = runner.stop()
5445
+ const second = waitForEvent(runner.emitter, 'ready')
5446
+ runner.start()
5447
+ await stopped
4846
5448
 
4847
- await server.stop()
4848
- await closed
4849
- expect(socket.destroyed).toBe(true)
4850
- } finally {
4851
- socket.destroy()
4852
- }
5449
+ const [url] = await second
5450
+ const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
5451
+ expect(response.status).toBe(200)
5452
+ expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
5453
+ const stopping = runner.stop()
5454
+ expect(runner.stop()).toBe(stopping)
5455
+ await stopping
5456
+ expect(process.exitCode).toBeUndefined()
4853
5457
  } finally {
4854
- await server.stop()
5458
+ process.exitCode = code
5459
+ await runner.stop()
4855
5460
  }
4856
5461
  })
4857
5462
 
4858
- it('bounds simultaneous idle connections and recovers after capacity is released', async () => {
4859
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4860
- const sockets = []
5463
+ it('cancels a startup still in flight so an immediate stop leaves the port free', async () => {
5464
+ const port = await reserveLoopbackPort()
5465
+ const ready = createRecorder<[url: string]>()
5466
+ const failed = createRecorder<[error: unknown]>()
5467
+ const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
5468
+ const runner = new ApplicationServerRunner(server, {
5469
+ on: { ready: ready.handler, fail: failed.handler },
5470
+ })
4861
5471
  try {
4862
- await server.start()
4863
- for (let index = 0; index < APP_MAX_CONNECTIONS; index += 1) {
4864
- const socket = connect({ host: server.host, port: server.port })
4865
- await once(socket, 'connect')
4866
- socket.write('GET / HTTP/1.1\\r\\nHost: localhost')
4867
- sockets.push(socket)
4868
- }
4869
- const overflow = connect({ host: server.host, port: server.port })
5472
+ runner.start()
5473
+ await runner.stop()
5474
+ expect(ready.count).toBe(0)
5475
+ expect(failed.count).toBe(0)
5476
+
5477
+ const released = await startLoopbackServer(port)
4870
5478
  try {
4871
- const closed = waitForSocketClose(overflow)
4872
- await once(overflow, 'connect')
4873
- await closed
4874
- expect(overflow.destroyed).toBe(true)
5479
+ expect(released.listening).toBe(true)
4875
5480
  } finally {
4876
- overflow.destroy()
5481
+ await stopNodeServer(released)
4877
5482
  }
4878
- const released = sockets.pop()
4879
- if (released === undefined) throw new Error('expected a held connection')
4880
- const releasedClose = waitForSocketClose(released)
4881
- released.destroy()
4882
- await releasedClose
4883
- const response = await waitForLoopbackResponse(server.port)
4884
- expect(response.status).toBe(200)
4885
5483
  } finally {
4886
- for (const socket of sockets) socket.destroy()
4887
- await server.stop()
5484
+ await runner.stop()
4888
5485
  }
4889
5486
  })
4890
5487
 
4891
- it('starts the built executable, serves loopback traffic, terminates on SIGTERM, and releases its port', async () => {
5488
+ it('announces readiness exactly once before the first response and releases on SIGTERM', async () => {
4892
5489
  const port = await reserveLoopbackPort()
4893
5490
  const application = startApplicationProcess(port)
4894
5491
  try {
4895
- const response = await waitForApplicationResponse(application, port)
5492
+ await application.ready
5493
+ const announcement = \`[READY] \${APP_NAME} http://127.0.0.1:\${port}\\n\`
5494
+ expect(application.output()).toBe(announcement)
5495
+
5496
+ const response = await fetch(\`http://127.0.0.1:\${port}\${APP_HEALTH_PATH}\`)
4896
5497
  expect(response.status).toBe(200)
4897
5498
  expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
5499
+ expect(application.output().split('[READY]')).toHaveLength(2)
4898
5500
 
4899
5501
  expect(application.child.kill('SIGTERM')).toBe(true)
4900
5502
  const exited = await waitForApplicationProcess(application)
@@ -4904,10 +5506,8 @@ describe('ApplicationServer', () => {
4904
5506
  : { code: 0, signal: null },
4905
5507
  )
4906
5508
 
4907
- const released = createServer()
5509
+ const released = await startLoopbackServer(port)
4908
5510
  try {
4909
- released.listen(port, '127.0.0.1')
4910
- await once(released, 'listening')
4911
5511
  expect(released.listening).toBe(true)
4912
5512
  } finally {
4913
5513
  await stopNodeServer(released)
@@ -4920,26 +5520,64 @@ describe('ApplicationServer', () => {
4920
5520
  }
4921
5521
  })
4922
5522
 
4923
- it('exits nonzero on a real executable port collision without disturbing the owner', async () => {
5523
+ it('never announces readiness when a real bind fails', async () => {
4924
5524
  const port = await reserveLoopbackPort()
4925
- const owner = createServer()
4926
- owner.listen(port, '127.0.0.1')
4927
- await once(owner, 'listening')
5525
+ const owner = await startLoopbackServer(port)
4928
5526
  const application = startApplicationProcess(port)
4929
5527
  try {
4930
5528
  const exited = await waitForApplicationProcess(application)
4931
5529
  expect(exited).toEqual({ code: 1, signal: null })
4932
5530
  expect(application.output()).toBe('[LIFECYCLE] Application server lifecycle failed\\n')
5531
+ expect(application.output()).not.toContain('[READY]')
4933
5532
  expect(application.output()).not.toContain('EADDRINUSE')
4934
- expect(application.output()).not.toContain('context')
4935
- expect(application.output()).not.toContain('cause')
4936
- expect(application.output()).not.toContain('at ')
4937
5533
  expect(owner.listening).toBe(true)
4938
5534
  } finally {
4939
- if (application.child.exitCode === null && application.child.signalCode === null) {
4940
- application.child.kill('SIGKILL')
4941
- await waitForApplicationProcess(application)
4942
- }
5535
+ await stopNodeServer(owner)
5536
+ }
5537
+ })
5538
+
5539
+ it('isolates a throwing fail listener, releases ownership, and starts again', async () => {
5540
+ const port = await reserveLoopbackPort()
5541
+ const owner = await startLoopbackServer(port)
5542
+ const interrupts = process.listenerCount('SIGINT')
5543
+ const terminations = process.listenerCount('SIGTERM')
5544
+ // The reporter owns the process exit code, so this in-process failure records the
5545
+ // code it set and then restores whatever the surrounding run had.
5546
+ const code = process.exitCode
5547
+ const errors = createRecorder<[error: unknown, event: string]>()
5548
+ const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
5549
+ const runner = new ApplicationServerRunner(server, {
5550
+ on: {
5551
+ fail: () => {
5552
+ throw new Error('listener boom')
5553
+ },
5554
+ },
5555
+ error: errors.handler,
5556
+ })
5557
+ try {
5558
+ process.exitCode = undefined
5559
+ const failed = waitForEvent(runner.emitter, 'fail')
5560
+ runner.start()
5561
+ expect(process.listenerCount('SIGINT')).toBe(interrupts + 1)
5562
+ expect(process.listenerCount('SIGTERM')).toBe(terminations + 1)
5563
+
5564
+ const [failure] = await failed
5565
+ expect(failure).toEqual(expect.objectContaining({ code: 'LIFECYCLE' }))
5566
+ expect(errors.count).toBe(1)
5567
+ expect(errors.calls[0]?.[1]).toBe('fail')
5568
+ expect(process.listenerCount('SIGINT')).toBe(interrupts)
5569
+ expect(process.listenerCount('SIGTERM')).toBe(terminations)
5570
+ expect(process.exitCode).toBe(1)
5571
+
5572
+ await stopNodeServer(owner)
5573
+ const ready = waitForEvent(runner.emitter, 'ready')
5574
+ runner.start()
5575
+ const [url] = await ready
5576
+ const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
5577
+ expect(response.status).toBe(200)
5578
+ } finally {
5579
+ process.exitCode = code
5580
+ await runner.stop()
4943
5581
  await stopNodeServer(owner)
4944
5582
  }
4945
5583
  })
@@ -4951,74 +5589,12 @@ describe('ApplicationServer', () => {
4951
5589
  expect(exited).toEqual({ code: 1, signal: null })
4952
5590
  expect(application.output()).toBe('[CONFIG] Application server configuration failed\\n')
4953
5591
  expect(application.output()).not.toContain(secret)
5592
+ expect(application.output()).not.toContain('[READY]')
4954
5593
  expect(application.output()).not.toContain('context')
4955
5594
  expect(application.output()).not.toContain('cause')
4956
5595
  expect(application.output()).not.toContain('at ')
4957
5596
  })
4958
-
4959
- it('releases process listeners through explicit, repeated, convenience, and signal cleanup', async () => {
4960
- const signalCount = process.listenerCount('SIGTERM')
4961
- const interruptCount = process.listenerCount('SIGINT')
4962
- const runner = new ApplicationServerRunner({ host: '127.0.0.1', port: 0 })
4963
- try {
4964
- expect(runner.start()).toBeUndefined()
4965
- expect(runner.start()).toBeUndefined()
4966
- expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
4967
- expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
4968
-
4969
- await runner.stop()
4970
- await runner.stop()
4971
- expect(process.listenerCount('SIGTERM')).toBe(signalCount)
4972
- expect(process.listenerCount('SIGINT')).toBe(interruptCount)
4973
-
4974
- const convenience = startApplicationServer({ host: '127.0.0.1', port: 0 })
4975
- expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
4976
- expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
4977
- await convenience.stop()
4978
- expect(process.listenerCount('SIGTERM')).toBe(signalCount)
4979
- expect(process.listenerCount('SIGINT')).toBe(interruptCount)
4980
-
4981
- runner.start()
4982
- expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
4983
- expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
4984
- process.emit('SIGTERM')
4985
- await runner.stop()
4986
- expect(process.listenerCount('SIGTERM')).toBe(signalCount)
4987
- expect(process.listenerCount('SIGINT')).toBe(interruptCount)
4988
- } finally {
4989
- await runner.stop()
4990
- }
4991
- })
4992
-
4993
- it('keeps a newer runner generation owned when an older start fails during restart', async () => {
4994
- const signalCount = process.listenerCount('SIGTERM')
4995
- const interruptCount = process.listenerCount('SIGINT')
4996
- const previousExitCode = process.exitCode
4997
- const port = await reserveLoopbackPort()
4998
- const owner = createServer()
4999
- owner.listen(port, '127.0.0.1')
5000
- await once(owner, 'listening')
5001
- const runner = new ApplicationServerRunner({ host: '127.0.0.1', port })
5002
- try {
5003
- runner.start()
5004
- await Promise.resolve()
5005
- const stopping = runner.stop()
5006
- runner.start()
5007
- await stopNodeServer(owner)
5008
- await stopping
5009
-
5010
- const response = await waitForLoopbackResponse(port)
5011
- expect(response.status).toBe(200)
5012
- expect(process.exitCode).toBe(previousExitCode)
5013
- expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
5014
- expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
5015
- } finally {
5016
- await runner.stop()
5017
- await stopNodeServer(owner)
5018
- process.exitCode = previousExitCode
5019
- }
5020
- })
5021
- })
5597
+ }){{boundary}}
5022
5598
  `
5023
5599
  }),
5024
5600
  appServerParsersTest: Object.freeze({
@@ -5042,6 +5618,7 @@ import { describe, expect, it } from 'vitest'
5042
5618
 
5043
5619
  describe('application environment parsers', () => {
5044
5620
  it('accepts port boundaries and trims a host', () => {
5621
+ expect(parseApplicationPort(3000)).toBe(3000)
5045
5622
  expect(parseApplicationPort('0')).toBe(0)
5046
5623
  expect(parseApplicationPort('65535')).toBe(65_535)
5047
5624
  expect(parseApplicationPort(' 3000 ')).toBe(3000)
@@ -5053,6 +5630,19 @@ describe('application environment parsers', () => {
5053
5630
  expect(parseApplicationStartTimeout(String(MAX_APP_START_TIMEOUT))).toBe(MAX_APP_START_TIMEOUT)
5054
5631
  })
5055
5632
 
5633
+ it('never coerces hostile numeric objects through toString', () => {
5634
+ let calls = 0
5635
+ const hostile = {
5636
+ toString() {
5637
+ calls += 1
5638
+ return '3000'
5639
+ },
5640
+ }
5641
+
5642
+ expect(() => parseApplicationPort(hostile)).toThrow(ApplicationServerError)
5643
+ expect(calls).toBe(0)
5644
+ })
5645
+
5056
5646
  it.each(['', '-1', '65536', '1.5', '+1', '0x10', '1e3', 'NaN', 'Infinity'])(
5057
5647
  'rejects hostile APP_PORT value %s',
5058
5648
  (value) => {
@@ -5094,7 +5684,15 @@ describe('application environment parsers', () => {
5094
5684
  expect(() => parseApplicationHost(value)).toThrow(expect.objectContaining({ code: 'CONFIG' }))
5095
5685
  })
5096
5686
 
5097
- it.each([null, 42, [], { port: [42] }, { timeout: 0 }])(
5687
+ it('parses the exact grouped server record including its timeout leaf', () => {
5688
+ expect(
5689
+ parseApplicationServerOptions({
5690
+ server: { host: ' 127.0.0.1 ', port: 0, timeout: 250 },
5691
+ }),
5692
+ ).toEqual({ server: { host: '127.0.0.1', port: 0, timeout: 250 } })
5693
+ })
5694
+
5695
+ it.each([null, 42, [], { server: null }, { server: { port: [42] } }, { server: { timeout: 0 } }])(
5098
5696
  'rejects hostile option container or leaf value %#',
5099
5697
  (value) => {
5100
5698
  expect(() => parseApplicationServerOptions(value)).toThrow(
@@ -5104,11 +5702,14 @@ describe('application environment parsers', () => {
5104
5702
  )
5105
5703
 
5106
5704
  it.each([
5107
- Object.create({ host: '0.0.0.0' }),
5705
+ Object.create({ server: { host: '0.0.0.0' } }),
5108
5706
  { post: 0 },
5109
5707
  new Date(),
5110
- { [Symbol('host')]: '0.0.0.0' },
5111
- Object.defineProperty({}, 'host', { get: () => '0.0.0.0' }),
5708
+ { server: Object.create({ host: '0.0.0.0' }) },
5709
+ { server: { post: 0 } },
5710
+ { server: { [Symbol('host')]: '0.0.0.0' } },
5711
+ Object.defineProperty({}, 'server', { get: () => ({ host: '0.0.0.0' }) }),
5712
+ { server: Object.defineProperty({}, 'host', { get: () => '0.0.0.0' }) },
5112
5713
  new Proxy(
5113
5714
  {},
5114
5715
  {
@@ -5117,6 +5718,16 @@ describe('application environment parsers', () => {
5117
5718
  },
5118
5719
  },
5119
5720
  ),
5721
+ {
5722
+ server: new Proxy(
5723
+ {},
5724
+ {
5725
+ ownKeys: () => {
5726
+ throw new Error('hostile nested ownKeys trap')
5727
+ },
5728
+ },
5729
+ ),
5730
+ },
5120
5731
  ])(
5121
5732
  'rejects inherited, unknown, symbolic, accessor, instance, and hostile proxy options %#',
5122
5733
  (value) => {
@@ -5126,6 +5737,19 @@ describe('application environment parsers', () => {
5126
5737
  },
5127
5738
  )
5128
5739
 
5740
+ it('does not invoke grouped option accessors', () => {
5741
+ let reads = 0
5742
+ const server = Object.defineProperty({}, 'timeout', {
5743
+ get() {
5744
+ reads += 1
5745
+ return 250
5746
+ },
5747
+ })
5748
+
5749
+ expect(() => parseApplicationServerOptions({ server })).toThrow(ApplicationServerError)
5750
+ expect(reads).toBe(0)
5751
+ })
5752
+
5129
5753
  it('never reflects a hostile symbolic option name into diagnostics', () => {
5130
5754
  let caught: unknown
5131
5755
  try {
@@ -5175,9 +5799,7 @@ ${IMPORT_KEYWORD} { createSource, parseManifest } from '@orkestrel/guide'
5175
5799
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_ROOT = fileURLToPath(new URL('../', import.meta.url))
5176
5800
 
5177
5801
  /** Repository roots whose TypeScript and Markdown files participate in guide parity. */
5178
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze([
5179
- {{walkDirs}}
5180
- ])
5802
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze({{walkDirs}})
5181
5803
 
5182
5804
  {{specifiers}}
5183
5805
 
@@ -5363,14 +5985,30 @@ for (const entry of GUIDE_MANIFEST) {
5363
5985
  * @example
5364
5986
  * ```ts
5365
5987
  * hostGroup('AGENTS.md') // 'docs'
5366
- * hostGroup('.agents') // 'orchestration'
5367
- * hostGroup('.claude') // 'orchestration'
5368
- * hostGroup('.codex') // 'orchestration'
5988
+ * hostGroup('.agents/orchestration.md') // 'orchestration'
5989
+ * hostGroup('.claude/rules') // 'orchestration'
5990
+ * hostGroup('.cursor/rules') // 'orchestration'
5991
+ * hostGroup('.mcp.json') // 'orchestration'
5992
+ * hostGroup('.oxlintrc.json') // 'configs'
5369
5993
  * ```
5994
+ *
5995
+ * @remarks
5996
+ * Takes a `HOST_PATHS` entry, so every example above is one. A bare directory
5997
+ * name is not: `ORCHESTRATION_PATH_PREFIXES` entries carry a trailing slash, so
5998
+ * `hostGroup('.cursor')` is `configs`, and no vendored entry has that form.
5999
+ *
6000
+ * Below the `docs` branch the split is by what a path governs rather than where
6001
+ * it sits, which is why both MCP registrations — `.mcp.json` and
6002
+ * `.cursor/mcp.json` — group with the harness bridges instead of with the root
6003
+ * dotfiles beside them. The `docs` branch is checked first and is deliberately
6004
+ * positional: `AGENTS.md`, `CLAUDE.md`, and `LICENSE` are the root documents, and
6005
+ * `CLAUDE.md` stays there as a root document even though it is also a harness
6006
+ * bridge. A plan selecting `orchestration` therefore carries two of the three
6007
+ * bridges; a plan selecting `docs` carries the third.
5370
6008
  */
5371
6009
  function hostGroup(path) {
5372
6010
  if (path === "AGENTS.md" || path === "CLAUDE.md" || path === "LICENSE") return "docs";
5373
- if (path.startsWith(".agents/") || path.startsWith(".claude/") || path.startsWith(".codex/") || path.startsWith("scripts/") || path.startsWith(".github/")) return "orchestration";
6011
+ if (matchesOrchestrationPath(path)) return "orchestration";
5374
6012
  if (path.startsWith("tests/")) return "tests";
5375
6013
  if (path === "guides/src/guide.md" || path === "guides/src/scaffold.md") return "guides";
5376
6014
  return "configs";
@@ -5569,7 +6207,9 @@ function devDependenciesFor(spec) {
5569
6207
  return {
5570
6208
  ...dependencies,
5571
6209
  ...spec.src.includes("browser") ? SOURCE_BROWSER_DEV_DEPENDENCIES : {},
6210
+ ...spec.app.length > 0 ? APP_DEV_DEPENDENCIES : {},
5572
6211
  ...spec.app.includes("browser") ? APP_BROWSER_DEV_DEPENDENCIES : {},
6212
+ ...spec.app.includes("server") ? APP_SERVER_DEV_DEPENDENCIES : {},
5573
6213
  ...spec.showcase ? { "vite-plugin-singlefile": "^2.3.3" } : {},
5574
6214
  ...spec.bin ? { "@vitest/browser-playwright": SOURCE_BROWSER_DEV_DEPENDENCIES["@vitest/browser-playwright"] } : {}
5575
6215
  };
@@ -5624,6 +6264,7 @@ function packageManifest(spec) {
5624
6264
  ...hasSource || spec.bin ? ["npm run test:src"] : [],
5625
6265
  ...spec.app.length > 0 ? ["npm run test:app"] : [],
5626
6266
  "npm run test:policy",
6267
+ "npm run test:config",
5627
6268
  "npm run test:guides"
5628
6269
  ].join(" && ");
5629
6270
  if (hasSource || spec.bin) {
@@ -5633,12 +6274,16 @@ function packageManifest(spec) {
5633
6274
  if (spec.bin) scripts["test:src:bin"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:bin";
5634
6275
  if (spec.integration) scripts["test:integration"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project integration";
5635
6276
  if (spec.bin && spec.integration) scripts["test:equivalence"] = "node -e \"const c=require('node:child_process'),p=process.env.npm_execpath;if(p===undefined)process.exit(1);const r=c.spawnSync(process.execPath,[p,'run','test:integration'],{stdio:'inherit',env:{...process.env,SCAFFOLD_BOUNDARY_EQUIVALENCE:'1'}});process.exit(r.status??1)\"";
5636
- if (spec.service) scripts["test:service"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project service";
6277
+ if (spec.services.length > 0) {
6278
+ scripts["test:service"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.services.map((service) => `--project service:${service}`).join(" ");
6279
+ for (const service of spec.services) scripts[`test:service:${service}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project service:${service}`;
6280
+ }
5637
6281
  if (spec.app.length > 0) {
5638
6282
  scripts["test:app"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.app.map((environment) => `--project ${APP_MATRIX[environment].project}`).join(" ");
5639
6283
  for (const environment of spec.app) scripts[`test:app:${environment}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project ${APP_MATRIX[environment].project}`;
5640
6284
  }
5641
6285
  scripts["test:policy"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy";
6286
+ scripts["test:config"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project config";
5642
6287
  scripts["test:guides"] = "vitest run --config vite.config.ts --reporter=dot --project guides";
5643
6288
  scripts.build = [
5644
6289
  "npm run clean",
@@ -5659,7 +6304,7 @@ function packageManifest(spec) {
5659
6304
  if (spec.showcase) {
5660
6305
  scripts.showcase = `vite --config ${SHOWCASE_CONFIG_PATH}`;
5661
6306
  scripts["build:showcase"] = `vite build --config ${SHOWCASE_CONFIG_PATH}`;
5662
- scripts.show = "npm run build:showcase && npm run copy dist/showcase/index.html demo/showcase.html";
6307
+ scripts.show = "npm run format && npm run build:showcase && npm run copy dist/showcase/index.html demo/showcase.html";
5663
6308
  }
5664
6309
  }
5665
6310
  if (spec.app.includes("server")) {
@@ -5671,7 +6316,7 @@ function packageManifest(spec) {
5671
6316
  scripts["build:src:bin"] = "vite build --config configs/src/vite.bin.config.ts";
5672
6317
  scripts["build:host"] = "node -e \"import('./dist/src/server/index.js').then((m)=>{const n=m.stageHost(process.cwd(),'dist/host').length;console.log('build-host: staged '+n+' file(s) into dist/host')})\"";
5673
6318
  }
5674
- scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "");
6319
+ scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "") + (spec.services.length > 0 ? " && npm run test:service" : "");
5675
6320
  const devDependencies = devDependenciesFor(spec);
5676
6321
  const manifest = {
5677
6322
  name: hasSource ? `@orkestrel/${spec.name}` : spec.name,
@@ -5679,13 +6324,15 @@ function packageManifest(spec) {
5679
6324
  ...hasSource ? {} : { private: true },
5680
6325
  description: spec.description ?? (hasSource ? `The @orkestrel/${spec.name} package.` : `The ${spec.name} application.`),
5681
6326
  keywords: [...spec.keywords].sort(),
5682
- homepage: `https://github.com/orkestrel/${spec.name}#readme`,
5683
- bugs: `https://github.com/orkestrel/${spec.name}/issues`,
6327
+ ...hasSource ? {
6328
+ homepage: `https://github.com/orkestrel/${spec.name}#readme`,
6329
+ bugs: `https://github.com/orkestrel/${spec.name}/issues`
6330
+ } : {},
5684
6331
  license: "MIT",
5685
- repository: {
6332
+ ...hasSource ? { repository: {
5686
6333
  type: "git",
5687
6334
  url: `git+https://github.com/orkestrel/${spec.name}.git`
5688
- },
6335
+ } } : {},
5689
6336
  ...spec.bin ? { bin: { scaffold: "./dist/bin/scaffold.js" } } : {},
5690
6337
  files: spec.bin ? [
5691
6338
  "dist/src",
@@ -5809,7 +6456,7 @@ function viteMachinery(src, app = [], bin = false, showcase = false) {
5809
6456
  * @example
5810
6457
  * ```ts
5811
6458
  * viteProjectRegistrations(['core'], [], { integration: true })
5812
- * // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'guides' }, { project: 'integration' }]
6459
+ * // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'config' }, { project: 'guides' }, { project: 'integration' }]
5813
6460
  * ```
5814
6461
  */
5815
6462
  function viteProjectRegistrations(src, app = [], facts = {}) {
@@ -5832,17 +6479,17 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
5832
6479
  });
5833
6480
  if (environment === "server") registrations.push({ project: "appServer" });
5834
6481
  }
5835
- registrations.push({ project: "policy" }, { project: "guides" });
6482
+ registrations.push({ project: "policy" }, { project: "config" }, { project: "guides" });
5836
6483
  if (facts.bin === true) registrations.push({ project: "srcBin" });
5837
6484
  if (facts.integration === true) registrations.push({ project: "integration" });
5838
- if (facts.service === true) registrations.push({ project: "service" });
6485
+ for (const service of facts.services ?? []) registrations.push({ project: `service${pascalCase(service)}` });
5839
6486
  return registrations;
5840
6487
  }
5841
6488
  /**
5842
6489
  * Render the one ordered proof and structural-axis project definition block.
5843
6490
  *
5844
6491
  * @param facts - Optional structural facts.
5845
- * @returns Policy, guides, then selected axis project definitions, separated by one blank line.
6492
+ * @returns Policy, config, guides, then selected axis project definitions, separated by one blank line.
5846
6493
  *
5847
6494
  * @example
5848
6495
  * ```ts
@@ -5850,10 +6497,14 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
5850
6497
  * ```
5851
6498
  */
5852
6499
  function viteProjectDefinitions(facts = {}) {
5853
- const definitions = [policyViteProject(), guidesViteProject()];
6500
+ const definitions = [
6501
+ policyViteProject(),
6502
+ configViteProject(),
6503
+ guidesViteProject()
6504
+ ];
5854
6505
  if (facts.bin === true) definitions.push(binViteProject());
5855
6506
  if (facts.integration === true) definitions.push(integrationViteProject(facts));
5856
- if (facts.service === true) definitions.push(serviceViteProject());
6507
+ for (const service of facts.services ?? []) definitions.push(serviceViteProject(service));
5857
6508
  return definitions.join("\n");
5858
6509
  }
5859
6510
  /**
@@ -5916,9 +6567,11 @@ import { chromium } from 'playwright'
5916
6567
  ` : "";
5917
6568
  const vueImports = needsVue ? `import vue from '@vitejs/plugin-vue'
5918
6569
  import { parse as parseVue } from 'vue/compiler-sfc'
6570
+ import { parseStartTag } from '@orkestrel/html'
5919
6571
  ` : "";
5920
6572
  const showcaseImports = needsShowcase ? `import { viteSingleFile } from 'vite-plugin-singlefile'
5921
6573
  ` : "";
6574
+ const showcaseHashImport = needsShowcase ? "import { createHash } from 'node:crypto'\n" : "";
5922
6575
  const viteTypeImports = needsVue ? `import type {
5923
6576
  CSSOptions,
5924
6577
  HtmlAssetSource,
@@ -6880,12 +7533,40 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_POLICY =
6880
7533
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_META =
6881
7534
  '<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
6882
7535
  HTML_SECURITY_POLICY +
7536
+ '"\\n\\t\\t/>'${needsShowcase ? `
7537
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_POLICY =
7538
+ "default-src 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src data:; font-src data:; script-src-attr 'none'"
7539
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_META =
7540
+ '<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
7541
+ SHOWCASE_SECURITY_POLICY +
7542
+ '"\\n\\t\\t/>'
7543
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_BUILD_SECURITY_POLICY =
7544
+ "default-src 'none'; base-uri 'none'; object-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; script-src-attr 'none'"
7545
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_BUILD_SECURITY_META =
7546
+ '<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
7547
+ SHOWCASE_BUILD_SECURITY_POLICY +
6883
7548
  '"\\n\\t\\t/>'
6884
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_PREFIX =
6885
- '<!doctype html>\\n<html lang="en">\\n\\t<head>\\n\\t\\t' + HTML_SECURITY_META + '\\n'
7549
+ ` : ""}
6886
7550
 
6887
- ${EXPORT_KEYWORD} function maskIgnoredHtml(environmentKeys: ReadonlySet<string>, html: string): string {
6888
- if (!html.replaceAll('\\r\\n', '\\n').startsWith(HTML_SECURITY_PREFIX)) {
7551
+ ${EXPORT_KEYWORD} function hasSecurityPrologue(html: string, security: string): boolean {
7552
+ const normalized = html.replaceAll('\\r\\n', '\\n')
7553
+ const doctype = '<!doctype html>\\n'
7554
+ if (!normalized.startsWith(doctype)) return false
7555
+ const root = parseStartTag(normalized, doctype.length)
7556
+ return (
7557
+ root !== undefined &&
7558
+ root.name === 'html' &&
7559
+ !root.slashed &&
7560
+ normalized.startsWith('\\n\\t<head>\\n\\t\\t' + security + '\\n', root.next)
7561
+ )
7562
+ }
7563
+
7564
+ ${EXPORT_KEYWORD} function maskIgnoredHtml(
7565
+ environmentKeys: ReadonlySet<string>,
7566
+ html: string,
7567
+ security: string,
7568
+ ): string {
7569
+ if (!hasSecurityPrologue(html, security)) {
6889
7570
  throw new Error(
6890
7571
  '[orkestrel-environment-boundary] Browser HTML must preserve the generated security prologue',
6891
7572
  )
@@ -6919,12 +7600,27 @@ ${EXPORT_KEYWORD} function maskIgnoredHtml(environmentKeys: ReadonlySet<string>,
6919
7600
  )
6920
7601
  }
6921
7602
 
6922
- ${EXPORT_KEYWORD} function isBrowserHtmlEntry(filename: string): boolean {
7603
+ ${needsShowcase ? `${EXPORT_KEYWORD} function isShowcaseHtmlEntry(filename: string): boolean {
6923
7604
  return (
6924
- physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
7605
+ physicalPath(filename) ===
7606
+ physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/showcase.html'))
6925
7607
  )
6926
7608
  }
6927
7609
 
7610
+ ` : ""}${EXPORT_KEYWORD} function isBrowserHtmlEntry(filename: string): boolean {
7611
+ ${needsShowcase ? `return (
7612
+ isShowcaseHtmlEntry(filename) ||
7613
+ physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
7614
+ )` : `return (
7615
+ physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
7616
+ )`}
7617
+ }${needsShowcase ? `
7618
+ ${EXPORT_KEYWORD} function browserHtmlSecurityMeta(filename: string, built: boolean): string | undefined {
7619
+ if (!isBrowserHtmlEntry(filename)) return undefined
7620
+ if (!isShowcaseHtmlEntry(filename)) return HTML_SECURITY_META
7621
+ return built ? SHOWCASE_BUILD_SECURITY_META : SHOWCASE_SECURITY_META
7622
+ }` : ""}
7623
+
6928
7624
  ${EXPORT_KEYWORD} function prepareHtml(): Plugin {
6929
7625
  const environmentKeys = new Set<string>()
6930
7626
  return {
@@ -6941,8 +7637,10 @@ ${EXPORT_KEYWORD} function prepareHtml(): Plugin {
6941
7637
  transformIndexHtml: {
6942
7638
  order: 'pre',
6943
7639
  handler(html, context) {
6944
- if (!isBrowserHtmlEntry(context.filename)) return undefined
6945
- return maskIgnoredHtml(environmentKeys, html)
7640
+ ${needsShowcase ? `const security = browserHtmlSecurityMeta(context.filename, false)
7641
+ if (security === undefined) return undefined
7642
+ return maskIgnoredHtml(environmentKeys, html, security)` : `if (!isBrowserHtmlEntry(context.filename)) return undefined
7643
+ return maskIgnoredHtml(environmentKeys, html, HTML_SECURITY_META)`}
6946
7644
  },
6947
7645
  },
6948
7646
  }
@@ -6966,8 +7664,13 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
6966
7664
  transformIndexHtml: {
6967
7665
  order: 'post',
6968
7666
  handler(html, context) {
6969
- if (!isBrowserHtmlEntry(context.filename)) return undefined
6970
- if (!html.includes(HTML_SECURITY_META)) {
7667
+ ${needsShowcase ? `const security = browserHtmlSecurityMeta(
7668
+ context.filename,
7669
+ context.bundle !== undefined,
7670
+ )
7671
+ if (security === undefined) return undefined
7672
+ if (!html.includes(security)) {` : `if (!isBrowserHtmlEntry(context.filename)) return undefined
7673
+ if (!html.includes(HTML_SECURITY_META)) {`}
6971
7674
  throw new Error(
6972
7675
  '[orkestrel-environment-boundary] Browser HTML must retain its security policy',
6973
7676
  )
@@ -6977,26 +7680,49 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
6977
7680
  }
6978
7681
  }
6979
7682
 
6980
- ` : ""}${needsShowcase ? `${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_POLICY =
6981
- "base-uri 'none'; object-src 'none'; script-src 'self' 'unsafe-inline'; script-src-attr 'none'; style-src 'self' 'unsafe-inline'"
6982
-
6983
- ${EXPORT_KEYWORD} function showcaseHtml(): Plugin {
7683
+ ` : ""}${needsShowcase ? `${EXPORT_KEYWORD} function showcaseHtml(): Plugin {
6984
7684
  return {
6985
7685
  name: 'orkestrel-showcase-html',
6986
- enforce: 'post',
6987
7686
  transformIndexHtml: {
6988
7687
  order: 'post',
6989
7688
  handler(html, context) {
6990
- if (!isBrowserHtmlEntry(context.filename)) return undefined
6991
- if (!html.includes(HTML_SECURITY_META)) {
6992
- throw new Error('[orkestrel-showcase] Browser HTML must retain its security policy')
7689
+ if (!isShowcaseHtmlEntry(context.filename) || context.bundle === undefined) {
7690
+ return undefined
6993
7691
  }
6994
- const stamp = new Date().toISOString()
6995
- return html.replace(
6996
- HTML_SECURITY_META,
6997
- HTML_SECURITY_META.replace(HTML_SECURITY_POLICY, SHOWCASE_SECURITY_POLICY) +
6998
- \`\\n\\t\\t<meta name="build-id" content="\${stamp}" />\`,
6999
- )
7692
+ if (!html.includes(SHOWCASE_SECURITY_META)) {
7693
+ throw new Error(
7694
+ '[orkestrel-showcase-html] Showcase build did not retain its development security policy',
7695
+ )
7696
+ }
7697
+ const secured = html.replace(SHOWCASE_SECURITY_META, SHOWCASE_BUILD_SECURITY_META)
7698
+ const build = createHash('sha256').update(secured).digest('hex')
7699
+ return {
7700
+ html: secured,
7701
+ tags: [
7702
+ {
7703
+ tag: 'meta',
7704
+ attrs: { name: 'build-id', content: build },
7705
+ injectTo: 'head',
7706
+ },
7707
+ ],
7708
+ }
7709
+ },
7710
+ },
7711
+ generateBundle: {
7712
+ order: 'post',
7713
+ handler(_options, bundle) {
7714
+ let html: (typeof bundle)[string] | undefined
7715
+ for (const output of Object.values(bundle)) {
7716
+ if (!output.fileName.endsWith('.html')) continue
7717
+ if (html !== undefined) {
7718
+ this.error('[orkestrel-showcase-html] Showcase build emitted multiple HTML entries')
7719
+ }
7720
+ html = output
7721
+ }
7722
+ if (html === undefined) {
7723
+ this.error('[orkestrel-showcase-html] Showcase build did not emit an HTML entry')
7724
+ }
7725
+ html.fileName = 'index.html'
7000
7726
  },
7001
7727
  },
7002
7728
  }
@@ -7293,7 +8019,7 @@ ${needsBrowser ? `import { isCSSRequest, parseSync, preprocessCSS, transformWith
7293
8019
  `}import { defineConfig, mergeConfig } from 'vitest/config'
7294
8020
  import tsconfig from './tsconfig.json' with { type: 'json' }
7295
8021
  import { fileURLToPath, URL } from 'node:url'
7296
- import { isBuiltin } from 'node:module'
8022
+ ${showcaseHashImport}import { isBuiltin } from 'node:module'
7297
8023
  import {
7298
8024
  ${needsBrowser ? " accessSync,\n" : ""} closeSync,
7299
8025
  constants as FS_CONSTANTS,
@@ -7616,7 +8342,7 @@ ${environmentBoundary}`;
7616
8342
  * ```
7617
8343
  */
7618
8344
  function policyViteProject() {
7619
- return `${EXPORT_KEYWORD} const policy = (config?: UserConfig): UserConfig =>
8345
+ return `${EXPORT_KEYWORD} const policy = (options?: UserConfig): UserConfig =>
7620
8346
  mergeConfig(
7621
8347
  {
7622
8348
  resolve,
@@ -7628,7 +8354,34 @@ function policyViteProject() {
7628
8354
  browser: { enabled: false },
7629
8355
  },
7630
8356
  },
7631
- config ?? {},
8357
+ options ?? {},
8358
+ )
8359
+ `;
8360
+ }
8361
+ /**
8362
+ * Build the standalone Node-only root-configuration Vitest project.
8363
+ *
8364
+ * @returns The emitted `config` project definition.
8365
+ *
8366
+ * @example
8367
+ * ```ts
8368
+ * configViteProject().includes("label: 'config'") // true
8369
+ * ```
8370
+ */
8371
+ function configViteProject() {
8372
+ return `${EXPORT_KEYWORD} const config = (options?: UserConfig): UserConfig =>
8373
+ mergeConfig(
8374
+ {
8375
+ resolve,
8376
+ test: {
8377
+ name: { label: 'config', color: 'yellow' },
8378
+ include: ['tests/config/**/*.test.ts'],
8379
+ setupFiles: ['./tests/setup.ts'],
8380
+ environment: 'node',
8381
+ browser: { enabled: false },
8382
+ },
8383
+ },
8384
+ options ?? {},
7632
8385
  )
7633
8386
  `;
7634
8387
  }
@@ -7643,7 +8396,7 @@ function policyViteProject() {
7643
8396
  * ```
7644
8397
  */
7645
8398
  function guidesViteProject() {
7646
- return `${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
8399
+ return `${EXPORT_KEYWORD} const guides = (options?: UserConfig): UserConfig =>
7647
8400
  mergeConfig(
7648
8401
  {
7649
8402
  resolve,
@@ -7656,7 +8409,7 @@ function guidesViteProject() {
7656
8409
  browser: { enabled: false },
7657
8410
  },
7658
8411
  },
7659
- config ?? {},
8412
+ options ?? {},
7660
8413
  )
7661
8414
  `;
7662
8415
  }
@@ -7671,7 +8424,7 @@ function guidesViteProject() {
7671
8424
  * ```
7672
8425
  */
7673
8426
  function binViteProject() {
7674
- return `${EXPORT_KEYWORD} const srcBin = (config?: UserConfig): UserConfig =>
8427
+ return `${EXPORT_KEYWORD} const srcBin = (options?: UserConfig): UserConfig =>
7675
8428
  mergeConfig(
7676
8429
  {
7677
8430
  resolve,
@@ -7698,7 +8451,7 @@ function binViteProject() {
7698
8451
  browser: { enabled: false },
7699
8452
  },
7700
8453
  },
7701
- config ?? {},
8454
+ options ?? {},
7702
8455
  )
7703
8456
  `;
7704
8457
  }
@@ -7716,7 +8469,7 @@ function binViteProject() {
7716
8469
  * ```
7717
8470
  */
7718
8471
  function integrationViteProject(facts = {}) {
7719
- return `${EXPORT_KEYWORD} const integration = (config?: UserConfig): UserConfig =>
8472
+ return `${EXPORT_KEYWORD} const integration = (options?: UserConfig): UserConfig =>
7720
8473
  mergeConfig(
7721
8474
  {
7722
8475
  resolve,
@@ -7733,29 +8486,32 @@ ${facts.bin === true && facts.integration === true && facts.global === true ? `
7733
8486
  fileParallelism: false,
7734
8487
  },
7735
8488
  },
7736
- config ?? {},
8489
+ options ?? {},
7737
8490
  )
7738
8491
  `;
7739
8492
  }
7740
8493
  /**
7741
- * Build the standalone Node-only live-service proof project.
8494
+ * Build one standalone Node-only live-service vendor proof project.
7742
8495
  *
7743
- * @returns The emitted `service` project definition.
8496
+ * @param name - The bounded vendor directory name.
8497
+ * @returns The emitted `service:<name>` project definition.
7744
8498
  *
7745
8499
  * @example
7746
8500
  * ```ts
7747
- * serviceViteProject().includes("label: 'service'") // true
8501
+ * serviceViteProject('claude').includes("label: 'service:claude'") // true
7748
8502
  * ```
7749
8503
  */
7750
- function serviceViteProject() {
7751
- return `${EXPORT_KEYWORD} const service = (config?: UserConfig): UserConfig =>
8504
+ function serviceViteProject(name) {
8505
+ if (!NAME_PATTERN.test(name) || name.length > 203) throw new Error("Service project name must be a bounded lowercase directory name");
8506
+ return `${EXPORT_KEYWORD} const ${`service${pascalCase(name)}`} = (options?: UserConfig): UserConfig =>
7752
8507
  mergeConfig(
7753
8508
  {
7754
8509
  resolve,
8510
+ plugins: [environmentBoundary('app/server')],
7755
8511
  test: {
7756
- name: { label: 'service', color: 'red' },
7757
- include: ['tests/service/**/*.test.ts'],
7758
- setupFiles: ['./tests/setup.ts', './tests/setupService.ts'],
8512
+ name: { label: ${serializeTypeScriptString(`service:${name}`)}, color: 'red' },
8513
+ include: [${serializeTypeScriptString(`tests/service/${name}/**/*.test.ts`)}],
8514
+ setupFiles: ['./tests/setup.ts', './tests/setupServer.ts', ${serializeTypeScriptString(`./tests/service/${name}/setup.ts`)}],
7759
8515
  environment: 'node',
7760
8516
  browser: { enabled: false },
7761
8517
  testTimeout: 120_000,
@@ -7763,7 +8519,7 @@ function serviceViteProject() {
7763
8519
  fileParallelism: false,
7764
8520
  },
7765
8521
  },
7766
- config ?? {},
8522
+ options ?? {},
7767
8523
  )
7768
8524
  `;
7769
8525
  }
@@ -7790,7 +8546,7 @@ function singleSrcViteConfig(environment, facts = {}) {
7790
8546
  const renderedTest = renderViteTest(viteProjectRegistrations([environment], [], facts), machinery.browser);
7791
8547
  const definitions = viteProjectDefinitions(facts);
7792
8548
  if (environment === "browser") return `${header}
7793
- ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8549
+ ${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
7794
8550
  mergeConfig(
7795
8551
  {
7796
8552
  resolve,
@@ -7815,7 +8571,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7815
8571
  name: { label: 'src:browser', color: 'yellow' },
7816
8572
  include: ['tests/src/browser/**/*.test.ts'],
7817
8573
  ${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
7818
- ...(config?.test?.browser?.enabled === false
8574
+ ...(options?.test?.browser?.enabled === false
7819
8575
  ? {}
7820
8576
  : {
7821
8577
  deps: {
@@ -7835,7 +8591,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7835
8591
  fileParallelism: false,
7836
8592
  },
7837
8593
  },
7838
- config ?? {},
8594
+ options ?? {},
7839
8595
  )
7840
8596
 
7841
8597
  ${definitions}
@@ -7845,7 +8601,7 @@ ${renderedTest}
7845
8601
  })
7846
8602
  `;
7847
8603
  return `${header}
7848
- ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8604
+ ${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
7849
8605
  mergeConfig(
7850
8606
  {
7851
8607
  resolve,
@@ -7875,7 +8631,7 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
7875
8631
  browser: { enabled: false },
7876
8632
  },
7877
8633
  },
7878
- config ?? {},
8634
+ options ?? {},
7879
8635
  )
7880
8636
 
7881
8637
  ${definitions}
@@ -7905,7 +8661,7 @@ ${renderedTest}
7905
8661
  *
7906
8662
  * @param src - The declared `Environment[]`.
7907
8663
  * @param facts - Optional structural facts. `bin` appends the standalone executable
7908
- * build-and-test project; `integration` and `service` append their standalone
8664
+ * build-and-test project; `integration` and `services` append their standalone
7909
8665
  * proof projects; `global` wires the shared global-setup module.
7910
8666
  * @returns The root `vite.config.ts` file content, newline-terminated.
7911
8667
  *
@@ -7924,7 +8680,7 @@ function rootViteConfig(src, facts = {}) {
7924
8680
  if (onlyEnvironment === "browser" || onlyEnvironment === "server") return singleSrcViteConfig(onlyEnvironment, facts);
7925
8681
  }
7926
8682
  const browserBlock = `
7927
- ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8683
+ ${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
7928
8684
  srcCore(
7929
8685
  mergeConfig(
7930
8686
  {
@@ -7948,7 +8704,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7948
8704
  include: ['tests/src/browser/**/*.test.ts'],
7949
8705
  exclude: ['tests/src/core/**/*.test.ts'],
7950
8706
  ${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
7951
- ...(config?.test?.browser?.enabled === false
8707
+ ...(options?.test?.browser?.enabled === false
7952
8708
  ? {}
7953
8709
  : {
7954
8710
  deps: {
@@ -7968,12 +8724,12 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7968
8724
  fileParallelism: false,
7969
8725
  },
7970
8726
  },
7971
- config ?? {},
8727
+ options ?? {},
7972
8728
  ),
7973
8729
  )
7974
8730
  `;
7975
8731
  const serverBlock = `
7976
- ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8732
+ ${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
7977
8733
  srcCore(
7978
8734
  mergeConfig(
7979
8735
  {
@@ -8011,14 +8767,14 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8011
8767
  setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
8012
8768
  },
8013
8769
  },
8014
- config ?? {},
8770
+ options ?? {},
8015
8771
  ),
8016
8772
  )
8017
8773
  `;
8018
8774
  const blocks = nonCore.map((environment) => environment === "browser" ? browserBlock : serverBlock).join("");
8019
8775
  const renderedTest = renderViteTest(viteProjectRegistrations(src, [], facts), machinery.browser);
8020
8776
  return `${header}
8021
- ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8777
+ ${EXPORT_KEYWORD} const srcCore = (options?: UserConfig): UserConfig =>
8022
8778
  mergeConfig(
8023
8779
  {
8024
8780
  resolve,
@@ -8036,7 +8792,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8036
8792
  browser: { enabled: false },
8037
8793
  },
8038
8794
  },
8039
- config ?? {},
8795
+ options ?? {},
8040
8796
  )
8041
8797
  ${blocks}
8042
8798
  ${viteProjectDefinitions(facts)}
@@ -8066,7 +8822,7 @@ function applicationViteConfig(src, app, facts = {}) {
8066
8822
  const header = viteHeader(machinery);
8067
8823
  const blocks = [];
8068
8824
  if (src.includes("core")) blocks.push(`
8069
- ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8825
+ ${EXPORT_KEYWORD} const srcCore = (options?: UserConfig): UserConfig =>
8070
8826
  mergeConfig(
8071
8827
  {
8072
8828
  resolve,
@@ -8081,7 +8837,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8081
8837
  browser: { enabled: false },
8082
8838
  },
8083
8839
  },
8084
- config ?? {},
8840
+ options ?? {},
8085
8841
  )
8086
8842
  `);
8087
8843
  if (src.includes("browser")) {
@@ -8089,7 +8845,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8089
8845
  output: { paths: { '@src/core': '../core/index.js' } },` : "";
8090
8846
  const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
8091
8847
  blocks.push(`
8092
- ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8848
+ ${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
8093
8849
  mergeConfig(
8094
8850
  {
8095
8851
  resolve,
@@ -8114,7 +8870,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8114
8870
  name: { label: 'src:browser', color: 'yellow' },
8115
8871
  include: ['tests/src/browser/**/*.test.ts'],
8116
8872
  ${hasSourceCore ? "exclude: ['tests/src/core/**/*.test.ts'],\n " : ""}${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
8117
- ...(config?.test?.browser?.enabled === false
8873
+ ...(options?.test?.browser?.enabled === false
8118
8874
  ? {}
8119
8875
  : {
8120
8876
  deps: {
@@ -8134,7 +8890,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8134
8890
  fileParallelism: false,
8135
8891
  },
8136
8892
  },
8137
- config ?? {},
8893
+ options ?? {},
8138
8894
  )
8139
8895
  `);
8140
8896
  }
@@ -8155,7 +8911,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8155
8911
  const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
8156
8912
  const formats = hasSourceCore ? "" : "\n formats: ['es', 'cjs'],";
8157
8913
  blocks.push(`
8158
- ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8914
+ ${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
8159
8915
  mergeConfig(
8160
8916
  {
8161
8917
  resolve,
@@ -8185,12 +8941,12 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8185
8941
  browser: { enabled: false },
8186
8942
  },
8187
8943
  },
8188
- config ?? {},
8944
+ options ?? {},
8189
8945
  )
8190
8946
  `);
8191
8947
  }
8192
8948
  if (app.includes("core")) blocks.push(`
8193
- ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
8949
+ ${EXPORT_KEYWORD} const appCore = (options?: UserConfig): UserConfig =>
8194
8950
  mergeConfig(
8195
8951
  {
8196
8952
  resolve,
@@ -8204,32 +8960,27 @@ ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
8204
8960
  browser: { enabled: false },
8205
8961
  },
8206
8962
  },
8207
- config ?? {},
8963
+ options ?? {},
8208
8964
  )
8209
8965
  `);
8210
8966
  if (app.includes("browser")) blocks.push(facts.showcase === true ? `
8211
- ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
8212
- const output = showcase ? 'dist/showcase' : 'dist/app/browser'
8967
+ ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
8968
+ if (config.length > 0) {
8969
+ throw new Error(
8970
+ '[orkestrel-environment-boundary] Browser configuration overrides are not permitted by the generated boundary',
8971
+ )
8972
+ }
8213
8973
  return {
8214
8974
  resolve,
8215
8975
  css: ENVIRONMENT_CSS,
8216
8976
  html: environmentHtml(),
8217
8977
  plugins: [
8218
8978
  restoreHtml(),
8219
- outputBoundary(output),
8979
+ outputBoundary('dist/app/browser'),
8220
8980
  environmentBoundary('app/browser'),
8221
8981
  vue(),
8222
8982
  prepareHtml(),
8223
8983
  finalizeHtml(),
8224
- ...(showcase
8225
- ? [
8226
- viteSingleFile({
8227
- removeViteModuleLoader: true,
8228
- useRecommendedBuildConfig: true,
8229
- }),
8230
- showcaseHtml(),
8231
- ]
8232
- : []),
8233
8984
  ],
8234
8985
  root: resolveWorkspacePath('app/browser'),
8235
8986
  publicDir: false,
@@ -8240,18 +8991,9 @@ ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
8240
8991
  },
8241
8992
  },
8242
8993
  build: {
8243
- ...(showcase
8244
- ? {
8245
- cssMinify: 'lightningcss',
8246
- minify: 'oxc',
8247
- modulePreload: false,
8248
- reportCompressedSize: false,
8249
- sourcemap: false,
8250
- target: 'esnext',
8251
- }
8252
- : { assetsInlineLimit: 0 }),
8994
+ assetsInlineLimit: 0,
8253
8995
  emptyOutDir: true,
8254
- outDir: resolveWorkspacePath(output),
8996
+ outDir: resolveWorkspacePath('dist/app/browser'),
8255
8997
  rolldownOptions: {
8256
8998
  input: resolveWorkspacePath('${APP_MATRIX.browser.entry}'),
8257
8999
  },
@@ -8280,22 +9022,54 @@ ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
8280
9022
  }
8281
9023
  }
8282
9024
 
8283
- ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
8284
- if (config.length > 0) {
8285
- throw new Error(
8286
- '[orkestrel-environment-boundary] Browser configuration overrides are not permitted by the generated boundary',
8287
- )
8288
- }
8289
- return applicationBrowser(false)
8290
- }
8291
-
8292
9025
  ${EXPORT_KEYWORD} function appShowcase(...config: never[]): UserConfig {
8293
9026
  if (config.length > 0) {
8294
9027
  throw new Error(
8295
9028
  '[orkestrel-environment-boundary] Showcase configuration overrides are not permitted by the generated boundary',
8296
9029
  )
8297
9030
  }
8298
- return applicationBrowser(true)
9031
+ return {
9032
+ base: './',
9033
+ resolve,
9034
+ css: ENVIRONMENT_CSS,
9035
+ html: environmentHtml(),
9036
+ plugins: [
9037
+ restoreHtml(),
9038
+ outputBoundary('dist/showcase'),
9039
+ environmentBoundary('app/browser'),
9040
+ vue(),
9041
+ prepareHtml(),
9042
+ showcaseHtml(),
9043
+ viteSingleFile({
9044
+ removeViteModuleLoader: true,
9045
+ useRecommendedBuildConfig: true,
9046
+ }),
9047
+ finalizeHtml(),
9048
+ ],
9049
+ root: resolveWorkspacePath('app/browser'),
9050
+ publicDir: false,
9051
+ server: {
9052
+ open: '/showcase.html',
9053
+ fs: {
9054
+ strict: true,
9055
+ allow: [...browserServerRoots()],
9056
+ },
9057
+ },
9058
+ build: {
9059
+ assetsInlineLimit: Number.MAX_SAFE_INTEGER,
9060
+ cssMinify: 'lightningcss',
9061
+ emptyOutDir: true,
9062
+ minify: 'oxc',
9063
+ modulePreload: false,
9064
+ outDir: resolveWorkspacePath('dist/showcase'),
9065
+ reportCompressedSize: false,
9066
+ rolldownOptions: {
9067
+ input: resolveWorkspacePath('app/browser/showcase.html'),
9068
+ },
9069
+ sourcemap: false,
9070
+ target: 'esnext',
9071
+ },
9072
+ }
8299
9073
  }
8300
9074
  ` : `
8301
9075
  ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
@@ -8357,7 +9131,7 @@ ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
8357
9131
  }
8358
9132
  `);
8359
9133
  if (app.includes("server")) blocks.push(`
8360
- ${EXPORT_KEYWORD} const appServer = (config?: UserConfig): UserConfig =>
9134
+ ${EXPORT_KEYWORD} const appServer = (options?: UserConfig): UserConfig =>
8361
9135
  mergeConfig(
8362
9136
  {
8363
9137
  resolve,
@@ -8384,7 +9158,7 @@ ${EXPORT_KEYWORD} const appServer = (config?: UserConfig): UserConfig =>
8384
9158
  browser: { enabled: false },
8385
9159
  },
8386
9160
  },
8387
- config ?? {},
9161
+ options ?? {},
8388
9162
  )
8389
9163
  `);
8390
9164
  const renderedTest = renderViteTest(viteProjectRegistrations(src, app, facts), machinery.browser);
@@ -8619,6 +9393,8 @@ function appTsconfig(environment, hasCore) {
8619
9393
  const include = TYPESCRIPT_EXTENSIONS.map((extension) => `../../app/${environment}/**/*.${extension}`);
8620
9394
  if (environment === "browser") include.push(`../../app/${environment}/**/*.vue`);
8621
9395
  if (environment !== "core" && hasCore) include.push(...TYPESCRIPT_EXTENSIONS.map((extension) => `../../app/core/**/*.${extension}`));
9396
+ include.push(...TYPESCRIPT_EXTENSIONS.map((extension) => `../../tests/app/${environment}/**/*.${extension}`));
9397
+ include.push(environment === "browser" ? "../../tests/setupBrowser.ts" : environment === "server" ? "../../tests/setupServer.ts" : "../../tests/setup.ts");
8622
9398
  return formatJson({
8623
9399
  extends: "../../tsconfig.json",
8624
9400
  compilerOptions: {
@@ -8665,7 +9441,7 @@ function ciWorkflow(spec) {
8665
9441
  const tail = [];
8666
9442
  if (spec.integration) tail.push(` - name: Run live consumer integration
8667
9443
  run: npm run test:integration`);
8668
- if (spec.service) {
9444
+ if (spec.services.length > 0) {
8669
9445
  tail.push(` - name: Provision live service
8670
9446
  run: bash ${SERVICE_SCRIPT_PATH}`);
8671
9447
  tail.push(` - name: Run live service tests
@@ -8799,7 +9575,7 @@ export default defineConfig(appShowcase())
8799
9575
  * factory per environment (AGENTS §5's per-environment centralized-file pattern), so
8800
9576
  * every environment gets the same uniform stub shape.
8801
9577
  *
8802
- * @param spec - The `Blueprint` to derive source stubs from.
9578
+ * @param spec - The blueprint carrying the declared source environment set.
8803
9579
  * @param pascal - The package's PascalCase entity name.
8804
9580
  * @returns The `source` group's `Artifact[]`.
8805
9581
  *
@@ -8832,20 +9608,129 @@ function sourceArtifacts(spec, pascal) {
8832
9608
  * Draft the application source artifacts for every selected app environment.
8833
9609
  *
8834
9610
  * @param spec - The blueprint carrying the application environment set.
9611
+ * @remarks
9612
+ * Two conditional shapes layer over the per-environment set. The health contract —
9613
+ * record, route constants, guard, and the one unknown-to-typed read — is declared by
9614
+ * `app/server` while the server alone reads it and RELOCATES to `app/core` the moment
9615
+ * the browser reads it too, because a contract two hosts share belongs to neither of
9616
+ * them. The showcase entry pair, its seeder, and its factory appear only for a
9617
+ * blueprint that declares the physical showcase wrapper alongside `app/browser`.
8835
9618
  * @returns Complete, runnable app/core, app/browser, and app/server artifacts.
8836
9619
  */
8837
9620
  function applicationArtifacts(spec) {
8838
9621
  const artifacts = [];
8839
9622
  const hasCore = spec.app.includes("core");
9623
+ const hasBrowser = spec.app.includes("browser");
9624
+ const hasBoundary = hasApplicationBoundary(spec);
9625
+ const hasShowcase = hasApplicationShowcase(spec);
9626
+ const showcaseSource = hasBoundary ? "its running server" : "its own configuration";
8840
9627
  const nameLiteral = serializeTypeScriptString(spec.name);
8841
- if (hasCore) artifacts.push(fillArtifact("app/core/types.ts", "source", "appCoreTypes", {}, "core"), fillArtifact("app/core/constants.ts", "source", "appCoreConstants", { nameLiteral }, "core"), fillArtifact("app/core/errors.ts", "source", "appCoreErrors", {}, "core"), fillArtifact("app/core/parsers.ts", "source", "appCoreParsers", {}, "core"), fillArtifact("app/core/factories.ts", "source", "appCoreFactories", {}, "core"), fillArtifact("app/core/index.ts", "source", "appCoreIndex", {}, "core"));
8842
- if (spec.app.includes("browser")) {
9628
+ const sharedRecord = `
9629
+ /** The application record both hosts read at the health route. */
9630
+ ${EXPORT_KEYWORD} interface ApplicationRecord {
9631
+ readonly name: string
9632
+ readonly status: 'ok'
9633
+ }
9634
+ `;
9635
+ const healthConstants = `
9636
+ /** The only HTTP method owned by the application health route. */
9637
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_METHOD = 'GET'
9638
+
9639
+ /** The only HTTP path owned by the generated application server. */
9640
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/health'
9641
+ `;
9642
+ if (hasCore) {
9643
+ artifacts.push(fillArtifact("app/core/types.ts", "source", "appCoreTypes", { record: hasBoundary ? sharedRecord : "" }, "core"), fillArtifact("app/core/constants.ts", "source", "appCoreConstants", {
9644
+ nameLiteral,
9645
+ health: hasBoundary ? `${healthConstants}
9646
+ /** Milliseconds allowed for one shared application health read. */
9647
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_TIMEOUT = 5_000
9648
+ ` : ""
9649
+ }, "core"), fillArtifact("app/core/errors.ts", "source", "appCoreErrors", {}, "core"), fillArtifact("app/core/parsers.ts", "source", "appCoreParsers", {}, "core"), fillArtifact("app/core/factories.ts", "source", "appCoreFactories", {}, "core"), fillArtifact("app/core/index.ts", "source", "appCoreIndex", {
9650
+ validators: hasBoundary ? "export * from './validators.js'\n" : "",
9651
+ handlers: hasBoundary ? "export * from './handlers.js'\n" : ""
9652
+ }, "core"));
9653
+ if (hasBoundary) artifacts.push(fillArtifact("app/core/validators.ts", "source", "appCoreValidators", {}, "core"), fillArtifact("app/core/handlers.ts", "source", "appCoreHandlers", {}, "core"));
9654
+ }
9655
+ if (hasBrowser) {
8843
9656
  const nameImport = hasCore ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from './constants.js'";
8844
9657
  const nameConstant = hasCore ? "" : `/** The browser-only application name. */
8845
9658
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
8846
9659
 
8847
9660
  `;
8848
- artifacts.push(fillArtifact("app/browser/types.ts", "source", "appBrowserTypes", {}, "browser"), fillArtifact("app/browser/constants.ts", "source", "appBrowserConstants", { nameConstant }, "browser"), fillArtifact("app/browser/errors.ts", "source", "appBrowserErrors", {}, "browser"), fillArtifact("app/browser/parsers.ts", "source", "appBrowserParsers", {}, "browser"), fillArtifact("app/browser/factories.ts", "source", "appBrowserFactories", { nameImport }, "browser"), fillArtifact("app/browser/index.ts", "source", "appBrowserIndex", {}, "browser"), fillArtifact("app/browser/main.ts", "source", "appBrowserMain", {}, "browser"), fillArtifact("app/browser/ApplicationView.vue", "source", "appBrowserView", {}, "browser"), fillArtifact("app/browser/index.html", "source", "appBrowserHtml", { name: escapeHtmlText(spec.name) }, "browser"), fillArtifact("app/browser/env.d.ts", "source", "appBrowserEnv", {}, "browser"));
9661
+ artifacts.push(fillArtifact("app/browser/types.ts", "source", "appBrowserTypes", { application: hasShowcase && !hasCore ? `
9662
+ /** The identity the root view renders. */
9663
+ ${EXPORT_KEYWORD} interface Application {
9664
+ readonly name: string
9665
+ }
9666
+ ` : "" }, "browser"), fillArtifact("app/browser/constants.ts", "source", "appBrowserConstants", { nameConstant }, "browser"), fillArtifact("app/browser/errors.ts", "source", "appBrowserErrors", {}, "browser"), fillArtifact("app/browser/parsers.ts", "source", "appBrowserParsers", {}, "browser"), fillArtifact("app/browser/factories.ts", "source", "appBrowserFactories", {
9667
+ nameImport: hasBoundary ? "import { APP_NAME, readApplicationHealth } from '@app/core'" : nameImport,
9668
+ seedImport: hasShowcase ? "import { seedApplication } from './seeders.js'\n" : "",
9669
+ showcase: hasShowcase ? `
9670
+ /**
9671
+ * Mount the showcase over its seeded, inert identity.
9672
+ *
9673
+ * @param target - The browser element or selector that receives the showcase.
9674
+ * @returns The mounted Vue application.
9675
+ *
9676
+ * @remarks
9677
+ * The showcase mounts the same {@link createBrowserApplication} root the shipped entry
9678
+ * mounts, so the two differ in exactly one expression — where the props come from. This
9679
+ * one reads {@link seedApplication}; the application reads ${showcaseSource}.
9680
+ *
9681
+ * @example
9682
+ * \`\`\`ts
9683
+ * import { mountShowcaseApplication } from '@app/browser'
9684
+ *
9685
+ * mountShowcaseApplication('#app')
9686
+ * \`\`\`
9687
+ */
9688
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} mountShowcaseApplication(target: string | Element): App<Element> {
9689
+ const seed = seedApplication()
9690
+ const application = createBrowserApplication({ name: seed.name })
9691
+ application.mount(target)
9692
+ return application
9693
+ }
9694
+ ` : "",
9695
+ boundary: hasBoundary ? `
9696
+ /**
9697
+ * Mount the application over its real server boundary.
9698
+ *
9699
+ * @param target - The browser element or selector that receives the application.
9700
+ * @returns The mounted Vue application, after one health read settles.
9701
+ *
9702
+ * @remarks
9703
+ * One health read runs before the mount, so the root view renders the identity the
9704
+ * running server reported. An unreachable or off-contract boundary yields \`undefined\`
9705
+ * and the application falls back to its own configuration rather than failing to mount.
9706
+ *
9707
+ * @example
9708
+ * \`\`\`ts
9709
+ * import { mountBrowserApplication } from '@app/browser'
9710
+ *
9711
+ * await mountBrowserApplication('#app')
9712
+ * \`\`\`
9713
+ */
9714
+ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} mountBrowserApplication(target: string | Element): Promise<App<Element>> {
9715
+ const seed = (await readApplicationHealth(window.location.origin)) ?? { name: APP_NAME }
9716
+ const application = createBrowserApplication({ name: seed.name })
9717
+ application.mount(target)
9718
+ return application
9719
+ }
9720
+ ` : ""
9721
+ }, "browser"), fillArtifact("app/browser/index.ts", "source", "appBrowserIndex", { seeders: hasShowcase ? "export * from './seeders.js'\n" : "" }, "browser"), fillArtifact("app/browser/main.ts", "source", "appBrowserMain", hasBoundary ? {
9722
+ factory: "mountBrowserApplication",
9723
+ mount: `void mountBrowserApplication('#app').catch(() => {
9724
+ console.error('[ERROR] Browser application failed')
9725
+ })`
9726
+ } : {
9727
+ factory: "createBrowserApplication",
9728
+ mount: "createBrowserApplication().mount('#app')"
9729
+ }, "browser"), fillArtifact("app/browser/ApplicationView.vue", "source", "appBrowserView", {}, "browser"), fillArtifact("app/browser/index.html", "source", "appBrowserHtml", { name: escapeHtmlText(spec.name) }, "browser"), fillArtifact("app/browser/env.d.ts", "source", "appBrowserEnv", {}, "browser"));
9730
+ if (hasShowcase) artifacts.push(fillArtifact("app/browser/seeders.ts", "source", "appBrowserSeeders", {
9731
+ applicationImport: hasCore ? "import type { Application } from '@app/core'" : "import type { Application } from './types.js'",
9732
+ nameImport
9733
+ }, "browser"), fillArtifact("app/browser/showcase.ts", "source", "appBrowserShowcase", {}, "browser"), fillArtifact("app/browser/showcase.html", "source", "appBrowserShowcaseHtml", { name: escapeHtmlText(spec.name) }, "browser"));
8849
9734
  }
8850
9735
  if (spec.app.includes("server")) {
8851
9736
  const nameImport = hasCore ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from './constants.js'";
@@ -8853,7 +9738,19 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
8853
9738
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
8854
9739
 
8855
9740
  `;
8856
- artifacts.push(fillArtifact("app/server/types.ts", "source", "appServerTypes", {}, "server"), fillArtifact("app/server/constants.ts", "source", "appServerConstants", { nameConstant }, "server"), fillArtifact("app/server/errors.ts", "source", "appServerErrors", {}, "server"), fillArtifact("app/server/parsers.ts", "source", "appServerParsers", {}, "server"), fillArtifact("app/server/handlers.ts", "source", "appServerHandlers", { nameImport }, "server"), fillArtifact("app/server/ApplicationServer.ts", "source", "appServerEntity", {}, "server"), fillArtifact("app/server/factories.ts", "source", "appServerFactories", {}, "server"), fillArtifact("app/server/ApplicationServerRunner.ts", "source", "appServerRunner", {}, "server"), fillArtifact("app/server/index.ts", "source", "appServerIndex", {}, "server"), fillArtifact("app/server/main.ts", "source", "appServerMain", {}, "server"));
9741
+ artifacts.push(fillArtifact("app/server/types.ts", "source", "appServerTypes", { record: hasBoundary ? "" : `/** The application record returned by the health route. */
9742
+ ${EXPORT_KEYWORD} interface ApplicationRecord {
9743
+ readonly name: string
9744
+ readonly status: 'ok'
9745
+ }
9746
+
9747
+ ` }, "server"), fillArtifact("app/server/constants.ts", "source", "appServerConstants", {
9748
+ nameConstant,
9749
+ health: hasBoundary ? "" : healthConstants
9750
+ }, "server"), fillArtifact("app/server/errors.ts", "source", "appServerErrors", {}, "server"), fillArtifact("app/server/parsers.ts", "source", "appServerParsers", {}, "server"), fillArtifact("app/server/routes.ts", "source", "appServerRoutes", { healthImport: hasBoundary ? "import { APP_HEALTH_METHOD, APP_HEALTH_PATH } from '@app/core'" : "import { APP_HEALTH_METHOD, APP_HEALTH_PATH } from './constants.js'" }, "server"), fillArtifact("app/server/handlers.ts", "source", "appServerHandlers", {
9751
+ recordImport: hasBoundary ? "import type { ApplicationRecord } from '@app/core'" : "import type { ApplicationRecord } from './types.js'",
9752
+ nameImport
9753
+ }, "server"), fillArtifact("app/server/ApplicationServer.ts", "source", "appServerEntity", {}, "server"), fillArtifact("app/server/factories.ts", "source", "appServerFactories", {}, "server"), fillArtifact("app/server/ApplicationServerRunner.ts", "source", "appServerRunner", { nameImport }, "server"), fillArtifact("app/server/index.ts", "source", "appServerIndex", {}, "server"), fillArtifact("app/server/main.ts", "source", "appServerMain", {}, "server"));
8857
9754
  }
8858
9755
  return artifacts;
8859
9756
  }
@@ -8926,7 +9823,7 @@ function paritySpecifiers(spec) {
8926
9823
  function testArtifacts(spec, pascal) {
8927
9824
  const hasBrowser = spec.src.includes("browser") || spec.app.includes("browser");
8928
9825
  const hasVue = spec.app.includes("browser");
8929
- const browserPolicyImport = hasBrowser ? "\nimport { chromium } from 'playwright'\nimport { isBrowserExecutable, resolveBrowser, SYSTEM_BROWSER_CHANNELS } from '../vite.config.js'" : "";
9826
+ const machinery = viteMachinery(spec.src, spec.app, spec.bin, spec.showcase);
8930
9827
  const vuePolicyImport = hasVue ? "\nimport { parse as parseVue } from 'vue/compiler-sfc'" : "";
8931
9828
  const workspacePolicyAssertion = hasVue ? `expect(
8932
9829
  inspectCodingWorkspace(process.cwd(), (path, content) => {
@@ -8937,37 +9834,219 @@ function testArtifacts(spec, pascal) {
8937
9834
  )
8938
9835
  }),
8939
9836
  ).toEqual([])` : "expect(inspectCodingWorkspace(process.cwd())).toEqual([])";
8940
- const browserPolicyTest = hasBrowser ? `
9837
+ const configNames = [
9838
+ "containedPath",
9839
+ "environmentPathError",
9840
+ "environmentSourceError",
9841
+ "resolveWorkspacePath",
9842
+ "workspacePath"
9843
+ ];
9844
+ if (machinery.output) configNames.push("enforceOutputPath");
9845
+ if (hasBrowser) configNames.push("isBrowserExecutable", "resolveBrowser", "resolveManagedBrowser", "resolveSystemBrowser", "SYSTEM_BROWSER_CHANNELS");
9846
+ if (hasVue) configNames.push("hasSecurityPrologue", "HTML_SECURITY_META", "HTML_SECURITY_POLICY");
9847
+ const configImports = [
9848
+ ...hasVue ? ["import { readFileSync } from 'node:fs'"] : [],
9849
+ ...hasBrowser ? ["import { chromium } from 'playwright'"] : [],
9850
+ "import { describe, expect, it } from 'vitest'",
9851
+ "import {",
9852
+ ...configNames.map((name) => `\t${name},`),
9853
+ "} from '../../vite.config.js'"
9854
+ ].join("\n");
9855
+ const configCases = [];
9856
+ if (machinery.output) configCases.push(`
9857
+
9858
+ it('contains build output in its exact workspace directory', () => {
9859
+ const expected = resolveWorkspacePath('dist/config-proof')
9860
+
9861
+ expect(() => enforceOutputPath(expected, expected)).not.toThrow()
9862
+ expect(() => enforceOutputPath(resolveWorkspacePath('dist/other'), expected)).toThrow(
9863
+ 'exact configured workspace directory',
9864
+ )
9865
+ expect(() =>
9866
+ enforceOutputPath(
9867
+ resolveWorkspacePath('../config-proof'),
9868
+ resolveWorkspacePath('../config-proof'),
9869
+ ),
9870
+ ).toThrow('remain inside the workspace')
9871
+ })`);
9872
+ if (hasBrowser) configCases.push(`
8941
9873
 
8942
9874
  it('resolves only a real managed executable or stable system browser channel', () => {
8943
- const options = resolveBrowser(chromium.executablePath(), process.platform, process.env)
8944
- let valid = options === undefined
8945
- if (options !== undefined) {
8946
- const channel = options.launchOptions?.channel
8947
- valid =
8948
- channel === undefined
8949
- ? isBrowserExecutable(options.launchOptions?.executablePath ?? chromium.executablePath())
8950
- : SYSTEM_BROWSER_CHANNELS.some((browser) => browser.channel === channel)
8951
- }
8952
- expect(valid).toBe(true)
8953
- })` : "";
8954
- const artifacts = [fillArtifact("tests/setup.ts", "tests", "setup", {}), fillArtifact("tests/policy.test.ts", "tests", "policyTest", {
8955
- browserPolicySpecifier: "",
8956
- browserPolicyImport,
8957
- browserPolicyTest,
8958
- vuePolicyImport,
8959
- workspacePolicyAssertion
8960
- })];
8961
- if (spec.src.includes("server") || spec.app.includes("server")) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
9875
+ const pinned = chromium.executablePath()
9876
+ const managed = resolveManagedBrowser(pinned)
9877
+ const channel = resolveSystemBrowser(process.platform, process.env)
9878
+ const options = resolveBrowser(pinned, process.platform, process.env)
9879
+ const expected =
9880
+ managed === undefined
9881
+ ? channel === undefined
9882
+ ? undefined
9883
+ : { launchOptions: { channel } }
9884
+ : managed === pinned
9885
+ ? {}
9886
+ : { launchOptions: { executablePath: managed } }
9887
+ const executable = managed === undefined || isBrowserExecutable(managed)
9888
+ const stable =
9889
+ managed !== undefined ||
9890
+ channel === undefined ||
9891
+ SYSTEM_BROWSER_CHANNELS.some((browser) => browser.channel === channel)
9892
+
9893
+ expect(options).toEqual(expected)
9894
+ expect(executable).toBe(true)
9895
+ expect(stable).toBe(true)
9896
+ })`);
9897
+ if (hasVue) configCases.push(`
9898
+
9899
+ it('preserves the generated browser security prologue', () => {
9900
+ const document = readFileSync(resolveWorkspacePath('app/browser/index.html'), 'utf8')
9901
+
9902
+ expect(hasSecurityPrologue(document, HTML_SECURITY_META)).toBe(true)
9903
+ expect(HTML_SECURITY_META).toContain(HTML_SECURITY_POLICY)
9904
+ })`);
9905
+ const artifacts = [
9906
+ fillArtifact("tests/setup.ts", "tests", "setup", {
9907
+ eventImport: spec.app.includes("server") ? "import type { EmitterInterface, EventMap } from '@orkestrel/emitter'\n\n" : "",
9908
+ eventHelper: spec.app.includes("server") ? `
9909
+ /** Wait for one typed event occurrence and return its argument tuple. */
9910
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} waitForEvent<TMap extends EventMap, K extends keyof TMap>(
9911
+ emitter: EmitterInterface<TMap>,
9912
+ event: K,
9913
+ ): Promise<TMap[K]> {
9914
+ return new Promise((resolvePromise) => emitter.once(event, (...args) => resolvePromise(args)))
9915
+ }
9916
+ ` : ""
9917
+ }),
9918
+ fillArtifact("tests/policy.test.ts", "tests", "policyTest", {
9919
+ vuePolicyImport,
9920
+ workspacePolicyAssertion
9921
+ }),
9922
+ fillArtifact("tests/config/vite.test.ts", "tests", "configTest", {
9923
+ imports: configImports,
9924
+ cases: configCases.join("")
9925
+ })
9926
+ ];
9927
+ if (spec.src.includes("server") || spec.app.includes("server") || spec.services.length > 0) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
9928
+ if (spec.services.length > 0) artifacts.push(fillArtifact("tests/config/services.test.ts", "tests", "serviceConformance", { services: renderStringArray(spec.services, " ", "const declared = ", "") }, "server"));
8962
9929
  if (spec.src.includes("browser") || spec.app.includes("browser")) artifacts.push(fillArtifact("tests/setupBrowser.ts", "tests", "setupBrowser", {}, "browser"));
8963
- if (spec.app.includes("core")) artifacts.push(fillArtifact("tests/app/core/factories.test.ts", "tests", "appCoreTest", {}, "core"));
9930
+ const hasBoundary = hasApplicationBoundary(spec);
9931
+ const hasShowcase = hasApplicationShowcase(spec);
9932
+ if (spec.app.includes("core")) artifacts.push(fillArtifact("tests/app/core/factories.test.ts", "tests", "appCoreTest", {
9933
+ guardImport: hasBoundary ? " isApplicationRecord,\n" : "",
9934
+ readImport: hasBoundary ? " readApplicationHealth,\n" : "",
9935
+ boundary: hasBoundary ? `
9936
+
9937
+ describe('shared application health boundary', () => {
9938
+ it('accepts the shared record and refuses every off-contract value', () => {
9939
+ expect(isApplicationRecord({ name: APP_NAME, status: 'ok' })).toBe(true)
9940
+ expect(isApplicationRecord({ name: ' ', status: 'ok' })).toBe(true)
9941
+ for (const value of [
9942
+ null,
9943
+ [],
9944
+ 'ok',
9945
+ { name: APP_NAME },
9946
+ { name: '', status: 'ok' },
9947
+ { name: 1, status: 'ok' },
9948
+ { name: APP_NAME, status: 'down' },
9949
+ ]) {
9950
+ expect(isApplicationRecord(value)).toBe(false)
9951
+ }
9952
+ const revocable = Proxy.revocable({}, {})
9953
+ revocable.revoke()
9954
+ expect(isApplicationRecord(revocable.proxy)).toBe(false)
9955
+ })
9956
+
9957
+ it('refuses a malformed origin before reaching the network', async () => {
9958
+ expect(await readApplicationHealth('not-an-origin')).toBeUndefined()
9959
+ })
9960
+ })` : ""
9961
+ }, "core"));
8964
9962
  if (spec.app.includes("browser")) {
8965
9963
  const browserTestNameImport = spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/browser'";
8966
- artifacts.push(fillArtifact("tests/app/browser/factories.test.ts", "tests", "appBrowserTest", { browserTestNameImport }, "browser"));
9964
+ artifacts.push(fillArtifact("tests/app/browser/factories.test.ts", "tests", "appBrowserTest", {
9965
+ browserTestNameImport,
9966
+ showcaseImport: hasShowcase ? " mountShowcaseApplication,\n" : "",
9967
+ entryImport: `${hasShowcase ? " seedApplication,\n" : ""}${hasBoundary ? " mountBrowserApplication,\n" : ""}`,
9968
+ showcase: hasShowcase ? `
9969
+
9970
+ describe('mountShowcaseApplication', () => {
9971
+ it('mounts the shipped root view over one frozen, inert seed', () => {
9972
+ const element = buildElement()
9973
+ const seeded = seedApplication()
9974
+ const application = mountShowcaseApplication(element)
9975
+ try {
9976
+ expect(element.textContent).toContain(seeded.name)
9977
+ expect(seeded).toEqual(seedApplication())
9978
+ expect(seeded).not.toBe(seedApplication())
9979
+ expect(Object.isFrozen(seeded)).toBe(true)
9980
+ } finally {
9981
+ application.unmount()
9982
+ element.remove()
9983
+ }
9984
+ })
9985
+ })` : "",
9986
+ boundary: hasBoundary ? `
9987
+
9988
+ describe('mountBrowserApplication', () => {
9989
+ it('mounts the configured identity when the boundary answers off-contract', async () => {
9990
+ const element = buildElement()
9991
+ const application = await mountBrowserApplication(element)
9992
+ try {
9993
+ expect(element.textContent).toContain(APP_NAME)
9994
+ } finally {
9995
+ application.unmount()
9996
+ element.remove()
9997
+ }
9998
+ })
9999
+ })` : ""
10000
+ }, "browser"));
8967
10001
  }
8968
10002
  if (spec.app.includes("server")) {
8969
- const testNameImport = spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/server'";
8970
- artifacts.push(fillArtifact("tests/app/server/ApplicationServer.test.ts", "tests", "appServerTest", { testNameImport }, "server"), fillArtifact("tests/app/server/parsers.test.ts", "tests", "appServerParsersTest", {}, "server"));
10003
+ const testNameImport = hasBoundary ? `import {
10004
+ APP_HEALTH_METHOD,
10005
+ APP_HEALTH_PATH,
10006
+ APP_NAME,
10007
+ isApplicationRecord,
10008
+ readApplicationHealth,
10009
+ } from '@app/core'` : spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/server'";
10010
+ const serverImport = hasBoundary ? `import {
10011
+ ApplicationServerRunner,
10012
+ createApplicationDispatcher,
10013
+ createApplicationServer,
10014
+ } from '@app/server'` : `import {
10015
+ APP_HEALTH_METHOD,
10016
+ APP_HEALTH_PATH,
10017
+ ApplicationServerRunner,
10018
+ createApplicationDispatcher,
10019
+ createApplicationServer,
10020
+ } from '@app/server'`;
10021
+ artifacts.push(fillArtifact("tests/app/server/ApplicationServer.test.ts", "tests", "appServerTest", {
10022
+ testNameImport,
10023
+ serverImport,
10024
+ boundary: hasBoundary ? `
10025
+
10026
+ describe('shared application boundary', () => {
10027
+ it('answers the shared record and translates it into the shared identity', async () => {
10028
+ const server = createApplicationServer({ server: { host: '127.0.0.1', port: 0 } })
10029
+ try {
10030
+ await server.start()
10031
+ const url = server.url
10032
+ if (url === undefined) throw new Error('Expected a bound application URL')
10033
+ const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
10034
+ const record: unknown = await response.json()
10035
+
10036
+ expect(isApplicationRecord(record)).toBe(true)
10037
+ expect(await readApplicationHealth(url)).toEqual({ name: APP_NAME })
10038
+ } finally {
10039
+ await server.destroy()
10040
+ }
10041
+ })
10042
+
10043
+ it('reads undefined from a released loopback port', async () => {
10044
+ const port = await reserveLoopbackPort()
10045
+
10046
+ expect(await readApplicationHealth(\`http://127.0.0.1:\${port}\`)).toBeUndefined()
10047
+ })
10048
+ })` : ""
10049
+ }, "server"), fillArtifact("tests/app/server/parsers.test.ts", "tests", "appServerParsersTest", {}, "server"));
8971
10050
  }
8972
10051
  const inlineExplicitInstance = `instance: ${pascal}Interface = new ${pascal}({ id: 'example' })`;
8973
10052
  const multilineExplicitInstance = `instance: ${pascal}Interface = new ${pascal}({
@@ -9011,12 +10090,12 @@ function testArtifacts(spec, pascal) {
9011
10090
  }
9012
10091
  artifacts.push(fillArtifact("tests/setupGuides.ts", "tests", "setupGuides", {
9013
10092
  specifiers: paritySpecifiers(spec),
9014
- walkDirs: [
9015
- ...spec.src.length > 0 ? ["'src'"] : [],
9016
- ...spec.app.length > 0 ? ["'app'"] : [],
9017
- "'guides'",
9018
- "'tests'"
9019
- ].map((directory) => `\t${directory},`).join("\n")
10093
+ walkDirs: renderStringArray([
10094
+ ...spec.src.length > 0 ? ["src"] : [],
10095
+ ...spec.app.length > 0 ? ["app"] : [],
10096
+ "guides",
10097
+ "tests"
10098
+ ], "", "export const GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze(", ")")
9020
10099
  }), fillArtifact("tests/guides/src/parity.test.ts", "tests", "parityTest", { name: spec.name }));
9021
10100
  return artifacts;
9022
10101
  }
@@ -9065,6 +10144,8 @@ function guideMemberTable(category, members) {
9065
10144
  */
9066
10145
  function guideUsage(spec, pascal) {
9067
10146
  const examples = [];
10147
+ const hasBoundary = hasApplicationBoundary(spec);
10148
+ const hasShowcase = hasApplicationShowcase(spec);
9068
10149
  if (spec.src.length > 0) examples.push(`\`\`\`ts
9069
10150
  import { create${pascal} } from '@orkestrel/${spec.name}'
9070
10151
 
@@ -9081,6 +10162,24 @@ import {
9081
10162
  ${CONST_KEYWORD} name = parseApplicationName(' ${spec.name} ')
9082
10163
  ${CONST_KEYWORD} application = createApplication(name)
9083
10164
  isApplicationError(new ApplicationError('CONFIG', 'invalid')) // true
10165
+ \`\`\``);
10166
+ if (hasBoundary) examples.push(`\`\`\`ts
10167
+ import type { ApplicationRecord } from '@app/core'
10168
+ import {
10169
+ APP_HEALTH_METHOD,
10170
+ APP_HEALTH_PATH,
10171
+ APP_HEALTH_TIMEOUT,
10172
+ isApplicationRecord,
10173
+ readApplicationHealth,
10174
+ } from '@app/core'
10175
+
10176
+ APP_HEALTH_METHOD // 'GET'
10177
+ APP_HEALTH_PATH // '/health'
10178
+ APP_HEALTH_TIMEOUT // 5000
10179
+ ${CONST_KEYWORD} healthy: ApplicationRecord = { name: '${spec.name}', status: 'ok' }
10180
+ isApplicationRecord(healthy) // true
10181
+ isApplicationRecord({ name: '${spec.name}', status: 'down' }) // false
10182
+ await readApplicationHealth('http://127.0.0.1:3000') // { name: '${spec.name}' } or undefined
9084
10183
  \`\`\``);
9085
10184
  if (spec.app.includes("browser")) examples.push(`\`\`\`ts
9086
10185
  import {
@@ -9096,27 +10195,36 @@ ${CONST_KEYWORD} browserOptions = parseBrowserApplicationOptions({
9096
10195
  ${CONST_KEYWORD} browser = createBrowserApplication(browserOptions)
9097
10196
  browser.mount('#app')
9098
10197
  isBrowserApplicationError(new BrowserApplicationError('CONFIG', 'invalid')) // true
10198
+ \`\`\``);
10199
+ if (hasShowcase) examples.push(`\`\`\`ts
10200
+ import { mountShowcaseApplication, seedApplication } from '@app/browser'
10201
+
10202
+ ${CONST_KEYWORD} seed = seedApplication()
10203
+ ${CONST_KEYWORD} showcase = mountShowcaseApplication('#app')
10204
+ seed.name // '${spec.name} showcase'
10205
+ showcase.unmount()
10206
+ \`\`\``);
10207
+ if (hasBoundary) examples.push(`\`\`\`ts
10208
+ import { mountBrowserApplication } from '@app/browser'
10209
+
10210
+ ${CONST_KEYWORD} application = await mountBrowserApplication('#app')
10211
+ application.unmount()
9099
10212
  \`\`\``);
9100
10213
  if (spec.app.includes("server")) examples.push(`\`\`\`ts
9101
- import { once } from 'node:events'
9102
- import { createServer } from 'node:http'
10214
+ ${hasBoundary ? `import type { ApplicationRecord } from '@app/core'
10215
+ import type { ApplicationState } from '@app/server'
10216
+ import { APP_HEALTH_METHOD, APP_HEALTH_PATH, isApplicationRecord } from '@app/core'` : "import type { ApplicationRecord, ApplicationState } from '@app/server'"}
9103
10217
  import {
9104
- APP_HEALTH_METHOD,
10218
+ ${hasBoundary ? "" : ` APP_HEALTH_METHOD,
9105
10219
  APP_HEALTH_PATH,
9106
- APP_HEADERS_TIMEOUT,
9107
- APP_HOST_LABEL_PATTERN,
9108
- APP_KEEP_ALIVE_TIMEOUT,
9109
- APP_MAX_CONNECTIONS,
9110
- APP_MAX_HEADERS,
9111
- APP_MAX_REQUESTS_PER_SOCKET,
9112
- APP_REQUEST_TIMEOUT,
10220
+ `} APP_HOST_LABEL_PATTERN,
9113
10221
  APP_NUMERIC_HOST_PATTERN,
9114
- ApplicationServer,
9115
10222
  ApplicationServerError,
9116
10223
  DEFAULT_APP_START_TIMEOUT,
9117
10224
  MAX_APP_START_TIMEOUT,
10225
+ createApplicationDispatcher,
9118
10226
  createApplicationServer,
9119
- handleApplicationRequest,
10227
+ handleApplicationHealth,
9120
10228
  isApplicationServerError,
9121
10229
  parseApplicationHost,
9122
10230
  parseApplicationPort,
@@ -9128,41 +10236,51 @@ import {
9128
10236
  ${CONST_KEYWORD} host = parseApplicationHost('127.0.0.1')
9129
10237
  ${CONST_KEYWORD} port = parseApplicationPort('0')
9130
10238
  ${CONST_KEYWORD} timeout = parseApplicationStartTimeout('5000')
9131
- ${CONST_KEYWORD} options = parseApplicationServerOptions({ host, port, timeout })
10239
+ ${CONST_KEYWORD} options = parseApplicationServerOptions({ server: { host, port, timeout } })
10240
+ parseApplicationStartTimeout(String(DEFAULT_APP_START_TIMEOUT)) // 10000
10241
+ MAX_APP_START_TIMEOUT // 300000
9132
10242
  APP_HOST_LABEL_PATTERN.test('api') // true
9133
10243
  APP_NUMERIC_HOST_PATTERN.test('999.999.999.999') // true (and therefore rejected as a host)
9134
- APP_HEALTH_METHOD // 'GET'
9135
- APP_HEALTH_PATH // '/'
9136
- APP_MAX_CONNECTIONS // 16
9137
- APP_MAX_HEADERS // 100
9138
- APP_HEADERS_TIMEOUT // 10000
9139
- APP_REQUEST_TIMEOUT // 30000
9140
- APP_KEEP_ALIVE_TIMEOUT // 5000
9141
- APP_MAX_REQUESTS_PER_SOCKET // 100
9142
- DEFAULT_APP_START_TIMEOUT // 10000
9143
- MAX_APP_START_TIMEOUT // 300000
9144
- ${CONST_KEYWORD} handlerServer = createServer(handleApplicationRequest)
9145
- handlerServer.listen(0, host)
9146
- await once(handlerServer, 'listening')
9147
- ${CONST_KEYWORD} handlerClosed = once(handlerServer, 'close')
9148
- handlerServer.close()
9149
- await handlerClosed
9150
-
9151
- ${CONST_KEYWORD} error = new ApplicationServerError('CONFIG', 'invalid')
9152
- isApplicationServerError(error) // true
9153
- reportApplicationServerError(error) // writes only a stable CONFIG diagnostic
9154
- new ApplicationServer(options) // stopped entity
10244
+ ${CONST_KEYWORD} state: ApplicationState = { connection: { encrypted: false } }
10245
+ ${CONST_KEYWORD} record: ApplicationRecord = { name: '${spec.name}', status: 'ok' }
10246
+ ${CONST_KEYWORD} dispatcher = createApplicationDispatcher()
10247
+ try {
10248
+ ${CONST_KEYWORD} response = await dispatcher.handle(
10249
+ new Request(\`http://application.test\${APP_HEALTH_PATH}\`, { method: APP_HEALTH_METHOD }),
10250
+ state,
10251
+ )
10252
+ ${CONST_KEYWORD} health = handleApplicationHealth()
10253
+ ${CONST_KEYWORD} encoded = Response.json(record)
10254
+ ${hasBoundary ? `${CONST_KEYWORD} value: unknown = await health.clone().json()
10255
+ isApplicationRecord(value) // true
10256
+ ` : ""}if (!response.ok || !health.ok || !encoded.ok) throw new Error('Application health failed')
10257
+ } finally {
10258
+ dispatcher.destroy()
10259
+ }
10260
+
10261
+ ${CONST_KEYWORD} failure: unknown = new ApplicationServerError('CONFIG', 'invalid')
10262
+ if (isApplicationServerError(failure)) {
10263
+ reportApplicationServerError(failure) // writes only a stable CONFIG diagnostic
10264
+ }
9155
10265
 
9156
10266
  ${CONST_KEYWORD} server = createApplicationServer(options)
9157
10267
  ${CONST_KEYWORD} controller = new AbortController()
9158
10268
  await server.start(controller.signal)
9159
10269
  await server.stop()
10270
+ await server.destroy()
9160
10271
  \`\`\`
9161
10272
 
9162
10273
  \`\`\`ts
9163
- import { ApplicationServerRunner } from '@app/server'
9164
-
9165
- ${CONST_KEYWORD} runner = new ApplicationServerRunner({ port: 0 })
10274
+ import type { ApplicationServerRunnerEventMap, ApplicationServerRunnerOptions } from '@app/server'
10275
+ import { ApplicationServerRunner, createApplicationServer } from '@app/server'
10276
+
10277
+ ${CONST_KEYWORD} event: keyof ApplicationServerRunnerEventMap = 'ready'
10278
+ ${CONST_KEYWORD} observe: ApplicationServerRunnerOptions = { on: { fail: () => undefined } }
10279
+ ${CONST_KEYWORD} runner = new ApplicationServerRunner(
10280
+ createApplicationServer({ server: { port: 0 } }),
10281
+ observe,
10282
+ )
10283
+ runner.emitter.once(event, (url) => console.log(url))
9166
10284
  runner.start() // process owns shutdown signals
9167
10285
  await runner.stop()
9168
10286
  \`\`\`
@@ -9170,7 +10288,7 @@ await runner.stop()
9170
10288
  \`\`\`ts
9171
10289
  import { startApplicationServer } from '@app/server'
9172
10290
 
9173
- ${CONST_KEYWORD} processRunner = startApplicationServer({ port: 0 })
10291
+ ${CONST_KEYWORD} processRunner = startApplicationServer({ server: { port: 0 } })
9174
10292
  await processRunner.stop()
9175
10293
  \`\`\``);
9176
10294
  return examples.join("\n\n");
@@ -9193,15 +10311,23 @@ ${alignTable([
9193
10311
  "Method",
9194
10312
  "Returns",
9195
10313
  "Behavior"
9196
- ], [[
9197
- "`start`",
9198
- "`Promise<void>`",
9199
- "Serialize in call order; start only when stopped, and repeat safely when already listening. The optional `AbortSignal` and bounded startup timeout cancel pending name resolution/listen work. Rejects with `ApplicationServerError` code `LIFECYCLE` when startup fails, times out, or the caller aborts."
9200
10314
  ], [
9201
- "`stop`",
9202
- "`Promise<void>`",
9203
- "Cancel every pending start before its queued stop, force active and idle connections closed, and repeat safely when already stopped. Rejects with `ApplicationServerError` code `LIFECYCLE` when closing fails."
9204
- ]])}
10315
+ [
10316
+ "`start`",
10317
+ "`Promise<void>`",
10318
+ "Bind the installed `@orkestrel/server` substrate when idle or stopped. The optional `AbortSignal` and bounded startup timeout cancel pending binding. Rejects with `ApplicationServerError` code `LIFECYCLE` when startup fails, times out, or the caller aborts."
10319
+ ],
10320
+ [
10321
+ "`stop`",
10322
+ "`Promise<void>`",
10323
+ "Drain and stop the installed server; repeated calls while stopped are safe. Rejects with `ApplicationServerError` code `LIFECYCLE` when closing fails."
10324
+ ],
10325
+ [
10326
+ "`destroy`",
10327
+ "`Promise<void>`",
10328
+ "Perform terminal idempotent teardown through the installed server lifecycle, then destroy its owned dispatcher. Rejects with `ApplicationServerError` code `LIFECYCLE` when server teardown fails."
10329
+ ]
10330
+ ])}
9205
10331
 
9206
10332
  #### \`ApplicationServerRunnerInterface\`
9207
10333
 
@@ -9212,17 +10338,18 @@ ${alignTable([
9212
10338
  ], [[
9213
10339
  "`start`",
9214
10340
  "`void`",
9215
- "Register one idempotent set of SIGINT/SIGTERM cleanup listeners, start the server, and translate asynchronous startup failures into a non-zero process exit code."
10341
+ "Register one generation-owned set of SIGINT/SIGTERM cleanup listeners, queue the substrate start behind any shutdown already in flight, emit `ready` after binding, and emit `fail` for a current lifecycle failure."
9216
10342
  ], [
9217
10343
  "`stop`",
9218
10344
  "`Promise<void>`",
9219
- "Release both process listeners before stopping the server; repeated calls are safe and lifecycle failures reject."
10345
+ "Abort a startup still in flight and release both process listeners, then wait for that startup to settle before stopping the server; concurrent calls join one substrate stop, and lifecycle failures emit `fail` and reject."
9220
10346
  ]])}
9221
10347
 
9222
- The constructor validates direct options plus \`APP_HOST\`, \`APP_PORT\`, and
9223
- \`APP_START_TIMEOUT\` before allocating
9224
- a listener. Direct options must be an exact plain own-key data record containing only
9225
- \`host\`, \`port\`, and/or \`timeout\`; inherited properties, accessors, symbols, instances, proxies that
10348
+ The runner exposes its readonly \`emitter\`. \`ApplicationServerRunnerEventMap\` emits \`ready\` with the bound URL and \`fail\` with an \`unknown\` error. \`ApplicationServerRunnerOptions\` accepts initial \`on\` hooks and an emitter \`error\` handler; initial hooks run before the runner's own announcement and reporting listeners, so when no earlier failure set an exit code, a synchronous \`fail\` hook sees \`process.exitCode === undefined\` before the default reporter sets it to \`1\`. The default listeners preserve one exact \`[READY] <name> <url>\` stderr line and the stable redacted failure diagnostics. In-process consumers and tests park on runner events; a child process still observes the \`[READY]\` line because that byte stream is its process-boundary channel.
10349
+
10350
+ The application server constructor validates grouped direct options plus \`APP_HOST\`, \`APP_PORT\`, and
10351
+ \`APP_START_TIMEOUT\` before binding. Direct options must be an exact plain own-key data record containing only a
10352
+ \`server\` record with \`host\`, \`port\`, and/or \`timeout\`; inherited properties, accessors, symbols, instances, proxies that
9226
10353
  throw during reflection, and unknown keys fail closed. Invalid values throw
9227
10354
  \`ApplicationServerError\` code \`CONFIG\`; the default host is loopback and port \`0\` is
9228
10355
  supported for collision-free ephemeral allocation. Startup defaults to 10 seconds and accepts
@@ -9230,10 +10357,15 @@ only integer timeouts from 1 through 300,000 milliseconds. Lifecycle failures us
9230
10357
  \`LIFECYCLE\`; both may carry \`context.cause\` or \`context.value\`. Narrow caught values with
9231
10358
  \`isApplicationServerError\` before reading either field.
9232
10359
 
9233
- The generated server owns exactly \`GET /\`. It serializes
9234
- \`{ name: APP_NAME, status: 'ok' }\` as JSON with \`cache-control: no-store\`;
9235
- every other path returns deterministic plain-text \`404 Not Found\`, and every unsupported
9236
- method returns deterministic plain-text \`405 Method Not Allowed\` with \`Allow: GET\`.`;
10360
+ Before binding, \`url\` is \`undefined\`; after a successful start it reflects the real bound port,
10361
+ and it returns to \`undefined\` after stop or destroy. \`ApplicationState\` extends middleware's
10362
+ \`IdentifierState\` and adds only its \`connection\` property; there is no redundant \`listening\` member.
10363
+
10364
+ Each \`createApplicationDispatcher()\` call returns a fresh dispatcher that owns exactly \`GET /health\`
10365
+ and serializes the shared \`ApplicationRecord\` shape \`{ name: APP_NAME, status: 'ok' }\` as JSON. The
10366
+ server composes \`createBoundary()\`, \`createSecurity()\`, then \`createDeadline({ ms: timeout })\`
10367
+ around that owned dispatcher; standalone callers destroy theirs after use. Every other path returns
10368
+ \`404\`, and every unsupported method returns \`405\` with \`Allow: GET\`.`;
9237
10369
  }
9238
10370
  /**
9239
10371
  * Build links to every generated source and application test file.
@@ -9243,7 +10375,7 @@ method returns deterministic plain-text \`405 Method Not Allowed\` with \`Allow:
9243
10375
  * @returns A newline-separated Markdown test inventory.
9244
10376
  */
9245
10377
  function guideTests(spec, pascal) {
9246
- const tests = ["- [`tests/policy.test.ts`](../../tests/policy.test.ts) — filename placement and real browser capability probing."];
10378
+ const tests = ["- [`tests/policy.test.ts`](../../tests/policy.test.ts) — repository coding law and filename placement.", "- [`tests/config/vite.test.ts`](../../tests/config/vite.test.ts) — executable root Vite invariants and conditional browser capability."];
9247
10379
  for (const environment of spec.src) tests.push(`- [\`tests/src/${environment}/${pascal}.test.ts\`](../../tests/src/${environment}/${pascal}.test.ts) — entity boundaries.`, `- [\`tests/src/${environment}/factories.test.ts\`](../../tests/src/${environment}/factories.test.ts) — factory behavior.`);
9248
10380
  if (spec.app.includes("core")) tests.push("- [`tests/app/core/factories.test.ts`](../../tests/app/core/factories.test.ts) — host-independent identity behavior.");
9249
10381
  if (spec.app.includes("browser")) tests.push("- [`tests/app/browser/factories.test.ts`](../../tests/app/browser/factories.test.ts) — real-browser mount and cleanup.");
@@ -9396,12 +10528,15 @@ npm install @orkestrel/${blueprint.name}
9396
10528
  \`\`\`` : "This is a private application workspace and is not published to npm.",
9397
10529
  usage: guideUsage(blueprint, pascal)
9398
10530
  }));
9399
- if (selected.includes("orchestration")) artifacts.push({
9400
- path: ".github/workflows/ci.yml",
9401
- group: "orchestration",
9402
- origin: "computed",
9403
- content: ciWorkflow(blueprint)
9404
- });
10531
+ if (selected.includes("orchestration")) {
10532
+ artifacts.push({
10533
+ path: ".github/workflows/ci.yml",
10534
+ group: "orchestration",
10535
+ origin: "computed",
10536
+ content: ciWorkflow(blueprint)
10537
+ });
10538
+ if (blueprint.services.length > 0) artifacts.push(fillArtifact(SERVICE_SCRIPT_PATH, "orchestration", "serviceProvisioner", {}));
10539
+ }
9405
10540
  for (const path of selectHostPaths(HOST_PATHS, blueprint.name)) {
9406
10541
  const group = hostGroup(path);
9407
10542
  if (!selected.includes(group)) continue;
@@ -9525,9 +10660,10 @@ var Compiler = class Compiler {
9525
10660
  this.#emitter.emit("audit", result);
9526
10661
  return result;
9527
10662
  }
10663
+ const diff = diffPlan(scaffolding.plan, current);
9528
10664
  const result = {
9529
- ...diffPlan(scaffolding.plan, current),
9530
- questions: scaffolding.questions
10665
+ ...diff,
10666
+ questions: [...scaffolding.questions, ...diff.questions]
9531
10667
  };
9532
10668
  this.#emitter.emit("audit", result);
9533
10669
  return result;
@@ -9924,9 +11060,12 @@ function createBlueprint(data) {
9924
11060
  }
9925
11061
  //#endregion
9926
11062
  exports.APP_BROWSER_DEV_DEPENDENCIES = APP_BROWSER_DEV_DEPENDENCIES;
11063
+ exports.APP_DEV_DEPENDENCIES = APP_DEV_DEPENDENCIES;
9927
11064
  exports.APP_MATRIX = APP_MATRIX;
11065
+ exports.APP_SERVER_DEV_DEPENDENCIES = APP_SERVER_DEV_DEPENDENCIES;
9928
11066
  exports.BASE_DEV_DEPENDENCIES = BASE_DEV_DEPENDENCIES;
9929
11067
  exports.BIN_CONFIGS = BIN_CONFIGS;
11068
+ exports.CATALOG_AGENT_PATH = CATALOG_AGENT_PATH;
9930
11069
  exports.CATEGORIES = CATEGORIES;
9931
11070
  exports.CHECKOUT_ACTION_SHA = CHECKOUT_ACTION_SHA;
9932
11071
  exports.COMPILER_ID = COMPILER_ID;
@@ -9966,6 +11105,8 @@ exports.MAX_SERIALIZED_INPUT_BYTES = MAX_SERIALIZED_INPUT_BYTES;
9966
11105
  exports.MAX_TOTAL_ARTIFACT_BYTES = MAX_TOTAL_ARTIFACT_BYTES;
9967
11106
  exports.MINIMUM_NODE_VERSION = MINIMUM_NODE_VERSION;
9968
11107
  exports.NAME_PATTERN = NAME_PATTERN;
11108
+ exports.ORCHESTRATION_PATH_NAMES = ORCHESTRATION_PATH_NAMES;
11109
+ exports.ORCHESTRATION_PATH_PREFIXES = ORCHESTRATION_PATH_PREFIXES;
9969
11110
  exports.ORIGINS = ORIGINS;
9970
11111
  exports.ORKESTREL_RANGE_PATTERN = ORKESTREL_RANGE_PATTERN;
9971
11112
  exports.PlanManager = PlanManager;
@@ -10003,6 +11144,7 @@ exports.compareCodeUnit = compareCodeUnit;
10003
11144
  exports.computeColumnWidth = computeColumnWidth;
10004
11145
  exports.computeHash = computeHash;
10005
11146
  exports.configArtifacts = configArtifacts;
11147
+ exports.configViteProject = configViteProject;
10006
11148
  exports.contentByteLength = contentByteLength;
10007
11149
  exports.contentCodePoint = contentCodePoint;
10008
11150
  exports.contentToBytes = contentToBytes;
@@ -10032,6 +11174,8 @@ exports.guideMethods = guideMethods;
10032
11174
  exports.guideTests = guideTests;
10033
11175
  exports.guideUsage = guideUsage;
10034
11176
  exports.guidesViteProject = guidesViteProject;
11177
+ exports.hasApplicationBoundary = hasApplicationBoundary;
11178
+ exports.hasApplicationShowcase = hasApplicationShowcase;
10035
11179
  exports.hasBlueprintEnvironment = hasBlueprintEnvironment;
10036
11180
  exports.hasOnlyDataProperties = hasOnlyDataProperties;
10037
11181
  exports.hasValidArtifactBytes = hasValidArtifactBytes;
@@ -10062,6 +11206,7 @@ exports.isSyncReport = isSyncReport;
10062
11206
  exports.isWorkspaceName = isWorkspaceName;
10063
11207
  exports.manifestToDependencies = manifestToDependencies;
10064
11208
  exports.manifestToName = manifestToName;
11209
+ exports.matchesOrchestrationPath = matchesOrchestrationPath;
10065
11210
  exports.member = member;
10066
11211
  exports.memberShape = memberShape;
10067
11212
  exports.override = override;
@@ -10087,6 +11232,7 @@ exports.policyViteProject = policyViteProject;
10087
11232
  exports.rangeToFreshness = rangeToFreshness;
10088
11233
  exports.renderArray = renderArray;
10089
11234
  exports.renderObject = renderObject;
11235
+ exports.renderStringArray = renderStringArray;
10090
11236
  exports.renderValue = renderValue;
10091
11237
  exports.renderViteTest = renderViteTest;
10092
11238
  exports.rootTsconfig = rootTsconfig;