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