@orkestrel/scaffold 0.0.19 → 0.0.21

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 (31) hide show
  1. package/dist/bin/scaffold.js +15 -9
  2. package/dist/bin/scaffold.js.map +1 -1
  3. package/dist/host/CLAUDE.md +51 -24
  4. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +69 -59
  5. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +2 -2
  6. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +82 -0
  7. package/dist/host/claude/agents/application.md +34 -0
  8. package/dist/host/claude/agents/codex.md +7 -0
  9. package/dist/host/claude/agents/orkestrel.md +3 -3
  10. package/dist/host/claude/agents/researcher.md +31 -0
  11. package/dist/host/claude/agents/reviewer.md +5 -0
  12. package/dist/host/claude/agents/scout.md +25 -0
  13. package/dist/host/claude/rules/quality.md +10 -0
  14. package/dist/host/claude/rules/tests.md +10 -0
  15. package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
  16. package/dist/host/codex/agents/analyst.toml +5 -1
  17. package/dist/host/codex/agents/application.toml +23 -0
  18. package/dist/host/codex/agents/researcher.toml +22 -0
  19. package/dist/host/codex/agents/reviewer.toml +3 -1
  20. package/dist/host/codex/agents/scout.toml +18 -0
  21. package/dist/host/codex/config.toml +15 -6
  22. package/dist/host/guides/src/scaffold.md +120 -29
  23. package/dist/host/manifest.json +35 -0
  24. package/dist/host/tests/setupPolicy.ts +49 -11
  25. package/dist/src/core/index.cjs +1661 -856
  26. package/dist/src/core/index.cjs.map +1 -1
  27. package/dist/src/core/index.d.cts +84 -7
  28. package/dist/src/core/index.d.ts +84 -7
  29. package/dist/src/core/index.js +1656 -857
  30. package/dist/src/core/index.js.map +1 -1
  31. package/package.json +4 -2
@@ -229,15 +229,15 @@ var DEFAULT_VERSION = "0.0.1";
229
229
  /** The `engines.node` range the `blueprint` builder fills. */
230
230
  var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
231
231
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
232
- var SCAFFOLD_RANGE = "^0.0.19";
232
+ var SCAFFOLD_RANGE = "^0.0.21";
233
233
  /** Tooling versions shared by scaffold and every generated workspace. */
234
234
  var BASE_DEV_DEPENDENCIES = Object.freeze({
235
235
  "@microsoft/api-extractor": "^7.58.12",
236
236
  "@orkestrel/guide": "^0.0.8",
237
237
  "@orkestrel/scaffold": SCAFFOLD_RANGE,
238
238
  "@types/node": "^26.1.2",
239
- oxfmt: "^0.61.0",
240
- oxlint: "^1.76.0",
239
+ oxfmt: "^0.62.0",
240
+ oxlint: "^1.77.0",
241
241
  typescript: "^6.0.3",
242
242
  vite: "~8.2.0",
243
243
  "vite-plugin-dts": "^5.0.3",
@@ -248,13 +248,23 @@ var SOURCE_BROWSER_DEV_DEPENDENCIES = Object.freeze({
248
248
  "@vitest/browser-playwright": "^4.1.10",
249
249
  playwright: "^1.62.1"
250
250
  });
251
+ /** Baseline development dependency required by every private application environment. */
252
+ var APP_DEV_DEPENDENCIES = Object.freeze({ "@orkestrel/contract": "^0.0.9" });
251
253
  /** Additional development dependencies required by a private Vue browser application. */
252
254
  var APP_BROWSER_DEV_DEPENDENCIES = Object.freeze({
253
255
  ...SOURCE_BROWSER_DEV_DEPENDENCIES,
256
+ "@orkestrel/html": "^0.0.2",
254
257
  "@vitejs/plugin-vue": "^6.0.8",
255
258
  vue: "^3.5.40",
256
259
  "vue-tsc": "^3.3.7"
257
260
  });
261
+ /** Additional development dependencies required by a private server application. */
262
+ var APP_SERVER_DEV_DEPENDENCIES = Object.freeze({
263
+ "@orkestrel/emitter": "^0.0.5",
264
+ "@orkestrel/middleware": "^0.0.9",
265
+ "@orkestrel/router": "^0.0.8",
266
+ "@orkestrel/server": "^0.0.10"
267
+ });
258
268
  /** Immutable official actions/checkout v6.0.2 commit used by generated CI. */
259
269
  var CHECKOUT_ACTION_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
260
270
  /** Immutable official actions/setup-node v6.4.0 commit used by generated CI. */
@@ -528,13 +538,45 @@ function serializeTypeScriptString(value) {
528
538
  return `${output}'`;
529
539
  }
530
540
  /**
541
+ * Determine whether an application blueprint spans the shared browser/server boundary.
542
+ *
543
+ * @param spec - The blueprint to inspect.
544
+ * @returns True only when app/core, app/browser, and app/server are all selected.
545
+ *
546
+ * @example
547
+ * ```ts
548
+ * hasApplicationBoundary(blueprint('application', { app: ['core', 'browser', 'server'] }))
549
+ * ```
550
+ */
551
+ function hasApplicationBoundary(spec) {
552
+ return spec.app.includes("core") && spec.app.includes("browser") && spec.app.includes("server");
553
+ }
554
+ /**
555
+ * Determine whether an application blueprint emits its browser showcase.
556
+ *
557
+ * @param spec - The blueprint to inspect.
558
+ * @returns True only when showcase intent accompanies app/browser.
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * hasApplicationShowcase(blueprint('application', { app: ['browser'], showcase: true }))
563
+ * ```
564
+ */
565
+ function hasApplicationShowcase(spec) {
566
+ return spec.showcase && spec.app.includes("browser");
567
+ }
568
+ /**
531
569
  * Derive the declared public `Member[]` from a blueprint.
532
570
  *
533
571
  * @param spec - The blueprint to derive members from.
534
572
  * @remarks
535
573
  * Published source environments receive the canonical entity/type/factory/constant
536
574
  * inventory. Application environments receive their exact public declaration kinds,
537
- * including parsers, guards, handlers, errors, and runners where present.
575
+ * including parsers, guards, handlers, errors, and runners where present. Two groups
576
+ * move rather than duplicate: the health contract is declared against `app/server`
577
+ * while the server alone reads it and against `app/core` once the browser reads it
578
+ * too, and the showcase seed, factory, and root-view identity appear only for a
579
+ * blueprint whose showcase accompanies `app/browser`.
538
580
  * @returns The declared `Member[]`, one set per environment.
539
581
  *
540
582
  * @example
@@ -547,6 +589,8 @@ function serializeTypeScriptString(value) {
547
589
  function blueprintToMembers(spec) {
548
590
  const pascal = pascalCase(spec.name);
549
591
  const members = [];
592
+ const hasBoundary = hasApplicationBoundary(spec);
593
+ const hasShowcase = hasApplicationShowcase(spec);
550
594
  for (const environment of spec.src) {
551
595
  members.push(member(pascal, "entity", `The ${pascal} entity.`, environment));
552
596
  members.push(member(`${pascal}Options`, "type", `Options for creating a ${pascal}.`, environment));
@@ -564,15 +608,21 @@ function blueprintToMembers(spec) {
564
608
  members.push(member("isApplicationError", "guard", "Narrow a caught value to ApplicationError.", "core"));
565
609
  members.push(member("parseApplicationName", "parser", "Parse an application name.", "core"));
566
610
  members.push(member("createApplication", "factory", "Create an application identity.", "core"));
611
+ if (hasBoundary) members.push(member("ApplicationRecord", "type", "The shared application health record.", "core"), member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "core"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "core"), member("APP_HEALTH_TIMEOUT", "constant", "The shared health read timeout.", "core"), member("isApplicationRecord", "guard", "Narrow a transport value to the shared record.", "core"), member("readApplicationHealth", "handler", "Read the shared health boundary as the application identity.", "core"));
567
612
  }
568
613
  if (spec.app.includes("browser")) {
569
614
  members.push(member("BrowserApplicationErrorCode", "alias", "A browser application configuration error reason.", "browser"), member("BrowserApplicationErrorContext", "type", "Browser application boundary-failure context.", "browser"), member("BrowserApplicationOptions", "type", "Options for creating the browser application.", "browser"));
570
615
  members.push(member("MAX_BROWSER_APPLICATION_NAME_LENGTH", "constant", "The maximum browser application-name length.", "browser"), member("MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH", "constant", "The maximum raw browser application-name input length.", "browser"), member("BrowserApplicationError", "error", "A browser application configuration error.", "browser"), member("isBrowserApplicationError", "guard", "Narrow a caught value to BrowserApplicationError.", "browser"), member("parseBrowserApplicationOptions", "parser", "Parse browser application options.", "browser"));
571
616
  if (!spec.app.includes("core")) members.push(member("APP_NAME", "constant", "The browser application name.", "browser"));
617
+ if (hasShowcase && !spec.app.includes("core")) members.push(member("Application", "type", "The identity the root view renders.", "browser"));
618
+ if (hasShowcase) members.push(member("seedApplication", "factory", "Seed the inert showcase identity.", "browser"));
572
619
  members.push(member("createBrowserApplication", "factory", "Create an unmounted Vue application.", "browser"));
620
+ if (hasShowcase) members.push(member("mountShowcaseApplication", "factory", "Mount the seeded showcase.", "browser"));
621
+ if (hasBoundary) members.push(member("mountBrowserApplication", "factory", "Mount the application over its server boundary.", "browser"));
573
622
  }
574
623
  if (spec.app.includes("server")) {
575
- members.push(member("ApplicationServerErrorCode", "alias", "An application server error reason.", "server"), member("ApplicationServerErrorContext", "type", "Application server boundary-failure context.", "server"), member("ApplicationServerOptions", "type", "Options for creating an application server.", "server"), member("ApplicationServerInterface", "type", "The application server lifecycle contract.", "server"), member("ApplicationServerRunnerInterface", "type", "The application server process lifecycle contract.", "server"), member("DEFAULT_APP_HOST", "constant", "The loopback host default.", "server"), member("DEFAULT_APP_PORT", "constant", "The application port default.", "server"), member("DEFAULT_APP_START_TIMEOUT", "constant", "The application startup timeout default.", "server"), member("MAX_APP_START_TIMEOUT", "constant", "The maximum application startup timeout.", "server"), member("MAX_APP_HOST_INPUT_LENGTH", "constant", "The maximum raw application-host input length.", "server"), member("MAX_APP_NUMBER_INPUT_LENGTH", "constant", "The maximum raw application numeric input length.", "server"), member("APP_MAX_CONNECTIONS", "constant", "The simultaneous connection limit.", "server"), member("APP_MAX_HEADERS", "constant", "The request-header count limit.", "server"), member("APP_HEADERS_TIMEOUT", "constant", "The request-header timeout.", "server"), member("APP_REQUEST_TIMEOUT", "constant", "The complete-request timeout.", "server"), member("APP_KEEP_ALIVE_TIMEOUT", "constant", "The idle keep-alive timeout.", "server"), member("APP_MAX_REQUESTS_PER_SOCKET", "constant", "The keep-alive request limit.", "server"), member("APP_PORT_PATTERN", "constant", "The decimal application-port syntax.", "server"), member("APP_HOST_LABEL_PATTERN", "constant", "The DNS application-host label syntax.", "server"), member("APP_NUMERIC_HOST_PATTERN", "constant", "The ambiguous numeric-host rejection syntax.", "server"), member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "server"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "server"), member("ApplicationServer", "entity", "The Node HTTP application server.", "server"), member("ApplicationServerRunner", "entity", "The application server process lifecycle owner.", "server"), member("ApplicationServerError", "error", "A server configuration or lifecycle error.", "server"), member("isApplicationServerError", "guard", "Narrow a caught value to ApplicationServerError.", "server"), member("parseApplicationHost", "parser", "Parse an application host.", "server"), member("parseApplicationPort", "parser", "Parse an application port.", "server"), member("parseApplicationStartTimeout", "parser", "Parse an application startup timeout.", "server"), member("parseApplicationServerOptions", "parser", "Parse application server options.", "server"), member("handleApplicationRequest", "handler", "Handle an application HTTP request.", "server"), member("reportApplicationServerError", "handler", "Report a process-owned failure without exposing diagnostic context.", "server"), member("createApplicationServer", "factory", "Create a stopped application server.", "server"), member("startApplicationServer", "factory", "Start the process-owned application server.", "server"));
624
+ if (!hasBoundary) members.push(member("ApplicationRecord", "type", "The application health record.", "server"));
625
+ members.push(member("ApplicationState", "type", "Per-request application state.", "server"), member("ApplicationServerErrorCode", "alias", "An application server error reason.", "server"), member("ApplicationServerErrorContext", "type", "Application server boundary-failure context.", "server"), member("ApplicationServerOptions", "type", "Options for creating an application server.", "server"), member("ApplicationServerInterface", "type", "The application server lifecycle contract.", "server"), member("ApplicationServerRunnerInterface", "type", "The application server process lifecycle contract.", "server"), member("ApplicationServerRunnerEventMap", "alias", "Observable application server runner outcomes.", "server"), member("ApplicationServerRunnerOptions", "type", "Options for observing an application server runner.", "server"), member("DEFAULT_APP_HOST", "constant", "The loopback host default.", "server"), member("DEFAULT_APP_PORT", "constant", "The application port default.", "server"), member("DEFAULT_APP_START_TIMEOUT", "constant", "The application startup timeout default.", "server"), member("MAX_APP_START_TIMEOUT", "constant", "The maximum application startup timeout.", "server"), member("MAX_APP_HOST_INPUT_LENGTH", "constant", "The maximum raw application-host input length.", "server"), member("MAX_APP_NUMBER_INPUT_LENGTH", "constant", "The maximum raw application numeric input length.", "server"), member("APP_PORT_PATTERN", "constant", "The decimal application-port syntax.", "server"), member("APP_HOST_LABEL_PATTERN", "constant", "The DNS application-host label syntax.", "server"), member("APP_NUMERIC_HOST_PATTERN", "constant", "The ambiguous numeric-host rejection syntax.", "server"), ...hasBoundary ? [] : [member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "server"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "server")], member("createApplicationDispatcher", "factory", "Create a standalone application route dispatcher.", "server"), member("ApplicationServer", "entity", "The composed application server.", "server"), member("ApplicationServerRunner", "entity", "The application server process lifecycle owner.", "server"), member("ApplicationServerError", "error", "A server configuration or lifecycle error.", "server"), member("isApplicationServerError", "guard", "Narrow a caught value to ApplicationServerError.", "server"), member("parseApplicationHost", "parser", "Parse an application host.", "server"), member("parseApplicationPort", "parser", "Parse an application port.", "server"), member("parseApplicationStartTimeout", "parser", "Parse an application startup timeout.", "server"), member("parseApplicationServerOptions", "parser", "Parse application server options.", "server"), member("handleApplicationHealth", "handler", "Return the application health record.", "server"), member("reportApplicationServerError", "handler", "Report a process-owned failure without exposing diagnostic context.", "server"), member("createApplicationServer", "factory", "Create a stopped application server.", "server"), member("startApplicationServer", "factory", "Start the process-owned application server.", "server"));
576
626
  if (!spec.app.includes("core")) members.push(member("APP_NAME", "constant", "The server application name.", "server"));
577
627
  }
578
628
  return members;
@@ -1653,6 +1703,34 @@ function renderArray(entries, indent, prefix, suffix) {
1653
1703
  return `[\n${items.map((item) => `${childIndent}${item}`).join(",\n")}\n${indent}]`;
1654
1704
  }
1655
1705
  /**
1706
+ * Render a single-quoted TypeScript string array literal through `oxfmt`'s
1707
+ * inline-or-broken rule — inline when the rendered width fits
1708
+ * `JSON_PRINT_WIDTH`, one item per line with a trailing comma on every line
1709
+ * (including the last) otherwise, matching `.oxfmtrc.json`'s
1710
+ * `trailingComma: "all"` for non-JSON files.
1711
+ *
1712
+ * @param entries - The array's string elements, in order.
1713
+ * @param indent - The current indentation prefix.
1714
+ * @param prefix - The text already emitted on this line before the array.
1715
+ * @param suffix - The text that will follow the array on this line.
1716
+ * @returns The rendered array fragment (no trailing newline).
1717
+ *
1718
+ * @example
1719
+ * ```ts
1720
+ * import { renderStringArray } from '@orkestrel/scaffold'
1721
+ *
1722
+ * renderStringArray(['app', 'guides', 'tests'], '', '', '') // "['app', 'guides', 'tests']"
1723
+ * ```
1724
+ */
1725
+ function renderStringArray(entries, indent, prefix, suffix) {
1726
+ if (entries.length === 0) return "[]";
1727
+ const items = entries.map((entry) => serializeTypeScriptString(entry));
1728
+ const inline = `[${items.join(", ")}]`;
1729
+ if (fitsPrintWidth(`${prefix}${inline}${suffix}`)) return inline;
1730
+ const childIndent = `${indent}\t`;
1731
+ return `[\n${items.map((item) => `${childIndent}${item},`).join("\n")}\n${indent}]`;
1732
+ }
1733
+ /**
1656
1734
  * Render a JSON object through `formatJson`'s one-key-per-line rule.
1657
1735
  *
1658
1736
  * @param entry - The object to render.
@@ -2895,7 +2973,10 @@ export * from './factories.js'
2895
2973
  name: "appCoreTypes",
2896
2974
  summary: "The host-independent application contract.",
2897
2975
  category: "source",
2898
- placeholders: Object.freeze([]),
2976
+ placeholders: Object.freeze([Object.freeze({
2977
+ name: "record",
2978
+ description: "The shared health record declared once both hosts read it."
2979
+ })]),
2899
2980
  content: `/** A rejected shared application boundary. */
2900
2981
  ${EXPORT_KEYWORD} type ApplicationErrorCode = 'CONFIG'
2901
2982
 
@@ -2909,7 +2990,7 @@ ${EXPORT_KEYWORD} interface ApplicationErrorContext {
2909
2990
  ${EXPORT_KEYWORD} interface Application {
2910
2991
  readonly name: string
2911
2992
  }
2912
- `
2993
+ {{record}}`
2913
2994
  }),
2914
2995
  appCoreConstants: Object.freeze({
2915
2996
  id: "appCoreConstants",
@@ -2919,6 +3000,9 @@ ${EXPORT_KEYWORD} interface Application {
2919
3000
  placeholders: Object.freeze([Object.freeze({
2920
3001
  name: "nameLiteral",
2921
3002
  description: "The JSON-serialized application name."
3003
+ }), Object.freeze({
3004
+ name: "health",
3005
+ description: "The shared health route constants declared once both hosts read them."
2922
3006
  })]),
2923
3007
  content: `/** The application name shared by every host environment. */
2924
3008
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = {{nameLiteral}}
@@ -2928,7 +3012,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_LENGTH = 203
2928
3012
 
2929
3013
  /** Maximum raw Unicode code units inspected before trimming an application name. */
2930
3014
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_INPUT_LENGTH = 255
2931
- `
3015
+ {{health}}`
2932
3016
  }),
2933
3017
  appCoreErrors: Object.freeze({
2934
3018
  id: "appCoreErrors",
@@ -2937,6 +3021,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_INPUT_LENGTH = 255
2937
3021
  category: "source",
2938
3022
  placeholders: Object.freeze([]),
2939
3023
  content: `import type { ApplicationErrorCode, ApplicationErrorContext } from './types.js'
3024
+ import { holds } from '@orkestrel/contract'
2940
3025
 
2941
3026
  /** A rejected shared application configuration value. */
2942
3027
  ${EXPORT_KEYWORD} class ApplicationError extends Error {
@@ -2965,11 +3050,34 @@ ${EXPORT_KEYWORD} class ApplicationError extends Error {
2965
3050
  * \`\`\`
2966
3051
  */
2967
3052
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationError(value: unknown): value is ApplicationError {
2968
- try {
2969
- return value instanceof ApplicationError
2970
- } catch {
2971
- return false
2972
- }
3053
+ return holds(() => value instanceof ApplicationError)
3054
+ }
3055
+ `
3056
+ }),
3057
+ appCoreValidators: Object.freeze({
3058
+ id: "appCoreValidators",
3059
+ name: "appCoreValidators",
3060
+ summary: "The host-independent guard over the shared health record.",
3061
+ category: "source",
3062
+ placeholders: Object.freeze([]),
3063
+ content: `import type { ApplicationRecord } from './types.js'
3064
+ import { holds, isNonEmptyString, isRecord } from '@orkestrel/contract'
3065
+
3066
+ /**
3067
+ * Narrow one unvalidated transport value to the shared application record.
3068
+ *
3069
+ * @param value - The value read from the health route, before validation.
3070
+ * @returns True only for the exact record both hosts agreed on.
3071
+ *
3072
+ * @example
3073
+ * \`\`\`ts
3074
+ * import { isApplicationRecord } from '@app/core'
3075
+ *
3076
+ * isApplicationRecord({ name: 'example', status: 'ok' }) // true
3077
+ * \`\`\`
3078
+ */
3079
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationRecord(value: unknown): value is ApplicationRecord {
3080
+ return holds(() => isRecord(value) && isNonEmptyString(value.name) && value.status === 'ok')
2973
3081
  }
2974
3082
  `
2975
3083
  }),
@@ -2979,7 +3087,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationError(value: unknown): value
2979
3087
  summary: "The host-independent application value parsers.",
2980
3088
  category: "source",
2981
3089
  placeholders: Object.freeze([]),
2982
- content: `import { MAX_APPLICATION_NAME_INPUT_LENGTH, MAX_APPLICATION_NAME_LENGTH } from './constants.js'
3090
+ content: `import { isNonEmptyString } from '@orkestrel/contract'
3091
+ import { MAX_APPLICATION_NAME_INPUT_LENGTH, MAX_APPLICATION_NAME_LENGTH } from './constants.js'
2983
3092
  import { ApplicationError } from './errors.js'
2984
3093
 
2985
3094
  /**
@@ -3001,7 +3110,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationName(value: unknown): stri
3001
3110
  throw new ApplicationError('CONFIG', 'Application name must be a string', { value })
3002
3111
  }
3003
3112
  const name = value.trim()
3004
- if (name.length === 0 || name.length > MAX_APPLICATION_NAME_LENGTH) {
3113
+ if (!isNonEmptyString(name) || name.length > MAX_APPLICATION_NAME_LENGTH) {
3005
3114
  throw new ApplicationError(
3006
3115
  'CONFIG',
3007
3116
  \`Application name must contain 1 through \${MAX_APPLICATION_NAME_LENGTH} characters\`,
@@ -3010,6 +3119,52 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationName(value: unknown): stri
3010
3119
  }
3011
3120
  return name
3012
3121
  }
3122
+ `
3123
+ }),
3124
+ appCoreHandlers: Object.freeze({
3125
+ id: "appCoreHandlers",
3126
+ name: "appCoreHandlers",
3127
+ summary: "The host-independent read of the shared health boundary.",
3128
+ category: "source",
3129
+ placeholders: Object.freeze([]),
3130
+ content: `import type { Application } from './types.js'
3131
+ import { APP_HEALTH_PATH, APP_HEALTH_TIMEOUT } from './constants.js'
3132
+ import { isApplicationRecord } from './validators.js'
3133
+
3134
+ /**
3135
+ * Read the application server's health route and translate its unvalidated JSON
3136
+ * into the shared application identity.
3137
+ *
3138
+ * @param origin - The absolute origin serving the application health route.
3139
+ * @returns The identity the running server reported, or undefined when the boundary
3140
+ * is unreachable, too slow, or off-contract.
3141
+ *
3142
+ * @remarks
3143
+ * The single translation point between the two hosts: the response body is read as
3144
+ * \`unknown\` and narrowed by {@link isApplicationRecord} before any field is consumed,
3145
+ * so a missing, slow, or foreign server degrades to \`undefined\` instead of leaking an
3146
+ * unvalidated value into the application. The record's \`status\` proves liveness; the
3147
+ * identity is what the caller renders.
3148
+ *
3149
+ * @example
3150
+ * \`\`\`ts
3151
+ * import { readApplicationHealth } from '@app/core'
3152
+ *
3153
+ * await readApplicationHealth('http://127.0.0.1:3000') // { name: 'example' }
3154
+ * \`\`\`
3155
+ */
3156
+ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} readApplicationHealth(origin: string): Promise<Application | undefined> {
3157
+ try {
3158
+ const response = await fetch(new URL(APP_HEALTH_PATH, origin), {
3159
+ signal: AbortSignal.timeout(APP_HEALTH_TIMEOUT),
3160
+ })
3161
+ if (!response.ok) return undefined
3162
+ const record: unknown = await response.json()
3163
+ return isApplicationRecord(record) ? Object.freeze({ name: record.name }) : undefined
3164
+ } catch {
3165
+ return undefined
3166
+ }
3167
+ }
3013
3168
  `
3014
3169
  }),
3015
3170
  appCoreFactories: Object.freeze({
@@ -3045,12 +3200,18 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplication(name: string = APP_NAME)
3045
3200
  name: "appCoreIndex",
3046
3201
  summary: "The application core barrel.",
3047
3202
  category: "source",
3048
- placeholders: Object.freeze([]),
3203
+ placeholders: Object.freeze([Object.freeze({
3204
+ name: "validators",
3205
+ description: "The optional shared-record guard barrel row."
3206
+ }), Object.freeze({
3207
+ name: "handlers",
3208
+ description: "The optional shared health-boundary barrel row."
3209
+ })]),
3049
3210
  content: `export * from './types.js'
3050
3211
  export * from './constants.js'
3051
3212
  export * from './errors.js'
3052
- export * from './parsers.js'
3053
- export * from './factories.js'
3213
+ {{validators}}export * from './parsers.js'
3214
+ {{handlers}}export * from './factories.js'
3054
3215
  `
3055
3216
  }),
3056
3217
  appBrowserTypes: Object.freeze({
@@ -3058,7 +3219,10 @@ export * from './factories.js'
3058
3219
  name: "appBrowserTypes",
3059
3220
  summary: "The browser application options.",
3060
3221
  category: "source",
3061
- placeholders: Object.freeze([]),
3222
+ placeholders: Object.freeze([Object.freeze({
3223
+ name: "application",
3224
+ description: "The browser-owned root-view identity declared without application core."
3225
+ })]),
3062
3226
  content: `/** A rejected browser application boundary. */
3063
3227
  ${EXPORT_KEYWORD} type BrowserApplicationErrorCode = 'CONFIG'
3064
3228
 
@@ -3072,7 +3236,7 @@ ${EXPORT_KEYWORD} interface BrowserApplicationErrorContext {
3072
3236
  ${EXPORT_KEYWORD} interface BrowserApplicationOptions {
3073
3237
  readonly name?: string
3074
3238
  }
3075
- `
3239
+ {{application}}`
3076
3240
  }),
3077
3241
  appBrowserConstants: Object.freeze({
3078
3242
  id: "appBrowserConstants",
@@ -3097,6 +3261,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH = 2
3097
3261
  category: "source",
3098
3262
  placeholders: Object.freeze([]),
3099
3263
  content: `import type { BrowserApplicationErrorCode, BrowserApplicationErrorContext } from './types.js'
3264
+ import { holds } from '@orkestrel/contract'
3100
3265
 
3101
3266
  /** A rejected browser application configuration value. */
3102
3267
  ${EXPORT_KEYWORD} class BrowserApplicationError extends Error {
@@ -3129,11 +3294,7 @@ ${EXPORT_KEYWORD} class BrowserApplicationError extends Error {
3129
3294
  * \`\`\`
3130
3295
  */
3131
3296
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserApplicationError(value: unknown): value is BrowserApplicationError {
3132
- try {
3133
- return value instanceof BrowserApplicationError
3134
- } catch {
3135
- return false
3136
- }
3297
+ return holds(() => value instanceof BrowserApplicationError)
3137
3298
  }
3138
3299
  `
3139
3300
  }),
@@ -3144,6 +3305,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserApplicationError(value: unknown):
3144
3305
  category: "source",
3145
3306
  placeholders: Object.freeze([]),
3146
3307
  content: `import type { BrowserApplicationOptions } from './types.js'
3308
+ import { isNonEmptyString } from '@orkestrel/contract'
3147
3309
  import {
3148
3310
  MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH,
3149
3311
  MAX_BROWSER_APPLICATION_NAME_LENGTH,
@@ -3180,6 +3342,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
3180
3342
  { value },
3181
3343
  )
3182
3344
  }
3345
+ // Walk own descriptors directly: reading values through a getter would run caller code.
3183
3346
  const keys = Reflect.ownKeys(value)
3184
3347
  if (keys.some((key) => key !== 'name')) {
3185
3348
  throw new BrowserApplicationError('CONFIG', 'Unknown browser application option', {
@@ -3204,7 +3367,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
3204
3367
  })
3205
3368
  }
3206
3369
  const name = descriptor.value.trim()
3207
- if (name.length === 0 || name.length > MAX_BROWSER_APPLICATION_NAME_LENGTH) {
3370
+ if (!isNonEmptyString(name) || name.length > MAX_BROWSER_APPLICATION_NAME_LENGTH) {
3208
3371
  throw new BrowserApplicationError(
3209
3372
  'CONFIG',
3210
3373
  \`Browser application name must contain 1 through \${MAX_BROWSER_APPLICATION_NAME_LENGTH} characters\`,
@@ -3221,22 +3384,73 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
3221
3384
  }
3222
3385
  `
3223
3386
  }),
3224
- appBrowserFactories: Object.freeze({
3225
- id: "appBrowserFactories",
3226
- name: "appBrowserFactories",
3227
- summary: "The Vue browser application factory.",
3387
+ appBrowserSeeders: Object.freeze({
3388
+ id: "appBrowserSeeders",
3389
+ name: "appBrowserSeeders",
3390
+ summary: "The frozen, inert identity the showcase renders.",
3228
3391
  category: "source",
3229
3392
  placeholders: Object.freeze([Object.freeze({
3393
+ name: "applicationImport",
3394
+ description: "The selected layer type import for the root-view identity."
3395
+ }), Object.freeze({
3230
3396
  name: "nameImport",
3231
3397
  description: "The selected layer import for APP_NAME."
3232
3398
  })]),
3399
+ content: `{{applicationImport}}
3400
+ {{nameImport}}
3401
+
3402
+ /**
3403
+ * Seed the inert identity the showcase renders.
3404
+ *
3405
+ * @returns A fresh frozen identity, identical on every call.
3406
+ *
3407
+ * @remarks
3408
+ * The showcase's only data. It is exactly the value the shipped root view receives from
3409
+ * the running application, so the showcase exercises the shipped view rather than a
3410
+ * parallel copy of it.
3411
+ *
3412
+ * @example
3413
+ * \`\`\`ts
3414
+ * import { seedApplication } from '@app/browser'
3415
+ *
3416
+ * seedApplication().name // the seeded showcase identity
3417
+ * \`\`\`
3418
+ */
3419
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} seedApplication(): Application {
3420
+ return Object.freeze({ name: \`\${APP_NAME} showcase\` })
3421
+ }
3422
+ `
3423
+ }),
3424
+ appBrowserFactories: Object.freeze({
3425
+ id: "appBrowserFactories",
3426
+ name: "appBrowserFactories",
3427
+ summary: "The Vue browser application factory.",
3428
+ category: "source",
3429
+ placeholders: Object.freeze([
3430
+ Object.freeze({
3431
+ name: "nameImport",
3432
+ description: "The selected layer import for APP_NAME."
3433
+ }),
3434
+ Object.freeze({
3435
+ name: "seedImport",
3436
+ description: "The optional showcase seeder import."
3437
+ }),
3438
+ Object.freeze({
3439
+ name: "showcase",
3440
+ description: "The optional seeded showcase factory."
3441
+ }),
3442
+ Object.freeze({
3443
+ name: "boundary",
3444
+ description: "The optional server-boundary startup factory."
3445
+ })
3446
+ ]),
3233
3447
  content: `import type { App } from 'vue'
3234
3448
  import type { BrowserApplicationOptions } from './types.js'
3235
3449
  import { createApp } from 'vue'
3236
3450
  import ApplicationView from './ApplicationView.vue'
3237
3451
  {{nameImport}}
3238
3452
  import { parseBrowserApplicationOptions } from './parsers.js'
3239
-
3453
+ {{seedImport}}
3240
3454
  /**
3241
3455
  * Create an unmounted Vue application.
3242
3456
  *
@@ -3254,19 +3468,22 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createBrowserApplication(options: BrowserA
3254
3468
  const parsed = parseBrowserApplicationOptions(options)
3255
3469
  return createApp(ApplicationView, { name: parsed.name ?? APP_NAME })
3256
3470
  }
3257
- `
3471
+ {{showcase}}{{boundary}}`
3258
3472
  }),
3259
3473
  appBrowserIndex: Object.freeze({
3260
3474
  id: "appBrowserIndex",
3261
3475
  name: "appBrowserIndex",
3262
3476
  summary: "The browser application barrel.",
3263
3477
  category: "source",
3264
- placeholders: Object.freeze([]),
3478
+ placeholders: Object.freeze([Object.freeze({
3479
+ name: "seeders",
3480
+ description: "The optional showcase seeder barrel row."
3481
+ })]),
3265
3482
  content: `export * from './types.js'
3266
3483
  export * from './constants.js'
3267
3484
  export * from './errors.js'
3268
3485
  export * from './parsers.js'
3269
- export * from './factories.js'
3486
+ {{seeders}}export * from './factories.js'
3270
3487
  `
3271
3488
  }),
3272
3489
  appBrowserMain: Object.freeze({
@@ -3274,10 +3491,27 @@ export * from './factories.js'
3274
3491
  name: "appBrowserMain",
3275
3492
  summary: "The browser executable entry.",
3276
3493
  category: "source",
3494
+ placeholders: Object.freeze([Object.freeze({
3495
+ name: "factory",
3496
+ description: "The factory the shipped entry mounts through."
3497
+ }), Object.freeze({
3498
+ name: "mount",
3499
+ description: "The mounting statement."
3500
+ })]),
3501
+ content: `import { {{factory}} } from './index.js'
3502
+
3503
+ {{mount}}
3504
+ `
3505
+ }),
3506
+ appBrowserShowcase: Object.freeze({
3507
+ id: "appBrowserShowcase",
3508
+ name: "appBrowserShowcase",
3509
+ summary: "The showcase executable entry.",
3510
+ category: "source",
3277
3511
  placeholders: Object.freeze([]),
3278
- content: `import { createBrowserApplication } from './index.js'
3512
+ content: `import { mountShowcaseApplication } from './index.js'
3279
3513
 
3280
- createBrowserApplication().mount('#app')
3514
+ mountShowcaseApplication('#app')
3281
3515
  `
3282
3516
  }),
3283
3517
  appBrowserView: Object.freeze({
@@ -3322,6 +3556,33 @@ defineProps<{ readonly name: string }>()
3322
3556
  <script type="module" src="/main.ts"><\/script>
3323
3557
  </body>
3324
3558
  </html>
3559
+ `
3560
+ }),
3561
+ appBrowserShowcaseHtml: Object.freeze({
3562
+ id: "appBrowserShowcaseHtml",
3563
+ name: "appBrowserShowcaseHtml",
3564
+ summary: "The showcase HTML entry with its development security policy.",
3565
+ category: "source",
3566
+ placeholders: Object.freeze([Object.freeze({
3567
+ name: "name",
3568
+ description: "The application name."
3569
+ })]),
3570
+ content: `<!doctype html>
3571
+ <html lang="en">
3572
+ <head>
3573
+ <meta
3574
+ http-equiv="Content-Security-Policy"
3575
+ 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'"
3576
+ />
3577
+ <meta charset="UTF-8" />
3578
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3579
+ <title>{{name}} showcase</title>
3580
+ </head>
3581
+ <body>
3582
+ <div id="app"></div>
3583
+ <script type="module" src="/showcase.ts"><\/script>
3584
+ </body>
3585
+ </html>
3325
3586
  `
3326
3587
  }),
3327
3588
  appBrowserEnv: Object.freeze({
@@ -3345,8 +3606,20 @@ declare module '*.vue' {
3345
3606
  name: "appServerTypes",
3346
3607
  summary: "The application server contract.",
3347
3608
  category: "source",
3348
- placeholders: Object.freeze([]),
3349
- content: `/** A rejected application server boundary. */
3609
+ placeholders: Object.freeze([Object.freeze({
3610
+ name: "record",
3611
+ description: "The health record, declared here only while the server alone reads it."
3612
+ })]),
3613
+ content: `import type { EmitterErrorHandler, EmitterHooks, EmitterInterface } from '@orkestrel/emitter'
3614
+ import type { IdentifierState } from '@orkestrel/middleware'
3615
+ import type { ConnectionInfo, ServerStatus } from '@orkestrel/server'
3616
+
3617
+ {{record}}/** Per-request application state derived from connection facts. */
3618
+ ${EXPORT_KEYWORD} interface ApplicationState extends IdentifierState {
3619
+ readonly connection: ConnectionInfo
3620
+ }
3621
+
3622
+ /** A rejected application server boundary. */
3350
3623
  ${EXPORT_KEYWORD} type ApplicationServerErrorCode = 'CONFIG' | 'LIFECYCLE'
3351
3624
 
3352
3625
  /** Diagnostic context attached to an application server boundary error. */
@@ -3357,26 +3630,49 @@ ${EXPORT_KEYWORD} interface ApplicationServerErrorContext {
3357
3630
 
3358
3631
  /** Options for creating an application server. */
3359
3632
  ${EXPORT_KEYWORD} interface ApplicationServerOptions {
3360
- readonly host?: string
3361
- readonly port?: number
3362
- readonly timeout?: number
3633
+ readonly server?: {
3634
+ readonly host?: string
3635
+ readonly port?: number
3636
+ readonly timeout?: number
3637
+ }
3363
3638
  }
3364
3639
 
3365
3640
  /** A lifecycle-safe application server. */
3366
3641
  ${EXPORT_KEYWORD} interface ApplicationServerInterface {
3367
3642
  readonly host: string
3368
- readonly port: number
3369
- readonly listening: boolean
3370
- readonly url: string
3643
+ readonly port: number | undefined
3644
+ readonly status: ServerStatus
3645
+ readonly url: string | undefined
3371
3646
  start(signal?: AbortSignal): Promise<void>
3372
3647
  stop(): Promise<void>
3648
+ destroy(): Promise<void>
3649
+ }
3650
+
3651
+ /** Observable process-lifecycle outcomes for an application server runner. */
3652
+ ${EXPORT_KEYWORD} type ApplicationServerRunnerEventMap = {
3653
+ readonly ready: readonly [url: string]
3654
+ readonly fail: readonly [error: unknown]
3373
3655
  }
3374
3656
 
3375
3657
  /** The process lifecycle owner for an application server. */
3376
3658
  ${EXPORT_KEYWORD} interface ApplicationServerRunnerInterface {
3659
+ readonly emitter: EmitterInterface<ApplicationServerRunnerEventMap>
3377
3660
  start(): void
3378
3661
  stop(): Promise<void>
3379
3662
  }
3663
+
3664
+ /**
3665
+ * Options for observing an application server runner.
3666
+ *
3667
+ * @remarks
3668
+ * Initial \`on\` hooks run before the runner's own readiness and failure effects. When no earlier
3669
+ * failure set an exit code, a synchronous fail hook therefore observes \`process.exitCode\` as
3670
+ * \`undefined\` before the default reporter sets it to \`1\`.
3671
+ */
3672
+ ${EXPORT_KEYWORD} interface ApplicationServerRunnerOptions {
3673
+ readonly on?: EmitterHooks<ApplicationServerRunnerEventMap>
3674
+ readonly error?: EmitterErrorHandler
3675
+ }
3380
3676
  `
3381
3677
  }),
3382
3678
  appServerConstants: Object.freeze({
@@ -3387,6 +3683,9 @@ ${EXPORT_KEYWORD} interface ApplicationServerRunnerInterface {
3387
3683
  placeholders: Object.freeze([Object.freeze({
3388
3684
  name: "nameConstant",
3389
3685
  description: "The optional server-only APP_NAME declaration."
3686
+ }), Object.freeze({
3687
+ name: "health",
3688
+ description: "The health route constants, declared here only while the server alone reads them."
3390
3689
  })]),
3391
3690
  content: `{{nameConstant}}/** The fail-closed loopback host default. */
3392
3691
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} DEFAULT_APP_HOST = '127.0.0.1'
@@ -3406,24 +3705,6 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APP_HOST_INPUT_LENGTH = 255
3406
3705
  /** Maximum raw characters inspected at an application numeric boundary. */
3407
3706
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APP_NUMBER_INPUT_LENGTH = 32
3408
3707
 
3409
- /** Maximum simultaneous connections accepted by the generated server. */
3410
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_CONNECTIONS = 16
3411
-
3412
- /** Maximum request headers accepted before Node rejects the request. */
3413
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_HEADERS = 100
3414
-
3415
- /** Maximum milliseconds allowed to receive complete request headers. */
3416
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEADERS_TIMEOUT = 10_000
3417
-
3418
- /** Maximum milliseconds allowed for one complete request. */
3419
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_REQUEST_TIMEOUT = 30_000
3420
-
3421
- /** Idle keep-alive milliseconds before a connection is closed. */
3422
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_KEEP_ALIVE_TIMEOUT = 5_000
3423
-
3424
- /** Maximum requests served through one keep-alive connection. */
3425
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_REQUESTS_PER_SOCKET = 100
3426
-
3427
3708
  /** The decimal-only syntax accepted at the APP_PORT string boundary. */
3428
3709
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_PORT_PATTERN = /^\\d+$/
3429
3710
 
@@ -3432,13 +3713,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HOST_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Z
3432
3713
 
3433
3714
  /** Numeric-looking non-IP hosts rejected before platform DNS interpretation. */
3434
3715
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NUMERIC_HOST_PATTERN = /^[0-9.]+$/
3435
-
3436
- /** The only HTTP method owned by the application health route. */
3437
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_METHOD = 'GET'
3438
-
3439
- /** The only HTTP path owned by the generated application server. */
3440
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/'
3441
- `
3716
+ {{health}}`
3442
3717
  }),
3443
3718
  appServerErrors: Object.freeze({
3444
3719
  id: "appServerErrors",
@@ -3447,6 +3722,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/'
3447
3722
  category: "source",
3448
3723
  placeholders: Object.freeze([]),
3449
3724
  content: `import type { ApplicationServerErrorCode, ApplicationServerErrorContext } from './types.js'
3725
+ import { holds } from '@orkestrel/contract'
3450
3726
 
3451
3727
  /** A rejected application server configuration or lifecycle operation. */
3452
3728
  ${EXPORT_KEYWORD} class ApplicationServerError extends Error {
@@ -3479,11 +3755,7 @@ ${EXPORT_KEYWORD} class ApplicationServerError extends Error {
3479
3755
  * \`\`\`
3480
3756
  */
3481
3757
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationServerError(value: unknown): value is ApplicationServerError {
3482
- try {
3483
- return value instanceof ApplicationServerError
3484
- } catch {
3485
- return false
3486
- }
3758
+ return holds(() => value instanceof ApplicationServerError)
3487
3759
  }
3488
3760
  `
3489
3761
  }),
@@ -3494,6 +3766,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationServerError(value: unknown):
3494
3766
  category: "source",
3495
3767
  placeholders: Object.freeze([]),
3496
3768
  content: `import type { ApplicationServerOptions } from './types.js'
3769
+ import { isNonEmptyString, parseString } from '@orkestrel/contract'
3497
3770
  import { isIP } from 'node:net'
3498
3771
  import {
3499
3772
  APP_HOST_LABEL_PATTERN,
@@ -3523,7 +3796,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationHost(value: unknown): stri
3523
3796
  const host = value.trim()
3524
3797
  const family = isIP(host)
3525
3798
  if (
3526
- host.length === 0 ||
3799
+ !isNonEmptyString(host) ||
3527
3800
  host.length > 253 ||
3528
3801
  (family === 0 &&
3529
3802
  (APP_NUMERIC_HOST_PATTERN.test(host) ||
@@ -3545,18 +3818,14 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationHost(value: unknown): stri
3545
3818
  */
3546
3819
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationPort(value: unknown): number {
3547
3820
  if (value === undefined) return DEFAULT_APP_PORT
3548
- if (typeof value !== 'string' && typeof value !== 'number') {
3549
- throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
3550
- value,
3551
- })
3552
- }
3553
- if (typeof value === 'string' && value.length > MAX_APP_NUMBER_INPUT_LENGTH) {
3821
+ const text = parseString(value)
3822
+ if (text === undefined || text.length > MAX_APP_NUMBER_INPUT_LENGTH) {
3554
3823
  throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
3555
3824
  value,
3556
3825
  })
3557
3826
  }
3558
- const text = typeof value === 'string' ? value.trim() : String(value)
3559
- const port = APP_PORT_PATTERN.test(text) ? Number(text) : Number.NaN
3827
+ const normalized = text.trim()
3828
+ const port = APP_PORT_PATTERN.test(normalized) ? Number(normalized) : Number.NaN
3560
3829
  if (!Number.isInteger(port) || port < 0 || port > 65_535) {
3561
3830
  throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
3562
3831
  value,
@@ -3574,22 +3843,16 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationPort(value: unknown): numb
3574
3843
  */
3575
3844
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationStartTimeout(value: unknown): number {
3576
3845
  if (value === undefined) return DEFAULT_APP_START_TIMEOUT
3577
- if (typeof value !== 'string' && typeof value !== 'number') {
3578
- throw new ApplicationServerError(
3579
- 'CONFIG',
3580
- \`APP_START_TIMEOUT must be an integer from 1 through \${MAX_APP_START_TIMEOUT}\`,
3581
- { value },
3582
- )
3583
- }
3584
- if (typeof value === 'string' && value.length > MAX_APP_NUMBER_INPUT_LENGTH) {
3846
+ const text = parseString(value)
3847
+ if (text === undefined || text.length > MAX_APP_NUMBER_INPUT_LENGTH) {
3585
3848
  throw new ApplicationServerError(
3586
3849
  'CONFIG',
3587
3850
  \`APP_START_TIMEOUT must be an integer from 1 through \${MAX_APP_START_TIMEOUT}\`,
3588
3851
  { value },
3589
3852
  )
3590
3853
  }
3591
- const text = typeof value === 'string' ? value.trim() : String(value)
3592
- const timeout = APP_PORT_PATTERN.test(text) ? Number(text) : Number.NaN
3854
+ const normalized = text.trim()
3855
+ const timeout = APP_PORT_PATTERN.test(normalized) ? Number(normalized) : Number.NaN
3593
3856
  if (!Number.isInteger(timeout) || timeout < 1 || timeout > MAX_APP_START_TIMEOUT) {
3594
3857
  throw new ApplicationServerError(
3595
3858
  'CONFIG',
@@ -3625,24 +3888,58 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
3625
3888
  )
3626
3889
  }
3627
3890
  const keys = Reflect.ownKeys(value)
3628
- const unknown = keys.filter((key) => key !== 'host' && key !== 'port' && key !== 'timeout')
3891
+ const unknown = keys.filter((key) => key !== 'server')
3629
3892
  if (unknown.length > 0) {
3630
3893
  throw new ApplicationServerError('CONFIG', 'Unknown application server option', { value })
3631
3894
  }
3632
- const hostDescriptor = Reflect.getOwnPropertyDescriptor(value, 'host')
3633
- const portDescriptor = Reflect.getOwnPropertyDescriptor(value, 'port')
3634
- const timeoutDescriptor = Reflect.getOwnPropertyDescriptor(value, 'timeout')
3895
+ const serverDescriptor = Reflect.getOwnPropertyDescriptor(value, 'server')
3896
+ if (
3897
+ keys.includes('server') &&
3898
+ (serverDescriptor === undefined || !Reflect.has(serverDescriptor, 'value'))
3899
+ ) {
3900
+ throw new ApplicationServerError(
3901
+ 'CONFIG',
3902
+ 'Application server options must use data properties',
3903
+ { value },
3904
+ )
3905
+ }
3906
+ const server = serverDescriptor?.value
3907
+ if (server === undefined) return {}
3908
+ if (typeof server !== 'object' || server === null || Array.isArray(server)) {
3909
+ throw new ApplicationServerError('CONFIG', 'Application server settings must be an object', {
3910
+ value,
3911
+ })
3912
+ }
3913
+ const serverPrototype = Reflect.getPrototypeOf(server)
3914
+ if (serverPrototype !== Object.prototype && serverPrototype !== null) {
3915
+ throw new ApplicationServerError(
3916
+ 'CONFIG',
3917
+ 'Application server settings must be a plain record',
3918
+ { value },
3919
+ )
3920
+ }
3921
+ // Walk own descriptors directly: reading values through a getter would run caller code.
3922
+ const serverKeys = Reflect.ownKeys(server)
3923
+ const serverUnknown = serverKeys.filter(
3924
+ (key) => key !== 'host' && key !== 'port' && key !== 'timeout',
3925
+ )
3926
+ if (serverUnknown.length > 0) {
3927
+ throw new ApplicationServerError('CONFIG', 'Unknown application server setting', { value })
3928
+ }
3929
+ const hostDescriptor = Reflect.getOwnPropertyDescriptor(server, 'host')
3930
+ const portDescriptor = Reflect.getOwnPropertyDescriptor(server, 'port')
3931
+ const timeoutDescriptor = Reflect.getOwnPropertyDescriptor(server, 'timeout')
3635
3932
  if (
3636
- (keys.includes('host') &&
3933
+ (serverKeys.includes('host') &&
3637
3934
  (hostDescriptor === undefined || !Reflect.has(hostDescriptor, 'value'))) ||
3638
- (keys.includes('port') &&
3935
+ (serverKeys.includes('port') &&
3639
3936
  (portDescriptor === undefined || !Reflect.has(portDescriptor, 'value'))) ||
3640
- (keys.includes('timeout') &&
3937
+ (serverKeys.includes('timeout') &&
3641
3938
  (timeoutDescriptor === undefined || !Reflect.has(timeoutDescriptor, 'value')))
3642
3939
  ) {
3643
3940
  throw new ApplicationServerError(
3644
3941
  'CONFIG',
3645
- 'Application server options must use data properties',
3942
+ 'Application server settings must use data properties',
3646
3943
  { value },
3647
3944
  )
3648
3945
  }
@@ -3650,9 +3947,11 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
3650
3947
  const port = portDescriptor?.value
3651
3948
  const timeout = timeoutDescriptor?.value
3652
3949
  return {
3653
- ...(host === undefined ? {} : { host: parseApplicationHost(host) }),
3654
- ...(port === undefined ? {} : { port: parseApplicationPort(port) }),
3655
- ...(timeout === undefined ? {} : { timeout: parseApplicationStartTimeout(timeout) }),
3950
+ server: {
3951
+ ...(host === undefined ? {} : { host: parseApplicationHost(host) }),
3952
+ ...(port === undefined ? {} : { port: parseApplicationPort(port) }),
3953
+ ...(timeout === undefined ? {} : { timeout: parseApplicationStartTimeout(timeout) }),
3954
+ },
3656
3955
  }
3657
3956
  } catch (error) {
3658
3957
  if (isApplicationServerError(error)) throw error
@@ -3661,47 +3960,56 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
3661
3960
  })
3662
3961
  }
3663
3962
  }
3963
+ `
3964
+ }),
3965
+ appServerRoutes: Object.freeze({
3966
+ id: "appServerRoutes",
3967
+ name: "appServerRoutes",
3968
+ summary: "The standalone application route dispatcher factory.",
3969
+ category: "source",
3970
+ placeholders: Object.freeze([Object.freeze({
3971
+ name: "healthImport",
3972
+ description: "The selected layer import for the health route constants."
3973
+ })]),
3974
+ content: `import type { DispatcherInterface } from '@orkestrel/router'
3975
+ import type { ApplicationState } from './types.js'
3976
+ import { createDispatcher } from '@orkestrel/router'
3977
+ {{healthImport}}
3978
+ import { handleApplicationHealth } from './handlers.js'
3979
+
3980
+ /** Create a fresh dispatcher for the generated application's fetch-standard health boundary. */
3981
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationDispatcher(): DispatcherInterface<ApplicationState> {
3982
+ return createDispatcher<ApplicationState>({
3983
+ routes: [
3984
+ {
3985
+ method: APP_HEALTH_METHOD,
3986
+ path: APP_HEALTH_PATH,
3987
+ handler: handleApplicationHealth,
3988
+ },
3989
+ ],
3990
+ })
3991
+ }
3664
3992
  `
3665
3993
  }),
3666
3994
  appServerHandlers: Object.freeze({
3667
3995
  id: "appServerHandlers",
3668
3996
  name: "appServerHandlers",
3669
- summary: "The server HTTP request handler.",
3997
+ summary: "The application server diagnostic reporter.",
3670
3998
  category: "source",
3671
3999
  placeholders: Object.freeze([Object.freeze({
4000
+ name: "recordImport",
4001
+ description: "The selected layer type import for the health record."
4002
+ }), Object.freeze({
3672
4003
  name: "nameImport",
3673
4004
  description: "The selected layer import for APP_NAME."
3674
4005
  })]),
3675
- content: `import type { IncomingMessage, ServerResponse } from 'node:http'
4006
+ content: `{{recordImport}}
3676
4007
  {{nameImport}}
3677
- import { APP_HEALTH_METHOD, APP_HEALTH_PATH } from './constants.js'
3678
4008
  import { isApplicationServerError } from './errors.js'
3679
4009
 
3680
- /**
3681
- * Respond to the application health endpoint and reject every other route.
3682
- *
3683
- * @param request - The incoming Node request.
3684
- * @param response - The Node response to complete exactly once.
3685
- */
3686
- ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} handleApplicationRequest(request: IncomingMessage, response: ServerResponse): void {
3687
- if (request.method !== APP_HEALTH_METHOD) {
3688
- response.writeHead(405, {
3689
- allow: APP_HEALTH_METHOD,
3690
- 'content-type': 'text/plain; charset=utf-8',
3691
- })
3692
- response.end('Method Not Allowed')
3693
- return
3694
- }
3695
- if (request.url !== APP_HEALTH_PATH) {
3696
- response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
3697
- response.end('Not Found')
3698
- return
3699
- }
3700
- response.writeHead(200, {
3701
- 'cache-control': 'no-store',
3702
- 'content-type': 'application/json; charset=utf-8',
3703
- })
3704
- response.end(JSON.stringify({ name: APP_NAME, status: 'ok' }))
4010
+ /** Return the generated application's shared health record. */
4011
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} handleApplicationHealth(): Response {
4012
+ return Response.json({ name: APP_NAME, status: 'ok' } satisfies ApplicationRecord)
3705
4013
  }
3706
4014
 
3707
4015
  /**
@@ -3729,8 +4037,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} reportApplicationServerError(error: unknow
3729
4037
  } catch {
3730
4038
  message = '[ERROR] Application server failed'
3731
4039
  }
3732
- process.stderr.write(\`\${message}\\n\`)
3733
4040
  process.exitCode = 1
4041
+ process.stderr.write(\`\${message}\\n\`)
3734
4042
  }
3735
4043
  `
3736
4044
  }),
@@ -3740,176 +4048,106 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} reportApplicationServerError(error: unknow
3740
4048
  summary: "The application server implementation.",
3741
4049
  category: "source",
3742
4050
  placeholders: Object.freeze([]),
3743
- content: `import type { Server } from 'node:http'
3744
- import type { ApplicationServerInterface, ApplicationServerOptions } from './types.js'
3745
- import { once } from 'node:events'
3746
- import { createServer } from 'node:http'
3747
- import { promisify } from 'node:util'
3748
- import {
3749
- APP_HEADERS_TIMEOUT,
3750
- APP_KEEP_ALIVE_TIMEOUT,
3751
- APP_MAX_CONNECTIONS,
3752
- APP_MAX_HEADERS,
3753
- APP_MAX_REQUESTS_PER_SOCKET,
3754
- APP_REQUEST_TIMEOUT,
3755
- } from './constants.js'
3756
- import { ApplicationServerError, isApplicationServerError } from './errors.js'
3757
- import { handleApplicationRequest } from './handlers.js'
4051
+ content: `import type { DispatcherInterface } from '@orkestrel/router'
4052
+ import type { ConnectionInfo, ServerInterface } from '@orkestrel/server'
4053
+ import type {
4054
+ ApplicationServerInterface,
4055
+ ApplicationServerOptions,
4056
+ ApplicationState,
4057
+ } from './types.js'
4058
+ import { createBoundary, createDeadline, createSecurity } from '@orkestrel/middleware'
4059
+ import { createServer } from '@orkestrel/server'
4060
+ import { ApplicationServerError } from './errors.js'
3758
4061
  import {
3759
4062
  parseApplicationHost,
3760
4063
  parseApplicationPort,
3761
4064
  parseApplicationServerOptions,
3762
4065
  parseApplicationStartTimeout,
3763
4066
  } from './parsers.js'
4067
+ import { createApplicationDispatcher } from './routes.js'
3764
4068
 
3765
- /** A repeat-safe Node HTTP application server. */
4069
+ /** A repeat-safe application server composed from the installed server substrate. */
3766
4070
  ${EXPORT_KEYWORD} class ApplicationServer implements ApplicationServerInterface {
3767
4071
  readonly host: string
3768
- readonly #requestedPort: number
3769
- readonly #timeout: number
3770
- #port: number
3771
- readonly #server: Server
3772
- #transition: Promise<void> = Promise.resolve()
3773
- readonly #starts = new Set<AbortController>()
3774
- readonly #stopped = new WeakSet<AbortController>()
4072
+ readonly #dispatcher: DispatcherInterface<ApplicationState>
4073
+ readonly #server: ServerInterface<ApplicationState>
3775
4074
 
3776
4075
  constructor(options: ApplicationServerOptions = {}) {
3777
4076
  const parsed = parseApplicationServerOptions(options)
3778
- this.host = parseApplicationHost(parsed.host === undefined ? process.env.APP_HOST : parsed.host)
3779
- this.#requestedPort = parseApplicationPort(
3780
- parsed.port === undefined ? process.env.APP_PORT : parsed.port,
4077
+ const server = parsed.server
4078
+ const host = parseApplicationHost(
4079
+ server?.host === undefined ? process.env.APP_HOST : server.host,
3781
4080
  )
3782
- this.#port = this.#requestedPort
3783
- this.#timeout = parseApplicationStartTimeout(
3784
- parsed.timeout === undefined ? process.env.APP_START_TIMEOUT : parsed.timeout,
4081
+ const port = parseApplicationPort(
4082
+ server?.port === undefined ? process.env.APP_PORT : server.port,
3785
4083
  )
3786
- this.#server = createServer(handleApplicationRequest)
3787
- this.#server.maxConnections = APP_MAX_CONNECTIONS
3788
- this.#server.maxHeadersCount = APP_MAX_HEADERS
3789
- this.#server.headersTimeout = APP_HEADERS_TIMEOUT
3790
- this.#server.requestTimeout = APP_REQUEST_TIMEOUT
3791
- this.#server.keepAliveTimeout = APP_KEEP_ALIVE_TIMEOUT
3792
- this.#server.maxRequestsPerSocket = APP_MAX_REQUESTS_PER_SOCKET
4084
+ const timeout = parseApplicationStartTimeout(
4085
+ server?.timeout === undefined ? process.env.APP_START_TIMEOUT : server.timeout,
4086
+ )
4087
+ this.host = host
4088
+ this.#dispatcher = createApplicationDispatcher()
4089
+ this.#server = createServer<ApplicationState>({
4090
+ dispatcher: this.#dispatcher,
4091
+ state: ApplicationServer.#state,
4092
+ middleware: [
4093
+ createBoundary<ApplicationState>(),
4094
+ createSecurity<ApplicationState>(),
4095
+ createDeadline<ApplicationState>({ ms: timeout }),
4096
+ ],
4097
+ host,
4098
+ port,
4099
+ timeouts: { start: timeout },
4100
+ })
3793
4101
  }
3794
4102
 
3795
- get port(): number {
3796
- return this.#port
4103
+ static #state(connection: ConnectionInfo): ApplicationState {
4104
+ return { connection }
3797
4105
  }
3798
4106
 
3799
- get listening(): boolean {
3800
- return this.#server.listening
4107
+ get port(): number | undefined {
4108
+ return this.#server.port
3801
4109
  }
3802
4110
 
3803
- get url(): string {
3804
- const hostname = this.host.includes(':') ? \`[\${this.host}]\` : this.host
3805
- return \`http://\${hostname}:\${this.port}\`
4111
+ get status(): ApplicationServerInterface['status'] {
4112
+ return this.#server.status
3806
4113
  }
3807
4114
 
3808
- start(signal?: AbortSignal): Promise<void> {
3809
- const controller = new AbortController()
3810
- this.#starts.add(controller)
3811
- const queued = this.#queue(this.#start.bind(this, controller, signal))
3812
- void queued.then(this.#settle.bind(this, controller), this.#settle.bind(this, controller))
3813
- return queued
4115
+ get url(): string | undefined {
4116
+ const port = this.port
4117
+ if (port === undefined) return undefined
4118
+ const hostname = this.host.includes(':') ? \`[\${this.host}]\` : this.host
4119
+ return \`http://\${hostname}:\${port}\`
3814
4120
  }
3815
4121
 
3816
- stop(): Promise<void> {
3817
- for (const controller of this.#starts) {
3818
- this.#stopped.add(controller)
3819
- controller.abort()
3820
- }
3821
- return this.#queue(this.#stop.bind(this))
3822
- }
3823
-
3824
- #queue(operation: () => Promise<void>): Promise<void> {
3825
- const queued = this.#transition.then(operation, operation)
3826
- this.#transition = queued.then(
3827
- () => undefined,
3828
- () => undefined,
3829
- )
3830
- return queued
3831
- }
3832
-
3833
- async #start(controller: AbortController, signal?: AbortSignal): Promise<void> {
3834
- if (this.listening) return
3835
- if (controller.signal.aborted) {
3836
- if (this.#stopped.has(controller)) return
3837
- throw new ApplicationServerError('LIFECYCLE', 'Application server startup was cancelled')
3838
- }
4122
+ async start(signal?: AbortSignal): Promise<void> {
3839
4123
  try {
3840
- if (signal?.aborted === true) {
3841
- throw new ApplicationServerError('LIFECYCLE', 'Application server startup was cancelled', {
3842
- cause: signal.reason,
3843
- })
3844
- }
3845
- await this.#listen(controller, signal)
4124
+ await this.#server.start(signal)
3846
4125
  } catch (error) {
3847
- if (this.#stopped.has(controller)) return
3848
- if (isApplicationServerError(error)) throw error
3849
- throw new ApplicationServerError('LIFECYCLE', 'Failed to inspect startup signal', {
4126
+ throw new ApplicationServerError('LIFECYCLE', 'Failed to start application server', {
3850
4127
  cause: error,
3851
4128
  })
3852
4129
  }
3853
4130
  }
3854
4131
 
3855
- async #stop(): Promise<void> {
3856
- if (!this.listening) return
3857
- await this.#close()
3858
- }
3859
-
3860
- async #listen(controller: AbortController, signal?: AbortSignal): Promise<void> {
3861
- const relay = signal === undefined ? undefined : this.#abort.bind(this, controller, signal)
3862
- const timer = setTimeout(this.#expire.bind(this, controller), this.#timeout)
4132
+ async stop(): Promise<void> {
3863
4133
  try {
3864
- if (signal !== undefined && relay !== undefined) {
3865
- signal.addEventListener('abort', relay, { once: true })
3866
- }
3867
- this.#server.listen({
3868
- port: this.#requestedPort,
3869
- host: this.host,
3870
- signal: controller.signal,
3871
- })
3872
- await once(this.#server, 'listening', { signal: controller.signal })
4134
+ await this.#server.stop()
3873
4135
  } catch (error) {
3874
- throw new ApplicationServerError('LIFECYCLE', 'Failed to start application server', {
4136
+ throw new ApplicationServerError('LIFECYCLE', 'Failed to stop application server', {
3875
4137
  cause: error,
3876
4138
  })
3877
- } finally {
3878
- clearTimeout(timer)
3879
- if (relay !== undefined && signal !== undefined) {
3880
- signal.removeEventListener('abort', relay)
3881
- }
3882
- }
3883
- const address = this.#server.address()
3884
- if (address === null || typeof address === 'string') {
3885
- await this.#close()
3886
- throw new ApplicationServerError('LIFECYCLE', 'Server did not expose a TCP address')
3887
4139
  }
3888
- this.#port = address.port
3889
4140
  }
3890
4141
 
3891
- #abort(controller: AbortController, signal: AbortSignal): void {
3892
- controller.abort(signal.reason)
3893
- }
3894
-
3895
- #expire(controller: AbortController): void {
3896
- controller.abort(new Error(\`Application server startup exceeded \${this.#timeout} milliseconds\`))
3897
- }
3898
-
3899
- #settle(controller: AbortController): void {
3900
- this.#starts.delete(controller)
3901
- }
3902
-
3903
- async #close(): Promise<void> {
4142
+ async destroy(): Promise<void> {
3904
4143
  try {
3905
- const closed = promisify(this.#server.close.bind(this.#server))()
3906
- this.#server.closeIdleConnections()
3907
- this.#server.closeAllConnections()
3908
- await closed
4144
+ await this.#server.destroy()
3909
4145
  } catch (error) {
3910
- throw new ApplicationServerError('LIFECYCLE', 'Failed to stop application server', {
4146
+ throw new ApplicationServerError('LIFECYCLE', 'Failed to destroy application server', {
3911
4147
  cause: error,
3912
4148
  })
4149
+ } finally {
4150
+ this.#dispatcher.destroy()
3913
4151
  }
3914
4152
  }
3915
4153
  }
@@ -3932,16 +4170,17 @@ import { ApplicationServerRunner } from './ApplicationServerRunner.js'
3932
4170
  /**
3933
4171
  * Create a stopped application server.
3934
4172
  *
3935
- * @param options - Optional host and port overrides.
4173
+ * @param options - Optional grouped server overrides.
3936
4174
  * @returns A lifecycle-safe application server.
3937
4175
  *
3938
4176
  * @example
3939
4177
  * \`\`\`ts
3940
4178
  * import { createApplicationServer } from '@app/server'
3941
4179
  *
3942
- * ${CONST_KEYWORD} server = createApplicationServer({ port: 0 })
4180
+ * ${CONST_KEYWORD} server = createApplicationServer({ server: { port: 0 } })
3943
4181
  * await server.start()
3944
4182
  * await server.stop()
4183
+ * await server.destroy()
3945
4184
  * \`\`\`
3946
4185
  */
3947
4186
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationServer(
@@ -3953,21 +4192,21 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationServer(
3953
4192
  /**
3954
4193
  * Start a process-owned application server.
3955
4194
  *
3956
- * @param options - Optional host and port overrides.
4195
+ * @param options - Optional grouped server overrides.
3957
4196
  * @returns The runner that owns signals and provides explicit asynchronous cleanup.
3958
4197
  *
3959
4198
  * @example
3960
4199
  * \`\`\`ts
3961
4200
  * import { startApplicationServer } from '@app/server'
3962
4201
  *
3963
- * ${CONST_KEYWORD} runner = startApplicationServer({ port: 0 })
4202
+ * ${CONST_KEYWORD} runner = startApplicationServer({ server: { port: 0 } })
3964
4203
  * await runner.stop()
3965
4204
  * \`\`\`
3966
4205
  */
3967
4206
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationServer(
3968
4207
  options: ApplicationServerOptions = {},
3969
4208
  ): ApplicationServerRunnerInterface {
3970
- const runner = new ApplicationServerRunner(options)
4209
+ const runner = new ApplicationServerRunner(new ApplicationServer(options))
3971
4210
  runner.start()
3972
4211
  return runner
3973
4212
  }
@@ -3978,57 +4217,144 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationServer(
3978
4217
  name: "appServerRunner",
3979
4218
  summary: "The application server process lifecycle owner.",
3980
4219
  category: "source",
3981
- placeholders: Object.freeze([]),
3982
- content: `import type {
4220
+ placeholders: Object.freeze([Object.freeze({
4221
+ name: "nameImport",
4222
+ description: "The selected layer import for APP_NAME."
4223
+ })]),
4224
+ content: `import type { EmitterInterface } from '@orkestrel/emitter'
4225
+ import type {
3983
4226
  ApplicationServerInterface,
3984
- ApplicationServerOptions,
4227
+ ApplicationServerRunnerEventMap,
3985
4228
  ApplicationServerRunnerInterface,
4229
+ ApplicationServerRunnerOptions,
3986
4230
  } from './types.js'
3987
- import { ApplicationServer } from './ApplicationServer.js'
4231
+ import { Emitter } from '@orkestrel/emitter'
4232
+ {{nameImport}}
4233
+ import { ApplicationServerError } from './errors.js'
3988
4234
  import { reportApplicationServerError } from './handlers.js'
3989
4235
 
3990
4236
  /** Own process signals and startup failure handling for one application server. */
3991
4237
  ${EXPORT_KEYWORD} class ApplicationServerRunner implements ApplicationServerRunnerInterface {
4238
+ readonly #emitter: Emitter<ApplicationServerRunnerEventMap>
3992
4239
  readonly #server: ApplicationServerInterface
3993
- readonly #signal: () => void
4240
+ readonly #handler: () => void
4241
+ #controller: AbortController | undefined
4242
+ #queue: Promise<void> = Promise.resolve()
4243
+ #stopping: Promise<void> | undefined
4244
+ #announcement: number | undefined
3994
4245
  #generation = 0
3995
4246
  #started = false
3996
4247
 
3997
- constructor(options: ApplicationServerOptions = {}) {
3998
- this.#server = new ApplicationServer(options)
3999
- this.#signal = this.#shutdown.bind(this)
4248
+ constructor(server: ApplicationServerInterface, options: ApplicationServerRunnerOptions = {}) {
4249
+ this.#emitter = new Emitter({
4250
+ ...(options.on === undefined ? {} : { on: options.on }),
4251
+ ...(options.error === undefined ? {} : { error: options.error }),
4252
+ })
4253
+ this.#server = server
4254
+ this.#handler = this.#shutdown.bind(this)
4255
+ this.#emitter.on('ready', this.#announce.bind(this))
4256
+ this.#emitter.on('fail', reportApplicationServerError)
4257
+ }
4258
+
4259
+ get emitter(): EmitterInterface<ApplicationServerRunnerEventMap> {
4260
+ return this.#emitter
4000
4261
  }
4001
4262
 
4002
4263
  start(): void {
4003
4264
  if (this.#started) return
4004
4265
  this.#started = true
4266
+ this.#stopping = undefined
4005
4267
  const generation = ++this.#generation
4006
- process.once('SIGINT', this.#signal)
4007
- process.once('SIGTERM', this.#signal)
4008
- void this.#server.start().catch(this.#fail.bind(this, generation))
4268
+ const controller = new AbortController()
4269
+ this.#controller = controller
4270
+ process.once('SIGINT', this.#handler)
4271
+ process.once('SIGTERM', this.#handler)
4272
+ this.#queue = this.#queue.then(this.#begin.bind(this, generation, controller))
4009
4273
  }
4010
4274
 
4011
4275
  stop(): Promise<void> {
4012
- this.#generation += 1
4276
+ if (this.#stopping !== undefined) return this.#stopping
4277
+ // Aborting first lets an in-flight substrate startup settle before stop inspects its state,
4278
+ // and the generation bump is what discards that abort's rejection while leaving a genuine
4279
+ // stop failure reportable. The shared queue then keeps a subsequent restart behind this complete
4280
+ // shutdown; concurrent callers join one substrate stop through #stopping, and the trailing catch
4281
+ // discards nothing — it only keeps one failed stop from wedging every later one.
4282
+ ++this.#generation
4283
+ this.#controller?.abort()
4013
4284
  this.#release()
4014
- return this.#server.stop()
4285
+ const stopping = this.#queue.then(() => this.#server.stop())
4286
+ this.#stopping = stopping
4287
+ this.#queue = stopping.catch(() => undefined)
4288
+ void stopping.then(this.#finishStop.bind(this, stopping), this.#rejectStop.bind(this, stopping))
4289
+ return stopping
4290
+ }
4291
+
4292
+ async #begin(generation: number, controller: AbortController): Promise<void> {
4293
+ if (generation !== this.#generation || !this.#started) return
4294
+ try {
4295
+ await this.#server.start(controller.signal)
4296
+ await this.#ready(generation)
4297
+ } catch (error) {
4298
+ this.#fail(generation, error)
4299
+ }
4015
4300
  }
4016
4301
 
4017
4302
  #fail(generation: number, error: unknown): void {
4018
4303
  if (generation !== this.#generation) return
4019
4304
  this.#release()
4020
- reportApplicationServerError(error)
4305
+ this.#emitter.emit('fail', error)
4306
+ }
4307
+
4308
+ #finishStop(stopping: Promise<void>): void {
4309
+ if (this.#stopping === stopping) this.#stopping = undefined
4310
+ }
4311
+
4312
+ #rejectStop(stopping: Promise<void>, error: unknown): void {
4313
+ this.#finishStop(stopping)
4314
+ this.#emitter.emit('fail', error)
4315
+ }
4316
+
4317
+ async #ready(generation: number): Promise<void> {
4318
+ if (generation !== this.#generation || !this.#started) return
4319
+ const url = this.#server.url
4320
+ if (url === undefined) {
4321
+ const failure = new ApplicationServerError(
4322
+ 'LIFECYCLE',
4323
+ 'Application server did not expose a URL after successful startup',
4324
+ )
4325
+ try {
4326
+ await this.#server.stop()
4327
+ } catch (error) {
4328
+ this.#fail(generation, error)
4329
+ return
4330
+ }
4331
+ this.#fail(generation, failure)
4332
+ return
4333
+ }
4334
+ this.#announcement = generation
4335
+ this.#emitter.emit('ready', url)
4336
+ this.#announcement = undefined
4337
+ }
4338
+
4339
+ #announce(url: string): void {
4340
+ if (this.#announcement !== this.#generation) return
4341
+ try {
4342
+ process.stderr.write(\`[READY] \${APP_NAME} \${url}\\n\`)
4343
+ } catch (error) {
4344
+ const stopped = this.stop()
4345
+ const generation = this.#generation
4346
+ void stopped.then(this.#fail.bind(this, generation, error), () => undefined)
4347
+ }
4021
4348
  }
4022
4349
 
4023
4350
  #shutdown(): void {
4024
4351
  const stopped = this.stop()
4025
- const generation = this.#generation
4026
- void stopped.catch(this.#fail.bind(this, generation))
4352
+ void stopped.catch(() => undefined)
4027
4353
  }
4028
4354
 
4029
4355
  #release(): void {
4030
- process.off('SIGINT', this.#signal)
4031
- process.off('SIGTERM', this.#signal)
4356
+ process.off('SIGINT', this.#handler)
4357
+ process.off('SIGTERM', this.#handler)
4032
4358
  this.#started = false
4033
4359
  }
4034
4360
  }
@@ -4044,6 +4370,7 @@ ${EXPORT_KEYWORD} class ApplicationServerRunner implements ApplicationServerRunn
4044
4370
  export * from './constants.js'
4045
4371
  export * from './errors.js'
4046
4372
  export * from './parsers.js'
4373
+ export * from './routes.js'
4047
4374
  export * from './handlers.js'
4048
4375
  export * from './ApplicationServer.js'
4049
4376
  export * from './ApplicationServerRunner.js'
@@ -4068,10 +4395,16 @@ try {
4068
4395
  setup: Object.freeze({
4069
4396
  id: "setup",
4070
4397
  name: "setup",
4071
- summary: "The generated-minimal `tests/setup.ts` recorder helper — no placeholders.",
4398
+ summary: "The generated-minimal `tests/setup.ts` shared test helpers.",
4072
4399
  category: "tests",
4073
- placeholders: Object.freeze([]),
4074
- content: `// ── Call recorder (a real callback, not a mock) ──────────────────────────────
4400
+ placeholders: Object.freeze([Object.freeze({
4401
+ name: "eventImport",
4402
+ description: "The optional emitter types used by application server tests."
4403
+ }), Object.freeze({
4404
+ name: "eventHelper",
4405
+ description: "The optional typed event waiter used by application server tests."
4406
+ })]),
4407
+ content: `{{eventImport}}// ── Call recorder (a real callback, not a mock) ──────────────────────────────
4075
4408
  //
4076
4409
  // The test rules require a recording callback when a test only needs to count calls or inspect arguments:
4077
4410
  // recorder — a real listener that records every invocation — rather than a test-
@@ -4111,12 +4444,64 @@ ${EXPORT_KEYWORD} function createRecorder<TArgs extends readonly unknown[]>(): T
4111
4444
  },
4112
4445
  }
4113
4446
  }
4114
-
4447
+ {{eventHelper}}
4115
4448
  /** Whether a repository-relative Vue SFC belongs to the private browser application. */
4116
4449
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserVuePath(path: string): boolean {
4117
4450
  const normalized = path.replaceAll('\\\\', '/')
4118
4451
  return normalized.startsWith('app/browser/')
4119
4452
  }
4453
+ `
4454
+ }),
4455
+ configTest: Object.freeze({
4456
+ id: "configTest",
4457
+ name: "configTest",
4458
+ summary: "The generated root Vite configuration behavior test.",
4459
+ category: "tests",
4460
+ placeholders: Object.freeze([Object.freeze({
4461
+ name: "imports",
4462
+ description: "The machinery-aware root configuration imports."
4463
+ }), Object.freeze({
4464
+ name: "cases",
4465
+ description: "The machinery-aware root configuration cases."
4466
+ })]),
4467
+ content: `{{imports}}
4468
+
4469
+ describe('root Vite configuration', () => {
4470
+ it('keeps workspace paths physically contained', () => {
4471
+ const root = resolveWorkspacePath('.')
4472
+ const source = resolveWorkspacePath('src')
4473
+ const parent = resolveWorkspacePath('..')
4474
+
4475
+ expect(workspacePath(root)).toBe('')
4476
+ expect(workspacePath(source)).toBe('src')
4477
+ expect(workspacePath(parent)).toBeUndefined()
4478
+ expect(containedPath(root, source)).toBe(true)
4479
+ expect(containedPath(root, parent)).toBe(false)
4480
+ })
4481
+
4482
+ it('enforces environment direction for paths and module sources', () => {
4483
+ expect(environmentPathError('src/core', 'app/core/index.ts')).toBe(
4484
+ 'Published modules cannot depend on private application modules',
4485
+ )
4486
+ expect(environmentPathError('app/core', 'src/browser/index.ts')).toBe(
4487
+ 'Core modules must remain host-independent',
4488
+ )
4489
+ expect(environmentPathError('app/browser', 'src/server/index.ts')).toBe(
4490
+ 'Browser modules cannot depend on Node or server-only modules',
4491
+ )
4492
+ expect(environmentPathError('app/server', 'src/browser/index.ts')).toBe(
4493
+ 'Server modules cannot depend on Vue or browser-only modules',
4494
+ )
4495
+ expect(environmentPathError('app/browser', 'src/core/index.ts')).toBeUndefined()
4496
+ expect(environmentSourceError('src/core', 'node:path')).toBe(
4497
+ 'Core modules must remain host-independent',
4498
+ )
4499
+ expect(environmentSourceError('app/server', 'vue')).toBe(
4500
+ 'Server modules cannot depend on Vue or browser-only modules',
4501
+ )
4502
+ expect(environmentSourceError('app/browser', '@app/core')).toBeUndefined()
4503
+ }){{cases}}
4504
+ })
4120
4505
  `
4121
4506
  }),
4122
4507
  policyTest: Object.freeze({
@@ -4124,32 +4509,17 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserVuePath(path: string): boolean {
4124
4509
  name: "policyTest",
4125
4510
  summary: "The generated repository filename-policy test.",
4126
4511
  category: "tests",
4127
- placeholders: Object.freeze([
4128
- Object.freeze({
4129
- name: "browserPolicySpecifier",
4130
- description: "The optional real Chromium filesystem-probe import."
4131
- }),
4132
- Object.freeze({
4133
- name: "browserPolicyImport",
4134
- description: "The optional real Chromium package import."
4135
- }),
4136
- Object.freeze({
4137
- name: "browserPolicyTest",
4138
- description: "The optional capability-gated Chromium policy test."
4139
- }),
4140
- Object.freeze({
4141
- name: "vuePolicyImport",
4142
- description: "The optional official Vue SFC compiler import."
4143
- }),
4144
- Object.freeze({
4145
- name: "workspacePolicyAssertion",
4146
- description: "The formatter-stable workspace policy assertion."
4147
- })
4148
- ]),
4149
- content: `import { globSync{{browserPolicySpecifier}} } from 'node:fs'
4512
+ placeholders: Object.freeze([Object.freeze({
4513
+ name: "vuePolicyImport",
4514
+ description: "The optional official Vue SFC compiler import."
4515
+ }), Object.freeze({
4516
+ name: "workspacePolicyAssertion",
4517
+ description: "The formatter-stable workspace policy assertion."
4518
+ })]),
4519
+ content: `import { globSync } from 'node:fs'
4150
4520
  import { describe, expect, it } from 'vitest'
4151
4521
  import { isBrowserVuePath } from './setup.js'
4152
- import { inspectCodingWorkspace } from './setupPolicy.js'{{browserPolicyImport}}{{vuePolicyImport}}
4522
+ import { inspectCodingWorkspace } from './setupPolicy.js'{{vuePolicyImport}}
4153
4523
 
4154
4524
  describe('repository coding law', () => {
4155
4525
  it('keeps Vue single-file components exclusively in browser environments', () => {
@@ -4160,7 +4530,7 @@ describe('repository coding law', () => {
4160
4530
 
4161
4531
  it('enforces source placement, exports, readonly contracts, and syntax law', () => {
4162
4532
  {{workspacePolicyAssertion}}
4163
- }){{browserPolicyTest}}
4533
+ })
4164
4534
  })
4165
4535
  `
4166
4536
  }),
@@ -4180,6 +4550,7 @@ ${IMPORT_KEYWORD} { createServer } from 'node:http'
4180
4550
  /** One real application child process plus its captured diagnostic output. */
4181
4551
  ${EXPORT_KEYWORD} interface ApplicationProcessInterface {
4182
4552
  readonly child: ChildProcess
4553
+ readonly ready: Promise<void>
4183
4554
  output(): string
4184
4555
  }
4185
4556
 
@@ -4219,11 +4590,17 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} buildApplicationServer(): void {
4219
4590
  }
4220
4591
  }
4221
4592
 
4222
- /** Reserve and release a real loopback port for an immediate child-process bind. */
4223
- ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<number> {
4593
+ /** Start a real Node server bound to one loopback port, or 0 for an ephemeral one. */
4594
+ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} startLoopbackServer(port: number): Promise<Server> {
4224
4595
  const server = createServer()
4225
- server.listen(0, '127.0.0.1')
4596
+ server.listen(port, '127.0.0.1')
4226
4597
  await once(server, 'listening')
4598
+ return server
4599
+ }
4600
+
4601
+ /** Reserve and release a real loopback port for an immediate child-process bind. */
4602
+ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<number> {
4603
+ const server = await startLoopbackServer(0)
4227
4604
  const address = server.address()
4228
4605
  if (address === null || typeof address === 'string') {
4229
4606
  await stopNodeServer(server)
@@ -4233,23 +4610,6 @@ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<numbe
4233
4610
  return address.port
4234
4611
  }
4235
4612
 
4236
- /** Wait until one in-process application server responds on loopback. */
4237
- ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} waitForLoopbackResponse(port: number): Promise<Response> {
4238
- const deadline = Date.now() + 10_000
4239
- let failure: unknown
4240
- while (Date.now() < deadline) {
4241
- try {
4242
- return await fetch(\`http://127.0.0.1:\${port}\`, {
4243
- signal: AbortSignal.timeout(250),
4244
- })
4245
- } catch (error) {
4246
- failure = error
4247
- await new Promise<void>((resolvePromise) => setTimeout(resolvePromise, 25))
4248
- }
4249
- }
4250
- throw new Error(\`application server did not become ready: \${String(failure)}\`)
4251
- }
4252
-
4253
4613
  /** Start the built application entry as a real child process. */
4254
4614
  ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationProcess(
4255
4615
  port: number,
@@ -4266,16 +4626,20 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationProcess(
4266
4626
  stdio: ['ignore', 'pipe', 'pipe'],
4267
4627
  })
4268
4628
  const output: string[] = []
4629
+ const readiness = Promise.withResolvers<void>()
4269
4630
  child.stdout?.setEncoding('utf8')
4270
4631
  child.stderr?.setEncoding('utf8')
4271
4632
  child.stdout?.on('data', (chunk: unknown) => {
4272
4633
  if (typeof chunk === 'string') output.push(chunk)
4273
4634
  })
4274
4635
  child.stderr?.on('data', (chunk: unknown) => {
4275
- if (typeof chunk === 'string') output.push(chunk)
4636
+ if (typeof chunk !== 'string') return
4637
+ output.push(chunk)
4638
+ if (output.join('').includes('[READY] ')) readiness.resolve()
4276
4639
  })
4277
4640
  return {
4278
4641
  child,
4642
+ ready: readiness.promise,
4279
4643
  output() {
4280
4644
  return output.join('')
4281
4645
  },
@@ -4440,15 +4804,28 @@ describe('create{{pascal}}', () => {
4440
4804
  name: "appCoreTest",
4441
4805
  summary: "The host-independent application test.",
4442
4806
  category: "tests",
4443
- placeholders: Object.freeze([]),
4807
+ placeholders: Object.freeze([
4808
+ Object.freeze({
4809
+ name: "guardImport",
4810
+ description: "The optional shared-record guard test import."
4811
+ }),
4812
+ Object.freeze({
4813
+ name: "readImport",
4814
+ description: "The optional shared health-boundary read test import."
4815
+ }),
4816
+ Object.freeze({
4817
+ name: "boundary",
4818
+ description: "The optional shared health-boundary contract test."
4819
+ })
4820
+ ]),
4444
4821
  content: `import {
4445
4822
  APP_NAME,
4446
4823
  ApplicationError,
4447
4824
  createApplication,
4448
4825
  isApplicationError,
4449
- MAX_APPLICATION_NAME_INPUT_LENGTH,
4826
+ {{guardImport}} MAX_APPLICATION_NAME_INPUT_LENGTH,
4450
4827
  parseApplicationName,
4451
- } from '@app/core'
4828
+ {{readImport}}} from '@app/core'
4452
4829
  import { describe, expect, it } from 'vitest'
4453
4830
 
4454
4831
  describe('createApplication', () => {
@@ -4485,7 +4862,14 @@ describe('createApplication', () => {
4485
4862
  revocable.revoke()
4486
4863
  expect(isApplicationError(revocable.proxy)).toBe(false)
4487
4864
  })
4488
- })
4865
+
4866
+ it('refuses a foreign application error', () => {
4867
+ const foreign = new Error('foreign')
4868
+ foreign.name = 'ApplicationError'
4869
+
4870
+ expect(isApplicationError(foreign)).toBe(false)
4871
+ })
4872
+ }){{boundary}}
4489
4873
  `
4490
4874
  }),
4491
4875
  appBrowserTest: Object.freeze({
@@ -4493,18 +4877,36 @@ describe('createApplication', () => {
4493
4877
  name: "appBrowserTest",
4494
4878
  summary: "The real-browser application mount test.",
4495
4879
  category: "tests",
4496
- placeholders: Object.freeze([Object.freeze({
4497
- name: "browserTestNameImport",
4498
- description: "The layer-correct browser APP_NAME test import."
4499
- })]),
4880
+ placeholders: Object.freeze([
4881
+ Object.freeze({
4882
+ name: "browserTestNameImport",
4883
+ description: "The layer-correct browser APP_NAME test import."
4884
+ }),
4885
+ Object.freeze({
4886
+ name: "showcaseImport",
4887
+ description: "The optional showcase factory test import."
4888
+ }),
4889
+ Object.freeze({
4890
+ name: "entryImport",
4891
+ description: "The optional seeder and boundary-entry test imports."
4892
+ }),
4893
+ Object.freeze({
4894
+ name: "showcase",
4895
+ description: "The optional real-browser showcase mount test."
4896
+ }),
4897
+ Object.freeze({
4898
+ name: "boundary",
4899
+ description: "The optional real-browser boundary degradation test."
4900
+ })
4901
+ ]),
4500
4902
  content: `{{browserTestNameImport}}
4501
4903
  import {
4502
4904
  BrowserApplicationError,
4503
4905
  createBrowserApplication,
4504
- isBrowserApplicationError,
4906
+ {{showcaseImport}} isBrowserApplicationError,
4505
4907
  MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH,
4506
4908
  parseBrowserApplicationOptions,
4507
- } from '@app/browser'
4909
+ {{entryImport}}} from '@app/browser'
4508
4910
  import { buildElement } from '../../setupBrowser.js'
4509
4911
  import { describe, expect, it } from 'vitest'
4510
4912
 
@@ -4588,313 +4990,205 @@ describe('createBrowserApplication', () => {
4588
4990
  expect(isBrowserApplicationError(new Error('plain'))).toBe(false)
4589
4991
  expect(isBrowserApplicationError(revocable.proxy)).toBe(false)
4590
4992
  })
4591
- })
4993
+ }){{showcase}}{{boundary}}
4592
4994
  `
4593
4995
  }),
4594
4996
  appServerTest: Object.freeze({
4595
4997
  id: "appServerTest",
4596
4998
  name: "appServerTest",
4597
- summary: "The real loopback application server lifecycle test.",
4999
+ summary: "The real dispatcher, server-substrate, and runner integration test.",
4598
5000
  category: "tests",
4599
- placeholders: Object.freeze([Object.freeze({
4600
- name: "testNameImport",
4601
- description: "The layer-correct APP_NAME test import."
4602
- })]),
5001
+ placeholders: Object.freeze([
5002
+ Object.freeze({
5003
+ name: "testNameImport",
5004
+ description: "The layer-correct APP_NAME and health-contract test import."
5005
+ }),
5006
+ Object.freeze({
5007
+ name: "serverImport",
5008
+ description: "The application server test import, minus any relocated health contract."
5009
+ }),
5010
+ Object.freeze({
5011
+ name: "boundary",
5012
+ description: "The optional real-server shared-boundary test."
5013
+ })
5014
+ ]),
4603
5015
  content: `{{testNameImport}}
4604
- import {
4605
- APP_HEALTH_METHOD,
4606
- APP_HEALTH_PATH,
4607
- APP_MAX_CONNECTIONS,
4608
- ApplicationServerRunner,
4609
- createApplicationServer,
4610
- startApplicationServer,
4611
- } from '@app/server'
4612
- import { once } from 'node:events'
4613
- import { createServer } from 'node:http'
4614
- import { connect } from 'node:net'
5016
+ {{serverImport}}
4615
5017
  import { beforeAll, describe, expect, it } from 'vitest'
4616
5018
  import {
4617
5019
  buildApplicationServer,
4618
5020
  reserveLoopbackPort,
4619
5021
  startApplicationProcess,
5022
+ startLoopbackServer,
4620
5023
  stopNodeServer,
4621
5024
  waitForApplicationProcess,
4622
- waitForApplicationResponse,
4623
- waitForLoopbackResponse,
4624
- waitForSocketClose,
4625
5025
  } from '../../setupServer.js'
5026
+ import { createRecorder, waitForEvent } from '../../setup.js'
4626
5027
 
4627
5028
  beforeAll(() => buildApplicationServer(), 60_000)
4628
5029
 
4629
- describe('ApplicationServer', () => {
4630
- it('serves a real loopback request and tolerates repeated lifecycle calls', async () => {
4631
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
5030
+ describe('application dispatcher', () => {
5031
+ const state = { connection: { encrypted: false } }
5032
+
5033
+ it('returns the typed shared application record for the health route', async () => {
5034
+ const dispatcher = createApplicationDispatcher()
4632
5035
  try {
4633
- await Promise.all([server.start(), server.start(), server.start()])
4634
- await server.start()
5036
+ const response = await dispatcher.handle(
5037
+ new Request(\`http://application.test\${APP_HEALTH_PATH}\`),
5038
+ state,
5039
+ )
4635
5040
 
4636
- const response = await fetch(server.url)
4637
5041
  expect(response.status).toBe(200)
4638
- expect(response.headers.get('cache-control')).toBe('no-store')
4639
- expect(response.headers.get('content-type')).toBe('application/json; charset=utf-8')
4640
5042
  expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
5043
+ expect(APP_HEALTH_PATH).toBe('/health')
4641
5044
  } finally {
4642
- await Promise.all([server.stop(), server.stop(), server.stop()])
4643
- await server.stop()
5045
+ dispatcher.destroy()
4644
5046
  }
4645
5047
  })
4646
5048
 
4647
- it('rejects unsupported methods and unknown routes', async () => {
4648
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
5049
+ it('distinguishes a wrong method from a missing route', async () => {
5050
+ const dispatcher = createApplicationDispatcher()
4649
5051
  try {
4650
- await server.start()
4651
- const method = await fetch(server.url, { method: 'POST' })
4652
- expect(method.status).toBe(405)
4653
- expect(method.headers.get('allow')).toBe(APP_HEALTH_METHOD)
4654
- expect(method.headers.get('content-type')).toBe('text/plain; charset=utf-8')
4655
- expect(await method.text()).toBe('Method Not Allowed')
4656
- const route = await fetch(\`\${server.url}/missing\`)
4657
- expect(route.status).toBe(404)
4658
- expect(route.headers.get('content-type')).toBe('text/plain; charset=utf-8')
4659
- expect(await route.text()).toBe('Not Found')
4660
- expect(APP_HEALTH_PATH).toBe('/')
4661
- } finally {
4662
- await server.stop()
4663
- }
4664
- })
4665
-
4666
- it('serves concurrent loopback requests without cross-request state', async () => {
4667
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4668
- try {
4669
- await server.start()
4670
- const responses = await Promise.all(
4671
- Array.from({ length: 8 }, async () => {
4672
- const response = await fetch(server.url)
4673
- return { status: response.status, body: await response.json() }
4674
- }),
5052
+ const method = await dispatcher.handle(
5053
+ new Request(\`http://application.test\${APP_HEALTH_PATH}\`, { method: 'POST' }),
5054
+ state,
4675
5055
  )
4676
- expect(responses).toHaveLength(8)
4677
- for (const response of responses) {
4678
- expect(response).toEqual({
4679
- status: 200,
4680
- body: { name: APP_NAME, status: 'ok' },
4681
- })
4682
- }
4683
- } finally {
4684
- await server.stop()
4685
- }
4686
- })
4687
-
4688
- it('validates direct options before allocating a listener', () => {
4689
- expect(() => createApplicationServer({ host: ' ' })).toThrow(
4690
- expect.objectContaining({ code: 'CONFIG' }),
4691
- )
4692
- expect(() => createApplicationServer({ port: Number.NaN })).toThrow(
4693
- expect.objectContaining({ code: 'CONFIG' }),
4694
- )
4695
- expect(() => createApplicationServer({ port: -1 })).toThrow(
4696
- expect.objectContaining({ code: 'CONFIG' }),
4697
- )
4698
- expect(() => createApplicationServer({ timeout: 0 })).toThrow(
4699
- expect.objectContaining({ code: 'CONFIG' }),
4700
- )
4701
- for (const value of [null, 42, [], { host: 42 }, { port: [42] }]) {
4702
- expect(() => Reflect.apply(createApplicationServer, undefined, [value])).toThrow(
4703
- expect.objectContaining({ code: 'CONFIG' }),
4704
- )
4705
- }
4706
- expect(createApplicationServer({ host: '::1' }).url).toBe('http://[::1]:3000')
4707
- })
5056
+ const missing = await dispatcher.handle(new Request('http://application.test/missing'), state)
4708
5057
 
4709
- it('parses the real APP_HOST, APP_PORT, and APP_START_TIMEOUT environment boundary and restores it', () => {
4710
- const previousHost = process.env.APP_HOST
4711
- const previousPort = process.env.APP_PORT
4712
- const previousTimeout = process.env.APP_START_TIMEOUT
4713
- try {
4714
- process.env.APP_HOST = ' 127.0.0.1 '
4715
- process.env.APP_PORT = '0'
4716
- process.env.APP_START_TIMEOUT = '250'
4717
- const server = createApplicationServer()
4718
- expect(server.host).toBe('127.0.0.1')
4719
- expect(server.port).toBe(0)
4720
-
4721
- process.env.APP_PORT = '1e3'
4722
- expect(() => createApplicationServer()).toThrow(expect.objectContaining({ code: 'CONFIG' }))
4723
- process.env.APP_PORT = '0'
4724
- process.env.APP_START_TIMEOUT = '0'
4725
- expect(() => createApplicationServer()).toThrow(expect.objectContaining({ code: 'CONFIG' }))
5058
+ expect(method.status).toBe(405)
5059
+ expect(method.headers.get('allow')).toContain(APP_HEALTH_METHOD)
5060
+ expect(missing.status).toBe(404)
4726
5061
  } finally {
4727
- if (previousHost === undefined) delete process.env.APP_HOST
4728
- else process.env.APP_HOST = previousHost
4729
- if (previousPort === undefined) delete process.env.APP_PORT
4730
- else process.env.APP_PORT = previousPort
4731
- if (previousTimeout === undefined) delete process.env.APP_START_TIMEOUT
4732
- else process.env.APP_START_TIMEOUT = previousTimeout
5062
+ dispatcher.destroy()
4733
5063
  }
4734
5064
  })
5065
+ })
4735
5066
 
4736
- it('rejects an aborted startup promptly, cleans up, and remains restartable', async () => {
5067
+ describe('ApplicationServer', () => {
5068
+ it('composes the real substrate on loopback with security and repeatable lifecycle', async () => {
4737
5069
  const server = createApplicationServer({
4738
- host: '127.0.0.1',
4739
- port: 0,
4740
- timeout: 1_000,
5070
+ server: { host: '127.0.0.1', port: 0, timeout: 1_000 },
4741
5071
  })
4742
- const controller = new AbortController()
4743
- controller.abort(new Error('cancelled by test'))
4744
5072
  try {
4745
- const started = Date.now()
4746
- await expect(server.start(controller.signal)).rejects.toMatchObject({ code: 'LIFECYCLE' })
4747
- expect(Date.now() - started).toBeLessThan(5_000)
4748
- expect(server.listening).toBe(false)
4749
-
4750
- await server.stop()
5073
+ expect(server.host).toBe('127.0.0.1')
5074
+ expect(server.port).toBeUndefined()
5075
+ expect(server.url).toBeUndefined()
5076
+ expect(server.status).toBe('idle')
4751
5077
  await server.start()
4752
- expect((await fetch(server.url)).status).toBe(200)
4753
- } finally {
4754
- await server.stop()
4755
- }
4756
- })
4757
-
4758
- it('contains a revoked startup signal without listening or leaking transition state', async () => {
4759
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4760
- const revocable = Proxy.revocable(new AbortController().signal, {})
4761
- revocable.revoke()
4762
- try {
4763
- await expect(Reflect.apply(server.start, server, [revocable.proxy])).rejects.toMatchObject({
4764
- code: 'LIFECYCLE',
4765
- })
4766
- expect(server.listening).toBe(false)
5078
+ expect(server.status).toBe('listening')
5079
+ expect(server.port).toEqual(expect.any(Number))
5080
+ const firstUrl = server.url
5081
+ if (firstUrl === undefined) throw new Error('Expected a bound application URL')
5082
+
5083
+ const first = await fetch(\`\${firstUrl}\${APP_HEALTH_PATH}\`)
5084
+ expect(first.status).toBe(200)
5085
+ expect(first.headers.get('x-content-type-options')).toBe('nosniff')
5086
+ expect(first.headers.get('x-frame-options')).toBe('DENY')
5087
+ expect(first.headers.get('content-security-policy')).not.toBeNull()
5088
+ expect(first.headers.get('x-request-id')).not.toBeNull()
5089
+ expect(await first.json()).toEqual({ name: APP_NAME, status: 'ok' })
4767
5090
 
4768
- await server.start()
4769
- expect((await fetch(server.url)).status).toBe(200)
4770
- } finally {
4771
5091
  await server.stop()
4772
- }
4773
- })
4774
-
4775
- it('fails closed on a port collision and preserves the owning server', async () => {
4776
- const owner = createApplicationServer({ host: '127.0.0.1', port: 0 })
4777
- let blocked: ReturnType<typeof createApplicationServer> | undefined
4778
- try {
4779
- await owner.start()
4780
- blocked = createApplicationServer({ host: '127.0.0.1', port: owner.port })
4781
- await expect(blocked.start()).rejects.toMatchObject({ code: 'LIFECYCLE' })
4782
- expect(blocked.listening).toBe(false)
4783
- expect((await fetch(owner.url)).status).toBe(200)
4784
- } finally {
4785
- await blocked?.stop()
4786
- await owner.stop()
4787
- }
4788
- })
4789
-
4790
- it('restarts cleanly after a completed stop', async () => {
4791
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4792
- try {
4793
- await server.start()
4794
5092
  await server.stop()
4795
- await Promise.all([server.start(), server.start()])
4796
- expect((await fetch(server.url)).status).toBe(200)
4797
- } finally {
4798
- await server.stop()
4799
- }
4800
- })
5093
+ expect(server.port).toBeUndefined()
5094
+ expect(server.url).toBeUndefined()
5095
+ expect(server.status).toBe('stopped')
4801
5096
 
4802
- it('requests a fresh ephemeral port when the previous port is occupied', async () => {
4803
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4804
- const occupant = createServer()
4805
- try {
4806
5097
  await server.start()
4807
- const previousPort = server.port
5098
+ const secondUrl = server.url
5099
+ if (secondUrl === undefined) throw new Error('Expected a rebound application URL')
5100
+ const second = await fetch(\`\${secondUrl}\${APP_HEALTH_PATH}\`)
5101
+ expect(second.status).toBe(200)
4808
5102
  await server.stop()
4809
-
4810
- occupant.listen(previousPort, server.host)
4811
- await once(occupant, 'listening')
4812
- await server.start()
4813
-
4814
- expect(server.port).not.toBe(previousPort)
4815
- expect((await fetch(server.url)).status).toBe(200)
5103
+ await server.destroy()
5104
+ await server.destroy()
5105
+ expect(server.status).toBe('stopped')
4816
5106
  } finally {
4817
- await server.stop()
4818
- await stopNodeServer(occupant)
5107
+ await server.destroy()
4819
5108
  }
4820
5109
  })
4821
5110
 
4822
- it('honors the latest requested state across opposing concurrent transitions', async () => {
4823
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4824
- try {
4825
- const firstStart = server.start()
4826
- const stopping = server.stop()
4827
- const latestStart = server.start()
4828
- await Promise.all([firstStart, stopping, latestStart])
4829
-
4830
- expect(server.listening).toBe(true)
4831
- expect((await fetch(server.url)).status).toBe(200)
4832
- } finally {
4833
- await server.stop()
5111
+ it('rejects invalid grouped host and port options before binding', () => {
5112
+ for (const value of [
5113
+ { server: { host: ' ' } },
5114
+ { server: { port: Number.NaN } },
5115
+ { server: { port: -1 } },
5116
+ { server: { timeout: 0 } },
5117
+ ]) {
5118
+ expect(() => Reflect.apply(createApplicationServer, undefined, [value])).toThrow(
5119
+ expect.objectContaining({ code: 'CONFIG' }),
5120
+ )
4834
5121
  }
4835
5122
  })
5123
+ })
4836
5124
 
4837
- it('forces a hostile partial-header connection closed during stop', async () => {
4838
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
5125
+ describe('ApplicationServerRunner', () => {
5126
+ it('serves a real request after a restart queued during shutdown', async () => {
5127
+ const port = await reserveLoopbackPort()
5128
+ const code = process.exitCode
5129
+ const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
5130
+ const runner = new ApplicationServerRunner(server)
4839
5131
  try {
4840
- await server.start()
4841
- const socket = connect({ host: server.host, port: server.port })
4842
- try {
4843
- await once(socket, 'connect')
4844
- socket.write('GET / HTTP/1.1\\r\\nHost: localhost')
4845
- const closed = waitForSocketClose(socket)
5132
+ process.exitCode = undefined
5133
+ const first = waitForEvent(runner.emitter, 'ready')
5134
+ runner.start()
5135
+ await first
5136
+ const stopped = runner.stop()
5137
+ const second = waitForEvent(runner.emitter, 'ready')
5138
+ runner.start()
5139
+ await stopped
4846
5140
 
4847
- await server.stop()
4848
- await closed
4849
- expect(socket.destroyed).toBe(true)
4850
- } finally {
4851
- socket.destroy()
4852
- }
5141
+ const [url] = await second
5142
+ const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
5143
+ expect(response.status).toBe(200)
5144
+ expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
5145
+ const stopping = runner.stop()
5146
+ expect(runner.stop()).toBe(stopping)
5147
+ await stopping
5148
+ expect(process.exitCode).toBeUndefined()
4853
5149
  } finally {
4854
- await server.stop()
5150
+ process.exitCode = code
5151
+ await runner.stop()
4855
5152
  }
4856
5153
  })
4857
5154
 
4858
- it('bounds simultaneous idle connections and recovers after capacity is released', async () => {
4859
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4860
- const sockets = []
5155
+ it('cancels a startup still in flight so an immediate stop leaves the port free', async () => {
5156
+ const port = await reserveLoopbackPort()
5157
+ const ready = createRecorder<[url: string]>()
5158
+ const failed = createRecorder<[error: unknown]>()
5159
+ const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
5160
+ const runner = new ApplicationServerRunner(server, {
5161
+ on: { ready: ready.handler, fail: failed.handler },
5162
+ })
4861
5163
  try {
4862
- await server.start()
4863
- for (let index = 0; index < APP_MAX_CONNECTIONS; index += 1) {
4864
- const socket = connect({ host: server.host, port: server.port })
4865
- await once(socket, 'connect')
4866
- socket.write('GET / HTTP/1.1\\r\\nHost: localhost')
4867
- sockets.push(socket)
4868
- }
4869
- const overflow = connect({ host: server.host, port: server.port })
5164
+ runner.start()
5165
+ await runner.stop()
5166
+ expect(ready.count).toBe(0)
5167
+ expect(failed.count).toBe(0)
5168
+
5169
+ const released = await startLoopbackServer(port)
4870
5170
  try {
4871
- const closed = waitForSocketClose(overflow)
4872
- await once(overflow, 'connect')
4873
- await closed
4874
- expect(overflow.destroyed).toBe(true)
5171
+ expect(released.listening).toBe(true)
4875
5172
  } finally {
4876
- overflow.destroy()
5173
+ await stopNodeServer(released)
4877
5174
  }
4878
- const released = sockets.pop()
4879
- if (released === undefined) throw new Error('expected a held connection')
4880
- const releasedClose = waitForSocketClose(released)
4881
- released.destroy()
4882
- await releasedClose
4883
- const response = await waitForLoopbackResponse(server.port)
4884
- expect(response.status).toBe(200)
4885
5175
  } finally {
4886
- for (const socket of sockets) socket.destroy()
4887
- await server.stop()
5176
+ await runner.stop()
4888
5177
  }
4889
5178
  })
4890
5179
 
4891
- it('starts the built executable, serves loopback traffic, terminates on SIGTERM, and releases its port', async () => {
5180
+ it('announces readiness exactly once before the first response and releases on SIGTERM', async () => {
4892
5181
  const port = await reserveLoopbackPort()
4893
5182
  const application = startApplicationProcess(port)
4894
5183
  try {
4895
- const response = await waitForApplicationResponse(application, port)
5184
+ await application.ready
5185
+ const announcement = \`[READY] \${APP_NAME} http://127.0.0.1:\${port}\\n\`
5186
+ expect(application.output()).toBe(announcement)
5187
+
5188
+ const response = await fetch(\`http://127.0.0.1:\${port}\${APP_HEALTH_PATH}\`)
4896
5189
  expect(response.status).toBe(200)
4897
5190
  expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
5191
+ expect(application.output().split('[READY]')).toHaveLength(2)
4898
5192
 
4899
5193
  expect(application.child.kill('SIGTERM')).toBe(true)
4900
5194
  const exited = await waitForApplicationProcess(application)
@@ -4904,10 +5198,8 @@ describe('ApplicationServer', () => {
4904
5198
  : { code: 0, signal: null },
4905
5199
  )
4906
5200
 
4907
- const released = createServer()
5201
+ const released = await startLoopbackServer(port)
4908
5202
  try {
4909
- released.listen(port, '127.0.0.1')
4910
- await once(released, 'listening')
4911
5203
  expect(released.listening).toBe(true)
4912
5204
  } finally {
4913
5205
  await stopNodeServer(released)
@@ -4920,105 +5212,81 @@ describe('ApplicationServer', () => {
4920
5212
  }
4921
5213
  })
4922
5214
 
4923
- it('exits nonzero on a real executable port collision without disturbing the owner', async () => {
5215
+ it('never announces readiness when a real bind fails', async () => {
4924
5216
  const port = await reserveLoopbackPort()
4925
- const owner = createServer()
4926
- owner.listen(port, '127.0.0.1')
4927
- await once(owner, 'listening')
5217
+ const owner = await startLoopbackServer(port)
4928
5218
  const application = startApplicationProcess(port)
4929
5219
  try {
4930
5220
  const exited = await waitForApplicationProcess(application)
4931
5221
  expect(exited).toEqual({ code: 1, signal: null })
4932
5222
  expect(application.output()).toBe('[LIFECYCLE] Application server lifecycle failed\\n')
5223
+ expect(application.output()).not.toContain('[READY]')
4933
5224
  expect(application.output()).not.toContain('EADDRINUSE')
4934
- expect(application.output()).not.toContain('context')
4935
- expect(application.output()).not.toContain('cause')
4936
- expect(application.output()).not.toContain('at ')
4937
5225
  expect(owner.listening).toBe(true)
4938
5226
  } finally {
4939
- if (application.child.exitCode === null && application.child.signalCode === null) {
4940
- application.child.kill('SIGKILL')
4941
- await waitForApplicationProcess(application)
4942
- }
4943
5227
  await stopNodeServer(owner)
4944
5228
  }
4945
5229
  })
4946
5230
 
4947
- it('redacts malformed environment values from executable diagnostics', async () => {
4948
- const secret = 'SENTINEL_SECRET/invalid'
4949
- const application = startApplicationProcess(0, { APP_HOST: secret })
4950
- const exited = await waitForApplicationProcess(application)
4951
- expect(exited).toEqual({ code: 1, signal: null })
4952
- expect(application.output()).toBe('[CONFIG] Application server configuration failed\\n')
4953
- expect(application.output()).not.toContain(secret)
4954
- expect(application.output()).not.toContain('context')
4955
- expect(application.output()).not.toContain('cause')
4956
- expect(application.output()).not.toContain('at ')
4957
- })
4958
-
4959
- it('releases process listeners through explicit, repeated, convenience, and signal cleanup', async () => {
4960
- const signalCount = process.listenerCount('SIGTERM')
4961
- const interruptCount = process.listenerCount('SIGINT')
4962
- const runner = new ApplicationServerRunner({ host: '127.0.0.1', port: 0 })
4963
- try {
4964
- expect(runner.start()).toBeUndefined()
4965
- expect(runner.start()).toBeUndefined()
4966
- expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
4967
- expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
4968
-
4969
- await runner.stop()
4970
- await runner.stop()
4971
- expect(process.listenerCount('SIGTERM')).toBe(signalCount)
4972
- expect(process.listenerCount('SIGINT')).toBe(interruptCount)
4973
-
4974
- const convenience = startApplicationServer({ host: '127.0.0.1', port: 0 })
4975
- expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
4976
- expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
4977
- await convenience.stop()
4978
- expect(process.listenerCount('SIGTERM')).toBe(signalCount)
4979
- expect(process.listenerCount('SIGINT')).toBe(interruptCount)
4980
-
4981
- runner.start()
4982
- expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
4983
- expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
4984
- process.emit('SIGTERM')
4985
- await runner.stop()
4986
- expect(process.listenerCount('SIGTERM')).toBe(signalCount)
4987
- expect(process.listenerCount('SIGINT')).toBe(interruptCount)
4988
- } finally {
4989
- await runner.stop()
4990
- }
4991
- })
4992
-
4993
- it('keeps a newer runner generation owned when an older start fails during restart', async () => {
4994
- const signalCount = process.listenerCount('SIGTERM')
4995
- const interruptCount = process.listenerCount('SIGINT')
4996
- const previousExitCode = process.exitCode
5231
+ it('isolates a throwing fail listener, releases ownership, and starts again', async () => {
4997
5232
  const port = await reserveLoopbackPort()
4998
- const owner = createServer()
4999
- owner.listen(port, '127.0.0.1')
5000
- await once(owner, 'listening')
5001
- const runner = new ApplicationServerRunner({ host: '127.0.0.1', port })
5233
+ const owner = await startLoopbackServer(port)
5234
+ const interrupts = process.listenerCount('SIGINT')
5235
+ const terminations = process.listenerCount('SIGTERM')
5236
+ // The reporter owns the process exit code, so this in-process failure records the
5237
+ // code it set and then restores whatever the surrounding run had.
5238
+ const code = process.exitCode
5239
+ const errors = createRecorder<[error: unknown, event: string]>()
5240
+ const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
5241
+ const runner = new ApplicationServerRunner(server, {
5242
+ on: {
5243
+ fail: () => {
5244
+ throw new Error('listener boom')
5245
+ },
5246
+ },
5247
+ error: errors.handler,
5248
+ })
5002
5249
  try {
5250
+ process.exitCode = undefined
5251
+ const failed = waitForEvent(runner.emitter, 'fail')
5003
5252
  runner.start()
5004
- await Promise.resolve()
5005
- const stopping = runner.stop()
5006
- runner.start()
5007
- await stopNodeServer(owner)
5008
- await stopping
5253
+ expect(process.listenerCount('SIGINT')).toBe(interrupts + 1)
5254
+ expect(process.listenerCount('SIGTERM')).toBe(terminations + 1)
5009
5255
 
5010
- const response = await waitForLoopbackResponse(port)
5256
+ const [failure] = await failed
5257
+ expect(failure).toEqual(expect.objectContaining({ code: 'LIFECYCLE' }))
5258
+ expect(errors.count).toBe(1)
5259
+ expect(errors.calls[0]?.[1]).toBe('fail')
5260
+ expect(process.listenerCount('SIGINT')).toBe(interrupts)
5261
+ expect(process.listenerCount('SIGTERM')).toBe(terminations)
5262
+ expect(process.exitCode).toBe(1)
5263
+
5264
+ await stopNodeServer(owner)
5265
+ const ready = waitForEvent(runner.emitter, 'ready')
5266
+ runner.start()
5267
+ const [url] = await ready
5268
+ const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
5011
5269
  expect(response.status).toBe(200)
5012
- expect(process.exitCode).toBe(previousExitCode)
5013
- expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
5014
- expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
5015
5270
  } finally {
5271
+ process.exitCode = code
5016
5272
  await runner.stop()
5017
5273
  await stopNodeServer(owner)
5018
- process.exitCode = previousExitCode
5019
5274
  }
5020
5275
  })
5021
- })
5276
+
5277
+ it('redacts malformed environment values from executable diagnostics', async () => {
5278
+ const secret = 'SENTINEL_SECRET/invalid'
5279
+ const application = startApplicationProcess(0, { APP_HOST: secret })
5280
+ const exited = await waitForApplicationProcess(application)
5281
+ expect(exited).toEqual({ code: 1, signal: null })
5282
+ expect(application.output()).toBe('[CONFIG] Application server configuration failed\\n')
5283
+ expect(application.output()).not.toContain(secret)
5284
+ expect(application.output()).not.toContain('[READY]')
5285
+ expect(application.output()).not.toContain('context')
5286
+ expect(application.output()).not.toContain('cause')
5287
+ expect(application.output()).not.toContain('at ')
5288
+ })
5289
+ }){{boundary}}
5022
5290
  `
5023
5291
  }),
5024
5292
  appServerParsersTest: Object.freeze({
@@ -5042,6 +5310,7 @@ import { describe, expect, it } from 'vitest'
5042
5310
 
5043
5311
  describe('application environment parsers', () => {
5044
5312
  it('accepts port boundaries and trims a host', () => {
5313
+ expect(parseApplicationPort(3000)).toBe(3000)
5045
5314
  expect(parseApplicationPort('0')).toBe(0)
5046
5315
  expect(parseApplicationPort('65535')).toBe(65_535)
5047
5316
  expect(parseApplicationPort(' 3000 ')).toBe(3000)
@@ -5053,6 +5322,19 @@ describe('application environment parsers', () => {
5053
5322
  expect(parseApplicationStartTimeout(String(MAX_APP_START_TIMEOUT))).toBe(MAX_APP_START_TIMEOUT)
5054
5323
  })
5055
5324
 
5325
+ it('never coerces hostile numeric objects through toString', () => {
5326
+ let calls = 0
5327
+ const hostile = {
5328
+ toString() {
5329
+ calls += 1
5330
+ return '3000'
5331
+ },
5332
+ }
5333
+
5334
+ expect(() => parseApplicationPort(hostile)).toThrow(ApplicationServerError)
5335
+ expect(calls).toBe(0)
5336
+ })
5337
+
5056
5338
  it.each(['', '-1', '65536', '1.5', '+1', '0x10', '1e3', 'NaN', 'Infinity'])(
5057
5339
  'rejects hostile APP_PORT value %s',
5058
5340
  (value) => {
@@ -5094,7 +5376,15 @@ describe('application environment parsers', () => {
5094
5376
  expect(() => parseApplicationHost(value)).toThrow(expect.objectContaining({ code: 'CONFIG' }))
5095
5377
  })
5096
5378
 
5097
- it.each([null, 42, [], { port: [42] }, { timeout: 0 }])(
5379
+ it('parses the exact grouped server record including its timeout leaf', () => {
5380
+ expect(
5381
+ parseApplicationServerOptions({
5382
+ server: { host: ' 127.0.0.1 ', port: 0, timeout: 250 },
5383
+ }),
5384
+ ).toEqual({ server: { host: '127.0.0.1', port: 0, timeout: 250 } })
5385
+ })
5386
+
5387
+ it.each([null, 42, [], { server: null }, { server: { port: [42] } }, { server: { timeout: 0 } }])(
5098
5388
  'rejects hostile option container or leaf value %#',
5099
5389
  (value) => {
5100
5390
  expect(() => parseApplicationServerOptions(value)).toThrow(
@@ -5104,11 +5394,14 @@ describe('application environment parsers', () => {
5104
5394
  )
5105
5395
 
5106
5396
  it.each([
5107
- Object.create({ host: '0.0.0.0' }),
5397
+ Object.create({ server: { host: '0.0.0.0' } }),
5108
5398
  { post: 0 },
5109
5399
  new Date(),
5110
- { [Symbol('host')]: '0.0.0.0' },
5111
- Object.defineProperty({}, 'host', { get: () => '0.0.0.0' }),
5400
+ { server: Object.create({ host: '0.0.0.0' }) },
5401
+ { server: { post: 0 } },
5402
+ { server: { [Symbol('host')]: '0.0.0.0' } },
5403
+ Object.defineProperty({}, 'server', { get: () => ({ host: '0.0.0.0' }) }),
5404
+ { server: Object.defineProperty({}, 'host', { get: () => '0.0.0.0' }) },
5112
5405
  new Proxy(
5113
5406
  {},
5114
5407
  {
@@ -5117,6 +5410,16 @@ describe('application environment parsers', () => {
5117
5410
  },
5118
5411
  },
5119
5412
  ),
5413
+ {
5414
+ server: new Proxy(
5415
+ {},
5416
+ {
5417
+ ownKeys: () => {
5418
+ throw new Error('hostile nested ownKeys trap')
5419
+ },
5420
+ },
5421
+ ),
5422
+ },
5120
5423
  ])(
5121
5424
  'rejects inherited, unknown, symbolic, accessor, instance, and hostile proxy options %#',
5122
5425
  (value) => {
@@ -5126,6 +5429,19 @@ describe('application environment parsers', () => {
5126
5429
  },
5127
5430
  )
5128
5431
 
5432
+ it('does not invoke grouped option accessors', () => {
5433
+ let reads = 0
5434
+ const server = Object.defineProperty({}, 'timeout', {
5435
+ get() {
5436
+ reads += 1
5437
+ return 250
5438
+ },
5439
+ })
5440
+
5441
+ expect(() => parseApplicationServerOptions({ server })).toThrow(ApplicationServerError)
5442
+ expect(reads).toBe(0)
5443
+ })
5444
+
5129
5445
  it('never reflects a hostile symbolic option name into diagnostics', () => {
5130
5446
  let caught: unknown
5131
5447
  try {
@@ -5175,9 +5491,7 @@ ${IMPORT_KEYWORD} { createSource, parseManifest } from '@orkestrel/guide'
5175
5491
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_ROOT = fileURLToPath(new URL('../', import.meta.url))
5176
5492
 
5177
5493
  /** Repository roots whose TypeScript and Markdown files participate in guide parity. */
5178
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze([
5179
- {{walkDirs}}
5180
- ])
5494
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze({{walkDirs}})
5181
5495
 
5182
5496
  {{specifiers}}
5183
5497
 
@@ -5569,7 +5883,9 @@ function devDependenciesFor(spec) {
5569
5883
  return {
5570
5884
  ...dependencies,
5571
5885
  ...spec.src.includes("browser") ? SOURCE_BROWSER_DEV_DEPENDENCIES : {},
5886
+ ...spec.app.length > 0 ? APP_DEV_DEPENDENCIES : {},
5572
5887
  ...spec.app.includes("browser") ? APP_BROWSER_DEV_DEPENDENCIES : {},
5888
+ ...spec.app.includes("server") ? APP_SERVER_DEV_DEPENDENCIES : {},
5573
5889
  ...spec.showcase ? { "vite-plugin-singlefile": "^2.3.3" } : {},
5574
5890
  ...spec.bin ? { "@vitest/browser-playwright": SOURCE_BROWSER_DEV_DEPENDENCIES["@vitest/browser-playwright"] } : {}
5575
5891
  };
@@ -5624,6 +5940,7 @@ function packageManifest(spec) {
5624
5940
  ...hasSource || spec.bin ? ["npm run test:src"] : [],
5625
5941
  ...spec.app.length > 0 ? ["npm run test:app"] : [],
5626
5942
  "npm run test:policy",
5943
+ "npm run test:config",
5627
5944
  "npm run test:guides"
5628
5945
  ].join(" && ");
5629
5946
  if (hasSource || spec.bin) {
@@ -5639,6 +5956,7 @@ function packageManifest(spec) {
5639
5956
  for (const environment of spec.app) scripts[`test:app:${environment}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project ${APP_MATRIX[environment].project}`;
5640
5957
  }
5641
5958
  scripts["test:policy"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy";
5959
+ scripts["test:config"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project config";
5642
5960
  scripts["test:guides"] = "vitest run --config vite.config.ts --reporter=dot --project guides";
5643
5961
  scripts.build = [
5644
5962
  "npm run clean",
@@ -5659,7 +5977,7 @@ function packageManifest(spec) {
5659
5977
  if (spec.showcase) {
5660
5978
  scripts.showcase = `vite --config ${SHOWCASE_CONFIG_PATH}`;
5661
5979
  scripts["build:showcase"] = `vite build --config ${SHOWCASE_CONFIG_PATH}`;
5662
- scripts.show = "npm run build:showcase && npm run copy dist/showcase/index.html demo/showcase.html";
5980
+ scripts.show = "npm run format && npm run build:showcase && npm run copy dist/showcase/index.html demo/showcase.html";
5663
5981
  }
5664
5982
  }
5665
5983
  if (spec.app.includes("server")) {
@@ -5809,7 +6127,7 @@ function viteMachinery(src, app = [], bin = false, showcase = false) {
5809
6127
  * @example
5810
6128
  * ```ts
5811
6129
  * viteProjectRegistrations(['core'], [], { integration: true })
5812
- * // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'guides' }, { project: 'integration' }]
6130
+ * // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'config' }, { project: 'guides' }, { project: 'integration' }]
5813
6131
  * ```
5814
6132
  */
5815
6133
  function viteProjectRegistrations(src, app = [], facts = {}) {
@@ -5832,7 +6150,7 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
5832
6150
  });
5833
6151
  if (environment === "server") registrations.push({ project: "appServer" });
5834
6152
  }
5835
- registrations.push({ project: "policy" }, { project: "guides" });
6153
+ registrations.push({ project: "policy" }, { project: "config" }, { project: "guides" });
5836
6154
  if (facts.bin === true) registrations.push({ project: "srcBin" });
5837
6155
  if (facts.integration === true) registrations.push({ project: "integration" });
5838
6156
  if (facts.service === true) registrations.push({ project: "service" });
@@ -5842,7 +6160,7 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
5842
6160
  * Render the one ordered proof and structural-axis project definition block.
5843
6161
  *
5844
6162
  * @param facts - Optional structural facts.
5845
- * @returns Policy, guides, then selected axis project definitions, separated by one blank line.
6163
+ * @returns Policy, config, guides, then selected axis project definitions, separated by one blank line.
5846
6164
  *
5847
6165
  * @example
5848
6166
  * ```ts
@@ -5850,7 +6168,11 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
5850
6168
  * ```
5851
6169
  */
5852
6170
  function viteProjectDefinitions(facts = {}) {
5853
- const definitions = [policyViteProject(), guidesViteProject()];
6171
+ const definitions = [
6172
+ policyViteProject(),
6173
+ configViteProject(),
6174
+ guidesViteProject()
6175
+ ];
5854
6176
  if (facts.bin === true) definitions.push(binViteProject());
5855
6177
  if (facts.integration === true) definitions.push(integrationViteProject(facts));
5856
6178
  if (facts.service === true) definitions.push(serviceViteProject());
@@ -5916,9 +6238,11 @@ import { chromium } from 'playwright'
5916
6238
  ` : "";
5917
6239
  const vueImports = needsVue ? `import vue from '@vitejs/plugin-vue'
5918
6240
  import { parse as parseVue } from 'vue/compiler-sfc'
6241
+ import { parseStartTag } from '@orkestrel/html'
5919
6242
  ` : "";
5920
6243
  const showcaseImports = needsShowcase ? `import { viteSingleFile } from 'vite-plugin-singlefile'
5921
6244
  ` : "";
6245
+ const showcaseHashImport = needsShowcase ? "import { createHash } from 'node:crypto'\n" : "";
5922
6246
  const viteTypeImports = needsVue ? `import type {
5923
6247
  CSSOptions,
5924
6248
  HtmlAssetSource,
@@ -6880,12 +7204,40 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_POLICY =
6880
7204
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_META =
6881
7205
  '<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
6882
7206
  HTML_SECURITY_POLICY +
7207
+ '"\\n\\t\\t/>'${needsShowcase ? `
7208
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_POLICY =
7209
+ "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'"
7210
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_META =
7211
+ '<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
7212
+ SHOWCASE_SECURITY_POLICY +
7213
+ '"\\n\\t\\t/>'
7214
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_BUILD_SECURITY_POLICY =
7215
+ "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'"
7216
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_BUILD_SECURITY_META =
7217
+ '<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
7218
+ SHOWCASE_BUILD_SECURITY_POLICY +
6883
7219
  '"\\n\\t\\t/>'
6884
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_PREFIX =
6885
- '<!doctype html>\\n<html lang="en">\\n\\t<head>\\n\\t\\t' + HTML_SECURITY_META + '\\n'
7220
+ ` : ""}
7221
+
7222
+ ${EXPORT_KEYWORD} function hasSecurityPrologue(html: string, security: string): boolean {
7223
+ const normalized = html.replaceAll('\\r\\n', '\\n')
7224
+ const doctype = '<!doctype html>\\n'
7225
+ if (!normalized.startsWith(doctype)) return false
7226
+ const root = parseStartTag(normalized, doctype.length)
7227
+ return (
7228
+ root !== undefined &&
7229
+ root.name === 'html' &&
7230
+ !root.slashed &&
7231
+ normalized.startsWith('\\n\\t<head>\\n\\t\\t' + security + '\\n', root.next)
7232
+ )
7233
+ }
6886
7234
 
6887
- ${EXPORT_KEYWORD} function maskIgnoredHtml(environmentKeys: ReadonlySet<string>, html: string): string {
6888
- if (!html.replaceAll('\\r\\n', '\\n').startsWith(HTML_SECURITY_PREFIX)) {
7235
+ ${EXPORT_KEYWORD} function maskIgnoredHtml(
7236
+ environmentKeys: ReadonlySet<string>,
7237
+ html: string,
7238
+ security: string,
7239
+ ): string {
7240
+ if (!hasSecurityPrologue(html, security)) {
6889
7241
  throw new Error(
6890
7242
  '[orkestrel-environment-boundary] Browser HTML must preserve the generated security prologue',
6891
7243
  )
@@ -6919,12 +7271,27 @@ ${EXPORT_KEYWORD} function maskIgnoredHtml(environmentKeys: ReadonlySet<string>,
6919
7271
  )
6920
7272
  }
6921
7273
 
6922
- ${EXPORT_KEYWORD} function isBrowserHtmlEntry(filename: string): boolean {
7274
+ ${needsShowcase ? `${EXPORT_KEYWORD} function isShowcaseHtmlEntry(filename: string): boolean {
6923
7275
  return (
6924
- physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
7276
+ physicalPath(filename) ===
7277
+ physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/showcase.html'))
6925
7278
  )
6926
7279
  }
6927
7280
 
7281
+ ` : ""}${EXPORT_KEYWORD} function isBrowserHtmlEntry(filename: string): boolean {
7282
+ ${needsShowcase ? `return (
7283
+ isShowcaseHtmlEntry(filename) ||
7284
+ physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
7285
+ )` : `return (
7286
+ physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
7287
+ )`}
7288
+ }${needsShowcase ? `
7289
+ ${EXPORT_KEYWORD} function browserHtmlSecurityMeta(filename: string, built: boolean): string | undefined {
7290
+ if (!isBrowserHtmlEntry(filename)) return undefined
7291
+ if (!isShowcaseHtmlEntry(filename)) return HTML_SECURITY_META
7292
+ return built ? SHOWCASE_BUILD_SECURITY_META : SHOWCASE_SECURITY_META
7293
+ }` : ""}
7294
+
6928
7295
  ${EXPORT_KEYWORD} function prepareHtml(): Plugin {
6929
7296
  const environmentKeys = new Set<string>()
6930
7297
  return {
@@ -6941,8 +7308,10 @@ ${EXPORT_KEYWORD} function prepareHtml(): Plugin {
6941
7308
  transformIndexHtml: {
6942
7309
  order: 'pre',
6943
7310
  handler(html, context) {
6944
- if (!isBrowserHtmlEntry(context.filename)) return undefined
6945
- return maskIgnoredHtml(environmentKeys, html)
7311
+ ${needsShowcase ? `const security = browserHtmlSecurityMeta(context.filename, false)
7312
+ if (security === undefined) return undefined
7313
+ return maskIgnoredHtml(environmentKeys, html, security)` : `if (!isBrowserHtmlEntry(context.filename)) return undefined
7314
+ return maskIgnoredHtml(environmentKeys, html, HTML_SECURITY_META)`}
6946
7315
  },
6947
7316
  },
6948
7317
  }
@@ -6966,8 +7335,13 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
6966
7335
  transformIndexHtml: {
6967
7336
  order: 'post',
6968
7337
  handler(html, context) {
6969
- if (!isBrowserHtmlEntry(context.filename)) return undefined
6970
- if (!html.includes(HTML_SECURITY_META)) {
7338
+ ${needsShowcase ? `const security = browserHtmlSecurityMeta(
7339
+ context.filename,
7340
+ context.bundle !== undefined,
7341
+ )
7342
+ if (security === undefined) return undefined
7343
+ if (!html.includes(security)) {` : `if (!isBrowserHtmlEntry(context.filename)) return undefined
7344
+ if (!html.includes(HTML_SECURITY_META)) {`}
6971
7345
  throw new Error(
6972
7346
  '[orkestrel-environment-boundary] Browser HTML must retain its security policy',
6973
7347
  )
@@ -6977,26 +7351,49 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
6977
7351
  }
6978
7352
  }
6979
7353
 
6980
- ` : ""}${needsShowcase ? `${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_POLICY =
6981
- "base-uri 'none'; object-src 'none'; script-src 'self' 'unsafe-inline'; script-src-attr 'none'; style-src 'self' 'unsafe-inline'"
6982
-
6983
- ${EXPORT_KEYWORD} function showcaseHtml(): Plugin {
7354
+ ` : ""}${needsShowcase ? `${EXPORT_KEYWORD} function showcaseHtml(): Plugin {
6984
7355
  return {
6985
7356
  name: 'orkestrel-showcase-html',
6986
- enforce: 'post',
6987
7357
  transformIndexHtml: {
6988
7358
  order: 'post',
6989
7359
  handler(html, context) {
6990
- if (!isBrowserHtmlEntry(context.filename)) return undefined
6991
- if (!html.includes(HTML_SECURITY_META)) {
6992
- throw new Error('[orkestrel-showcase] Browser HTML must retain its security policy')
7360
+ if (!isShowcaseHtmlEntry(context.filename) || context.bundle === undefined) {
7361
+ return undefined
7362
+ }
7363
+ if (!html.includes(SHOWCASE_SECURITY_META)) {
7364
+ throw new Error(
7365
+ '[orkestrel-showcase-html] Showcase build did not retain its development security policy',
7366
+ )
7367
+ }
7368
+ const secured = html.replace(SHOWCASE_SECURITY_META, SHOWCASE_BUILD_SECURITY_META)
7369
+ const build = createHash('sha256').update(secured).digest('hex')
7370
+ return {
7371
+ html: secured,
7372
+ tags: [
7373
+ {
7374
+ tag: 'meta',
7375
+ attrs: { name: 'build-id', content: build },
7376
+ injectTo: 'head',
7377
+ },
7378
+ ],
6993
7379
  }
6994
- const stamp = new Date().toISOString()
6995
- return html.replace(
6996
- HTML_SECURITY_META,
6997
- HTML_SECURITY_META.replace(HTML_SECURITY_POLICY, SHOWCASE_SECURITY_POLICY) +
6998
- \`\\n\\t\\t<meta name="build-id" content="\${stamp}" />\`,
6999
- )
7380
+ },
7381
+ },
7382
+ generateBundle: {
7383
+ order: 'post',
7384
+ handler(_options, bundle) {
7385
+ let html: (typeof bundle)[string] | undefined
7386
+ for (const output of Object.values(bundle)) {
7387
+ if (!output.fileName.endsWith('.html')) continue
7388
+ if (html !== undefined) {
7389
+ this.error('[orkestrel-showcase-html] Showcase build emitted multiple HTML entries')
7390
+ }
7391
+ html = output
7392
+ }
7393
+ if (html === undefined) {
7394
+ this.error('[orkestrel-showcase-html] Showcase build did not emit an HTML entry')
7395
+ }
7396
+ html.fileName = 'index.html'
7000
7397
  },
7001
7398
  },
7002
7399
  }
@@ -7293,7 +7690,7 @@ ${needsBrowser ? `import { isCSSRequest, parseSync, preprocessCSS, transformWith
7293
7690
  `}import { defineConfig, mergeConfig } from 'vitest/config'
7294
7691
  import tsconfig from './tsconfig.json' with { type: 'json' }
7295
7692
  import { fileURLToPath, URL } from 'node:url'
7296
- import { isBuiltin } from 'node:module'
7693
+ ${showcaseHashImport}import { isBuiltin } from 'node:module'
7297
7694
  import {
7298
7695
  ${needsBrowser ? " accessSync,\n" : ""} closeSync,
7299
7696
  constants as FS_CONSTANTS,
@@ -7616,7 +8013,7 @@ ${environmentBoundary}`;
7616
8013
  * ```
7617
8014
  */
7618
8015
  function policyViteProject() {
7619
- return `${EXPORT_KEYWORD} const policy = (config?: UserConfig): UserConfig =>
8016
+ return `${EXPORT_KEYWORD} const policy = (options?: UserConfig): UserConfig =>
7620
8017
  mergeConfig(
7621
8018
  {
7622
8019
  resolve,
@@ -7628,7 +8025,34 @@ function policyViteProject() {
7628
8025
  browser: { enabled: false },
7629
8026
  },
7630
8027
  },
7631
- config ?? {},
8028
+ options ?? {},
8029
+ )
8030
+ `;
8031
+ }
8032
+ /**
8033
+ * Build the standalone Node-only root-configuration Vitest project.
8034
+ *
8035
+ * @returns The emitted `config` project definition.
8036
+ *
8037
+ * @example
8038
+ * ```ts
8039
+ * configViteProject().includes("label: 'config'") // true
8040
+ * ```
8041
+ */
8042
+ function configViteProject() {
8043
+ return `${EXPORT_KEYWORD} const config = (options?: UserConfig): UserConfig =>
8044
+ mergeConfig(
8045
+ {
8046
+ resolve,
8047
+ test: {
8048
+ name: { label: 'config', color: 'yellow' },
8049
+ include: ['tests/config/**/*.test.ts'],
8050
+ setupFiles: ['./tests/setup.ts'],
8051
+ environment: 'node',
8052
+ browser: { enabled: false },
8053
+ },
8054
+ },
8055
+ options ?? {},
7632
8056
  )
7633
8057
  `;
7634
8058
  }
@@ -7643,7 +8067,7 @@ function policyViteProject() {
7643
8067
  * ```
7644
8068
  */
7645
8069
  function guidesViteProject() {
7646
- return `${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
8070
+ return `${EXPORT_KEYWORD} const guides = (options?: UserConfig): UserConfig =>
7647
8071
  mergeConfig(
7648
8072
  {
7649
8073
  resolve,
@@ -7656,7 +8080,7 @@ function guidesViteProject() {
7656
8080
  browser: { enabled: false },
7657
8081
  },
7658
8082
  },
7659
- config ?? {},
8083
+ options ?? {},
7660
8084
  )
7661
8085
  `;
7662
8086
  }
@@ -7671,7 +8095,7 @@ function guidesViteProject() {
7671
8095
  * ```
7672
8096
  */
7673
8097
  function binViteProject() {
7674
- return `${EXPORT_KEYWORD} const srcBin = (config?: UserConfig): UserConfig =>
8098
+ return `${EXPORT_KEYWORD} const srcBin = (options?: UserConfig): UserConfig =>
7675
8099
  mergeConfig(
7676
8100
  {
7677
8101
  resolve,
@@ -7698,7 +8122,7 @@ function binViteProject() {
7698
8122
  browser: { enabled: false },
7699
8123
  },
7700
8124
  },
7701
- config ?? {},
8125
+ options ?? {},
7702
8126
  )
7703
8127
  `;
7704
8128
  }
@@ -7716,7 +8140,7 @@ function binViteProject() {
7716
8140
  * ```
7717
8141
  */
7718
8142
  function integrationViteProject(facts = {}) {
7719
- return `${EXPORT_KEYWORD} const integration = (config?: UserConfig): UserConfig =>
8143
+ return `${EXPORT_KEYWORD} const integration = (options?: UserConfig): UserConfig =>
7720
8144
  mergeConfig(
7721
8145
  {
7722
8146
  resolve,
@@ -7733,7 +8157,7 @@ ${facts.bin === true && facts.integration === true && facts.global === true ? `
7733
8157
  fileParallelism: false,
7734
8158
  },
7735
8159
  },
7736
- config ?? {},
8160
+ options ?? {},
7737
8161
  )
7738
8162
  `;
7739
8163
  }
@@ -7748,7 +8172,7 @@ ${facts.bin === true && facts.integration === true && facts.global === true ? `
7748
8172
  * ```
7749
8173
  */
7750
8174
  function serviceViteProject() {
7751
- return `${EXPORT_KEYWORD} const service = (config?: UserConfig): UserConfig =>
8175
+ return `${EXPORT_KEYWORD} const service = (options?: UserConfig): UserConfig =>
7752
8176
  mergeConfig(
7753
8177
  {
7754
8178
  resolve,
@@ -7763,7 +8187,7 @@ function serviceViteProject() {
7763
8187
  fileParallelism: false,
7764
8188
  },
7765
8189
  },
7766
- config ?? {},
8190
+ options ?? {},
7767
8191
  )
7768
8192
  `;
7769
8193
  }
@@ -7790,7 +8214,7 @@ function singleSrcViteConfig(environment, facts = {}) {
7790
8214
  const renderedTest = renderViteTest(viteProjectRegistrations([environment], [], facts), machinery.browser);
7791
8215
  const definitions = viteProjectDefinitions(facts);
7792
8216
  if (environment === "browser") return `${header}
7793
- ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8217
+ ${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
7794
8218
  mergeConfig(
7795
8219
  {
7796
8220
  resolve,
@@ -7815,7 +8239,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7815
8239
  name: { label: 'src:browser', color: 'yellow' },
7816
8240
  include: ['tests/src/browser/**/*.test.ts'],
7817
8241
  ${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
7818
- ...(config?.test?.browser?.enabled === false
8242
+ ...(options?.test?.browser?.enabled === false
7819
8243
  ? {}
7820
8244
  : {
7821
8245
  deps: {
@@ -7835,7 +8259,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7835
8259
  fileParallelism: false,
7836
8260
  },
7837
8261
  },
7838
- config ?? {},
8262
+ options ?? {},
7839
8263
  )
7840
8264
 
7841
8265
  ${definitions}
@@ -7845,7 +8269,7 @@ ${renderedTest}
7845
8269
  })
7846
8270
  `;
7847
8271
  return `${header}
7848
- ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8272
+ ${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
7849
8273
  mergeConfig(
7850
8274
  {
7851
8275
  resolve,
@@ -7875,7 +8299,7 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
7875
8299
  browser: { enabled: false },
7876
8300
  },
7877
8301
  },
7878
- config ?? {},
8302
+ options ?? {},
7879
8303
  )
7880
8304
 
7881
8305
  ${definitions}
@@ -7924,7 +8348,7 @@ function rootViteConfig(src, facts = {}) {
7924
8348
  if (onlyEnvironment === "browser" || onlyEnvironment === "server") return singleSrcViteConfig(onlyEnvironment, facts);
7925
8349
  }
7926
8350
  const browserBlock = `
7927
- ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8351
+ ${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
7928
8352
  srcCore(
7929
8353
  mergeConfig(
7930
8354
  {
@@ -7948,7 +8372,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7948
8372
  include: ['tests/src/browser/**/*.test.ts'],
7949
8373
  exclude: ['tests/src/core/**/*.test.ts'],
7950
8374
  ${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
7951
- ...(config?.test?.browser?.enabled === false
8375
+ ...(options?.test?.browser?.enabled === false
7952
8376
  ? {}
7953
8377
  : {
7954
8378
  deps: {
@@ -7968,12 +8392,12 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7968
8392
  fileParallelism: false,
7969
8393
  },
7970
8394
  },
7971
- config ?? {},
8395
+ options ?? {},
7972
8396
  ),
7973
8397
  )
7974
8398
  `;
7975
8399
  const serverBlock = `
7976
- ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8400
+ ${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
7977
8401
  srcCore(
7978
8402
  mergeConfig(
7979
8403
  {
@@ -8011,14 +8435,14 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8011
8435
  setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
8012
8436
  },
8013
8437
  },
8014
- config ?? {},
8438
+ options ?? {},
8015
8439
  ),
8016
8440
  )
8017
8441
  `;
8018
8442
  const blocks = nonCore.map((environment) => environment === "browser" ? browserBlock : serverBlock).join("");
8019
8443
  const renderedTest = renderViteTest(viteProjectRegistrations(src, [], facts), machinery.browser);
8020
8444
  return `${header}
8021
- ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8445
+ ${EXPORT_KEYWORD} const srcCore = (options?: UserConfig): UserConfig =>
8022
8446
  mergeConfig(
8023
8447
  {
8024
8448
  resolve,
@@ -8036,7 +8460,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8036
8460
  browser: { enabled: false },
8037
8461
  },
8038
8462
  },
8039
- config ?? {},
8463
+ options ?? {},
8040
8464
  )
8041
8465
  ${blocks}
8042
8466
  ${viteProjectDefinitions(facts)}
@@ -8066,7 +8490,7 @@ function applicationViteConfig(src, app, facts = {}) {
8066
8490
  const header = viteHeader(machinery);
8067
8491
  const blocks = [];
8068
8492
  if (src.includes("core")) blocks.push(`
8069
- ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8493
+ ${EXPORT_KEYWORD} const srcCore = (options?: UserConfig): UserConfig =>
8070
8494
  mergeConfig(
8071
8495
  {
8072
8496
  resolve,
@@ -8081,7 +8505,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8081
8505
  browser: { enabled: false },
8082
8506
  },
8083
8507
  },
8084
- config ?? {},
8508
+ options ?? {},
8085
8509
  )
8086
8510
  `);
8087
8511
  if (src.includes("browser")) {
@@ -8089,7 +8513,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
8089
8513
  output: { paths: { '@src/core': '../core/index.js' } },` : "";
8090
8514
  const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
8091
8515
  blocks.push(`
8092
- ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8516
+ ${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
8093
8517
  mergeConfig(
8094
8518
  {
8095
8519
  resolve,
@@ -8114,7 +8538,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8114
8538
  name: { label: 'src:browser', color: 'yellow' },
8115
8539
  include: ['tests/src/browser/**/*.test.ts'],
8116
8540
  ${hasSourceCore ? "exclude: ['tests/src/core/**/*.test.ts'],\n " : ""}${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
8117
- ...(config?.test?.browser?.enabled === false
8541
+ ...(options?.test?.browser?.enabled === false
8118
8542
  ? {}
8119
8543
  : {
8120
8544
  deps: {
@@ -8134,7 +8558,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8134
8558
  fileParallelism: false,
8135
8559
  },
8136
8560
  },
8137
- config ?? {},
8561
+ options ?? {},
8138
8562
  )
8139
8563
  `);
8140
8564
  }
@@ -8155,7 +8579,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
8155
8579
  const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
8156
8580
  const formats = hasSourceCore ? "" : "\n formats: ['es', 'cjs'],";
8157
8581
  blocks.push(`
8158
- ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8582
+ ${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
8159
8583
  mergeConfig(
8160
8584
  {
8161
8585
  resolve,
@@ -8185,12 +8609,12 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
8185
8609
  browser: { enabled: false },
8186
8610
  },
8187
8611
  },
8188
- config ?? {},
8612
+ options ?? {},
8189
8613
  )
8190
8614
  `);
8191
8615
  }
8192
8616
  if (app.includes("core")) blocks.push(`
8193
- ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
8617
+ ${EXPORT_KEYWORD} const appCore = (options?: UserConfig): UserConfig =>
8194
8618
  mergeConfig(
8195
8619
  {
8196
8620
  resolve,
@@ -8204,32 +8628,27 @@ ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
8204
8628
  browser: { enabled: false },
8205
8629
  },
8206
8630
  },
8207
- config ?? {},
8631
+ options ?? {},
8208
8632
  )
8209
8633
  `);
8210
8634
  if (app.includes("browser")) blocks.push(facts.showcase === true ? `
8211
- ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
8212
- const output = showcase ? 'dist/showcase' : 'dist/app/browser'
8635
+ ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
8636
+ if (config.length > 0) {
8637
+ throw new Error(
8638
+ '[orkestrel-environment-boundary] Browser configuration overrides are not permitted by the generated boundary',
8639
+ )
8640
+ }
8213
8641
  return {
8214
8642
  resolve,
8215
8643
  css: ENVIRONMENT_CSS,
8216
8644
  html: environmentHtml(),
8217
8645
  plugins: [
8218
8646
  restoreHtml(),
8219
- outputBoundary(output),
8647
+ outputBoundary('dist/app/browser'),
8220
8648
  environmentBoundary('app/browser'),
8221
8649
  vue(),
8222
8650
  prepareHtml(),
8223
8651
  finalizeHtml(),
8224
- ...(showcase
8225
- ? [
8226
- viteSingleFile({
8227
- removeViteModuleLoader: true,
8228
- useRecommendedBuildConfig: true,
8229
- }),
8230
- showcaseHtml(),
8231
- ]
8232
- : []),
8233
8652
  ],
8234
8653
  root: resolveWorkspacePath('app/browser'),
8235
8654
  publicDir: false,
@@ -8240,18 +8659,9 @@ ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
8240
8659
  },
8241
8660
  },
8242
8661
  build: {
8243
- ...(showcase
8244
- ? {
8245
- cssMinify: 'lightningcss',
8246
- minify: 'oxc',
8247
- modulePreload: false,
8248
- reportCompressedSize: false,
8249
- sourcemap: false,
8250
- target: 'esnext',
8251
- }
8252
- : { assetsInlineLimit: 0 }),
8662
+ assetsInlineLimit: 0,
8253
8663
  emptyOutDir: true,
8254
- outDir: resolveWorkspacePath(output),
8664
+ outDir: resolveWorkspacePath('dist/app/browser'),
8255
8665
  rolldownOptions: {
8256
8666
  input: resolveWorkspacePath('${APP_MATRIX.browser.entry}'),
8257
8667
  },
@@ -8280,22 +8690,54 @@ ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
8280
8690
  }
8281
8691
  }
8282
8692
 
8283
- ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
8284
- if (config.length > 0) {
8285
- throw new Error(
8286
- '[orkestrel-environment-boundary] Browser configuration overrides are not permitted by the generated boundary',
8287
- )
8288
- }
8289
- return applicationBrowser(false)
8290
- }
8291
-
8292
8693
  ${EXPORT_KEYWORD} function appShowcase(...config: never[]): UserConfig {
8293
8694
  if (config.length > 0) {
8294
8695
  throw new Error(
8295
8696
  '[orkestrel-environment-boundary] Showcase configuration overrides are not permitted by the generated boundary',
8296
8697
  )
8297
8698
  }
8298
- return applicationBrowser(true)
8699
+ return {
8700
+ base: './',
8701
+ resolve,
8702
+ css: ENVIRONMENT_CSS,
8703
+ html: environmentHtml(),
8704
+ plugins: [
8705
+ restoreHtml(),
8706
+ outputBoundary('dist/showcase'),
8707
+ environmentBoundary('app/browser'),
8708
+ vue(),
8709
+ prepareHtml(),
8710
+ showcaseHtml(),
8711
+ viteSingleFile({
8712
+ removeViteModuleLoader: true,
8713
+ useRecommendedBuildConfig: true,
8714
+ }),
8715
+ finalizeHtml(),
8716
+ ],
8717
+ root: resolveWorkspacePath('app/browser'),
8718
+ publicDir: false,
8719
+ server: {
8720
+ open: '/showcase.html',
8721
+ fs: {
8722
+ strict: true,
8723
+ allow: [...browserServerRoots()],
8724
+ },
8725
+ },
8726
+ build: {
8727
+ assetsInlineLimit: Number.MAX_SAFE_INTEGER,
8728
+ cssMinify: 'lightningcss',
8729
+ emptyOutDir: true,
8730
+ minify: 'oxc',
8731
+ modulePreload: false,
8732
+ outDir: resolveWorkspacePath('dist/showcase'),
8733
+ reportCompressedSize: false,
8734
+ rolldownOptions: {
8735
+ input: resolveWorkspacePath('app/browser/showcase.html'),
8736
+ },
8737
+ sourcemap: false,
8738
+ target: 'esnext',
8739
+ },
8740
+ }
8299
8741
  }
8300
8742
  ` : `
8301
8743
  ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
@@ -8357,7 +8799,7 @@ ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
8357
8799
  }
8358
8800
  `);
8359
8801
  if (app.includes("server")) blocks.push(`
8360
- ${EXPORT_KEYWORD} const appServer = (config?: UserConfig): UserConfig =>
8802
+ ${EXPORT_KEYWORD} const appServer = (options?: UserConfig): UserConfig =>
8361
8803
  mergeConfig(
8362
8804
  {
8363
8805
  resolve,
@@ -8384,7 +8826,7 @@ ${EXPORT_KEYWORD} const appServer = (config?: UserConfig): UserConfig =>
8384
8826
  browser: { enabled: false },
8385
8827
  },
8386
8828
  },
8387
- config ?? {},
8829
+ options ?? {},
8388
8830
  )
8389
8831
  `);
8390
8832
  const renderedTest = renderViteTest(viteProjectRegistrations(src, app, facts), machinery.browser);
@@ -8619,6 +9061,8 @@ function appTsconfig(environment, hasCore) {
8619
9061
  const include = TYPESCRIPT_EXTENSIONS.map((extension) => `../../app/${environment}/**/*.${extension}`);
8620
9062
  if (environment === "browser") include.push(`../../app/${environment}/**/*.vue`);
8621
9063
  if (environment !== "core" && hasCore) include.push(...TYPESCRIPT_EXTENSIONS.map((extension) => `../../app/core/**/*.${extension}`));
9064
+ include.push(...TYPESCRIPT_EXTENSIONS.map((extension) => `../../tests/app/${environment}/**/*.${extension}`));
9065
+ include.push(environment === "browser" ? "../../tests/setupBrowser.ts" : environment === "server" ? "../../tests/setupServer.ts" : "../../tests/setup.ts");
8622
9066
  return formatJson({
8623
9067
  extends: "../../tsconfig.json",
8624
9068
  compilerOptions: {
@@ -8799,7 +9243,7 @@ export default defineConfig(appShowcase())
8799
9243
  * factory per environment (AGENTS §5's per-environment centralized-file pattern), so
8800
9244
  * every environment gets the same uniform stub shape.
8801
9245
  *
8802
- * @param spec - The `Blueprint` to derive source stubs from.
9246
+ * @param spec - The blueprint carrying the declared source environment set.
8803
9247
  * @param pascal - The package's PascalCase entity name.
8804
9248
  * @returns The `source` group's `Artifact[]`.
8805
9249
  *
@@ -8832,20 +9276,129 @@ function sourceArtifacts(spec, pascal) {
8832
9276
  * Draft the application source artifacts for every selected app environment.
8833
9277
  *
8834
9278
  * @param spec - The blueprint carrying the application environment set.
9279
+ * @remarks
9280
+ * Two conditional shapes layer over the per-environment set. The health contract —
9281
+ * record, route constants, guard, and the one unknown-to-typed read — is declared by
9282
+ * `app/server` while the server alone reads it and RELOCATES to `app/core` the moment
9283
+ * the browser reads it too, because a contract two hosts share belongs to neither of
9284
+ * them. The showcase entry pair, its seeder, and its factory appear only for a
9285
+ * blueprint that declares the physical showcase wrapper alongside `app/browser`.
8835
9286
  * @returns Complete, runnable app/core, app/browser, and app/server artifacts.
8836
9287
  */
8837
9288
  function applicationArtifacts(spec) {
8838
9289
  const artifacts = [];
8839
9290
  const hasCore = spec.app.includes("core");
9291
+ const hasBrowser = spec.app.includes("browser");
9292
+ const hasBoundary = hasApplicationBoundary(spec);
9293
+ const hasShowcase = hasApplicationShowcase(spec);
9294
+ const showcaseSource = hasBoundary ? "its running server" : "its own configuration";
8840
9295
  const nameLiteral = serializeTypeScriptString(spec.name);
8841
- if (hasCore) artifacts.push(fillArtifact("app/core/types.ts", "source", "appCoreTypes", {}, "core"), fillArtifact("app/core/constants.ts", "source", "appCoreConstants", { nameLiteral }, "core"), fillArtifact("app/core/errors.ts", "source", "appCoreErrors", {}, "core"), fillArtifact("app/core/parsers.ts", "source", "appCoreParsers", {}, "core"), fillArtifact("app/core/factories.ts", "source", "appCoreFactories", {}, "core"), fillArtifact("app/core/index.ts", "source", "appCoreIndex", {}, "core"));
8842
- if (spec.app.includes("browser")) {
9296
+ const sharedRecord = `
9297
+ /** The application record both hosts read at the health route. */
9298
+ ${EXPORT_KEYWORD} interface ApplicationRecord {
9299
+ readonly name: string
9300
+ readonly status: 'ok'
9301
+ }
9302
+ `;
9303
+ const healthConstants = `
9304
+ /** The only HTTP method owned by the application health route. */
9305
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_METHOD = 'GET'
9306
+
9307
+ /** The only HTTP path owned by the generated application server. */
9308
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/health'
9309
+ `;
9310
+ if (hasCore) {
9311
+ artifacts.push(fillArtifact("app/core/types.ts", "source", "appCoreTypes", { record: hasBoundary ? sharedRecord : "" }, "core"), fillArtifact("app/core/constants.ts", "source", "appCoreConstants", {
9312
+ nameLiteral,
9313
+ health: hasBoundary ? `${healthConstants}
9314
+ /** Milliseconds allowed for one shared application health read. */
9315
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_TIMEOUT = 5_000
9316
+ ` : ""
9317
+ }, "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", {
9318
+ validators: hasBoundary ? "export * from './validators.js'\n" : "",
9319
+ handlers: hasBoundary ? "export * from './handlers.js'\n" : ""
9320
+ }, "core"));
9321
+ if (hasBoundary) artifacts.push(fillArtifact("app/core/validators.ts", "source", "appCoreValidators", {}, "core"), fillArtifact("app/core/handlers.ts", "source", "appCoreHandlers", {}, "core"));
9322
+ }
9323
+ if (hasBrowser) {
8843
9324
  const nameImport = hasCore ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from './constants.js'";
8844
9325
  const nameConstant = hasCore ? "" : `/** The browser-only application name. */
8845
9326
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
8846
9327
 
8847
9328
  `;
8848
- artifacts.push(fillArtifact("app/browser/types.ts", "source", "appBrowserTypes", {}, "browser"), fillArtifact("app/browser/constants.ts", "source", "appBrowserConstants", { nameConstant }, "browser"), fillArtifact("app/browser/errors.ts", "source", "appBrowserErrors", {}, "browser"), fillArtifact("app/browser/parsers.ts", "source", "appBrowserParsers", {}, "browser"), fillArtifact("app/browser/factories.ts", "source", "appBrowserFactories", { nameImport }, "browser"), fillArtifact("app/browser/index.ts", "source", "appBrowserIndex", {}, "browser"), fillArtifact("app/browser/main.ts", "source", "appBrowserMain", {}, "browser"), fillArtifact("app/browser/ApplicationView.vue", "source", "appBrowserView", {}, "browser"), fillArtifact("app/browser/index.html", "source", "appBrowserHtml", { name: escapeHtmlText(spec.name) }, "browser"), fillArtifact("app/browser/env.d.ts", "source", "appBrowserEnv", {}, "browser"));
9329
+ artifacts.push(fillArtifact("app/browser/types.ts", "source", "appBrowserTypes", { application: hasShowcase && !hasCore ? `
9330
+ /** The identity the root view renders. */
9331
+ ${EXPORT_KEYWORD} interface Application {
9332
+ readonly name: string
9333
+ }
9334
+ ` : "" }, "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", {
9335
+ nameImport: hasBoundary ? "import { APP_NAME, readApplicationHealth } from '@app/core'" : nameImport,
9336
+ seedImport: hasShowcase ? "import { seedApplication } from './seeders.js'\n" : "",
9337
+ showcase: hasShowcase ? `
9338
+ /**
9339
+ * Mount the showcase over its seeded, inert identity.
9340
+ *
9341
+ * @param target - The browser element or selector that receives the showcase.
9342
+ * @returns The mounted Vue application.
9343
+ *
9344
+ * @remarks
9345
+ * The showcase mounts the same {@link createBrowserApplication} root the shipped entry
9346
+ * mounts, so the two differ in exactly one expression — where the props come from. This
9347
+ * one reads {@link seedApplication}; the application reads ${showcaseSource}.
9348
+ *
9349
+ * @example
9350
+ * \`\`\`ts
9351
+ * import { mountShowcaseApplication } from '@app/browser'
9352
+ *
9353
+ * mountShowcaseApplication('#app')
9354
+ * \`\`\`
9355
+ */
9356
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} mountShowcaseApplication(target: string | Element): App<Element> {
9357
+ const seed = seedApplication()
9358
+ const application = createBrowserApplication({ name: seed.name })
9359
+ application.mount(target)
9360
+ return application
9361
+ }
9362
+ ` : "",
9363
+ boundary: hasBoundary ? `
9364
+ /**
9365
+ * Mount the application over its real server boundary.
9366
+ *
9367
+ * @param target - The browser element or selector that receives the application.
9368
+ * @returns The mounted Vue application, after one health read settles.
9369
+ *
9370
+ * @remarks
9371
+ * One health read runs before the mount, so the root view renders the identity the
9372
+ * running server reported. An unreachable or off-contract boundary yields \`undefined\`
9373
+ * and the application falls back to its own configuration rather than failing to mount.
9374
+ *
9375
+ * @example
9376
+ * \`\`\`ts
9377
+ * import { mountBrowserApplication } from '@app/browser'
9378
+ *
9379
+ * await mountBrowserApplication('#app')
9380
+ * \`\`\`
9381
+ */
9382
+ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} mountBrowserApplication(target: string | Element): Promise<App<Element>> {
9383
+ const seed = (await readApplicationHealth(window.location.origin)) ?? { name: APP_NAME }
9384
+ const application = createBrowserApplication({ name: seed.name })
9385
+ application.mount(target)
9386
+ return application
9387
+ }
9388
+ ` : ""
9389
+ }, "browser"), fillArtifact("app/browser/index.ts", "source", "appBrowserIndex", { seeders: hasShowcase ? "export * from './seeders.js'\n" : "" }, "browser"), fillArtifact("app/browser/main.ts", "source", "appBrowserMain", hasBoundary ? {
9390
+ factory: "mountBrowserApplication",
9391
+ mount: `void mountBrowserApplication('#app').catch(() => {
9392
+ console.error('[ERROR] Browser application failed')
9393
+ })`
9394
+ } : {
9395
+ factory: "createBrowserApplication",
9396
+ mount: "createBrowserApplication().mount('#app')"
9397
+ }, "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"));
9398
+ if (hasShowcase) artifacts.push(fillArtifact("app/browser/seeders.ts", "source", "appBrowserSeeders", {
9399
+ applicationImport: hasCore ? "import type { Application } from '@app/core'" : "import type { Application } from './types.js'",
9400
+ nameImport
9401
+ }, "browser"), fillArtifact("app/browser/showcase.ts", "source", "appBrowserShowcase", {}, "browser"), fillArtifact("app/browser/showcase.html", "source", "appBrowserShowcaseHtml", { name: escapeHtmlText(spec.name) }, "browser"));
8849
9402
  }
8850
9403
  if (spec.app.includes("server")) {
8851
9404
  const nameImport = hasCore ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from './constants.js'";
@@ -8853,7 +9406,19 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
8853
9406
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
8854
9407
 
8855
9408
  `;
8856
- artifacts.push(fillArtifact("app/server/types.ts", "source", "appServerTypes", {}, "server"), fillArtifact("app/server/constants.ts", "source", "appServerConstants", { nameConstant }, "server"), fillArtifact("app/server/errors.ts", "source", "appServerErrors", {}, "server"), fillArtifact("app/server/parsers.ts", "source", "appServerParsers", {}, "server"), fillArtifact("app/server/handlers.ts", "source", "appServerHandlers", { nameImport }, "server"), fillArtifact("app/server/ApplicationServer.ts", "source", "appServerEntity", {}, "server"), fillArtifact("app/server/factories.ts", "source", "appServerFactories", {}, "server"), fillArtifact("app/server/ApplicationServerRunner.ts", "source", "appServerRunner", {}, "server"), fillArtifact("app/server/index.ts", "source", "appServerIndex", {}, "server"), fillArtifact("app/server/main.ts", "source", "appServerMain", {}, "server"));
9409
+ artifacts.push(fillArtifact("app/server/types.ts", "source", "appServerTypes", { record: hasBoundary ? "" : `/** The application record returned by the health route. */
9410
+ ${EXPORT_KEYWORD} interface ApplicationRecord {
9411
+ readonly name: string
9412
+ readonly status: 'ok'
9413
+ }
9414
+
9415
+ ` }, "server"), fillArtifact("app/server/constants.ts", "source", "appServerConstants", {
9416
+ nameConstant,
9417
+ health: hasBoundary ? "" : healthConstants
9418
+ }, "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", {
9419
+ recordImport: hasBoundary ? "import type { ApplicationRecord } from '@app/core'" : "import type { ApplicationRecord } from './types.js'",
9420
+ nameImport
9421
+ }, "server"), fillArtifact("app/server/ApplicationServer.ts", "source", "appServerEntity", {}, "server"), fillArtifact("app/server/factories.ts", "source", "appServerFactories", {}, "server"), fillArtifact("app/server/ApplicationServerRunner.ts", "source", "appServerRunner", { nameImport }, "server"), fillArtifact("app/server/index.ts", "source", "appServerIndex", {}, "server"), fillArtifact("app/server/main.ts", "source", "appServerMain", {}, "server"));
8857
9422
  }
8858
9423
  return artifacts;
8859
9424
  }
@@ -8926,7 +9491,7 @@ function paritySpecifiers(spec) {
8926
9491
  function testArtifacts(spec, pascal) {
8927
9492
  const hasBrowser = spec.src.includes("browser") || spec.app.includes("browser");
8928
9493
  const hasVue = spec.app.includes("browser");
8929
- const browserPolicyImport = hasBrowser ? "\nimport { chromium } from 'playwright'\nimport { isBrowserExecutable, resolveBrowser, SYSTEM_BROWSER_CHANNELS } from '../vite.config.js'" : "";
9494
+ const machinery = viteMachinery(spec.src, spec.app, spec.bin, spec.showcase);
8930
9495
  const vuePolicyImport = hasVue ? "\nimport { parse as parseVue } from 'vue/compiler-sfc'" : "";
8931
9496
  const workspacePolicyAssertion = hasVue ? `expect(
8932
9497
  inspectCodingWorkspace(process.cwd(), (path, content) => {
@@ -8937,37 +9502,218 @@ function testArtifacts(spec, pascal) {
8937
9502
  )
8938
9503
  }),
8939
9504
  ).toEqual([])` : "expect(inspectCodingWorkspace(process.cwd())).toEqual([])";
8940
- const browserPolicyTest = hasBrowser ? `
9505
+ const configNames = [
9506
+ "containedPath",
9507
+ "environmentPathError",
9508
+ "environmentSourceError",
9509
+ "resolveWorkspacePath",
9510
+ "workspacePath"
9511
+ ];
9512
+ if (machinery.output) configNames.push("enforceOutputPath");
9513
+ if (hasBrowser) configNames.push("isBrowserExecutable", "resolveBrowser", "resolveManagedBrowser", "resolveSystemBrowser", "SYSTEM_BROWSER_CHANNELS");
9514
+ if (hasVue) configNames.push("hasSecurityPrologue", "HTML_SECURITY_META", "HTML_SECURITY_POLICY");
9515
+ const configImports = [
9516
+ ...hasVue ? ["import { readFileSync } from 'node:fs'"] : [],
9517
+ ...hasBrowser ? ["import { chromium } from 'playwright'"] : [],
9518
+ "import { describe, expect, it } from 'vitest'",
9519
+ "import {",
9520
+ ...configNames.map((name) => `\t${name},`),
9521
+ "} from '../../vite.config.js'"
9522
+ ].join("\n");
9523
+ const configCases = [];
9524
+ if (machinery.output) configCases.push(`
9525
+
9526
+ it('contains build output in its exact workspace directory', () => {
9527
+ const expected = resolveWorkspacePath('dist/config-proof')
9528
+
9529
+ expect(() => enforceOutputPath(expected, expected)).not.toThrow()
9530
+ expect(() => enforceOutputPath(resolveWorkspacePath('dist/other'), expected)).toThrow(
9531
+ 'exact configured workspace directory',
9532
+ )
9533
+ expect(() =>
9534
+ enforceOutputPath(
9535
+ resolveWorkspacePath('../config-proof'),
9536
+ resolveWorkspacePath('../config-proof'),
9537
+ ),
9538
+ ).toThrow('remain inside the workspace')
9539
+ })`);
9540
+ if (hasBrowser) configCases.push(`
8941
9541
 
8942
9542
  it('resolves only a real managed executable or stable system browser channel', () => {
8943
- const options = resolveBrowser(chromium.executablePath(), process.platform, process.env)
8944
- let valid = options === undefined
8945
- if (options !== undefined) {
8946
- const channel = options.launchOptions?.channel
8947
- valid =
8948
- channel === undefined
8949
- ? isBrowserExecutable(options.launchOptions?.executablePath ?? chromium.executablePath())
8950
- : SYSTEM_BROWSER_CHANNELS.some((browser) => browser.channel === channel)
8951
- }
8952
- expect(valid).toBe(true)
8953
- })` : "";
8954
- const artifacts = [fillArtifact("tests/setup.ts", "tests", "setup", {}), fillArtifact("tests/policy.test.ts", "tests", "policyTest", {
8955
- browserPolicySpecifier: "",
8956
- browserPolicyImport,
8957
- browserPolicyTest,
8958
- vuePolicyImport,
8959
- workspacePolicyAssertion
8960
- })];
9543
+ const pinned = chromium.executablePath()
9544
+ const managed = resolveManagedBrowser(pinned)
9545
+ const channel = resolveSystemBrowser(process.platform, process.env)
9546
+ const options = resolveBrowser(pinned, process.platform, process.env)
9547
+ const expected =
9548
+ managed === undefined
9549
+ ? channel === undefined
9550
+ ? undefined
9551
+ : { launchOptions: { channel } }
9552
+ : managed === pinned
9553
+ ? {}
9554
+ : { launchOptions: { executablePath: managed } }
9555
+ const executable = managed === undefined || isBrowserExecutable(managed)
9556
+ const stable =
9557
+ managed !== undefined ||
9558
+ channel === undefined ||
9559
+ SYSTEM_BROWSER_CHANNELS.some((browser) => browser.channel === channel)
9560
+
9561
+ expect(options).toEqual(expected)
9562
+ expect(executable).toBe(true)
9563
+ expect(stable).toBe(true)
9564
+ })`);
9565
+ if (hasVue) configCases.push(`
9566
+
9567
+ it('preserves the generated browser security prologue', () => {
9568
+ const document = readFileSync(resolveWorkspacePath('app/browser/index.html'), 'utf8')
9569
+
9570
+ expect(hasSecurityPrologue(document, HTML_SECURITY_META)).toBe(true)
9571
+ expect(HTML_SECURITY_META).toContain(HTML_SECURITY_POLICY)
9572
+ })`);
9573
+ const artifacts = [
9574
+ fillArtifact("tests/setup.ts", "tests", "setup", {
9575
+ eventImport: spec.app.includes("server") ? "import type { EmitterInterface, EventMap } from '@orkestrel/emitter'\n\n" : "",
9576
+ eventHelper: spec.app.includes("server") ? `
9577
+ /** Wait for one typed event occurrence and return its argument tuple. */
9578
+ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} waitForEvent<TMap extends EventMap, K extends keyof TMap>(
9579
+ emitter: EmitterInterface<TMap>,
9580
+ event: K,
9581
+ ): Promise<TMap[K]> {
9582
+ return new Promise((resolvePromise) => emitter.once(event, (...args) => resolvePromise(args)))
9583
+ }
9584
+ ` : ""
9585
+ }),
9586
+ fillArtifact("tests/policy.test.ts", "tests", "policyTest", {
9587
+ vuePolicyImport,
9588
+ workspacePolicyAssertion
9589
+ }),
9590
+ fillArtifact("tests/config/vite.test.ts", "tests", "configTest", {
9591
+ imports: configImports,
9592
+ cases: configCases.join("")
9593
+ })
9594
+ ];
8961
9595
  if (spec.src.includes("server") || spec.app.includes("server")) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
8962
9596
  if (spec.src.includes("browser") || spec.app.includes("browser")) artifacts.push(fillArtifact("tests/setupBrowser.ts", "tests", "setupBrowser", {}, "browser"));
8963
- if (spec.app.includes("core")) artifacts.push(fillArtifact("tests/app/core/factories.test.ts", "tests", "appCoreTest", {}, "core"));
9597
+ const hasBoundary = hasApplicationBoundary(spec);
9598
+ const hasShowcase = hasApplicationShowcase(spec);
9599
+ if (spec.app.includes("core")) artifacts.push(fillArtifact("tests/app/core/factories.test.ts", "tests", "appCoreTest", {
9600
+ guardImport: hasBoundary ? " isApplicationRecord,\n" : "",
9601
+ readImport: hasBoundary ? " readApplicationHealth,\n" : "",
9602
+ boundary: hasBoundary ? `
9603
+
9604
+ describe('shared application health boundary', () => {
9605
+ it('accepts the shared record and refuses every off-contract value', () => {
9606
+ expect(isApplicationRecord({ name: APP_NAME, status: 'ok' })).toBe(true)
9607
+ expect(isApplicationRecord({ name: ' ', status: 'ok' })).toBe(true)
9608
+ for (const value of [
9609
+ null,
9610
+ [],
9611
+ 'ok',
9612
+ { name: APP_NAME },
9613
+ { name: '', status: 'ok' },
9614
+ { name: 1, status: 'ok' },
9615
+ { name: APP_NAME, status: 'down' },
9616
+ ]) {
9617
+ expect(isApplicationRecord(value)).toBe(false)
9618
+ }
9619
+ const revocable = Proxy.revocable({}, {})
9620
+ revocable.revoke()
9621
+ expect(isApplicationRecord(revocable.proxy)).toBe(false)
9622
+ })
9623
+
9624
+ it('refuses a malformed origin before reaching the network', async () => {
9625
+ expect(await readApplicationHealth('not-an-origin')).toBeUndefined()
9626
+ })
9627
+ })` : ""
9628
+ }, "core"));
8964
9629
  if (spec.app.includes("browser")) {
8965
9630
  const browserTestNameImport = spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/browser'";
8966
- artifacts.push(fillArtifact("tests/app/browser/factories.test.ts", "tests", "appBrowserTest", { browserTestNameImport }, "browser"));
9631
+ artifacts.push(fillArtifact("tests/app/browser/factories.test.ts", "tests", "appBrowserTest", {
9632
+ browserTestNameImport,
9633
+ showcaseImport: hasShowcase ? " mountShowcaseApplication,\n" : "",
9634
+ entryImport: `${hasShowcase ? " seedApplication,\n" : ""}${hasBoundary ? " mountBrowserApplication,\n" : ""}`,
9635
+ showcase: hasShowcase ? `
9636
+
9637
+ describe('mountShowcaseApplication', () => {
9638
+ it('mounts the shipped root view over one frozen, inert seed', () => {
9639
+ const element = buildElement()
9640
+ const seeded = seedApplication()
9641
+ const application = mountShowcaseApplication(element)
9642
+ try {
9643
+ expect(element.textContent).toContain(seeded.name)
9644
+ expect(seeded).toEqual(seedApplication())
9645
+ expect(seeded).not.toBe(seedApplication())
9646
+ expect(Object.isFrozen(seeded)).toBe(true)
9647
+ } finally {
9648
+ application.unmount()
9649
+ element.remove()
9650
+ }
9651
+ })
9652
+ })` : "",
9653
+ boundary: hasBoundary ? `
9654
+
9655
+ describe('mountBrowserApplication', () => {
9656
+ it('mounts the configured identity when the boundary answers off-contract', async () => {
9657
+ const element = buildElement()
9658
+ const application = await mountBrowserApplication(element)
9659
+ try {
9660
+ expect(element.textContent).toContain(APP_NAME)
9661
+ } finally {
9662
+ application.unmount()
9663
+ element.remove()
9664
+ }
9665
+ })
9666
+ })` : ""
9667
+ }, "browser"));
8967
9668
  }
8968
9669
  if (spec.app.includes("server")) {
8969
- const testNameImport = spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/server'";
8970
- artifacts.push(fillArtifact("tests/app/server/ApplicationServer.test.ts", "tests", "appServerTest", { testNameImport }, "server"), fillArtifact("tests/app/server/parsers.test.ts", "tests", "appServerParsersTest", {}, "server"));
9670
+ const testNameImport = hasBoundary ? `import {
9671
+ APP_HEALTH_METHOD,
9672
+ APP_HEALTH_PATH,
9673
+ APP_NAME,
9674
+ isApplicationRecord,
9675
+ readApplicationHealth,
9676
+ } from '@app/core'` : spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/server'";
9677
+ const serverImport = hasBoundary ? `import {
9678
+ ApplicationServerRunner,
9679
+ createApplicationDispatcher,
9680
+ createApplicationServer,
9681
+ } from '@app/server'` : `import {
9682
+ APP_HEALTH_METHOD,
9683
+ APP_HEALTH_PATH,
9684
+ ApplicationServerRunner,
9685
+ createApplicationDispatcher,
9686
+ createApplicationServer,
9687
+ } from '@app/server'`;
9688
+ artifacts.push(fillArtifact("tests/app/server/ApplicationServer.test.ts", "tests", "appServerTest", {
9689
+ testNameImport,
9690
+ serverImport,
9691
+ boundary: hasBoundary ? `
9692
+
9693
+ describe('shared application boundary', () => {
9694
+ it('answers the shared record and translates it into the shared identity', async () => {
9695
+ const server = createApplicationServer({ server: { host: '127.0.0.1', port: 0 } })
9696
+ try {
9697
+ await server.start()
9698
+ const url = server.url
9699
+ if (url === undefined) throw new Error('Expected a bound application URL')
9700
+ const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
9701
+ const record: unknown = await response.json()
9702
+
9703
+ expect(isApplicationRecord(record)).toBe(true)
9704
+ expect(await readApplicationHealth(url)).toEqual({ name: APP_NAME })
9705
+ } finally {
9706
+ await server.destroy()
9707
+ }
9708
+ })
9709
+
9710
+ it('reads undefined from a released loopback port', async () => {
9711
+ const port = await reserveLoopbackPort()
9712
+
9713
+ expect(await readApplicationHealth(\`http://127.0.0.1:\${port}\`)).toBeUndefined()
9714
+ })
9715
+ })` : ""
9716
+ }, "server"), fillArtifact("tests/app/server/parsers.test.ts", "tests", "appServerParsersTest", {}, "server"));
8971
9717
  }
8972
9718
  const inlineExplicitInstance = `instance: ${pascal}Interface = new ${pascal}({ id: 'example' })`;
8973
9719
  const multilineExplicitInstance = `instance: ${pascal}Interface = new ${pascal}({
@@ -9011,12 +9757,12 @@ function testArtifacts(spec, pascal) {
9011
9757
  }
9012
9758
  artifacts.push(fillArtifact("tests/setupGuides.ts", "tests", "setupGuides", {
9013
9759
  specifiers: paritySpecifiers(spec),
9014
- walkDirs: [
9015
- ...spec.src.length > 0 ? ["'src'"] : [],
9016
- ...spec.app.length > 0 ? ["'app'"] : [],
9017
- "'guides'",
9018
- "'tests'"
9019
- ].map((directory) => `\t${directory},`).join("\n")
9760
+ walkDirs: renderStringArray([
9761
+ ...spec.src.length > 0 ? ["src"] : [],
9762
+ ...spec.app.length > 0 ? ["app"] : [],
9763
+ "guides",
9764
+ "tests"
9765
+ ], "", "export const GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze(", ")")
9020
9766
  }), fillArtifact("tests/guides/src/parity.test.ts", "tests", "parityTest", { name: spec.name }));
9021
9767
  return artifacts;
9022
9768
  }
@@ -9065,6 +9811,8 @@ function guideMemberTable(category, members) {
9065
9811
  */
9066
9812
  function guideUsage(spec, pascal) {
9067
9813
  const examples = [];
9814
+ const hasBoundary = hasApplicationBoundary(spec);
9815
+ const hasShowcase = hasApplicationShowcase(spec);
9068
9816
  if (spec.src.length > 0) examples.push(`\`\`\`ts
9069
9817
  import { create${pascal} } from '@orkestrel/${spec.name}'
9070
9818
 
@@ -9081,6 +9829,24 @@ import {
9081
9829
  ${CONST_KEYWORD} name = parseApplicationName(' ${spec.name} ')
9082
9830
  ${CONST_KEYWORD} application = createApplication(name)
9083
9831
  isApplicationError(new ApplicationError('CONFIG', 'invalid')) // true
9832
+ \`\`\``);
9833
+ if (hasBoundary) examples.push(`\`\`\`ts
9834
+ import type { ApplicationRecord } from '@app/core'
9835
+ import {
9836
+ APP_HEALTH_METHOD,
9837
+ APP_HEALTH_PATH,
9838
+ APP_HEALTH_TIMEOUT,
9839
+ isApplicationRecord,
9840
+ readApplicationHealth,
9841
+ } from '@app/core'
9842
+
9843
+ APP_HEALTH_METHOD // 'GET'
9844
+ APP_HEALTH_PATH // '/health'
9845
+ APP_HEALTH_TIMEOUT // 5000
9846
+ ${CONST_KEYWORD} healthy: ApplicationRecord = { name: '${spec.name}', status: 'ok' }
9847
+ isApplicationRecord(healthy) // true
9848
+ isApplicationRecord({ name: '${spec.name}', status: 'down' }) // false
9849
+ await readApplicationHealth('http://127.0.0.1:3000') // { name: '${spec.name}' } or undefined
9084
9850
  \`\`\``);
9085
9851
  if (spec.app.includes("browser")) examples.push(`\`\`\`ts
9086
9852
  import {
@@ -9096,27 +9862,36 @@ ${CONST_KEYWORD} browserOptions = parseBrowserApplicationOptions({
9096
9862
  ${CONST_KEYWORD} browser = createBrowserApplication(browserOptions)
9097
9863
  browser.mount('#app')
9098
9864
  isBrowserApplicationError(new BrowserApplicationError('CONFIG', 'invalid')) // true
9865
+ \`\`\``);
9866
+ if (hasShowcase) examples.push(`\`\`\`ts
9867
+ import { mountShowcaseApplication, seedApplication } from '@app/browser'
9868
+
9869
+ ${CONST_KEYWORD} seed = seedApplication()
9870
+ ${CONST_KEYWORD} showcase = mountShowcaseApplication('#app')
9871
+ seed.name // '${spec.name} showcase'
9872
+ showcase.unmount()
9873
+ \`\`\``);
9874
+ if (hasBoundary) examples.push(`\`\`\`ts
9875
+ import { mountBrowserApplication } from '@app/browser'
9876
+
9877
+ ${CONST_KEYWORD} application = await mountBrowserApplication('#app')
9878
+ application.unmount()
9099
9879
  \`\`\``);
9100
9880
  if (spec.app.includes("server")) examples.push(`\`\`\`ts
9101
- import { once } from 'node:events'
9102
- import { createServer } from 'node:http'
9881
+ ${hasBoundary ? `import type { ApplicationRecord } from '@app/core'
9882
+ import type { ApplicationState } from '@app/server'
9883
+ import { APP_HEALTH_METHOD, APP_HEALTH_PATH, isApplicationRecord } from '@app/core'` : "import type { ApplicationRecord, ApplicationState } from '@app/server'"}
9103
9884
  import {
9104
- APP_HEALTH_METHOD,
9885
+ ${hasBoundary ? "" : ` APP_HEALTH_METHOD,
9105
9886
  APP_HEALTH_PATH,
9106
- APP_HEADERS_TIMEOUT,
9107
- APP_HOST_LABEL_PATTERN,
9108
- APP_KEEP_ALIVE_TIMEOUT,
9109
- APP_MAX_CONNECTIONS,
9110
- APP_MAX_HEADERS,
9111
- APP_MAX_REQUESTS_PER_SOCKET,
9112
- APP_REQUEST_TIMEOUT,
9887
+ `} APP_HOST_LABEL_PATTERN,
9113
9888
  APP_NUMERIC_HOST_PATTERN,
9114
- ApplicationServer,
9115
9889
  ApplicationServerError,
9116
9890
  DEFAULT_APP_START_TIMEOUT,
9117
9891
  MAX_APP_START_TIMEOUT,
9892
+ createApplicationDispatcher,
9118
9893
  createApplicationServer,
9119
- handleApplicationRequest,
9894
+ handleApplicationHealth,
9120
9895
  isApplicationServerError,
9121
9896
  parseApplicationHost,
9122
9897
  parseApplicationPort,
@@ -9128,41 +9903,51 @@ import {
9128
9903
  ${CONST_KEYWORD} host = parseApplicationHost('127.0.0.1')
9129
9904
  ${CONST_KEYWORD} port = parseApplicationPort('0')
9130
9905
  ${CONST_KEYWORD} timeout = parseApplicationStartTimeout('5000')
9131
- ${CONST_KEYWORD} options = parseApplicationServerOptions({ host, port, timeout })
9906
+ ${CONST_KEYWORD} options = parseApplicationServerOptions({ server: { host, port, timeout } })
9907
+ parseApplicationStartTimeout(String(DEFAULT_APP_START_TIMEOUT)) // 10000
9908
+ MAX_APP_START_TIMEOUT // 300000
9132
9909
  APP_HOST_LABEL_PATTERN.test('api') // true
9133
9910
  APP_NUMERIC_HOST_PATTERN.test('999.999.999.999') // true (and therefore rejected as a host)
9134
- APP_HEALTH_METHOD // 'GET'
9135
- APP_HEALTH_PATH // '/'
9136
- APP_MAX_CONNECTIONS // 16
9137
- APP_MAX_HEADERS // 100
9138
- APP_HEADERS_TIMEOUT // 10000
9139
- APP_REQUEST_TIMEOUT // 30000
9140
- APP_KEEP_ALIVE_TIMEOUT // 5000
9141
- APP_MAX_REQUESTS_PER_SOCKET // 100
9142
- DEFAULT_APP_START_TIMEOUT // 10000
9143
- MAX_APP_START_TIMEOUT // 300000
9144
- ${CONST_KEYWORD} handlerServer = createServer(handleApplicationRequest)
9145
- handlerServer.listen(0, host)
9146
- await once(handlerServer, 'listening')
9147
- ${CONST_KEYWORD} handlerClosed = once(handlerServer, 'close')
9148
- handlerServer.close()
9149
- await handlerClosed
9150
-
9151
- ${CONST_KEYWORD} error = new ApplicationServerError('CONFIG', 'invalid')
9152
- isApplicationServerError(error) // true
9153
- reportApplicationServerError(error) // writes only a stable CONFIG diagnostic
9154
- new ApplicationServer(options) // stopped entity
9911
+ ${CONST_KEYWORD} state: ApplicationState = { connection: { encrypted: false } }
9912
+ ${CONST_KEYWORD} record: ApplicationRecord = { name: '${spec.name}', status: 'ok' }
9913
+ ${CONST_KEYWORD} dispatcher = createApplicationDispatcher()
9914
+ try {
9915
+ ${CONST_KEYWORD} response = await dispatcher.handle(
9916
+ new Request(\`http://application.test\${APP_HEALTH_PATH}\`, { method: APP_HEALTH_METHOD }),
9917
+ state,
9918
+ )
9919
+ ${CONST_KEYWORD} health = handleApplicationHealth()
9920
+ ${CONST_KEYWORD} encoded = Response.json(record)
9921
+ ${hasBoundary ? `${CONST_KEYWORD} value: unknown = await health.clone().json()
9922
+ isApplicationRecord(value) // true
9923
+ ` : ""}if (!response.ok || !health.ok || !encoded.ok) throw new Error('Application health failed')
9924
+ } finally {
9925
+ dispatcher.destroy()
9926
+ }
9927
+
9928
+ ${CONST_KEYWORD} failure: unknown = new ApplicationServerError('CONFIG', 'invalid')
9929
+ if (isApplicationServerError(failure)) {
9930
+ reportApplicationServerError(failure) // writes only a stable CONFIG diagnostic
9931
+ }
9155
9932
 
9156
9933
  ${CONST_KEYWORD} server = createApplicationServer(options)
9157
9934
  ${CONST_KEYWORD} controller = new AbortController()
9158
9935
  await server.start(controller.signal)
9159
9936
  await server.stop()
9937
+ await server.destroy()
9160
9938
  \`\`\`
9161
9939
 
9162
9940
  \`\`\`ts
9163
- import { ApplicationServerRunner } from '@app/server'
9164
-
9165
- ${CONST_KEYWORD} runner = new ApplicationServerRunner({ port: 0 })
9941
+ import type { ApplicationServerRunnerEventMap, ApplicationServerRunnerOptions } from '@app/server'
9942
+ import { ApplicationServerRunner, createApplicationServer } from '@app/server'
9943
+
9944
+ ${CONST_KEYWORD} event: keyof ApplicationServerRunnerEventMap = 'ready'
9945
+ ${CONST_KEYWORD} observe: ApplicationServerRunnerOptions = { on: { fail: () => undefined } }
9946
+ ${CONST_KEYWORD} runner = new ApplicationServerRunner(
9947
+ createApplicationServer({ server: { port: 0 } }),
9948
+ observe,
9949
+ )
9950
+ runner.emitter.once(event, (url) => console.log(url))
9166
9951
  runner.start() // process owns shutdown signals
9167
9952
  await runner.stop()
9168
9953
  \`\`\`
@@ -9170,7 +9955,7 @@ await runner.stop()
9170
9955
  \`\`\`ts
9171
9956
  import { startApplicationServer } from '@app/server'
9172
9957
 
9173
- ${CONST_KEYWORD} processRunner = startApplicationServer({ port: 0 })
9958
+ ${CONST_KEYWORD} processRunner = startApplicationServer({ server: { port: 0 } })
9174
9959
  await processRunner.stop()
9175
9960
  \`\`\``);
9176
9961
  return examples.join("\n\n");
@@ -9193,15 +9978,23 @@ ${alignTable([
9193
9978
  "Method",
9194
9979
  "Returns",
9195
9980
  "Behavior"
9196
- ], [[
9197
- "`start`",
9198
- "`Promise<void>`",
9199
- "Serialize in call order; start only when stopped, and repeat safely when already listening. The optional `AbortSignal` and bounded startup timeout cancel pending name resolution/listen work. Rejects with `ApplicationServerError` code `LIFECYCLE` when startup fails, times out, or the caller aborts."
9200
9981
  ], [
9201
- "`stop`",
9202
- "`Promise<void>`",
9203
- "Cancel every pending start before its queued stop, force active and idle connections closed, and repeat safely when already stopped. Rejects with `ApplicationServerError` code `LIFECYCLE` when closing fails."
9204
- ]])}
9982
+ [
9983
+ "`start`",
9984
+ "`Promise<void>`",
9985
+ "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."
9986
+ ],
9987
+ [
9988
+ "`stop`",
9989
+ "`Promise<void>`",
9990
+ "Drain and stop the installed server; repeated calls while stopped are safe. Rejects with `ApplicationServerError` code `LIFECYCLE` when closing fails."
9991
+ ],
9992
+ [
9993
+ "`destroy`",
9994
+ "`Promise<void>`",
9995
+ "Perform terminal idempotent teardown through the installed server lifecycle, then destroy its owned dispatcher. Rejects with `ApplicationServerError` code `LIFECYCLE` when server teardown fails."
9996
+ ]
9997
+ ])}
9205
9998
 
9206
9999
  #### \`ApplicationServerRunnerInterface\`
9207
10000
 
@@ -9212,17 +10005,18 @@ ${alignTable([
9212
10005
  ], [[
9213
10006
  "`start`",
9214
10007
  "`void`",
9215
- "Register one idempotent set of SIGINT/SIGTERM cleanup listeners, start the server, and translate asynchronous startup failures into a non-zero process exit code."
10008
+ "Register one generation-owned set of SIGINT/SIGTERM cleanup listeners, queue the substrate start behind any shutdown already in flight, emit `ready` after binding, and emit `fail` for a current lifecycle failure."
9216
10009
  ], [
9217
10010
  "`stop`",
9218
10011
  "`Promise<void>`",
9219
- "Release both process listeners before stopping the server; repeated calls are safe and lifecycle failures reject."
10012
+ "Abort a startup still in flight and release both process listeners, then wait for that startup to settle before stopping the server; concurrent calls join one substrate stop, and lifecycle failures emit `fail` and reject."
9220
10013
  ]])}
9221
10014
 
9222
- The constructor validates direct options plus \`APP_HOST\`, \`APP_PORT\`, and
9223
- \`APP_START_TIMEOUT\` before allocating
9224
- a listener. Direct options must be an exact plain own-key data record containing only
9225
- \`host\`, \`port\`, and/or \`timeout\`; inherited properties, accessors, symbols, instances, proxies that
10015
+ 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.
10016
+
10017
+ The application server constructor validates grouped direct options plus \`APP_HOST\`, \`APP_PORT\`, and
10018
+ \`APP_START_TIMEOUT\` before binding. Direct options must be an exact plain own-key data record containing only a
10019
+ \`server\` record with \`host\`, \`port\`, and/or \`timeout\`; inherited properties, accessors, symbols, instances, proxies that
9226
10020
  throw during reflection, and unknown keys fail closed. Invalid values throw
9227
10021
  \`ApplicationServerError\` code \`CONFIG\`; the default host is loopback and port \`0\` is
9228
10022
  supported for collision-free ephemeral allocation. Startup defaults to 10 seconds and accepts
@@ -9230,10 +10024,15 @@ only integer timeouts from 1 through 300,000 milliseconds. Lifecycle failures us
9230
10024
  \`LIFECYCLE\`; both may carry \`context.cause\` or \`context.value\`. Narrow caught values with
9231
10025
  \`isApplicationServerError\` before reading either field.
9232
10026
 
9233
- The generated server owns exactly \`GET /\`. It serializes
9234
- \`{ name: APP_NAME, status: 'ok' }\` as JSON with \`cache-control: no-store\`;
9235
- every other path returns deterministic plain-text \`404 Not Found\`, and every unsupported
9236
- method returns deterministic plain-text \`405 Method Not Allowed\` with \`Allow: GET\`.`;
10027
+ Before binding, \`url\` is \`undefined\`; after a successful start it reflects the real bound port,
10028
+ and it returns to \`undefined\` after stop or destroy. \`ApplicationState\` extends middleware's
10029
+ \`IdentifierState\` and adds only its \`connection\` property; there is no redundant \`listening\` member.
10030
+
10031
+ Each \`createApplicationDispatcher()\` call returns a fresh dispatcher that owns exactly \`GET /health\`
10032
+ and serializes the shared \`ApplicationRecord\` shape \`{ name: APP_NAME, status: 'ok' }\` as JSON. The
10033
+ server composes \`createBoundary()\`, \`createSecurity()\`, then \`createDeadline({ ms: timeout })\`
10034
+ around that owned dispatcher; standalone callers destroy theirs after use. Every other path returns
10035
+ \`404\`, and every unsupported method returns \`405\` with \`Allow: GET\`.`;
9237
10036
  }
9238
10037
  /**
9239
10038
  * Build links to every generated source and application test file.
@@ -9243,7 +10042,7 @@ method returns deterministic plain-text \`405 Method Not Allowed\` with \`Allow:
9243
10042
  * @returns A newline-separated Markdown test inventory.
9244
10043
  */
9245
10044
  function guideTests(spec, pascal) {
9246
- const tests = ["- [`tests/policy.test.ts`](../../tests/policy.test.ts) — filename placement and real browser capability probing."];
10045
+ const tests = ["- [`tests/policy.test.ts`](../../tests/policy.test.ts) — repository coding law and filename placement.", "- [`tests/config/vite.test.ts`](../../tests/config/vite.test.ts) — executable root Vite invariants and conditional browser capability."];
9247
10046
  for (const environment of spec.src) tests.push(`- [\`tests/src/${environment}/${pascal}.test.ts\`](../../tests/src/${environment}/${pascal}.test.ts) — entity boundaries.`, `- [\`tests/src/${environment}/factories.test.ts\`](../../tests/src/${environment}/factories.test.ts) — factory behavior.`);
9248
10047
  if (spec.app.includes("core")) tests.push("- [`tests/app/core/factories.test.ts`](../../tests/app/core/factories.test.ts) — host-independent identity behavior.");
9249
10048
  if (spec.app.includes("browser")) tests.push("- [`tests/app/browser/factories.test.ts`](../../tests/app/browser/factories.test.ts) — real-browser mount and cleanup.");
@@ -9924,7 +10723,9 @@ function createBlueprint(data) {
9924
10723
  }
9925
10724
  //#endregion
9926
10725
  exports.APP_BROWSER_DEV_DEPENDENCIES = APP_BROWSER_DEV_DEPENDENCIES;
10726
+ exports.APP_DEV_DEPENDENCIES = APP_DEV_DEPENDENCIES;
9927
10727
  exports.APP_MATRIX = APP_MATRIX;
10728
+ exports.APP_SERVER_DEV_DEPENDENCIES = APP_SERVER_DEV_DEPENDENCIES;
9928
10729
  exports.BASE_DEV_DEPENDENCIES = BASE_DEV_DEPENDENCIES;
9929
10730
  exports.BIN_CONFIGS = BIN_CONFIGS;
9930
10731
  exports.CATEGORIES = CATEGORIES;
@@ -10003,6 +10804,7 @@ exports.compareCodeUnit = compareCodeUnit;
10003
10804
  exports.computeColumnWidth = computeColumnWidth;
10004
10805
  exports.computeHash = computeHash;
10005
10806
  exports.configArtifacts = configArtifacts;
10807
+ exports.configViteProject = configViteProject;
10006
10808
  exports.contentByteLength = contentByteLength;
10007
10809
  exports.contentCodePoint = contentCodePoint;
10008
10810
  exports.contentToBytes = contentToBytes;
@@ -10032,6 +10834,8 @@ exports.guideMethods = guideMethods;
10032
10834
  exports.guideTests = guideTests;
10033
10835
  exports.guideUsage = guideUsage;
10034
10836
  exports.guidesViteProject = guidesViteProject;
10837
+ exports.hasApplicationBoundary = hasApplicationBoundary;
10838
+ exports.hasApplicationShowcase = hasApplicationShowcase;
10035
10839
  exports.hasBlueprintEnvironment = hasBlueprintEnvironment;
10036
10840
  exports.hasOnlyDataProperties = hasOnlyDataProperties;
10037
10841
  exports.hasValidArtifactBytes = hasValidArtifactBytes;
@@ -10087,6 +10891,7 @@ exports.policyViteProject = policyViteProject;
10087
10891
  exports.rangeToFreshness = rangeToFreshness;
10088
10892
  exports.renderArray = renderArray;
10089
10893
  exports.renderObject = renderObject;
10894
+ exports.renderStringArray = renderStringArray;
10090
10895
  exports.renderValue = renderValue;
10091
10896
  exports.renderViteTest = renderViteTest;
10092
10897
  exports.rootTsconfig = rootTsconfig;