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