@orkestrel/scaffold 0.0.81 → 0.0.83

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 (47) hide show
  1. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +14 -3
  2. package/dist/bin/main.js +295 -13
  3. package/dist/bin/main.js.map +1 -1
  4. package/dist/host/AGENTS.md +5 -3
  5. package/dist/host/agents/orchestration.md +1 -0
  6. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +2 -2
  7. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +43 -39
  8. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +3 -3
  9. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +10 -9
  10. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +5 -5
  11. package/dist/host/agents/skills/orkestrel-journey/references/recorded.md +77 -0
  12. package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +3 -3
  13. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +9 -9
  14. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +4 -0
  15. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +22 -4
  16. package/dist/host/claude/agents/orkestrel.md +10 -10
  17. package/dist/host/claude/rules/application.md +20 -6
  18. package/dist/host/claude/rules/architecture.md +2 -2
  19. package/dist/host/claude/rules/browser.md +9 -0
  20. package/dist/host/claude/rules/documentation.md +2 -1
  21. package/dist/host/claude/rules/quality.md +5 -0
  22. package/dist/host/claude/rules/styles.md +35 -12
  23. package/dist/host/claude/rules/tests.md +15 -10
  24. package/dist/host/claude/rules/workspace.md +128 -98
  25. package/dist/host/claude/skills/orkestrel-journey/SKILL.md +1 -1
  26. package/dist/host/configs/helpers.ts +157 -9
  27. package/dist/host/configs/policy.ts +64 -61
  28. package/dist/host/dotfiles/oxlintrc.json +132 -16
  29. package/dist/host/dotfiles/prettierignore +1 -1
  30. package/dist/host/guides/README.md +9 -5
  31. package/dist/host/guides/guide.md +4 -1
  32. package/dist/host/guides/scaffold.md +482 -115
  33. package/dist/host/manifest.json +38 -32
  34. package/dist/host/scripts/codex.sh +0 -0
  35. package/dist/host/scripts/cursor.sh +0 -0
  36. package/dist/host/scripts/deps.sh +0 -0
  37. package/dist/host/scripts/ollama.sh +0 -0
  38. package/dist/host/tests/config.test.ts +996 -39
  39. package/dist/host/tests/policy.test.ts +11 -0
  40. package/dist/host/tests/setupPolicy.ts +396 -22
  41. package/dist/src/core/index.cjs +1289 -164
  42. package/dist/src/core/index.cjs.map +1 -1
  43. package/dist/src/core/index.d.cts +313 -35
  44. package/dist/src/core/index.d.ts +313 -35
  45. package/dist/src/core/index.js +1268 -165
  46. package/dist/src/core/index.js.map +1 -1
  47. package/package.json +7 -6
@@ -4,7 +4,7 @@ paths:
4
4
  - 'app/**/*'
5
5
  - 'tests/**/*'
6
6
  - 'configs/**/*'
7
- - 'demo/**/*'
7
+ - 'showcase/**/*'
8
8
  - 'package.json'
9
9
  - 'tsconfig.json'
10
10
  - 'vite.config.ts'
@@ -16,23 +16,27 @@ Use only the environments a project needs, and keep the root dependency model in
16
16
 
17
17
  ## Environments
18
18
 
19
- | Path | Purpose |
20
- | -------------- | ------------------------------------------------------------- |
21
- | `src/core/` | Published host-independent library |
22
- | `src/browser/` | Published browser-only library |
23
- | `src/server/` | Published Node-only library |
24
- | `src/styles/` | Optional SCSS bundle producing `index.css` |
25
- | `src/bin/` | Optional executable; `main.ts` entry, never a public barrel |
26
- | `app/core/` | Shared application logic with an `index.ts` barrel |
27
- | `app/browser/` | Browser app; `main.ts` entry, not a barrel |
28
- | `app/server/` | Node server app; `main.ts` entry |
29
- | `tests/` | Mirrors src/app environments; root holds cross-cutting proofs |
30
- | `configs/` | Thin target wrappers around root configs |
19
+ | Path | Purpose |
20
+ | -------------- | ------------------------------------------------------------------ |
21
+ | `src/core/` | Published host-independent library |
22
+ | `src/browser/` | Published browser-only library |
23
+ | `src/server/` | Published Node-only library |
24
+ | `src/styles/` | Optional styles surface: the base sheet face producing `index.css` |
25
+ | `src/<name>/` | Styles extension: a named sheet face beside `src/styles/` |
26
+ | `src/vue/` | Browser extension: the published `vue` face over `src/browser/` |
27
+ | `src/bin/` | Optional executable; `main.ts` entry, never a public barrel |
28
+ | `app/core/` | Shared application logic with an `index.ts` barrel |
29
+ | `app/browser/` | Browser app; `main.ts` entry, not a barrel |
30
+ | `app/vue/` | Browser extension: the Vue app beside `app/browser/`; `main.ts` |
31
+ | `app/server/` | Node server app; `main.ts` entry |
32
+ | `tests/` | Mirrors src/app environments; root holds cross-cutting proofs |
33
+ | `configs/` | Thin target wrappers around root configs |
31
34
 
32
35
  - Dependency direction is the root project model in `AGENTS.md` and is not restated here; this file governs where the environments live and how they are configured.
33
36
  - Typical browser-app domains: `components/`, `pages/`, `composables/`, `controllers/`, `services/`, `stores/`.
34
37
  - Typical server-app domains: `handlers.ts`, `middlewares.ts`, `routes.ts`.
35
- - `src/styles/index.ts` is a side-effect entry importing `./index.scss`.
38
+ - A sheet face (`src/styles/` and each `src/<name>/` styles extension) builds from `sheet.ts`, which imports `./index.scss` alone; its `index.ts` star-exports `./sheet.js`. The themes target builds from `src/styles/themes/sheet.ts` the same way.
39
+ - Name a styles extension with a name the `NAME_PATTERN` constant admits, and never `core`, `browser`, `server`, `bin`, `styles`, `themes`, or `vue`.
36
40
  - `src/bin/main.ts` is the executable entry, built to `dist/bin/main.js`. The name is fixed, as it
37
41
  is for `app/browser/main.ts` and `app/server/main.ts`, so every runtime entry in a workspace is
38
42
  found at the same name.
@@ -47,11 +51,14 @@ Use only the environments a project needs, and keep the root dependency model in
47
51
  | `@src/browser` | `src/browser/index.ts` |
48
52
  | `@src/server` | `src/server/index.ts` |
49
53
  | `@src/styles` | `src/styles/index.ts` |
54
+ | `@src/<name>` | `src/<name>/index.ts` |
55
+ | `@src/vue` | `src/vue/index.ts` |
50
56
  | `@app/core` | `app/core/index.ts` |
51
57
  | `@app/browser` | `app/browser/index.ts` |
58
+ | `@app/vue` | `app/vue/index.ts` |
52
59
  | `@app/server` | `app/server/index.ts` |
53
60
 
54
- Define aliases in `tsconfig.json` first. `vite.config.ts` derives from `compilerOptions.paths`; keep both aligned.
61
+ Give every selected environment and every face an alias: `src/styles`, each `src/<name>` styles extension, and each axis the `vue` extension occupies. Define aliases in `tsconfig.json` first. `vite.config.ts` derives from `compilerOptions.paths`; keep both aligned.
55
62
 
56
63
  ## Configuration authority
57
64
 
@@ -65,8 +72,8 @@ Define aliases in `tsconfig.json` first. `vite.config.ts` derives from `compiler
65
72
  under `configs/`. Each imports nothing from the workspace, which is what keeps it a leaf, so no
66
73
  `configs/types.ts` exists for one to import: each keeps its own types, data, and functions in its
67
74
  one file, and the centralized-kind placement in `.claude/rules/architecture.md` does not reach a
68
- leaf. Each `configs/src/*.config.ts` imports the root config rather than a leaf, so shared build
69
- logic stays in one place.
75
+ leaf. Each `configs/src/*.config.ts` wrapper imports the root config and may import the permitted
76
+ leaves; keep shared build and project composition in the root config.
70
77
  - Keep `configs/helpers.ts` free of any dependency a core-only workspace does not declare. It is
71
78
  vendored byte-identical to every workspace, so an import there must resolve in all of them.
72
79
  `configs/browsers.ts` exists for that reason: it imports `playwright` and
@@ -92,39 +99,55 @@ Environment rules:
92
99
 
93
100
  ## Build outputs
94
101
 
95
- | Output | Content | Format |
96
- | ------------------ | ------------------------------ | --------------- |
97
- | `dist/src/core` | Core library + declarations | ES and CJS |
98
- | `dist/src/browser` | Browser library + declarations | ES |
99
- | `dist/src/server` | Server library + declarations | ES and CJS |
100
- | `dist/src/styles` | Compiled `index.css` | ES wrapper |
101
- | `dist/bin` | Optional executable `main.js` | ES with shebang |
102
- | `dist/app/browser` | Browser application | target-defined |
103
- | `dist/app/server` | Server application | CJS |
104
- | `dist/showcase` | Single-file `index.html` demo | self-contained |
105
-
106
- - Library declarations are emitted by `tsc` through `configs/src/tsconfig.{core,browser,server}.json`, chained after each Vite build.
107
- - Styles ship CSS, not declarations.
108
- - Optional `appShowcase` uses `configs/app/vite.showcase.config.ts` and `vite-plugin-singlefile` to create a minified file-URL-safe `dist/showcase/index.html`.
109
- - The showcase is outside the default build.
102
+ | Output | Content | Format |
103
+ | ----------------------------- | -------------------------------- | ------------------------- |
104
+ | `dist/src/core` | Core library + declarations | ES and CJS |
105
+ | `dist/src/browser` | Browser library + declarations | ES |
106
+ | `dist/src/server` | Server library + declarations | ES and CJS |
107
+ | `dist/src/vue` | Vue face + declarations | ES |
108
+ | `dist/src/styles` | Compiled `index.css` | CSS |
109
+ | `dist/src/styles/themes` | Compiled themes `index.css` | CSS |
110
+ | `dist/src/<name>` | Compiled `index.css` | CSS |
111
+ | `dist/bin` | Optional executable `main.js` | ES with shebang |
112
+ | `dist/app/browser` | Browser application | target-defined |
113
+ | `dist/app/vue` | Vue application | target-defined |
114
+ | `dist/app/server` | Server application | CJS |
115
+ | `showcase/<application>.html` | Single-file page per application | self-contained, committed |
116
+
117
+ - Roll up each published TypeScript face's declarations in its Vite wrapper. A face that imports `@src/core` or `@src/browser` rewrites those specifiers to the published subpaths in its emitted declarations.
118
+ - A sheet face ships CSS, not declarations, and its JavaScript build stub stays out of `files`.
119
+ - Build each sheet face with `cssMinify: false`, bounded to its own output directory.
120
+ - Build `dist/src/styles/themes` after `dist/src/styles`; the themes build empties only its own directory.
121
+ - Build each selected showcase through `appShowcase(mode)`, `configs/app/vite.showcase.config.ts`, and `vite-plugin-singlefile` into root `showcase/<application>.html`; preserve sibling pages. Use `app/browser/index.html` and `browser.html` for the base modes, and `app/vue/index.html` and `vue.html` for `--mode vue`.
122
+ - Stamp each page after inlining with a `build-id` meta line whose value is the SHA-256 digest of the final page without that line.
123
+ - The showcase is outside the default build, and no test reads its pages.
110
124
  - Use Oxc for showcase JS minification and Lightning CSS for CSS.
111
- - Inject a `build-id` meta stamp so rebuilt `file://` demos cache-bust.
112
125
 
113
126
  ## Test project matrix
114
127
 
115
128
  `vite.config.ts` defines Vitest projects on an environment axis and a workspace-proof axis. The
116
- environment axis is one project per src/app axis × environment:
117
-
118
- | Project | Files | Environment | Setup |
119
- | ------------- | ---------------------- | ------------------- | ----------------------------------------------- |
120
- | `src:core` | `tests/src/core/**` | Node | `setup.ts` |
121
- | `src:browser` | `tests/src/browser/**` | Playwright Chromium | `setup.ts`, `setupBrowser.ts` |
122
- | `src:server` | `tests/src/server/**` | Node | `setup.ts`, `setupServer.ts` |
123
- | `src:styles` | `tests/src/styles/**` | Playwright Chromium | `setup.ts`, `setupBrowser.ts`, `setupStyles.ts` |
124
- | `src:bin` | `tests/src/bin/**` | Node | `setup.ts`, `setupServer.ts` |
125
- | `app:core` | `tests/app/core/**` | Node | `setup.ts` |
126
- | `app:browser` | `tests/app/browser/**` | Playwright Chromium | `setup.ts`, `setupBrowser.ts` |
127
- | `app:server` | `tests/app/server/**` | Node | `setup.ts`, `setupServer.ts` |
129
+ environment axis is one project per src/app axis × environment, plus one per extension face:
130
+
131
+ | Project | Files | Environment | Setup |
132
+ | ------------- | ---------------------- | ------------------------------------- | ----------------------------------------------- |
133
+ | `src:core` | `tests/src/core/**` | Node | `setup.ts` |
134
+ | `src:browser` | `tests/src/browser/**` | Playwright Chromium | `setup.ts`, `setupBrowser.ts` |
135
+ | `src:vue` | `tests/src/vue/**` | Playwright Chromium | `setup.ts`, `setupBrowser.ts` |
136
+ | `src:server` | `tests/src/server/**` | Node | `setup.ts`, `setupServer.ts` |
137
+ | `src:styles` | `tests/src/styles/**` | Playwright Chromium, `isolate: false` | `setup.ts`, `setupBrowser.ts`, `setupStyles.ts` |
138
+ | `src:<name>` | `tests/src/<name>/**` | Playwright Chromium, `isolate: false` | `setup.ts`, `setupBrowser.ts`, `setupStyles.ts` |
139
+ | `src:bin` | `tests/src/bin/**` | Node | `setup.ts`, `setupServer.ts` |
140
+ | `app:core` | `tests/app/core/**` | Node | `setup.ts` |
141
+ | `app:browser` | `tests/app/browser/**` | Playwright Chromium | `setup.ts`, `setupBrowser.ts` |
142
+ | `app:vue` | `tests/app/vue/**` | Playwright Chromium | `setup.ts`, `setupBrowser.ts` |
143
+ | `app:server` | `tests/app/server/**` | Node | `setup.ts`, `setupServer.ts` |
144
+
145
+ - Compose every sheet-face project (`src:styles` and each `src:<name>`) in its own wrapper through
146
+ the one root `sheetProject` factory, and run it through its `test:src:<face>` script, which builds
147
+ that face first.
148
+ - Give every browser project `optimizeDeps.include` of `@orkestrel/test`, `@orkestrel/test/browser`,
149
+ `@orkestrel/contract` where the manifest declares it, and `vue` where the project renders Vue.
150
+ Give a Node project no `optimizeDeps` setting.
128
151
 
129
152
  The workspace-proof axis is cross-cutting. Each proof covers the whole workspace rather than
130
153
  one environment, so each is its own project:
@@ -133,9 +156,9 @@ one environment, so each is its own project:
133
156
  | ------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
134
157
  | `policy` | `tests/policy.test.ts` | The path- and text-shaped policy laws: mirrors, suppressions, the rule map, filenames, manifest scripts, skills, and bridges | `test` |
135
158
  | `config` | `tests/config.test.ts` | Root configuration resolves its aliases, projects, and outputs | `test` |
136
- | `setup` | `tests/setup*.test.ts`, excluding `tests/setupBrowser.test.ts` | Prove root setup behavior in Node with `setup.ts`. | `test` |
137
- | `setup:browser` | `tests/setupBrowser.test.ts` | Prove browser setup behavior in Playwright Chromium with `setup.ts` and `setupBrowser.ts`. | `test` |
138
- | `journey:<variant>` | `tests/app/browser/integration.test.ts` | Drive the browser application at the declared variant viewport in Playwright Chromium with `setup.ts` and `setupBrowser.ts`. | `test` through `test:journey` |
159
+ | `setup` | `tests/setup*.test.ts` other than either `setup:browser` proof | Prove root setup behavior in Node with `setup.ts`. | `test` |
160
+ | `setup:browser` | `tests/setupBrowser.test.ts`, `tests/setupStyles.test.ts` | Prove browser and style setup behavior in Playwright Chromium with `setup.ts` and `setupBrowser.ts`. | `test` |
161
+ | `journey:<variant>` | `tests/app/<application>/integration.test.ts` | Drive the application the Vite mode selects at the declared variant viewport in Playwright Chromium with `setup.ts` and `setupBrowser.ts`. | `test` through the journey scripts |
139
162
  | `guides` | `tests/guides.test.ts` | Every documented API exists, every public API is documented, every compared summary, example, and pitch equals its source, and every executable fence returns what the guide says it returns | `test` |
140
163
  | `conformance` | `tests/conformance.test.ts` | Where this package drifts from the official tooling it tracks | `test` |
141
164
  | `skills` | `tests/agents/**/*.test.ts` | Each skill script under `.agents/skills/*/scripts/` does what its `SKILL.md` states, driven as a child process against a scratch fixture from its mirrored proof; `configs/agents/tsconfig.skills.json` selects the project | `test` |
@@ -143,19 +166,22 @@ one environment, so each is its own project:
143
166
  | `integration` | `tests/integration.test.ts` | The package's features work together end to end across environments | `test` |
144
167
  | `service` | `tests/service/**/*.test.ts` | The live external services this package drives, driven for real | `prepublishOnly`; `test` when private |
145
168
 
146
- - Define the Node `setup` project only when a root file matches `tests/setup*.test.ts`,
147
- exact-case, other than `tests/setupBrowser.test.ts`. Include those matching files and exclude
148
- `tests/setupBrowser.test.ts`. Define `setup:browser` only when that exact-case browser proof
149
- exists, and collect that path alone. For each registered project, emit its `test:setup` or
150
- `test:setup:browser` script and run it from `test`; otherwise emit neither its project nor its
151
- script.
169
+ - Define the Node `setup` project when `global` selects its seeded proof or a root file matches
170
+ `tests/setup*.test.ts`, exact-case, other than `tests/setupBrowser.test.ts` and
171
+ `tests/setupStyles.test.ts`. Include those matching files and exclude both browser proofs.
172
+ Define `setup:browser` when a sheet face or the themes target selects its seeded proof, or either
173
+ exact-case browser proof exists; collect those browser proof paths alone. For each registered project, emit
174
+ its `test:setup` or `test:setup:browser` script and run it from `test`; otherwise emit neither its
175
+ project nor its script.
152
176
  - When `tests/setupGlobal.ts` exists, give `src:browser`, `setup:browser`, and `integration` that
153
177
  module as their `globalSetup` option, and give no other project a global setup.
154
178
  - When a browser application selects the journey axis, register `journey:<variant>` projects
155
179
  through the birth-owned `configs/app/vite.journey.config.ts` wrapper. Keep the adopter's variant
156
- list there and compose each project through the root `appJourney` factory. Exclude
157
- `tests/app/browser/integration.test.ts` from `app:browser`, collect it in each variant project,
158
- and run the wrapper through `test:journey` after the application projects in `test`.
180
+ list there and compose each project through the root `appJourney(variant, variants, mode?)`
181
+ factory, which resolves the Vite mode to `browser` or an app-side browser extension and collects
182
+ `tests/app/<application>/integration.test.ts` for that application. Exclude each collected suite
183
+ from its application project, and run `test:journey` and one `test:journey:<framework>` per
184
+ app-side browser extension after the application projects in `test`.
159
185
 
160
186
  `conformance`, `integration`, `distribution`, and `service` are separate subjects, not names for
161
187
  one.
@@ -189,14 +215,13 @@ ignored by git; and `.claude/rules/tests.md` governs what may live there.
189
215
 
190
216
  Setup assets:
191
217
 
192
- - `tests/setup.css` declares cascade-layer order before `@import 'tailwindcss'` and its `@source`.
193
- - Browser setup wires `setup.css`.
194
- - Styles setup loads `setup.css` and the compiled cascade.
218
+ - Load only the setup assets and compiled sheets the selected proofs require.
219
+ - Import `tailwindcss` from a setup asset only where an authored proof declares it.
195
220
 
196
- Scope with `test:src`, `test:src:core`, `test:app`, `test:app:server`, and equivalent scripts. Each
197
- cross-cutting project has its own script too: `test:policy`, `test:config`, `test:setup`,
198
- `test:setup:browser`, `test:journey`, `test:guides`,
199
- `test:conformance`, `test:distribution`, `test:integration`, `test:service`.
221
+ Scope with `test:src`, `test:src:core`, `test:src:<face>`, `test:app`, `test:app:server`, and
222
+ equivalent scripts. Each cross-cutting project has its own script too: `test:policy`,
223
+ `test:config`, `test:setup`, `test:setup:browser`, `test:journey`, `test:journey:<framework>`,
224
+ `test:guides`, `test:conformance`, `test:distribution`, `test:integration`, `test:service`.
200
225
 
201
226
  ## Typechecking and environment isolation
202
227
 
@@ -214,12 +239,14 @@ then runs the configured scoped checks that prove environment isolation.
214
239
  - Lint is a separate complementary gate; neither lint nor root checking replaces
215
240
  environment-isolation checks.
216
241
 
217
- | Scope | `lib` | `types` | Permitted host globals |
218
- | ---------------------------- | --------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
219
- | `src:core`, `app:core` | `["ESNext","WebWorker"]` | `[]` | WHATWG web interop: fetch family, streams, URL, Abort, encoders, crypto, timers, console, DOMException, structuredClone; no DOM, no Node |
220
- | `src:browser`, `app:browser` | `["ESNext","DOM","DOM.Iterable"]` | default | DOM; no Node |
221
- | `src:server`, `app:server` | `["ESNext"]` | `["node"]` | Node; no DOM |
222
- | `src:styles` | `["ESNext"]` | `["vite/client"]` | Vite SCSS module declaration only |
242
+ | Scope | `lib` | `types` | Permitted host globals |
243
+ | ---------------------------- | --------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
244
+ | `src:core`, `app:core` | `["ESNext","WebWorker"]` | `[]` | WHATWG web interop: fetch family, streams, URL, Abort, encoders, crypto, timers, console, DOMException, structuredClone; no DOM, no Node |
245
+ | `src:browser`, `app:browser` | `["ESNext","DOM","DOM.Iterable"]` | default | DOM; no Node |
246
+ | `src:server`, `app:server` | `["ESNext"]` | `["node"]` | Node; no DOM |
247
+ | `src:styles`, `src:<name>` | `["ESNext"]` | `["vite/client"]` | Vite SCSS module declaration only |
248
+ | `src:vue` | `["ESNext","DOM","DOM.Iterable"]` | `["vite/client"]` | DOM; no Node |
249
+ | `app:vue` | `["ESNext","DOM","DOM.Iterable"]` | `["vite/client","vue"]` | DOM and Vue; no Node |
223
250
 
224
251
  Strict core is load-bearing:
225
252
 
@@ -228,35 +255,39 @@ Strict core is load-bearing:
228
255
 
229
256
  Build/check config alignment:
230
257
 
231
- - `configs/src/tsconfig.{core,browser,server}.json` serves emit and scoped checking.
232
- - `configs/src/tsconfig.styles.json` is check-only.
258
+ - `configs/src/tsconfig.{core,browser,vue,server}.json` serves emit and scoped checking.
259
+ - `configs/src/tsconfig.styles.json` and each `configs/src/tsconfig.<name>.json` of a styles
260
+ extension are check-only.
233
261
  - `configs/app/tsconfig.core.json` is check-only.
234
- - `configs/app/tsconfig.{browser,server}.json` is check-only.
262
+ - `configs/app/tsconfig.{browser,vue,server}.json` is check-only.
235
263
  - Root `tsconfig.json` keeps all libs/types for IDE and comprehensive checking; scoped configs tighten each environment.
236
264
 
237
265
  ## Script intent
238
266
 
239
- | Script | Contract |
240
- | ----------------------- | -------------------------------------------------------------------------- |
241
- | `dev` | Browser development entry |
242
- | `build` | Build configured library/application targets |
243
- | `serve` / `serve:build` | Run built server / build then run |
244
- | `showcase` | Showcase dev server |
245
- | `build:showcase` | Build `dist/showcase` |
246
- | `show` | Build and copy showcase to `demo/showcase.html` |
247
- | `lint` | `oxlint --config .oxlintrc.json --fix .`; separate from typecheck |
248
- | `lint:check` | Non-mutating whole-tree lint gate |
249
- | `check` | Comprehensive root typecheck plus configured isolation scopes |
250
- | `check:<scope>` | On-demand environment-isolation pass |
251
- | `format` | Format all files |
252
- | `format:check` | Non-mutating whole-tree format gate |
253
- | `test` | Environment projects plus non-isolated cross-cutting proofs |
254
- | `clean` | Remove `dist/` |
255
- | `copy <from> <to>` | Copy while creating parent directories |
256
- | `prepublishOnly` | Publishing workspaces only: the gate chain, then isolated proofs |
257
- | `prepack` | Publishing workspaces only: rebuild `dist/` so a pack ships current output |
258
-
259
- Run `show` only **after** formatting. The committed `demo/showcase.html` is generated/minified; formatting after generation would expand its inlined bundle.
267
+ | Script | Contract |
268
+ | ---------------------------- | -------------------------------------------------------------------------- |
269
+ | `dev` | Browser development entry |
270
+ | `build` | Build configured library/application targets |
271
+ | `serve` / `serve:build` | Run built server / build then run |
272
+ | `showcase` | Showcase dev server of the base mode |
273
+ | `showcase:<framework>` | Showcase dev server of that framework's mode |
274
+ | `build:showcase` | Build `showcase/browser.html` |
275
+ | `build:showcase:<framework>` | Build `showcase/<framework>.html` |
276
+ | `lint` | `oxlint --config .oxlintrc.json --fix .`; separate from typecheck |
277
+ | `lint:check` | Non-mutating whole-tree lint gate |
278
+ | `check` | Comprehensive root typecheck plus configured isolation scopes |
279
+ | `check:<scope>` | On-demand environment-isolation pass |
280
+ | `format` | Format all files |
281
+ | `format:check` | Non-mutating whole-tree format gate |
282
+ | `test` | Environment projects plus non-isolated cross-cutting proofs |
283
+ | `clean` | Remove `dist/` |
284
+ | `copy <from> <to>` | Copy while creating parent directories |
285
+ | `prepublishOnly` | Publishing workspaces only: the gate chain, then isolated proofs |
286
+ | `prepack` | Publishing workspaces only: rebuild `dist/` so a pack ships current output |
287
+
288
+ - In a publishing workspace, `prepublishOnly` runs `build:showcase` and every
289
+ `build:showcase:<framework>` script after `npm run build`.
290
+ - List `showcase/` in `.prettierignore`, so formatting never rewrites a committed page.
260
291
 
261
292
  ## Tooling
262
293
 
@@ -266,7 +297,7 @@ Run `show` only **after** formatting. The committed `demo/showcase.html` is gene
266
297
  - Bundler: Vite.
267
298
  - Tests: Vitest; `@vitest/browser-playwright` for browser projects.
268
299
  - Node build targets derive from the package's declared supported runtime. Keep `engines`, bundler targets, scoped configs, tests, and documentation aligned; never hard-code one Node version line-wide.
269
- - Browser framework: Vue 3 when present.
300
+ - Browser framework: the `vue` extension where selected.
270
301
 
271
302
  Policy instruments:
272
303
 
@@ -278,9 +309,8 @@ Policy instruments:
278
309
  thing it polices. A file-level `oxlint-disable` silently defeats every lint rule in its file,
279
310
  plugin rules included, and nothing inside a file can suppress the sweep.
280
311
  - Write each visitor in the plugin's visitor table as a one-line context-binding arrow delegating to
281
- a named module-scope `report{Noun}` function. Never write rule logic inline in the table. That
282
- arrow is the sanctioned exception to the in-body function-expression limits in
283
- `.claude/rules/architecture.md` for exactly that table.
312
+ a named module-scope `report{Noun}` function. Never write rule logic inline in the table. Treat the
313
+ visitor table as a returned object literal whose members are callbacks.
284
314
  - Name an individual rule id here only where the rule reads its evidence from outside the workspace
285
315
  its instrument runs in. This section fixes the instruments and how work is assigned between them;
286
316
  each rule's substance stays with the law it enforces.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: orkestrel-journey
3
- description: Prove a browser application the way a person uses it — real keystrokes, clicks, and Tab/Enter against only what is visible and reachable — through the journey layer @orkestrel/test/browser publishes, and generate the capture portfolio, the resolved-style matrix, and the statechart outcome from those same journeys. Use when accepting a UI build, proving an application end to end, deciding whether a surface is reachable by keyboard alone, proving what a screen refuses as well as what it does, proving the styles a browser actually resolved under each theme and viewport, driving a transition table through the interface and watching it run, auditing whether the interface speaks the user's vocabulary rather than the engine's, producing the screenshots a design review judges, routing a rendered question to an artifact a model can read, or whenever the only evidence a screen works is a test that drove it through JavaScript instead of through the interface.
3
+ description: Prove a browser application the way a person uses it — real keystrokes, clicks, and Tab/Enter against only what is visible and reachable — through the journey layer @orkestrel/test/browser publishes, and generate the capture portfolio, the resolved-style matrix, and the statechart outcome from those same journeys. Use when accepting a UI build, proving an application end to end, deciding whether a screen is reachable by keyboard alone, proving what a screen refuses as well as what it does, proving the styles a browser actually resolved under each theme and viewport, driving a transition table through the interface and watching it run, auditing whether the interface speaks the user's vocabulary rather than the engine's, producing the screenshots a design review judges, routing a rendered question to an artifact a model can read, or whenever the only evidence a screen works is a test that drove it through JavaScript instead of through the interface.
4
4
  ---
5
5
 
6
6
  # Load the canonical workflow
@@ -2,6 +2,7 @@ import type { Plugin, Rolldown } from 'vite'
2
2
  import { parseSync, transformWithOxc, Visitor } from 'vite'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { spawnSync } from 'node:child_process'
5
+ import { createHash } from 'node:crypto'
5
6
  import { createRequire, isBuiltin } from 'node:module'
6
7
  import { tmpdir } from 'node:os'
7
8
  import {
@@ -55,6 +56,69 @@ export const WORKSPACE_ROOT = realpathSync.native(
55
56
  resolvePath(dirname(fileURLToPath(import.meta.url)), '..'),
56
57
  )
57
58
 
59
+ /**
60
+ * Resolves a Vite mode to a declared application.
61
+ * @param mode - The requested mode; undefined, development, production, and test select browser.
62
+ * @param applications - The declared application factories.
63
+ * @returns The selected application name.
64
+ * @throws Thrown when the selected application is not declared.
65
+ * @example
66
+ * ```ts
67
+ * resolveApplication('test', { browser: true }) // 'browser'
68
+ * ```
69
+ */
70
+ export function resolveApplication<Application extends string>(
71
+ mode: string | undefined,
72
+ applications: Readonly<Record<Application, unknown>>,
73
+ ): Application {
74
+ const selected =
75
+ mode === undefined || mode === 'development' || mode === 'production' || mode === 'test'
76
+ ? 'browser'
77
+ : mode
78
+ for (const name in applications) {
79
+ if (Object.hasOwn(applications, name) && name === selected) return name
80
+ }
81
+ throw new Error(`The application mode "${selected}" is not declared.`)
82
+ }
83
+
84
+ /**
85
+ * Computes the SHA-256 digest of a page with its build stamp line removed.
86
+ * @param text - The page text, with or without its stamp line.
87
+ * @returns The lowercase hexadecimal digest.
88
+ * @example
89
+ * ```ts
90
+ * computeStamp('') // 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
91
+ * computeStamp('abc') // 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
92
+ * ```
93
+ */
94
+ export function computeStamp(text: string): string {
95
+ return createHash('sha256')
96
+ .update(text.replace(/^[\t ]*<meta name="build-id" content="[^"\r\n]*" \/>\r?\n/gmu, ''))
97
+ .digest('hex')
98
+ }
99
+
100
+ /**
101
+ * Stamps a final inlined page with its content digest on a separate line.
102
+ * @param html - The final page with a head closing on its own line.
103
+ * @returns The page with exactly one build stamp line.
104
+ * @throws Thrown when a stamp is malformed or repeated, or the head closing line is absent.
105
+ * @example
106
+ * ```ts
107
+ * computeStamp(stampPage('abc\n</head>\n')) === computeStamp('abc\n</head>\n') // true
108
+ * ```
109
+ */
110
+ export function stampPage(html: string): string {
111
+ const stamps = html.match(/<meta\b[^>]*\bname=["']build-id["'][^>]*>/gu) ?? []
112
+ const lines = html.match(/^[\t ]*<meta name="build-id" content="[^"\r\n]*" \/>\r?\n/gmu) ?? []
113
+ if (stamps.length > 1 || stamps.length !== lines.length) {
114
+ throw new Error('A showcase page must carry at most one well-formed build stamp line.')
115
+ }
116
+ const text = html.replace(/^[\t ]*<meta name="build-id" content="[^"\r\n]*" \/>\r?\n/gmu, '')
117
+ const close = /^[\t ]*<\/head>[\t ]*(?:\r?\n|$)/mu.exec(text)
118
+ if (close === null) throw new Error('A showcase page must close its head on its own line.')
119
+ return `${text.slice(0, close.index)}\t\t<meta name="build-id" content="${computeStamp(text)}" />\n${text.slice(close.index)}`
120
+ }
121
+
58
122
  export function fileSystemPath(pathname: string): string {
59
123
  if (!pathname.startsWith('/@fs/')) return pathname
60
124
  const candidate = pathname.slice('/@fs/'.length)
@@ -132,7 +196,7 @@ export function isWorkspaceBoundaryModule(id: string): boolean {
132
196
  } catch {
133
197
  return false
134
198
  }
135
- const rootRelative = /^\/(?:app|src)\/(?:core|browser|server)\//.test(candidate)
199
+ const rootRelative = /^\/(?:app|src)\/(?:core|browser|server|vue)\//.test(candidate)
136
200
  const absoluteCandidate = rootRelative
137
201
  ? resolvePath(WORKSPACE_ROOT, candidate.slice(1))
138
202
  : isAbsolute(candidate)
@@ -143,7 +207,7 @@ export function isWorkspaceBoundaryModule(id: string): boolean {
143
207
  relativeId !== '..' &&
144
208
  !relativeId.startsWith('../') &&
145
209
  !isAbsolute(relativeId) &&
146
- /^(?:app|src)\/(?:core|browser|server)\//.test(relativeId)
210
+ /^(?:app|src)\/(?:core|browser|server|vue)\//.test(relativeId)
147
211
  )
148
212
  }
149
213
 
@@ -312,9 +376,62 @@ export function isStylesheetPath(path: string): boolean {
312
376
  return /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:[?#]|$)/.test(path)
313
377
  }
314
378
 
379
+ /**
380
+ * Defines the manifest peers, refused packages, and published sibling entries for a build.
381
+ * @example Refuse framework imports until a peer is declared
382
+ * ```ts
383
+ * const options: ExternalOptions = { peers: [], refused: ['vue', '@vue/'], siblings: [] }
384
+ * ```
385
+ */
386
+ export interface ExternalOptions {
387
+ readonly peers: readonly string[]
388
+ readonly refused: readonly string[]
389
+ readonly siblings: readonly string[]
390
+ }
391
+
392
+ /**
393
+ * Resolves whether a build leaves a module external under its manifest and face boundaries.
394
+ * @param id - The module identifier supplied by the bundler.
395
+ * @param options - The peer names, refused package names or scope prefixes, and sibling entries.
396
+ * @returns True if the module stays external; false if the build bundles it.
397
+ * @throws Thrown when an import names a refused scope or a refused package without an admitting peer.
398
+ * @remarks
399
+ * A peer admits its own subpaths. A refused scope remains refused even when a package inside it
400
+ * is a peer. Node builtins and fleet packages retain their published identities. Workspace aliases
401
+ * resolve before a sibling entry can be recognized.
402
+ * @example Externalize a declared peer
403
+ * ```ts
404
+ * resolveExternal('vue/runtime-dom', { peers: ['vue'], refused: ['vue', '@vue/'], siblings: [] })
405
+ * // true
406
+ * ```
407
+ */
408
+ export function resolveExternal(id: string, options: ExternalOptions): boolean {
409
+ const peer = options.peers.some((name) => id === name || id.startsWith(`${name}/`))
410
+ const refused = options.refused.find((name) =>
411
+ name.endsWith('/') ? id.startsWith(name) : id === name || id.startsWith(`${name}/`),
412
+ )
413
+ if (refused?.endsWith('/')) {
414
+ throw new Error(
415
+ `[orkestrel-build] The import ${id} is refused; import from ${refused.slice(1, -1)} instead of the ${refused} implementation scope.`,
416
+ )
417
+ }
418
+ if (refused !== undefined && !peer) {
419
+ throw new Error(
420
+ `[orkestrel-build] The import ${id} is refused; declare its public package in peerDependencies with peerDependenciesMeta marking it optional.`,
421
+ )
422
+ }
423
+ if (id.startsWith('@src/') || id.startsWith('@app/')) return false
424
+ return (
425
+ id.startsWith('node:') ||
426
+ id.startsWith('@orkestrel/') ||
427
+ peer ||
428
+ options.siblings.some((sibling) => sibling.replaceAll('\\', '/') === id.replaceAll('\\', '/'))
429
+ )
430
+ }
431
+
315
432
  export function environmentPathError(owner: string, target: string): string | undefined {
316
433
  const targetApplication = target.startsWith('app/')
317
- const targetBrowser = target.startsWith('app/browser/') || target.startsWith('src/browser/')
434
+ const targetBrowser = /^(?:app|src)\/(?:browser|vue)\//.test(target)
318
435
  const targetServer = target.startsWith('app/server/') || target.startsWith('src/server/')
319
436
  const stylesheet = isStylesheetPath(target)
320
437
  if (owner.startsWith('src/') && targetApplication) {
@@ -323,7 +440,7 @@ export function environmentPathError(owner: string, target: string): string | un
323
440
  if (owner.endsWith('/core') && (stylesheet || targetBrowser || targetServer)) {
324
441
  return 'Core modules must remain host-independent'
325
442
  }
326
- if (owner.endsWith('/browser') && targetServer) {
443
+ if ((owner.endsWith('/browser') || owner.endsWith('/vue')) && targetServer) {
327
444
  return 'Browser modules cannot depend on Node or server-only modules'
328
445
  }
329
446
  if (owner.endsWith('/server') && (stylesheet || targetBrowser)) {
@@ -346,7 +463,7 @@ export function environmentSourceError(owner: string, source: string): string |
346
463
  !/^file:/i.test(sourcePath) &&
347
464
  !/^[A-Za-z]:\//.test(sourcePath)
348
465
  const browserPackage =
349
- /^(?:(?:vue|vite)(?:[/?#]|$)|@(?:vue|vitejs)\/|@(?:app|src)\/browser(?:[/?#]|$)|@orkestrel\/[^/]+\/browser(?:[/?#]|$))/.test(
466
+ /^(?:(?:vue|vite)(?:[/?#]|$)|@(?:vue|vitejs)\/|@(?:app|src)\/(?:browser|vue)(?:[/?#]|$)|@orkestrel\/[^/]+\/(?:browser|vue)(?:[/?#]|$))/.test(
350
467
  normalizedSource,
351
468
  )
352
469
  const serverPackage =
@@ -361,7 +478,7 @@ export function environmentSourceError(owner: string, source: string): string |
361
478
  if (owner.endsWith('/core') && (builtin || browserPackage || serverPackage || stylesheet)) {
362
479
  return 'Core modules must remain host-independent'
363
480
  }
364
- if (owner.endsWith('/browser') && (builtin || serverPackage)) {
481
+ if ((owner.endsWith('/browser') || owner.endsWith('/vue')) && (builtin || serverPackage)) {
365
482
  return 'Browser modules cannot depend on Node or server-only modules'
366
483
  }
367
484
  if (owner.endsWith('/server') && (browserPackage || stylesheet)) {
@@ -623,6 +740,26 @@ export function rewriteCoreSpecifier(content: string): string {
623
740
  return content.replaceAll(/(?:\.\.\/)+core\/index\.[jt]s/g, name).replaceAll('@src/core', name)
624
741
  }
625
742
 
743
+ /**
744
+ * Rewrites browser specifiers in declarations to the workspace package's browser export.
745
+ * @param content - The finished declaration roll-up.
746
+ * @returns The roll-up with browser aliases and relative entry paths replaced.
747
+ * @throws Thrown when the workspace manifest names no package.
748
+ * @example Rewrite a browser alias
749
+ * ```ts
750
+ * rewriteBrowserSpecifier("from '@src/browser'")
751
+ * ```
752
+ */
753
+ export function rewriteBrowserSpecifier(content: string): string {
754
+ const name = packageManifestName(WORKSPACE_ROOT)
755
+ if (name === undefined) {
756
+ throw new Error('[orkestrel-declaration-rollup] The workspace manifest names no package')
757
+ }
758
+ return content
759
+ .replaceAll(/(?:\.\.\/)+browser\/index\.[jt]s/g, `${name}/browser`)
760
+ .replaceAll('@src/browser', `${name}/browser`)
761
+ }
762
+
626
763
  /**
627
764
  * Rolls one published face's declarations into the single file that face ships.
628
765
  *
@@ -843,7 +980,15 @@ export async function environmentAssetSources(
843
980
  }
844
981
 
845
982
  export function environmentBoundary(
846
- owner: 'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server',
983
+ owner:
984
+ | 'src/core'
985
+ | 'src/browser'
986
+ | 'src/server'
987
+ | 'src/vue'
988
+ | 'app/core'
989
+ | 'app/browser'
990
+ | 'app/server'
991
+ | 'app/vue',
847
992
  ): Plugin {
848
993
  const trustedPackageRoots = new Set<string>()
849
994
  let environmentRoot = WORKSPACE_ROOT
@@ -867,7 +1012,10 @@ export function environmentBoundary(
867
1012
  if (
868
1013
  importerPackageRoot === undefined &&
869
1014
  ((layer !== 'app' && layer !== 'src') ||
870
- (environment !== 'core' && environment !== 'browser' && environment !== 'server'))
1015
+ (environment !== 'core' &&
1016
+ environment !== 'browser' &&
1017
+ environment !== 'server' &&
1018
+ environment !== 'vue'))
871
1019
  ) {
872
1020
  return null
873
1021
  }
@@ -1037,7 +1185,7 @@ export function environmentBoundary(
1037
1185
  if (pathError !== undefined) this.error(pathError)
1038
1186
  }
1039
1187
  const environmentModule =
1040
- target !== undefined && /^(?:app|src)\/(?:core|browser|server)\//.test(target)
1188
+ target !== undefined && /^(?:app|src)\/(?:core|browser|server|vue)\//.test(target)
1041
1189
  if (!environmentModule && importerPackageRoot === undefined) return null
1042
1190
  for (const source of await environmentAssetSources(code, id)) {
1043
1191
  const normalizedSource = source.replaceAll('\\', '/')