@orkestrel/scaffold 0.0.21 → 0.0.23

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 (68) hide show
  1. package/dist/bin/scaffold.js +371 -140
  2. package/dist/bin/scaffold.js.map +1 -1
  3. package/dist/host/AGENTS.md +61 -42
  4. package/dist/host/CLAUDE.md +39 -408
  5. package/dist/host/agents/orchestration.md +454 -0
  6. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +25 -7
  7. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +2 -2
  8. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +42 -31
  9. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
  10. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +3 -3
  11. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +2 -2
  12. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +167 -0
  13. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +98 -0
  14. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +148 -0
  15. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +4 -2
  16. package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +5 -5
  17. package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +2 -0
  18. package/dist/host/agents/skills/orkestrel-polish-surface/SKILL.md +11 -6
  19. package/dist/host/claude/agents/analyst.md +59 -0
  20. package/dist/host/claude/agents/application.md +1 -1
  21. package/dist/host/claude/agents/builder.md +1 -1
  22. package/dist/host/claude/agents/checker.md +5 -1
  23. package/dist/host/claude/agents/codex.md +120 -94
  24. package/dist/host/claude/agents/grok.md +44 -32
  25. package/dist/host/claude/agents/implementer.md +1 -1
  26. package/dist/host/claude/agents/orkestrel.md +5 -5
  27. package/dist/host/claude/agents/planner.md +9 -3
  28. package/dist/host/claude/agents/researcher.md +6 -5
  29. package/dist/host/claude/agents/reviewer.md +12 -6
  30. package/dist/host/claude/agents/scout.md +7 -4
  31. package/dist/host/claude/agents/verifier.md +1 -1
  32. package/dist/host/claude/rules/application.md +17 -16
  33. package/dist/host/claude/rules/architecture.md +41 -22
  34. package/dist/host/claude/rules/documentation.md +2 -1
  35. package/dist/host/claude/rules/names.md +7 -6
  36. package/dist/host/claude/rules/quality.md +65 -18
  37. package/dist/host/claude/rules/styles.md +1 -1
  38. package/dist/host/claude/rules/tests.md +12 -6
  39. package/dist/host/claude/rules/typescript.md +1 -1
  40. package/dist/host/claude/rules/workspace.md +6 -3
  41. package/dist/host/claude/settings.json +2 -0
  42. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +10 -0
  43. package/dist/host/codex/agents/analyst.toml +13 -4
  44. package/dist/host/codex/agents/checker.toml +1 -1
  45. package/dist/host/codex/agents/grok.toml +2 -1
  46. package/dist/host/codex/agents/opus.toml +1 -1
  47. package/dist/host/codex/agents/planner.toml +1 -1
  48. package/dist/host/codex/agents/researcher.toml +2 -2
  49. package/dist/host/codex/agents/reviewer.toml +1 -1
  50. package/dist/host/codex/agents/scout.toml +2 -2
  51. package/dist/host/codex/config.toml +23 -70
  52. package/dist/host/cursor/rules/orchestration.mdc +33 -0
  53. package/dist/host/guides/src/scaffold.md +339 -125
  54. package/dist/host/manifest.json +47 -1
  55. package/dist/host/tests/setupPolicy.ts +210 -3
  56. package/dist/src/core/index.cjs +477 -69
  57. package/dist/src/core/index.cjs.map +1 -1
  58. package/dist/src/core/index.d.cts +132 -43
  59. package/dist/src/core/index.d.ts +132 -43
  60. package/dist/src/core/index.js +474 -70
  61. package/dist/src/core/index.js.map +1 -1
  62. package/dist/src/server/index.cjs +83 -33
  63. package/dist/src/server/index.cjs.map +1 -1
  64. package/dist/src/server/index.d.cts +43 -27
  65. package/dist/src/server/index.d.ts +43 -27
  66. package/dist/src/server/index.js +84 -35
  67. package/dist/src/server/index.js.map +1 -1
  68. package/package.json +4 -4
@@ -102,20 +102,29 @@ var APP_MATRIX = Object.freeze({
102
102
  * The byte-copied host artifact paths, frozen.
103
103
  *
104
104
  * @remarks
105
- * The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`, `.agents`, `.claude`, `.codex`,
106
- * the four SessionStart hook scripts (`scripts/deps.sh` / `scripts/cursor.sh` /
107
- * `scripts/codex.sh` / `scripts/ollama.sh`), the repository coding-law policy module,
108
- * the line's seven byte-identical root dotfiles, and the two guides-grouped
109
- * mirror candidates: the line-wide dev-tooling guide
110
- * (`guides/src/guide.md`) and the scaffold bin's own self-guide
111
- * (`guides/src/scaffold.md`). `stageHost` vendors both; each plan carries the
112
- * subset selected by `selectHostPaths`, omitting the target blueprint's own
113
- * guide.
105
+ * The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`, the canonical
106
+ * orchestration contract (`.agents/orchestration.md`) every harness bridge
107
+ * points at, `.agents`, `.claude`, `.codex`, `.cursor`, the four SessionStart
108
+ * hook scripts (`scripts/deps.sh` / `scripts/cursor.sh` / `scripts/codex.sh` /
109
+ * `scripts/ollama.sh`), the repository coding-law policy module, the line's
110
+ * seven byte-identical root dotfiles, and the two guides-grouped mirror
111
+ * candidates: the line-wide dev-tooling guide (`guides/src/guide.md`) and the
112
+ * scaffold bin's own self-guide (`guides/src/scaffold.md`). `stageHost` vendors
113
+ * both; each plan carries the subset selected by `selectHostPaths`, omitting the
114
+ * target blueprint's own guide.
115
+ *
116
+ * Three harness bridges point at `.agents/orchestration.md` and carry only their
117
+ * own harness's specifics: `CLAUDE.md`, `.codex/config.toml`, and
118
+ * `.cursor/rules`. They are meaningless without the contract they reference, but
119
+ * they do not share a `Group` — `hostGroup` keeps `CLAUDE.md` in `docs` with the
120
+ * other root documents, so a plan selecting `orchestration` carries two of the
121
+ * three and a plan selecting `docs` carries the third.
114
122
  */
115
123
  var HOST_PATHS = Object.freeze([
116
124
  "AGENTS.md",
117
125
  "CLAUDE.md",
118
126
  "LICENSE",
127
+ ".agents/orchestration.md",
119
128
  ".agents/skills",
120
129
  ".claude/agents",
121
130
  ".claude/rules",
@@ -124,6 +133,7 @@ var HOST_PATHS = Object.freeze([
124
133
  ".codex/agents",
125
134
  ".codex/config.toml",
126
135
  ".cursor/mcp.json",
136
+ ".cursor/rules",
127
137
  ".mcp.json",
128
138
  "scripts/deps.sh",
129
139
  "scripts/cursor.sh",
@@ -140,12 +150,48 @@ var HOST_PATHS = Object.freeze([
140
150
  "guides/src/guide.md",
141
151
  "guides/src/scaffold.md"
142
152
  ]);
143
- /** The consumer-owned live-service provisioner expected only by service workspaces. */
153
+ /**
154
+ * The path prefixes whose contents instruct or wire an agent, frozen.
155
+ *
156
+ * @remarks
157
+ * Group classification splits by what a path governs, not by where it sits:
158
+ * anything under these prefixes is `orchestration`, and everything else that is
159
+ * not source, tests, guides, docs, or a manifest is `configs`. Both classifiers
160
+ * — `inferGroup` for a foreign target path and `hostGroup` for a `HOST_PATHS`
161
+ * entry — read this one list, so a new harness directory is admitted once.
162
+ */
163
+ var ORCHESTRATION_PATH_PREFIXES = Object.freeze([
164
+ ".agents/",
165
+ ".claude/",
166
+ ".codex/",
167
+ ".cursor/",
168
+ ".github/",
169
+ "scripts/"
170
+ ]);
171
+ /**
172
+ * The exact root filenames that wire an agent bench rather than the toolchain, frozen.
173
+ *
174
+ * @remarks
175
+ * `.mcp.json` registers MCP servers for the harness. It sits among the root
176
+ * dotfiles but governs agents, so it groups with the harness bridges.
177
+ */
178
+ var ORCHESTRATION_PATH_NAMES = Object.freeze([".mcp.json"]);
179
+ /** The birth-only provisioner skeleton retained by workspaces with declared service vendors. */
144
180
  var SERVICE_SCRIPT_PATH = "scripts/service.sh";
145
181
  /** The consumer-owned Vitest global-setup module shared by its independently selected projects. */
146
182
  var GLOBAL_SETUP_PATH = "tests/setupGlobal.ts";
147
183
  /** The consumer-owned Vite wrapper whose physical presence enables the optional app showcase. */
148
184
  var SHOWCASE_CONFIG_PATH = "configs/app/vite.showcase.config.ts";
185
+ /**
186
+ * The catalog agent file whose bounded marker region the catalog operation alone owns.
187
+ *
188
+ * @remarks
189
+ * Vendored like every other host artifact, but presence-owned after hydration:
190
+ * `diffPlan` compares this one path by presence, so a consumer restores it while
191
+ * absent and never replaces its bytes — not from an audit, not from a repair, and
192
+ * not under `replace`.
193
+ */
194
+ var CATALOG_AGENT_PATH = ".claude/agents/orkestrel.md";
149
195
  /** The package-name RegExp — lowercase alphanumeric-with-hyphens, letter-first. */
150
196
  var NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
151
197
  /** Maximum bare workspace name length beneath the generated `@orkestrel/` scope. */
@@ -228,7 +274,7 @@ var DEFAULT_VERSION = "0.0.1";
228
274
  /** The `engines.node` range the `blueprint` builder fills. */
229
275
  var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
230
276
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
231
- var SCAFFOLD_RANGE = "^0.0.21";
277
+ var SCAFFOLD_RANGE = "^0.0.23";
232
278
  /** Tooling versions shared by scaffold and every generated workspace. */
233
279
  var BASE_DEV_DEPENDENCIES = Object.freeze({
234
280
  "@microsoft/api-extractor": "^7.58.12",
@@ -437,8 +483,8 @@ function member(name, category, summary, environment = "core") {
437
483
  * @remarks
438
484
  * `version` / `engines` default `DEFAULT_VERSION` / `DEFAULT_ENGINES`,
439
485
  * `src` defaults `['core']`, and `app` / `keywords` / `dependencies` /
440
- * `peers` / `extras` / `overrides` default `[]`, and `bin` / `integration` /
441
- * `service` / `global` / `showcase` default `false`. `description` is OMITTED entirely
486
+ * `peers` / `extras` / `overrides` / `services` default `[]`, and `bin` /
487
+ * `integration` / `global` / `showcase` default `false`. `description` is OMITTED entirely
442
488
  * when absent, so the result round-trips the exact-record `Blueprint` guard.
443
489
  * @returns A complete `Blueprint`.
444
490
  *
@@ -463,7 +509,7 @@ function blueprint(name, options) {
463
509
  overrides: options?.overrides ?? [],
464
510
  bin: options?.bin ?? false,
465
511
  integration: options?.integration ?? false,
466
- service: options?.service ?? false,
512
+ services: [...options?.services ?? []].sort(),
467
513
  global: options?.global ?? false,
468
514
  showcase: options?.showcase ?? false
469
515
  };
@@ -952,7 +998,7 @@ function syncToReview(report) {
952
998
  }
953
999
  /**
954
1000
  * Project a fleet package catalog into a markdown table — the block
955
- * `.claude/agents/orkestrel.md`'s catalog markers wrap.
1001
+ * `CATALOG_AGENT_PATH`'s catalog markers wrap.
956
1002
  *
957
1003
  * @param entries - The catalog rows to render.
958
1004
  * @remarks
@@ -984,10 +1030,12 @@ function catalogToBlock(entries) {
984
1030
  *
985
1031
  * @param path - The target-relative path to classify.
986
1032
  * @remarks
987
- * Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`,
988
- * then `.agents/`, `.claude/`, `.codex/`, `.github/`, and `scripts/` as
989
- * `'orchestration'`, then the two manifest files by exact name. Anything else
990
- * (a root-level, prefix-less file) falls through to `'configs'`.
1033
+ * Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`, then
1034
+ * `matchesOrchestrationPath`, which owns the orchestration membership rule for
1035
+ * this function and for `hostGroup` alike, then the two manifest files by exact
1036
+ * name. Anything left falls through to `'configs'`. Read the rule at
1037
+ * `matchesOrchestrationPath` rather than here; one rule with two descriptions
1038
+ * drifts exactly as fast as one rule with two implementations.
991
1039
  * @returns The inferred `Group` for `path`.
992
1040
  *
993
1041
  * @example
@@ -1005,23 +1053,43 @@ function inferGroup(path) {
1005
1053
  if (path.startsWith("guides/")) return "guides";
1006
1054
  if (path.startsWith("docs/")) return "docs";
1007
1055
  if (path.startsWith("configs/")) return "configs";
1008
- if (path.startsWith(".agents/") || path.startsWith(".claude/") || path.startsWith(".codex/") || path.startsWith(".github/") || path.startsWith("scripts/")) return "orchestration";
1056
+ if (matchesOrchestrationPath(path)) return "orchestration";
1009
1057
  if (path === "package.json" || path === "package-lock.json") return "manifest";
1010
1058
  return "configs";
1011
1059
  }
1012
1060
  /**
1061
+ * Test whether a path instructs or wires an agent rather than the toolchain.
1062
+ *
1063
+ * @param path - The portable path to classify.
1064
+ * @returns `true` when the path is agent orchestration.
1065
+ *
1066
+ * @example
1067
+ * ```ts
1068
+ * import { matchesOrchestrationPath } from '@orkestrel/scaffold'
1069
+ *
1070
+ * matchesOrchestrationPath('.cursor/rules/orchestration.mdc') // true
1071
+ * matchesOrchestrationPath('.mcp.json') // true
1072
+ * matchesOrchestrationPath('.oxlintrc.json') // false
1073
+ * ```
1074
+ */
1075
+ function matchesOrchestrationPath(path) {
1076
+ if (ORCHESTRATION_PATH_NAMES.includes(path)) return true;
1077
+ return ORCHESTRATION_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
1078
+ }
1079
+ /**
1013
1080
  * Diff a plan's artifacts against a target's current content.
1014
1081
  *
1015
1082
  * @param plan - The plan whose artifacts are the source of truth.
1016
1083
  * @param current - The target's current content, keyed by artifact-relative path.
1017
1084
  * @remarks
1018
- * Audit semantics are per-origin. A `host`-origin artifact is audited by
1019
- * PRESENCE only `missing` or `aligned`, never `stale` UNLESS it has been
1020
- * hydrated with its real host bytes (`hydratePlan`'s `content`), in which case
1021
- * it is content-compared exactly like a `computed` artifact and CAN be
1022
- * `stale`. `hydratePlan` expands directory-shaped host artifacts into
1023
- * content-bearing file artifacts; only an unresolved degrade-path host
1024
- * artifact stays presence-only. A `computed` artifact is content-aware canon —
1085
+ * Audit semantics are per-origin. A `host`-origin artifact with canonical
1086
+ * `hex` is content-compared exactly like a `computed` artifact and CAN be
1087
+ * `stale`. A host artifact without canonical bytes is presence-owned:
1088
+ * present is `aligned`, absent is `missing`. `CATALOG_AGENT_PATH` remains
1089
+ * presence-owned after hydration because the catalog operation alone owns its
1090
+ * bounded marker region. `hydratePlan` expands directory-shaped host artifacts
1091
+ * into byte-aware file artifacts; only an intentional dependency-guide pointer
1092
+ * may remain without `hex`. A `computed` artifact is content-aware canon —
1025
1093
  * `missing` / `aligned` / `stale` — and gates the audit like any drifted
1026
1094
  * finding. A `template`-origin artifact is BIRTH-ONLY and AUDIT-EXEMPT: it is
1027
1095
  * always reported `aligned`, regardless of whether the target has it at all
@@ -1061,7 +1129,7 @@ function diffPlan(plan, current) {
1061
1129
  if (artifact.origin === "host") {
1062
1130
  let drift;
1063
1131
  if (seen === void 0) drift = "missing";
1064
- else if (artifact.hex === void 0) drift = "aligned";
1132
+ else if (artifact.path === ".claude/agents/orkestrel.md" || artifact.hex === void 0) drift = "aligned";
1065
1133
  else drift = seen === artifact.hex ? "aligned" : "stale";
1066
1134
  findings.push({
1067
1135
  path: artifact.path,
@@ -1418,6 +1486,35 @@ function validateBlueprint(spec) {
1418
1486
  text: "Showcase requires the app browser environment",
1419
1487
  blocking: true
1420
1488
  });
1489
+ const seenServices = /* @__PURE__ */ new Set();
1490
+ const serviceProjects = /* @__PURE__ */ new Set();
1491
+ let previousService;
1492
+ for (const service of spec.services) {
1493
+ if (!NAME_PATTERN.test(service) || service.length > 203) questions.push({
1494
+ field: "services",
1495
+ text: `Service name "${service}" must be a bounded lowercase directory name matching ${NAME_PATTERN.source}`,
1496
+ blocking: true
1497
+ });
1498
+ if (seenServices.has(service)) questions.push({
1499
+ field: "services",
1500
+ text: `Service "${service}" is declared more than once`,
1501
+ blocking: true
1502
+ });
1503
+ if (previousService !== void 0 && previousService > service) questions.push({
1504
+ field: "services",
1505
+ text: "Services must be sorted by directory name",
1506
+ blocking: true
1507
+ });
1508
+ const project = pascalCase(service);
1509
+ if (serviceProjects.has(project)) questions.push({
1510
+ field: "services",
1511
+ text: `Service "${service}" collides with another generated project name`,
1512
+ blocking: true
1513
+ });
1514
+ seenServices.add(service);
1515
+ serviceProjects.add(project);
1516
+ previousService = service;
1517
+ }
1421
1518
  if (spec.src.length > 0) {
1422
1519
  for (const environment of spec.src) if (!ENVIRONMENTS.includes(environment)) questions.push({
1423
1520
  field: "src",
@@ -1908,7 +2005,10 @@ function blueprintShape() {
1908
2005
  overrides: arrayShape(overrideShape(), { max: MAX_COLLECTION_ITEMS }),
1909
2006
  bin: booleanShape(),
1910
2007
  integration: booleanShape(),
1911
- service: booleanShape(),
2008
+ services: arrayShape(stringShape({
2009
+ min: 1,
2010
+ max: 203
2011
+ }), { max: MAX_COLLECTION_ITEMS }),
1912
2012
  global: booleanShape(),
1913
2013
  showcase: booleanShape()
1914
2014
  });
@@ -4501,6 +4601,87 @@ describe('root Vite configuration', () => {
4501
4601
  expect(environmentSourceError('app/browser', '@app/core')).toBeUndefined()
4502
4602
  }){{cases}}
4503
4603
  })
4604
+ `
4605
+ }),
4606
+ serviceConformance: Object.freeze({
4607
+ id: "serviceConformance",
4608
+ name: "serviceConformance",
4609
+ summary: "The generated service-vendor structure and configuration conformance test.",
4610
+ category: "tests",
4611
+ placeholders: Object.freeze([Object.freeze({
4612
+ name: "services",
4613
+ description: "The sorted service vendor directory names."
4614
+ })]),
4615
+ content: `import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
4616
+ import { join } from 'node:path'
4617
+ import { isRecord, parseJSON } from '@orkestrel/contract'
4618
+ import { expect, it } from 'vitest'
4619
+
4620
+ it('keeps every service vendor structurally complete and exactly configured', () => {
4621
+ const declared = {{services}}
4622
+ const root = join(process.cwd(), 'tests', 'service')
4623
+ const directories = readdirSync(root, { withFileTypes: true })
4624
+ .filter((entry) => entry.isDirectory())
4625
+ .map((entry) => entry.name)
4626
+ .sort()
4627
+ const manifest = parseJSON(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
4628
+ const scripts = isRecord(manifest) && isRecord(manifest.scripts) ? manifest.scripts : {}
4629
+ const config = readFileSync(join(process.cwd(), 'vite.config.ts'), 'utf8')
4630
+ const provisioner = join(process.cwd(), 'scripts', 'service.sh')
4631
+
4632
+ expect(directories).toEqual(declared)
4633
+ if (!existsSync(provisioner) || !lstatSync(provisioner).isFile()) {
4634
+ throw new Error(
4635
+ 'Service vendors require scripts/service.sh to provision every declared vendor idempotently or exit nonzero',
4636
+ )
4637
+ }
4638
+ for (const service of declared) {
4639
+ const setup = join(root, service, 'setup.ts')
4640
+ if (!existsSync(setup) || !lstatSync(setup).isFile()) {
4641
+ throw new Error(
4642
+ \`Service vendor "\${service}" is missing tests/service/\${service}/setup.ts; add a readiness module that probes and warms the vendor and throws when unavailable\`,
4643
+ )
4644
+ }
4645
+ const label = \`name: { label: 'service:\${service}', color: 'red' }\`
4646
+ const include = \`include: ['tests/service/\${service}/**/*.test.ts']\`
4647
+ const setupFiles = \`setupFiles: ['./tests/setup.ts', './tests/setupServer.ts', './tests/service/\${service}/setup.ts']\`
4648
+ if (!config.includes(label) || !config.includes(include) || !config.includes(setupFiles)) {
4649
+ throw new Error(\`Service vendor "\${service}" is missing its exact Vite project\`)
4650
+ }
4651
+ const command = \`vitest run --config vite.config.ts --no-cache --reporter=dot --project service:\${service}\`
4652
+ if (scripts[\`test:service:\${service}\`] !== command) {
4653
+ throw new Error(\`Service vendor "\${service}" is missing its exact npm script\`)
4654
+ }
4655
+ }
4656
+ const aggregate =
4657
+ 'vitest run --config vite.config.ts --no-cache --reporter=dot ' +
4658
+ declared.map((service) => \`--project service:\${service}\`).join(' ')
4659
+ if (scripts['test:service'] !== aggregate) {
4660
+ throw new Error('test:service must name every declared service exactly once')
4661
+ }
4662
+ if (typeof scripts.test !== 'string' || scripts.test.includes('test:service')) {
4663
+ throw new Error('The default test script must omit live service proofs')
4664
+ }
4665
+ if (
4666
+ typeof scripts.prepublishOnly !== 'string' ||
4667
+ !scripts.prepublishOnly.endsWith(' && npm run test:service')
4668
+ ) {
4669
+ throw new Error('prepublishOnly must end with the aggregate live service proof')
4670
+ }
4671
+ })
4672
+ `
4673
+ }),
4674
+ serviceProvisioner: Object.freeze({
4675
+ id: "serviceProvisioner",
4676
+ name: "serviceProvisioner",
4677
+ summary: "The birth-only workspace-owned service-vendor provisioner skeleton.",
4678
+ category: "orchestration",
4679
+ placeholders: Object.freeze([]),
4680
+ content: `#!/bin/sh
4681
+ set -eu
4682
+
4683
+ printf '%s\\n' 'Service provisioning is not configured. Implement scripts/service.sh to provision every declared vendor idempotently, then remove this failure.' >&2
4684
+ exit 1
4504
4685
  `
4505
4686
  }),
4506
4687
  policyTest: Object.freeze({
@@ -4510,15 +4691,34 @@ describe('root Vite configuration', () => {
4510
4691
  category: "tests",
4511
4692
  placeholders: Object.freeze([Object.freeze({
4512
4693
  name: "vuePolicyImport",
4513
- description: "The optional official Vue SFC compiler import."
4694
+ description: "The Vue SFC parser import, when the workspace declares app/browser."
4514
4695
  }), Object.freeze({
4515
4696
  name: "workspacePolicyAssertion",
4516
- description: "The formatter-stable workspace policy assertion."
4697
+ description: "The workspace coding-law assertion, carrying a Vue script extractor when the workspace declares app/browser."
4517
4698
  })]),
4518
4699
  content: `import { globSync } from 'node:fs'
4519
4700
  import { describe, expect, it } from 'vitest'
4520
4701
  import { isBrowserVuePath } from './setup.js'
4521
- import { inspectCodingWorkspace } from './setupPolicy.js'{{vuePolicyImport}}
4702
+ import {
4703
+ derivePolicyTokens,
4704
+ inspectCodingLaw,
4705
+ inspectCodingWorkspace,
4706
+ inspectPolicyPurity,
4707
+ inspectPolicyWorkspace,
4708
+ isFunctionDomainPath,
4709
+ POLICY_SOURCE_ENVIRONMENTS,
4710
+ readPackageName,
4711
+ } from './setupPolicy.js'{{vuePolicyImport}}
4712
+
4713
+ ${CONST_KEYWORD} POLICY_PLANTED_PATH = 'tests/setupPolicy.ts'
4714
+ ${CONST_KEYWORD} POLICY_TOKENS = derivePolicyTokens(readPackageName(process.cwd()))
4715
+ ${CONST_KEYWORD} FUNCTION_MODULE_PATH = 'app/browser/composables/useTheme.ts'
4716
+ ${CONST_KEYWORD} FUNCTION_MODULE_VIOLATION =
4717
+ FUNCTION_MODULE_PATH + ' declarations do not form one matching exported function implementation'
4718
+ ${CONST_KEYWORD} FUNCTION_DOMAIN_FILE_PATH = 'app/server/composables.ts'
4719
+ ${CONST_KEYWORD} FUNCTION_DOMAIN_FILE_VIOLATION =
4720
+ FUNCTION_DOMAIN_FILE_PATH +
4721
+ ' names a function domain, which belongs in a folder rather than a file'
4522
4722
 
4523
4723
  describe('repository coding law', () => {
4524
4724
  it('keeps Vue single-file components exclusively in browser environments', () => {
@@ -4530,6 +4730,149 @@ describe('repository coding law', () => {
4530
4730
  it('enforces source placement, exports, readonly contracts, and syntax law', () => {
4531
4731
  {{workspacePolicyAssertion}}
4532
4732
  })
4733
+
4734
+ it('accepts one matching exported function in a registered domain', () => {
4735
+ expect(
4736
+ inspectCodingLaw(
4737
+ FUNCTION_MODULE_PATH,
4738
+ "import type { Ref } from 'vue'\\nexport function useTheme(): Ref<undefined> { throw new Error() }",
4739
+ ),
4740
+ ).toEqual([])
4741
+ })
4742
+
4743
+ it('accepts one matching exported generator in a registered domain', () => {
4744
+ expect(
4745
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export function* useTheme(): Generator<void> {}'),
4746
+ ).toEqual([])
4747
+ })
4748
+
4749
+ it('accepts overload signatures beside one matching implementation', () => {
4750
+ expect(
4751
+ inspectCodingLaw(
4752
+ FUNCTION_MODULE_PATH,
4753
+ 'export function useTheme(): void\\nexport function useTheme(mode: string): void\\nexport function useTheme(_mode?: string): void {}',
4754
+ ),
4755
+ ).toEqual([])
4756
+ })
4757
+
4758
+ it('rejects a bodyless function-domain declaration', () => {
4759
+ expect(
4760
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export declare function useTheme(): void'),
4761
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4762
+ })
4763
+
4764
+ it('rejects a mismatched bodyless declaration beside one implementation', () => {
4765
+ expect(
4766
+ inspectCodingLaw(
4767
+ FUNCTION_MODULE_PATH,
4768
+ 'declare function smuggled(secret: string): void\\nexport function useTheme(): void {}',
4769
+ ),
4770
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4771
+ })
4772
+
4773
+ it('rejects two exported functions in a function module', () => {
4774
+ expect(
4775
+ inspectCodingLaw(
4776
+ FUNCTION_MODULE_PATH,
4777
+ 'export function useTheme(): void {}\\nexport function useMode(): void {}',
4778
+ ),
4779
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4780
+ })
4781
+
4782
+ it('rejects module data beside a function-domain export', () => {
4783
+ expect(
4784
+ inspectCodingLaw(
4785
+ FUNCTION_MODULE_PATH,
4786
+ "const THEME = 'dark'\\nexport function useTheme(): string { return THEME }",
4787
+ ),
4788
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4789
+ })
4790
+
4791
+ it('rejects a function whose name differs from its filename', () => {
4792
+ expect(inspectCodingLaw(FUNCTION_MODULE_PATH, 'export function useMode(): void {}')).toEqual([
4793
+ FUNCTION_MODULE_VIOLATION,
4794
+ ])
4795
+ })
4796
+
4797
+ it('rejects a non-exported function-domain declaration', () => {
4798
+ expect(inspectCodingLaw(FUNCTION_MODULE_PATH, 'function useTheme(): void {}')).toEqual([
4799
+ FUNCTION_MODULE_VIOLATION,
4800
+ ])
4801
+ })
4802
+
4803
+ it('rejects a default function-domain export', () => {
4804
+ expect(
4805
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export default function useTheme(): void {}'),
4806
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4807
+ })
4808
+
4809
+ it('keeps index modules outside the function-domain shape', () => {
4810
+ const path = 'app/browser/composables/index.ts'
4811
+
4812
+ expect(isFunctionDomainPath(path)).toBe(false)
4813
+ expect(inspectCodingLaw(path, 'export function index(): void {}')).toContain(
4814
+ path + ' places module functions in their centralized kind file',
4815
+ )
4816
+ })
4817
+
4818
+ it('keeps main modules outside the function-domain shape', () => {
4819
+ const path = 'app/browser/composables/main.ts'
4820
+
4821
+ expect(isFunctionDomainPath(path)).toBe(false)
4822
+ expect(inspectCodingLaw(path, 'export function main(): void {}')).toContain(
4823
+ path + ' places module functions in their centralized kind file',
4824
+ )
4825
+ })
4826
+
4827
+ it('rejects a file named for a registered function domain', () => {
4828
+ const content =
4829
+ "import { parentPort } from 'node:worker_threads'\\nexport function start(): void { parentPort?.close() }"
4830
+
4831
+ expect(inspectCodingLaw(FUNCTION_DOMAIN_FILE_PATH, content)).toEqual([
4832
+ FUNCTION_DOMAIN_FILE_VIOLATION,
4833
+ ])
4834
+ })
4835
+
4836
+ it('keeps camelCase modules in unregistered domains under centralized placement', () => {
4837
+ const path = 'app/browser/services/normalizePath.ts'
4838
+
4839
+ expect(isFunctionDomainPath(path)).toBe(false)
4840
+ expect(inspectCodingLaw(path, 'export function normalizePath(): void {}')).toEqual([
4841
+ path + ' places module functions in their centralized kind file',
4842
+ ])
4843
+ })
4844
+
4845
+ it('preserves the self-contained Node runtime exemption', () => {
4846
+ const path = 'app/server/worker.ts'
4847
+ const content =
4848
+ "import { parentPort } from 'node:worker_threads'\\nconst port = parentPort\\nexport function start(): void { port?.close() }"
4849
+
4850
+ expect(inspectCodingLaw(path, content)).toEqual([])
4851
+ })
4852
+ })
4853
+
4854
+ describe('fleet policy purity', () => {
4855
+ it('keeps fleet policy files free of this package architecture', () => {
4856
+ expect(inspectPolicyWorkspace(process.cwd())).toEqual([])
4857
+ })
4858
+
4859
+ it('reports planted package architecture, so a clean sweep is evidence', () => {
4860
+ const token = POLICY_TOKENS[0]
4861
+ const environment = POLICY_SOURCE_ENVIRONMENTS[0]
4862
+ if (token === undefined || environment === undefined) {
4863
+ throw new Error('The package manifest derived no policy token to plant')
4864
+ }
4865
+ const planted = [
4866
+ 'export const ' + token + '_PATH = []',
4867
+ "export const path = 'src/" + environment + "/index.ts'",
4868
+ '',
4869
+ ].join('\\n')
4870
+
4871
+ expect(inspectPolicyPurity(POLICY_PLANTED_PATH, planted, POLICY_TOKENS)).toEqual([
4872
+ POLICY_PLANTED_PATH + ':1:14 forbids the ' + token + ' package token',
4873
+ POLICY_PLANTED_PATH + ':2:21 forbids a source-environment path literal',
4874
+ ])
4875
+ })
4533
4876
  })
4534
4877
  `
4535
4878
  }),
@@ -5676,14 +6019,30 @@ for (const entry of GUIDE_MANIFEST) {
5676
6019
  * @example
5677
6020
  * ```ts
5678
6021
  * hostGroup('AGENTS.md') // 'docs'
5679
- * hostGroup('.agents') // 'orchestration'
5680
- * hostGroup('.claude') // 'orchestration'
5681
- * hostGroup('.codex') // 'orchestration'
6022
+ * hostGroup('.agents/orchestration.md') // 'orchestration'
6023
+ * hostGroup('.claude/rules') // 'orchestration'
6024
+ * hostGroup('.cursor/rules') // 'orchestration'
6025
+ * hostGroup('.mcp.json') // 'orchestration'
6026
+ * hostGroup('.oxlintrc.json') // 'configs'
5682
6027
  * ```
6028
+ *
6029
+ * @remarks
6030
+ * Takes a `HOST_PATHS` entry, so every example above is one. A bare directory
6031
+ * name is not: `ORCHESTRATION_PATH_PREFIXES` entries carry a trailing slash, so
6032
+ * `hostGroup('.cursor')` is `configs`, and no vendored entry has that form.
6033
+ *
6034
+ * Below the `docs` branch the split is by what a path governs rather than where
6035
+ * it sits, which is why both MCP registrations — `.mcp.json` and
6036
+ * `.cursor/mcp.json` — group with the harness bridges instead of with the root
6037
+ * dotfiles beside them. The `docs` branch is checked first and is deliberately
6038
+ * positional: `AGENTS.md`, `CLAUDE.md`, and `LICENSE` are the root documents, and
6039
+ * `CLAUDE.md` stays there as a root document even though it is also a harness
6040
+ * bridge. A plan selecting `orchestration` therefore carries two of the three
6041
+ * bridges; a plan selecting `docs` carries the third.
5683
6042
  */
5684
6043
  function hostGroup(path) {
5685
6044
  if (path === "AGENTS.md" || path === "CLAUDE.md" || path === "LICENSE") return "docs";
5686
- if (path.startsWith(".agents/") || path.startsWith(".claude/") || path.startsWith(".codex/") || path.startsWith("scripts/") || path.startsWith(".github/")) return "orchestration";
6045
+ if (matchesOrchestrationPath(path)) return "orchestration";
5687
6046
  if (path.startsWith("tests/")) return "tests";
5688
6047
  if (path === "guides/src/guide.md" || path === "guides/src/scaffold.md") return "guides";
5689
6048
  return "configs";
@@ -5949,7 +6308,10 @@ function packageManifest(spec) {
5949
6308
  if (spec.bin) scripts["test:src:bin"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:bin";
5950
6309
  if (spec.integration) scripts["test:integration"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project integration";
5951
6310
  if (spec.bin && spec.integration) scripts["test:equivalence"] = "node -e \"const c=require('node:child_process'),p=process.env.npm_execpath;if(p===undefined)process.exit(1);const r=c.spawnSync(process.execPath,[p,'run','test:integration'],{stdio:'inherit',env:{...process.env,SCAFFOLD_BOUNDARY_EQUIVALENCE:'1'}});process.exit(r.status??1)\"";
5952
- if (spec.service) scripts["test:service"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project service";
6311
+ if (spec.services.length > 0) {
6312
+ scripts["test:service"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.services.map((service) => `--project service:${service}`).join(" ");
6313
+ for (const service of spec.services) scripts[`test:service:${service}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project service:${service}`;
6314
+ }
5953
6315
  if (spec.app.length > 0) {
5954
6316
  scripts["test:app"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.app.map((environment) => `--project ${APP_MATRIX[environment].project}`).join(" ");
5955
6317
  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}`;
@@ -5988,7 +6350,7 @@ function packageManifest(spec) {
5988
6350
  scripts["build:src:bin"] = "vite build --config configs/src/vite.bin.config.ts";
5989
6351
  scripts["build:host"] = "node -e \"import('./dist/src/server/index.js').then((m)=>{const n=m.stageHost(process.cwd(),'dist/host').length;console.log('build-host: staged '+n+' file(s) into dist/host')})\"";
5990
6352
  }
5991
- scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "");
6353
+ scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "") + (spec.services.length > 0 ? " && npm run test:service" : "");
5992
6354
  const devDependencies = devDependenciesFor(spec);
5993
6355
  const manifest = {
5994
6356
  name: hasSource ? `@orkestrel/${spec.name}` : spec.name,
@@ -5996,13 +6358,15 @@ function packageManifest(spec) {
5996
6358
  ...hasSource ? {} : { private: true },
5997
6359
  description: spec.description ?? (hasSource ? `The @orkestrel/${spec.name} package.` : `The ${spec.name} application.`),
5998
6360
  keywords: [...spec.keywords].sort(),
5999
- homepage: `https://github.com/orkestrel/${spec.name}#readme`,
6000
- bugs: `https://github.com/orkestrel/${spec.name}/issues`,
6361
+ ...hasSource ? {
6362
+ homepage: `https://github.com/orkestrel/${spec.name}#readme`,
6363
+ bugs: `https://github.com/orkestrel/${spec.name}/issues`
6364
+ } : {},
6001
6365
  license: "MIT",
6002
- repository: {
6366
+ ...hasSource ? { repository: {
6003
6367
  type: "git",
6004
6368
  url: `git+https://github.com/orkestrel/${spec.name}.git`
6005
- },
6369
+ } } : {},
6006
6370
  ...spec.bin ? { bin: { scaffold: "./dist/bin/scaffold.js" } } : {},
6007
6371
  files: spec.bin ? [
6008
6372
  "dist/src",
@@ -6152,7 +6516,7 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
6152
6516
  registrations.push({ project: "policy" }, { project: "config" }, { project: "guides" });
6153
6517
  if (facts.bin === true) registrations.push({ project: "srcBin" });
6154
6518
  if (facts.integration === true) registrations.push({ project: "integration" });
6155
- if (facts.service === true) registrations.push({ project: "service" });
6519
+ for (const service of facts.services ?? []) registrations.push({ project: `service${pascalCase(service)}` });
6156
6520
  return registrations;
6157
6521
  }
6158
6522
  /**
@@ -6174,7 +6538,7 @@ function viteProjectDefinitions(facts = {}) {
6174
6538
  ];
6175
6539
  if (facts.bin === true) definitions.push(binViteProject());
6176
6540
  if (facts.integration === true) definitions.push(integrationViteProject(facts));
6177
- if (facts.service === true) definitions.push(serviceViteProject());
6541
+ for (const service of facts.services ?? []) definitions.push(serviceViteProject(service));
6178
6542
  return definitions.join("\n");
6179
6543
  }
6180
6544
  /**
@@ -8161,24 +8525,27 @@ ${facts.bin === true && facts.integration === true && facts.global === true ? `
8161
8525
  `;
8162
8526
  }
8163
8527
  /**
8164
- * Build the standalone Node-only live-service proof project.
8528
+ * Build one standalone Node-only live-service vendor proof project.
8165
8529
  *
8166
- * @returns The emitted `service` project definition.
8530
+ * @param name - The bounded vendor directory name.
8531
+ * @returns The emitted `service:<name>` project definition.
8167
8532
  *
8168
8533
  * @example
8169
8534
  * ```ts
8170
- * serviceViteProject().includes("label: 'service'") // true
8535
+ * serviceViteProject('claude').includes("label: 'service:claude'") // true
8171
8536
  * ```
8172
8537
  */
8173
- function serviceViteProject() {
8174
- return `${EXPORT_KEYWORD} const service = (options?: UserConfig): UserConfig =>
8538
+ function serviceViteProject(name) {
8539
+ if (!NAME_PATTERN.test(name) || name.length > 203) throw new Error("Service project name must be a bounded lowercase directory name");
8540
+ return `${EXPORT_KEYWORD} const ${`service${pascalCase(name)}`} = (options?: UserConfig): UserConfig =>
8175
8541
  mergeConfig(
8176
8542
  {
8177
8543
  resolve,
8544
+ plugins: [environmentBoundary('app/server')],
8178
8545
  test: {
8179
- name: { label: 'service', color: 'red' },
8180
- include: ['tests/service/**/*.test.ts'],
8181
- setupFiles: ['./tests/setup.ts', './tests/setupService.ts'],
8546
+ name: { label: ${serializeTypeScriptString(`service:${name}`)}, color: 'red' },
8547
+ include: [${serializeTypeScriptString(`tests/service/${name}/**/*.test.ts`)}],
8548
+ setupFiles: ['./tests/setup.ts', './tests/setupServer.ts', ${serializeTypeScriptString(`./tests/service/${name}/setup.ts`)}],
8182
8549
  environment: 'node',
8183
8550
  browser: { enabled: false },
8184
8551
  testTimeout: 120_000,
@@ -8328,7 +8695,7 @@ ${renderedTest}
8328
8695
  *
8329
8696
  * @param src - The declared `Environment[]`.
8330
8697
  * @param facts - Optional structural facts. `bin` appends the standalone executable
8331
- * build-and-test project; `integration` and `service` append their standalone
8698
+ * build-and-test project; `integration` and `services` append their standalone
8332
8699
  * proof projects; `global` wires the shared global-setup module.
8333
8700
  * @returns The root `vite.config.ts` file content, newline-terminated.
8334
8701
  *
@@ -8955,28 +9322,60 @@ function srcTsconfig(environment) {
8955
9322
  * `build.lib` / externals live in the root `srcBrowser` / `srcServer` export
8956
9323
  * instead (per the live exemplars).
8957
9324
  *
9325
+ * @remarks
9326
+ * `bundleTypes` rolls the face up through API Extractor, which leaves every
9327
+ * `src/core` re-export behind a relative `../core/index.ts` specifier — a path no
9328
+ * published tarball carries. A workspace that also declares `core` therefore emits
9329
+ * a `beforeWriteFile` rewrite turning that specifier into `@orkestrel/<name>`, the
9330
+ * package's own published root export. The rewrite is narrowed to the FINAL face
9331
+ * roll-up: applying it to the intermediate declarations makes API Extractor analyse
9332
+ * `src/core`'s own source and abort. A workspace with no `core` emits no rewrite,
9333
+ * because it has no cross-environment specifier to externalize.
9334
+ *
8958
9335
  * @param environment - The non-`core` environment to derive the `vite.config.ts` for.
9336
+ * @param spec - The blueprint slice naming the package and its declared source environments.
8959
9337
  * @returns The environment `vite.config.ts` file content, newline-terminated.
8960
9338
  *
8961
9339
  * @example
8962
9340
  * ```ts
8963
- * srcViteConfig('browser').includes('srcBrowser') // true
9341
+ * srcViteConfig('browser', { name: 'router', src: ['core', 'browser'] }).includes('srcBrowser')
8964
9342
  * ```
8965
9343
  */
8966
- function srcViteConfig(environment) {
9344
+ function srcViteConfig(environment, spec) {
8967
9345
  const anchor = environment === "browser" ? "srcBrowser" : "srcServer";
9346
+ const specifier = `'@orkestrel/${spec.name}'`;
9347
+ const pattern = "/(?:\\.\\.\\/)+core\\/index\\.ts/g";
9348
+ const inlineCall = `\t\t\t\t\t\t? content.replaceAll(${pattern}, ${specifier})`;
9349
+ const call = fitsPrintWidth(inlineCall) ? [inlineCall] : [
9350
+ " ? content.replaceAll(",
9351
+ `\t\t\t\t\t\t\t\t${pattern},`,
9352
+ `\t\t\t\t\t\t\t\t${specifier},`,
9353
+ " )"
9354
+ ];
9355
+ const face = `/[\\\\/]dist[\\\\/]src[\\\\/]${environment}[\\\\/]index\\.d\\.ts$/`;
9356
+ const external = spec.src.includes("core");
9357
+ const rewrite = external ? `\n${[
9358
+ " beforeWriteFile: (path, content) => ({",
9359
+ `\t\t\t\t\tcontent: ${face}.test(path)`,
9360
+ ...call,
9361
+ " : content,",
9362
+ " }),"
9363
+ ].join("\n")}` : "";
8968
9364
  return `import { defineConfig } from 'vite'
8969
9365
  import dts from 'vite-plugin-dts'
8970
9366
  import { ${anchor}, resolveWorkspacePath } from '../../vite.config.ts'
8971
9367
 
8972
- // Types are bundled inline by vite-plugin-dts (see configs/src/vite.core.config.ts
8973
- // for the same pattern).
9368
+ ${external ? `// vite-plugin-dts rolls this face into one declaration, and the roll-up reaches
9369
+ // src/core through a relative source path the tarball does not carry. The rewrite
9370
+ // below externalizes core through the package's own published root export, on the
9371
+ // final roll-up only.` : `// vite-plugin-dts rolls this face into one declaration. This workspace declares no
9372
+ // src/core, so the roll-up has no cross-environment specifier to externalize.`}
8974
9373
  export default defineConfig(
8975
9374
  ${anchor}({
8976
9375
  plugins: [
8977
9376
  dts({
8978
9377
  tsconfigPath: resolveWorkspacePath('configs/src/tsconfig.${environment}.json'),
8979
- bundleTypes: true,
9378
+ bundleTypes: true,${rewrite}
8980
9379
  }),
8981
9380
  ],
8982
9381
  }),
@@ -9108,7 +9507,7 @@ function ciWorkflow(spec) {
9108
9507
  const tail = [];
9109
9508
  if (spec.integration) tail.push(` - name: Run live consumer integration
9110
9509
  run: npm run test:integration`);
9111
- if (spec.service) {
9510
+ if (spec.services.length > 0) {
9112
9511
  tail.push(` - name: Provision live service
9113
9512
  run: bash ${SERVICE_SCRIPT_PATH}`);
9114
9513
  tail.push(` - name: Run live service tests
@@ -9192,7 +9591,7 @@ function configArtifacts(spec) {
9192
9591
  const row = SRC_MATRIX[environment];
9193
9592
  for (const path of row.configs) {
9194
9593
  const isTsconfig = path.endsWith(".json");
9195
- const content = environment === "core" ? isTsconfig ? coreTsconfig() : coreViteConfig() : isTsconfig ? srcTsconfig(environment) : srcViteConfig(environment);
9594
+ const content = environment === "core" ? isTsconfig ? coreTsconfig() : coreViteConfig() : isTsconfig ? srcTsconfig(environment) : srcViteConfig(environment, spec);
9196
9595
  artifacts.push({
9197
9596
  path,
9198
9597
  group: "configs",
@@ -9591,7 +9990,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} waitForEvent<TMap extends EventMap, K exte
9591
9990
  cases: configCases.join("")
9592
9991
  })
9593
9992
  ];
9594
- if (spec.src.includes("server") || spec.app.includes("server")) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
9993
+ if (spec.src.includes("server") || spec.app.includes("server") || spec.services.length > 0) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
9994
+ if (spec.services.length > 0) artifacts.push(fillArtifact("tests/config/services.test.ts", "tests", "serviceConformance", { services: renderStringArray(spec.services, " ", "const declared = ", "") }, "server"));
9595
9995
  if (spec.src.includes("browser") || spec.app.includes("browser")) artifacts.push(fillArtifact("tests/setupBrowser.ts", "tests", "setupBrowser", {}, "browser"));
9596
9996
  const hasBoundary = hasApplicationBoundary(spec);
9597
9997
  const hasShowcase = hasApplicationShowcase(spec);
@@ -10194,12 +10594,15 @@ npm install @orkestrel/${blueprint.name}
10194
10594
  \`\`\`` : "This is a private application workspace and is not published to npm.",
10195
10595
  usage: guideUsage(blueprint, pascal)
10196
10596
  }));
10197
- if (selected.includes("orchestration")) artifacts.push({
10198
- path: ".github/workflows/ci.yml",
10199
- group: "orchestration",
10200
- origin: "computed",
10201
- content: ciWorkflow(blueprint)
10202
- });
10597
+ if (selected.includes("orchestration")) {
10598
+ artifacts.push({
10599
+ path: ".github/workflows/ci.yml",
10600
+ group: "orchestration",
10601
+ origin: "computed",
10602
+ content: ciWorkflow(blueprint)
10603
+ });
10604
+ if (blueprint.services.length > 0) artifacts.push(fillArtifact(SERVICE_SCRIPT_PATH, "orchestration", "serviceProvisioner", {}));
10605
+ }
10203
10606
  for (const path of selectHostPaths(HOST_PATHS, blueprint.name)) {
10204
10607
  const group = hostGroup(path);
10205
10608
  if (!selected.includes(group)) continue;
@@ -10323,9 +10726,10 @@ var Compiler = class Compiler {
10323
10726
  this.#emitter.emit("audit", result);
10324
10727
  return result;
10325
10728
  }
10729
+ const diff = diffPlan(scaffolding.plan, current);
10326
10730
  const result = {
10327
- ...diffPlan(scaffolding.plan, current),
10328
- questions: scaffolding.questions
10731
+ ...diff,
10732
+ questions: [...scaffolding.questions, ...diff.questions]
10329
10733
  };
10330
10734
  this.#emitter.emit("audit", result);
10331
10735
  return result;
@@ -10721,6 +11125,6 @@ function createBlueprint(data) {
10721
11125
  return candidate;
10722
11126
  }
10723
11127
  //#endregion
10724
- export { APP_BROWSER_DEV_DEPENDENCIES, APP_DEV_DEPENDENCIES, APP_MATRIX, APP_SERVER_DEV_DEPENDENCIES, BASE_DEV_DEPENDENCIES, BIN_CONFIGS, CATEGORIES, CHECKOUT_ACTION_SHA, COMPILER_ID, COMPILE_STAGES, CONST_KEYWORD, CONTROL_CHARACTER_PATTERN, Compiler, DEFAULT_ENGINES, DEFAULT_VERSION, DEPENDENCY_NAME_PATTERN, ENGINES_PATTERN, ENVIRONMENTS, EXPORT_KEYWORD, EXTRA_NAME_PATTERN, EXTRA_RANGE_PATTERN, FRESHNESS, FUNCTION_KEYWORD, GLOBAL_SETUP_PATH, GROUPS, HEX_PATTERN, HOST_PATHS, IMPORT_KEYWORD, INVALID_PATH_CHARACTER_PATTERN, JSON_PRINT_WIDTH, JSON_TAB_WIDTH, MAX_ARTIFACT_BYTES, MAX_ARTIFACT_HEX_LENGTH, MAX_COLLECTION_ITEMS, MAX_DATA_GRAPH_KEYS, MAX_DATA_GRAPH_NODES, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_NAME_LENGTH, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_SERIALIZED_INPUT_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MINIMUM_NODE_VERSION, NAME_PATTERN, ORIGINS, ORKESTREL_RANGE_PATTERN, PlanManager, SCAFFOLD_RANGE, SERVICE_SCRIPT_PATH, SETUP_NODE_ACTION_SHA, SHOWCASE_CONFIG_PATH, SOURCE_BROWSER_DEV_DEPENDENCIES, SRC_MATRIX, SYNC_BASELINE_PATTERN, ScaffoldError, TEMPLATES, TYPESCRIPT_EXTENSIONS, VERSION_PATTERN, alignTable, appTsconfig, appViteConfig, applicationArtifacts, applicationViteConfig, applyOverrides, artifactShape, auditToReview, binTsconfig, binViteConfig, binViteProject, blueprint, blueprintShape, blueprintToMembers, blueprintToPlan, bytesToHex, catalogNames, catalogToBlock, ciWorkflow, compareCodeUnit, computeColumnWidth, computeHash, configArtifacts, configViteProject, contentByteLength, contentCodePoint, contentToBytes, contentToHex, coreTsconfig, coreViteConfig, createBlueprint, createCompiler, createPlanManager, delimiterCell, dependency, dependencyShape, devDependenciesFor, diffPlan, dualCondition, entryFields, escapeHtmlText, exportsMap, fillArtifact, findFileConflict, findPathConflict, fitsPrintWidth, formatJson, guideArtifacts, guideMemberTable, guideMethods, guideTests, guideUsage, guidesViteProject, hasApplicationBoundary, hasApplicationShowcase, hasBlueprintEnvironment, hasOnlyDataProperties, hasValidArtifactBytes, hasValidArtifactHex, hasValidAuditBytes, hasValidBlueprintBytes, hasValidOverrideBytes, hasValidPlanBytes, hasValidPlanHex, hasValidSnapshotBytes, hasValidSyncReportBytes, hostGroup, inferGroup, integrationViteProject, isArtifact, isBehind, isBlueprint, isCompilerEventHooks, isDenseDataArray, isDependency, isEmitterErrorHandler, isMember, isOverride, isPlan, isPlanManagerEventHooks, isScaffoldError, isSyncReport, isWorkspaceName, manifestToDependencies, manifestToName, member, memberShape, override, overrideShape, ownDataValue, packageManifest, padCell, paritySpecifiers, parseBlueprint, parseBoundedJSON, parseCompilerOptions, parsePlan, parsePlanIds, parsePlanManagerOptions, parseSyncReport, pascalCase, pinPlan, planPayload, planShape, planToReview, planToSummary, policyViteProject, rangeToFreshness, renderArray, renderObject, renderStringArray, renderValue, renderViteTest, rootTsconfig, rootViteConfig, selectHostPaths, serializeTypeScriptString, serviceViteProject, singleSrcViteConfig, snapshotOf, snapshotPlan, sourceArtifacts, splitTableRow, srcTsconfig, srcVariant, srcViteConfig, stableStringify, syncReportShape, syncToReview, testArtifacts, validateBlueprint, validateDependencyArray, validatePlan, viteHeader, viteMachinery, viteProjectDefinitions, viteProjectRegistrations };
11128
+ export { APP_BROWSER_DEV_DEPENDENCIES, APP_DEV_DEPENDENCIES, APP_MATRIX, APP_SERVER_DEV_DEPENDENCIES, BASE_DEV_DEPENDENCIES, BIN_CONFIGS, CATALOG_AGENT_PATH, CATEGORIES, CHECKOUT_ACTION_SHA, COMPILER_ID, COMPILE_STAGES, CONST_KEYWORD, CONTROL_CHARACTER_PATTERN, Compiler, DEFAULT_ENGINES, DEFAULT_VERSION, DEPENDENCY_NAME_PATTERN, ENGINES_PATTERN, ENVIRONMENTS, EXPORT_KEYWORD, EXTRA_NAME_PATTERN, EXTRA_RANGE_PATTERN, FRESHNESS, FUNCTION_KEYWORD, GLOBAL_SETUP_PATH, GROUPS, HEX_PATTERN, HOST_PATHS, IMPORT_KEYWORD, INVALID_PATH_CHARACTER_PATTERN, JSON_PRINT_WIDTH, JSON_TAB_WIDTH, MAX_ARTIFACT_BYTES, MAX_ARTIFACT_HEX_LENGTH, MAX_COLLECTION_ITEMS, MAX_DATA_GRAPH_KEYS, MAX_DATA_GRAPH_NODES, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_NAME_LENGTH, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_SERIALIZED_INPUT_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MINIMUM_NODE_VERSION, NAME_PATTERN, ORCHESTRATION_PATH_NAMES, ORCHESTRATION_PATH_PREFIXES, ORIGINS, ORKESTREL_RANGE_PATTERN, PlanManager, SCAFFOLD_RANGE, SERVICE_SCRIPT_PATH, SETUP_NODE_ACTION_SHA, SHOWCASE_CONFIG_PATH, SOURCE_BROWSER_DEV_DEPENDENCIES, SRC_MATRIX, SYNC_BASELINE_PATTERN, ScaffoldError, TEMPLATES, TYPESCRIPT_EXTENSIONS, VERSION_PATTERN, alignTable, appTsconfig, appViteConfig, applicationArtifacts, applicationViteConfig, applyOverrides, artifactShape, auditToReview, binTsconfig, binViteConfig, binViteProject, blueprint, blueprintShape, blueprintToMembers, blueprintToPlan, bytesToHex, catalogNames, catalogToBlock, ciWorkflow, compareCodeUnit, computeColumnWidth, computeHash, configArtifacts, configViteProject, contentByteLength, contentCodePoint, contentToBytes, contentToHex, coreTsconfig, coreViteConfig, createBlueprint, createCompiler, createPlanManager, delimiterCell, dependency, dependencyShape, devDependenciesFor, diffPlan, dualCondition, entryFields, escapeHtmlText, exportsMap, fillArtifact, findFileConflict, findPathConflict, fitsPrintWidth, formatJson, guideArtifacts, guideMemberTable, guideMethods, guideTests, guideUsage, guidesViteProject, hasApplicationBoundary, hasApplicationShowcase, hasBlueprintEnvironment, hasOnlyDataProperties, hasValidArtifactBytes, hasValidArtifactHex, hasValidAuditBytes, hasValidBlueprintBytes, hasValidOverrideBytes, hasValidPlanBytes, hasValidPlanHex, hasValidSnapshotBytes, hasValidSyncReportBytes, hostGroup, inferGroup, integrationViteProject, isArtifact, isBehind, isBlueprint, isCompilerEventHooks, isDenseDataArray, isDependency, isEmitterErrorHandler, isMember, isOverride, isPlan, isPlanManagerEventHooks, isScaffoldError, isSyncReport, isWorkspaceName, manifestToDependencies, manifestToName, matchesOrchestrationPath, member, memberShape, override, overrideShape, ownDataValue, packageManifest, padCell, paritySpecifiers, parseBlueprint, parseBoundedJSON, parseCompilerOptions, parsePlan, parsePlanIds, parsePlanManagerOptions, parseSyncReport, pascalCase, pinPlan, planPayload, planShape, planToReview, planToSummary, policyViteProject, rangeToFreshness, renderArray, renderObject, renderStringArray, renderValue, renderViteTest, rootTsconfig, rootViteConfig, selectHostPaths, serializeTypeScriptString, serviceViteProject, singleSrcViteConfig, snapshotOf, snapshotPlan, sourceArtifacts, splitTableRow, srcTsconfig, srcVariant, srcViteConfig, stableStringify, syncReportShape, syncToReview, testArtifacts, validateBlueprint, validateDependencyArray, validatePlan, viteHeader, viteMachinery, viteProjectDefinitions, viteProjectRegistrations };
10725
11129
 
10726
11130
  //# sourceMappingURL=index.js.map