@orkestrel/scaffold 0.0.73 → 0.0.75

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 (34) hide show
  1. package/dist/bin/main.js +75 -34
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/agents/skills/orkestrel-prove-journey/SKILL.md +212 -29
  4. package/dist/host/agents/skills/orkestrel-prove-journey/references/captures.md +40 -15
  5. package/dist/host/agents/skills/orkestrel-prove-journey/references/decide.md +34 -15
  6. package/dist/host/agents/skills/orkestrel-prove-journey/references/layer.md +248 -52
  7. package/dist/host/agents/skills/orkestrel-prove-journey/references/statechart.md +255 -49
  8. package/dist/host/agents/skills/orkestrel-prove-journey/references/styles.md +111 -23
  9. package/dist/host/agents/transports/codex.md +7 -0
  10. package/dist/host/claude/agents/orkestrel.md +2 -2
  11. package/dist/host/claude/rules/documentation.md +2 -0
  12. package/dist/host/claude/rules/tests.md +5 -3
  13. package/dist/host/claude/rules/workspace.md +26 -15
  14. package/dist/host/codex/config.toml +4 -7
  15. package/dist/host/guides/README.md +5 -0
  16. package/dist/host/guides/scaffold.md +104 -14
  17. package/dist/host/guides/test.md +744 -184
  18. package/dist/host/manifest.json +19 -19
  19. package/dist/host/tests/config.test.ts +159 -64
  20. package/dist/host/tests/policy.test.ts +11 -1
  21. package/dist/host/tests/setupPolicy.ts +571 -7
  22. package/dist/src/core/index.cjs +112 -18
  23. package/dist/src/core/index.cjs.map +1 -1
  24. package/dist/src/core/index.d.cts +19 -6
  25. package/dist/src/core/index.d.ts +19 -6
  26. package/dist/src/core/index.js +112 -19
  27. package/dist/src/core/index.js.map +1 -1
  28. package/dist/src/server/index.cjs +6 -4
  29. package/dist/src/server/index.cjs.map +1 -1
  30. package/dist/src/server/index.d.cts +9 -7
  31. package/dist/src/server/index.d.ts +9 -7
  32. package/dist/src/server/index.js +6 -4
  33. package/dist/src/server/index.js.map +1 -1
  34. package/package.json +2 -2
@@ -19,7 +19,7 @@ Read the current files in this order:
19
19
  3. [layer.md](references/layer.md) before importing, extending, or debugging the journey layer.
20
20
  4. [captures.md](references/captures.md) before registering a state or placing a capture.
21
21
  5. [styles.md](references/styles.md) before asserting anything the browser resolved.
22
- 6. [statechart.md](references/statechart.md) before declaring a transition or building the harness.
22
+ 6. [statechart.md](references/statechart.md) before declaring a transition or mounting the harness.
23
23
  7. [decide.md](references/decide.md) before routing a question to an instrument.
24
24
  8. `guides/README.md`, the governing guide for the surface, and `ROADMAP.md` when present.
25
25
  9. The `*/types.ts` of every environment the journeys drive, plus the application's root component,
@@ -48,21 +48,142 @@ families a surface owes. It never switches what a declared family proves.
48
48
  scope finding and stop; never prove the remaining families around it.
49
49
  - Assert the declaration itself: a family listed with no proof, and a proof belonging to no listed
50
50
  family, each fail the run.
51
+ - Bind the journey laws to every declared family, not to the journey family alone. A matrix reading
52
+ and a transport assertion reach their surface through the same verbs a journey reaches it through.
53
+ - Change route and theme through the interface in the matrix family and the transport family. A
54
+ family that navigates by calling the application's own router proves the router, and says nothing
55
+ about the screen it reads afterwards.
56
+ - Let the transport family construct the store it hands the application, and drive the application
57
+ from the interface after that. The constructed store is the fixture; it is never the drive.
58
+ - Take an entity read or a storage read as corroboration beside a rendered assertion, never in place
59
+ of one.
60
+
61
+ ## Resolve the population
62
+
63
+ Resolve every target and every population by ARIA role and accessible name as rendered.
64
+
65
+ - Admit a selector only where the population carries no role at all — a paragraph set, a token
66
+ census, a style-escape walk. Declare that selector in the workspace's browser test setup module
67
+ with the reason beside it, and never in a test file.
68
+ - Read a painted population through the platform: `element.checkVisibility()` reports what the box
69
+ tree renders, and a non-zero `getBoundingClientRect()` reports what occupies space. The layer
70
+ publishes no painted-population predicate.
71
+ - Reach for `isRendered` and `isReachable` where the subject is one element's own reachability
72
+ ([layer.md](references/layer.md) → The resolver).
51
73
 
52
74
  ## Read the variant once
53
75
 
54
- Read one `variant` value at run start, and let it choose the capture destination, the matrix row,
55
- and the statechart run together. Never split the theme from the viewport; a split writes a filename
56
- naming a combination the run did not render.
57
-
58
- - Declare every variant once as a published `CaptureVariant` — its `name`, its `width`, its
59
- `height`, and the `apply` that switches the theme — and read that list from the capture family and
60
- the matrix family alike.
76
+ A browser application born by `scaffold new` carries the journey axis: the
77
+ `configs/app/vite.journey.config.ts` wrapper, the root `appJourney` factory, the `test:journey`
78
+ script, and that script's place in the `test` chain. That wiring fans the journey suite out into one
79
+ Vitest project per variant, and each project provides its own variant. Read the variant once at run
80
+ start, and let it choose the capture destination, the matrix row, and the statechart run together.
81
+
82
+ - Read `inject('variant')` for this run's variant name, `inject('variants')` for the declared list,
83
+ and `inject('capture')` for whether this run writes frames.
84
+ - Declare the provided types once, by augmenting Vitest's own `ProvidedContext` in the workspace's
85
+ browser test setup module, so `inject` is typed rather than narrowed at each call.
86
+ - Declare the variant list in `configs/app/vite.journey.config.ts`, the birth-owned wrapper a
87
+ browser application is born with. Its presence is the journey axis. Rename and extend the seeded
88
+ viewports for this application.
89
+ - Activate the axis in a workspace born before it: write `configs/app/vite.journey.config.ts`, run
90
+ `scaffold repair`, then add `npm run test:journey` to the `test` script after `npm run test:app`.
91
+ The repair defines `appJourney` in the root configuration, emits the `test:journey` script, and
92
+ excludes the journey suite from the ordinary `app:browser` project. It does not rewrite the `test`
93
+ chain, so the chain entry is yours to add. Add that entry before the repair instead where you
94
+ prefer: `test:journey` names a configuration rather than a project, so neither order blocks the
95
+ repair.
96
+ - Name each variant for the theme and the viewport it renders, such as `dark-390`. Never split the
97
+ theme from the viewport; a split writes a filename naming a combination the run did not render.
98
+ - Keep the provided variants as serializable `JourneyVariant` data: `name`, `width`, and `height`.
99
+ Compose no callback merely to pass that data to a capture.
100
+ - Run the axis with `npm run test:journey`, which runs that wrapper and joins the `test` chain. Set
101
+ `CAPTURE` to `1` in your own shell and run `npm run test:journey` again to write the frames; the
102
+ root configuration reads that variable and provides it as `capture`.
103
+ - Keep the journeys in `tests/app/browser/integration.test.ts`. Each variant project collects that
104
+ file alone, and the ordinary `app:browser` project excludes it while the axis is on, so a journey
105
+ written anywhere else runs in no variant.
61
106
  - Loop every declared variant inside one run for the matrix family
62
107
  ([styles.md](references/styles.md) → Run per variant).
63
108
  - Render exactly one variant per run for the capture family
64
109
  ([captures.md](references/captures.md) → Variants).
65
110
 
111
+ `scaffold audit` reports a manifest and a wrapper that disagree as one of the following questions.
112
+ Settle the one it reports before trusting a green run.
113
+
114
+ | The question `scaffold audit` reports | Settle it by |
115
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
116
+ | `The manifest at <target> names a Vitest configuration the plan does not emit and the target does not hold: test:journey --config configs/app/vite.journey.config.ts. Add the configuration, or remove the script that names it and its invocation from the test chain.` | Writing the wrapper and running `scaffold repair`, or removing the `test:journey` script and its chain invocation |
117
+ | `The manifest at <target> does not invoke npm run test:journey from its test chain. Add npm run test:journey after npm run test:app.` | Adding `npm run test:journey` to the `test` script after `npm run test:app` |
118
+
119
+ `scaffold audit` reports the invocation question only when no chain from `test` reaches
120
+ `npm run test:journey` through literal `npm run` calls, so a chain reaching it through an
121
+ intermediate script raises none. It reports the configuration question only for a `test:*` script
122
+ whose text names `vitest`, so a script naming another runner's configuration raises none either.
123
+
124
+ ### Prepare the capture theme
125
+
126
+ - Where the application ships a theme control, await the theme action through that interface
127
+ before driving the state the journey captures. Pass the provided `JourneyVariant` list directly
128
+ to `createPortfolio` when capture needs no additional synchronous document change. Never attach
129
+ an asynchronous action to `CaptureVariant.apply` or re-resolve a covered theme control at capture
130
+ time.
131
+ - Where the surface has no theme control, prepare the theme through the attribute the surface
132
+ reads. Use the optional `CaptureVariant.apply` hook only for a synchronous document change,
133
+ such as setting that attribute. Treat its `() => void` contract as synchronous: `createPortfolio`
134
+ invokes the hook without awaiting a returned promise.
135
+ - Keep `applyTheme` as the workspace's own browser setup helper. Make it drive the theme control
136
+ through the published journey verbs and await the announced theme state and settled paint.
137
+ - Put the provided-context declaration in the browser test setup module, and consume the injected
138
+ data in `tests/app/browser/integration.test.ts` through the following portfolio configuration.
139
+ Mount the shipped application entry with its real provisions before the acceptance journey runs.
140
+ - From `tests/app/browser/integration.test.ts`, pass `../../../tmp/capture/states` as the capture
141
+ directory to write into the workspace's `tmp/capture/states` directory. The browser provider
142
+ resolves a custom screenshot path relative to the test file's directory.
143
+
144
+ ```ts
145
+ import type { JourneyVariant } from '@orkestrel/test'
146
+
147
+ declare module 'vitest' {
148
+ interface ProvidedContext {
149
+ readonly variant: string
150
+ readonly variants: readonly JourneyVariant[]
151
+ readonly capture: boolean
152
+ }
153
+ }
154
+ ```
155
+
156
+ ```ts
157
+ import { createPortfolio } from '@orkestrel/test/browser'
158
+ import { inject } from 'vitest'
159
+ import { applyTheme } from '../../setupBrowser.js'
160
+
161
+ const VARIANT = inject('variant')
162
+ const CAPTURE = inject('capture')
163
+ const VARIANTS = inject('variants')
164
+ const PORTFOLIO = createPortfolio({
165
+ states: ['home'],
166
+ variants: VARIANTS,
167
+ variant: VARIANT,
168
+ directory: '../../../tmp/capture/states',
169
+ enabled: CAPTURE,
170
+ })
171
+ ```
172
+
173
+ At the start of the existing home acceptance journey, insert the following theme preparation before
174
+ that journey's actions.
175
+
176
+ ```ts
177
+ await applyTheme(VARIANT)
178
+ ```
179
+
180
+ Immediately after that journey's actual rendered-home assertion, insert the following placement.
181
+ Keep the existing journey and its assertions as the consumer; add no screenshot-only test.
182
+
183
+ ```ts
184
+ await PORTFOLIO.place('home')
185
+ ```
186
+
66
187
  ## Apply the journey laws
67
188
 
68
189
  1. **Drive only what a person can see and reach.** Resolve every interactive target by its ARIA
@@ -80,9 +201,10 @@ naming a combination the run did not render.
80
201
  by an identity read of one frame.
81
202
  5. **Generate the portfolio from the acceptance journeys.** Place each registered state inside the
82
203
  journey that reaches it, and never register a state no journey reaches.
83
- 6. **Commit a value through an act a person performs:** Enter, Tab away, or a named button. Report a
84
- surface that commits on a timer, on an unpredictable event, or only after work the person cannot
85
- observe as a surface finding, and never work around it in the layer.
204
+ 6. **Commit a value through an act a person performs:** `pressKeys('{Enter}')` on the focused
205
+ control, a Tab away, or a named button. Report a surface that commits on a timer, on an
206
+ unpredictable event, or only after work the person cannot observe as a surface finding, and never
207
+ work around it in the layer.
86
208
  7. **Type only what a person would.** Journeys carry trusted input; adversarial payloads belong to
87
209
  the transport family and the parser suites.
88
210
  8. **Perform every interaction step unconditionally.** Never gate a step on whether the control it
@@ -92,15 +214,21 @@ naming a combination the run did not render.
92
214
 
93
215
  ## Import the journey layer
94
216
 
95
- - Import every journey verb, reader, and fixture builder from `@orkestrel/test/browser`. Write one
96
- of your own only where that package publishes none for the act
97
- ([layer.md](references/layer.md) → Import, never implement).
217
+ - Import every journey verb, reader, wait, and fixture builder from `@orkestrel/test/browser`, and
218
+ every host-independent wait and table type from `@orkestrel/test`. Write one of your own only
219
+ where those entries publish none for the act ([layer.md](references/layer.md) → Import, never
220
+ implement).
221
+ - Send every key sequence through `pressKeys`, which refuses a sequence sent while nothing but the
222
+ document body holds focus. Never reach past it to the provider's own keyboard function from a
223
+ journey, a matrix reading, or a statechart phase.
98
224
  - Place a helper you must write in the workspace's browser test setup module, name it for the act,
99
225
  and export it from there under `.claude/rules/tests.md`. Never declare a resolver inside a test
100
226
  file.
227
+ - Prove that setup module with `tests/setupBrowser.test.ts`, and activate its `setup:browser`
228
+ project in the order [layer.md](references/layer.md) → Import, never implement states.
101
229
  - Drive every step through the published verbs, and never dispatch a constructed event
102
230
  ([layer.md](references/layer.md) → What it drives).
103
- - Re-verify every target against what the application renders now whenever markup changes
231
+ - Re-verify every target against what the application renders after any markup change
104
232
  ([layer.md](references/layer.md) → Role vocabulary).
105
233
 
106
234
  ## Derive journeys from intents
@@ -119,8 +247,9 @@ placement and scope `.claude/rules/tests.md` fixes.
119
247
  action it observes; a predicate already true when the poll starts binds nothing.
120
248
  - Assert the state the flow must reach, never the transient path taken to it. A criterion that bans
121
249
  a harmless transient over-refuses and breaks on the next honest implementation.
122
- - Assert the negative beside the positive whenever a value replaces another: the new sentence is
123
- present **and** the old one is gone.
250
+ - Assert the negative beside the positive whenever a value replaces another: name the arriving
251
+ sentence and carry the replaced one in `absent`, so the reading that resolves carries one and not
252
+ the other ([layer.md](references/layer.md) → The waits).
124
253
  - Assert the state a control announces beside every drive that sets it, and on an unselected
125
254
  sibling. A control announcing state owes this assertion whether or not the surface carries the
126
255
  statechart family.
@@ -131,6 +260,25 @@ placement and scope `.claude/rules/tests.md` fixes.
131
260
  - Report a bare accessible name that answers for more than one reachable element on one screen as a surface
132
261
  finding, and target through role or region until the surface is fixed.
133
262
 
263
+ ### The intents every surface owes
264
+
265
+ Write a journey for each of the following wherever the surface has that state. Take the expected
266
+ outcome from the product guide; this skill supplies the mechanism and invents no copy, no redirect,
267
+ and no title scheme.
268
+
269
+ | Intent | The journey proves |
270
+ | ----------------- | ------------------------------------------------------------------------------- |
271
+ | Arrival | The entry route renders its own screen, read through a named region |
272
+ | An unknown route | What the application does with a route it does not carry, and what it says |
273
+ | An empty result | What a query matching nothing renders, in the product's own words |
274
+ | The document name | The title each screen publishes, asserted per screen |
275
+ | A render failure | What a person reads when a component throws, rather than an unexplained surface |
276
+
277
+ - Report a missing outcome as a product finding, with its evidence site, rather than inventing the
278
+ copy the surface owes.
279
+ - Assert the title from `document.title` per screen, against the title the product guide names for
280
+ that screen. Report a screen the guide gives no title as a product finding.
281
+
134
282
  ## Prove the refusals
135
283
 
136
284
  - Give every surface a refusal family: the controls a person must not reach in the state the
@@ -146,30 +294,38 @@ placement and scope `.claude/rules/tests.md` fixes.
146
294
  ## Declare the transport family
147
295
 
148
296
  - Name the block for what it proves — persistence, restart, storage failure.
149
- - Drive it through the application's real session and store contracts. Build a store that stalls a
150
- read, fails a fixed number of reads, or fails a write as an inert configurable implementation of
151
- the published interface, under the real-implementation law in `AGENTS.md`.
297
+ - Drive it through the application's real session and store contracts.
298
+ - Build the host's own storage conditions with `createStorage`: `reads: false` and `writes: false`
299
+ withhold what a browser with site data blocked withholds, `quota` caps the accepted `setItem`
300
+ calls, and `permit()` grants the withheld permissions the way a person allowing site data grants
301
+ them. It replenishes no quota, because room and permission are different refusals.
302
+ - Assert the withheld voice on its `name` as well as its message: a withheld operation raises a
303
+ `DOMException` named `SecurityError`, and a write past the quota raises one named
304
+ `QuotaExceededError`.
152
305
  - Prove the visible half in a journey: the failure sentence a person reads, and the retry control
153
- that clears it.
306
+ that clears it. A storage failure whose visible half is a control that silently does nothing is a
307
+ surface finding.
154
308
  - Assert restart by starting a second session over the same store and polling the restored value.
309
+ - Take a stalled read to the application's own asynchronous store contract, never to `Storage`.
310
+ `Storage` is synchronous, so a hanging read is not expressible against it and a fixture that fakes
311
+ one is proving a surface the application does not have.
155
312
 
156
313
  ## Prove the styles
157
314
 
158
315
  Follow [styles.md](references/styles.md) for the resolved-value law, the per-variant run, the
159
- composited contrast reading and its under-bar negative control, the authored-class census, the
316
+ composited contrast reading and its published control, the authored-class census, the
160
317
  `extractStyles` reading, and the token comparison.
161
318
 
162
319
  ## Prove the statechart
163
320
 
164
321
  Follow [statechart.md](references/statechart.md) for the transition table, the scenario per
165
- transition, the runner, the harness a person watches, and the gate that drives the harness through
166
- the interface.
322
+ transition, the runner, the mounted harness, and the gate that reads its tally.
167
323
 
168
324
  ## Generate the portfolio
169
325
 
170
326
  Follow [captures.md](references/captures.md) for the state registry and its placement rules, the
171
- theme-and-viewport variant matrix, the always-on filename proof, the capture-run membership proof,
172
- and how a state that exists only during an activation is captured.
327
+ variant matrix, the always-on filename proof, the capture-run membership proof, and how a state that
328
+ exists only during an activation is captured.
173
329
 
174
330
  Where a capture and a green suite disagree, take the capture as the evidence and the fixture as the
175
331
  defect.
@@ -181,27 +337,54 @@ Route review of the portfolio to the `orkestrel-polish-surface` campaign. Do not
181
337
  Follow [decide.md](references/decide.md) before spending a round on a question. It fixes which
182
338
  instrument judges which claim, what `prove` cannot serve, and what the run's written artifact holds.
183
339
 
340
+ ## Mutate each assertion class
341
+
342
+ Mutate each assertion class once, read the red, restore, and read the green.
343
+ `.claude/rules/quality.md` § Instruments owns the law this satisfies.
344
+
345
+ | Assertion class | The mutation | What must change |
346
+ | --------------- | -------------------------------------------------------------------- | --------------------------------- |
347
+ | Journey | Omit the act the journey performs, keeping collection valid | The destination assertion reddens |
348
+ | Refusal | Make the withheld control reachable, or present, without renaming it | The asserted voice changes |
349
+
350
+ - Omit the act rather than weakening the assertion. An assertion a missing act leaves green cannot
351
+ tell arrival from never having left.
352
+ - Change reachability rather than the name. A renamed control reddens on absence, which is a finding
353
+ the refusal family already carries.
354
+
184
355
  ## Accept
185
356
 
186
357
  Completion requires all of:
187
358
 
188
359
  - every in-scope user intent reaching its outcome through the interface, with no step that reaches
189
360
  past it;
361
+ - the intents every surface owes present wherever the surface has the state, each outcome taken from
362
+ the product guide and every missing one reported as a product finding;
190
363
  - keyboard-only reachability proven on every surface the journeys cover;
191
364
  - a refusal family per surface, each asserting one exact failure voice;
192
365
  - the transport family declared separately, driven through real implementations, and convergent;
193
366
  - the declared families each proven, and the declaration itself asserted;
194
- - the matrix family read once per declared variant, each style instrument carrying the negative
195
- control that must read under its bar in the same run;
367
+ - the matrix family read once per declared variant, each style reading carrying its published
368
+ control from [styles.md](references/styles.md) → The published controls in the same run, and the
369
+ contrast reading's control straddling its declared bar;
196
370
  - the authored-class census and the `extractStyles` reading taken on the mounted surface, each
197
371
  reporting the population it walked;
198
- - the statechart table driven to a terminal outcome with no failed row, and the harness gate green;
372
+ - the statechart table driven to a terminal status with no failed row, and the harness tally read
373
+ from the object and from its markup;
199
374
  - the registry-times-variants filename proof and the state-placement proof green in an ordinary run;
200
375
  - one capture run per variant writing every registered file, and the disk-membership proof green;
201
376
  - the written artifact produced for every variant the run rendered, named by that variant;
202
377
  - perception assertions quoting rendered text, and the vocabulary sweep green on the whole page;
378
+ - the browser test setup module proven by `tests/setupBrowser.test.ts` in the `setup:browser`
379
+ project `scaffold repair` registers;
380
+ - each assertion class mutated, with the red reading and the green reading recorded;
203
381
  - the repository gates green, under the independent-verification law in `.agents/orchestration.md`.
204
382
 
383
+ State the engine bound with the verdict. The gate renders one engine, so a claim about a second
384
+ engine is unproven until a reading on that engine records it. The emitted `configs/browsers.ts` doc
385
+ block is the home of that limit and of the condition that reopens it. Cite that doc block, and copy
386
+ neither into a verdict.
387
+
205
388
  Report each journey by the intent it proves, the refusals it establishes, the states it placed, the
206
389
  variants it read, the statechart outcome it reached, and every surface finding the layer's refusals
207
390
  exposed.
@@ -4,19 +4,38 @@ Take every screenshot from an acceptance journey, at the moment that journey is
4
4
  picture names. Never add a test whose only purpose is a screenshot, and never stage a state for the
5
5
  camera that a journey did not reach through the interface.
6
6
 
7
+ ## The vocabulary
8
+
9
+ ```ts
10
+ import type { JourneyVariant } from '@orkestrel/test'
11
+ import type {
12
+ CaptureVariant,
13
+ FrameOptions,
14
+ FrameReading,
15
+ PortfolioInterface,
16
+ PortfolioOptions,
17
+ } from '@orkestrel/test/browser'
18
+ import { captureFrame, createPortfolio, expandCaptures, readFrame } from '@orkestrel/test/browser'
19
+ import { inject } from 'vitest'
20
+ ```
21
+
7
22
  ## The hook
8
23
 
9
24
  `createPortfolio(options)` is the capture door, and `place(state, element?)` is the hook. Build the
10
25
  portfolio once per file from a `PortfolioOptions` value carrying the registry as `states`, every
11
- declared `CaptureVariant` as `variants`, this run's `variant`, the `directory` each file is written
12
- to, and the flag as `enabled`.
26
+ declared `CaptureVariant` as `variants`, this run's variant name as `variant`, the `directory` each
27
+ file is written to, and the flag as `enabled`.
13
28
 
14
29
  - Place a state with `place(state)` for the whole page, and `place(state, element)` where the
15
30
  picture is one element.
16
- - Leave `enabled` unset in an ordinary run. `place` then returns `undefined`, resizes nothing,
17
- writes nothing, and records nothing.
31
+ - Take `enabled` from `inject('capture')`, which the generated root configuration provides from the
32
+ `CAPTURE` environment variable set to `1` in the shell that runs the axis. An ordinary run leaves
33
+ it false, so `place` returns `undefined`, resizes nothing, writes nothing, and records nothing.
18
34
  - Read `files` for the registry expanded across every variant, `placements` for what this run placed,
19
35
  and `paths` for what it wrote. Each hands back a snapshot.
36
+ - Reach for `readFrame` where the written image itself is the subject — its decoded size, or its
37
+ pixels. `captureFrame` already reads each file back and compares its bytes against the frame it
38
+ shot.
20
39
 
21
40
  The package already refuses these, so assert none of them again:
22
41
 
@@ -30,8 +49,7 @@ The package already refuses these, so assert none of them again:
30
49
 
31
50
  ## The registry
32
51
 
33
- Declare the state names and the variants once in the journey file, and build the portfolio from
34
- that declaration.
52
+ Declare the state names once in the journey file, and build the portfolio from that declaration.
35
53
 
36
54
  - Name a state for its surface and its condition — `answer-partial`, `start-storage-failure`,
37
55
  `case-delete-confirmation`.
@@ -44,22 +62,26 @@ that declaration.
44
62
 
45
63
  ## Variants
46
64
 
47
- The run axis is fixed in [SKILL.md](../SKILL.md) → Read the variant once, and the theme switch each
48
- variant's `apply` performs is fixed in [styles.md](styles.md) → Run per variant. The capture family
49
- adds these.
65
+ The run axis is fixed in [SKILL.md](../SKILL.md) → Read the variant once. The capture family adds
66
+ these.
50
67
 
51
- - Produce the portfolio — the registry times the variants — by repeating the run once per variant.
68
+ - Declare the viewport list in the birth-owned `configs/app/vite.journey.config.ts` wrapper, as
69
+ `readonly JourneyVariant[]`. That file is the adopter's, so its names and its viewports are the
70
+ application's to set, and the presence of that file is the journey axis.
71
+ - Prepare the theme under [SKILL.md](../SKILL.md#prepare-the-capture-theme) before driving the
72
+ captured state, and use the variant list that preparation requires.
52
73
  - Name each variant for the theme and the viewport it renders, such as `dark-390`. The name is the
53
74
  second half of every filename the run writes, so a variant named for one alone produces a
54
75
  portfolio nobody can tell apart.
55
- - Pass the whole variant list as `variants` and this run's name as `variant`. `createPortfolio`
56
- refuses a `variant` no declared variant carries, and `files` expands the registry across the whole
57
- list rather than across the one being rendered.
76
+ - Pass the whole declared list as `variants` and `inject('variant')` as `variant`.
77
+ `createPortfolio` refuses a `variant` no declared variant carries, and `files` expands the
78
+ registry across the whole list rather than across the one being rendered.
79
+ - Produce the portfolio — the registry times the variants — by running the axis once per variant,
80
+ which `npm run test:journey` does in one command.
58
81
 
59
82
  ## The proofs the suite owes
60
83
 
61
- The package times the registry and refuses a bad placement. It asserts nothing about either, so the
62
- suite carries these.
84
+ Write each of the following proofs into the suite. The package asserts none of them.
63
85
 
64
86
  | Proof | Runs | Asserts |
65
87
  | -------------------- | -------------- | ---------------------------------------------------------------------------------- |
@@ -92,6 +114,9 @@ after the click returns.
92
114
  ## Hygiene
93
115
 
94
116
  - Keep the portfolio out of version control.
117
+ - Take each frame after the paint settles. Reach for `waitForAnimations` on the element whose
118
+ transition was running ([layer.md](layer.md) → The waits); a frame shot mid-transition pictures a
119
+ state the interface never rests in.
95
120
  - Regenerate the whole matrix from the journeys after any surface change. Never judge a round
96
121
  against a portfolio that is part old and part new.
97
122
  - Route review of the portfolio to the `orkestrel-polish-surface` campaign, which owns preflight,
@@ -7,7 +7,7 @@ instrument here answers as open, and never answer it with the nearest instrument
7
7
  | -------------------------------------- | -------------------------------------------- |
8
8
  | A compiler, a linter, or a Node runner | The `prove` tool, with its negative control |
9
9
  | A person's eye | The run's written artifact, named by variant |
10
- | A person watching a widget move | A statechart harness deep link |
10
+ | A person watching a widget move | The harness run's frames and its artifact |
11
11
  | The browser's own resolved value | The matrix family ([styles.md](styles.md)) |
12
12
 
13
13
  Never ask `prove` about pixels, and never ask a screenshot about types.
@@ -25,17 +25,26 @@ follow it there rather than restating it here.
25
25
 
26
26
  ## The limit that decides the split
27
27
 
28
- `prove` cannot serve a browser project in `@orkestrel/probe` 0.0.11. The following refusals are each
29
- reproduced:
28
+ `prove` cannot serve a browser project, and the limit belongs to the installed `@orkestrel/probe`
29
+ rather than to a version this file names. Confirm it against the copy this workspace holds before
30
+ routing a rendered question, and record the version you read beside the ruling.
30
31
 
31
- - The runtime stage looks a project up by the name it infers, and a browser project is instantiated
32
- under its browser-expanded name, so the lookup finds nothing and the claim is refused as missing.
33
- - The runtime stage pins the `threads` pool, so a browser project's specification runs in a Node
34
- worker. `@orkestrel/test/browser` imports `vitest/browser` at module scope, so the browser setup
35
- file throws before the case runs and the failure reads as the claim's.
32
+ Read the pool pin, the project lookup, and the guide's own statement. Any of them holding is the
33
+ limit holding:
36
34
 
37
- Never route a rendered question to `prove` while that holds. The pinned-pool refusal arrives as a
38
- case failure, which reads exactly like a broken claim.
35
+ - **The pool pin.** Read the runtime stage in the installed server entry. Where it pins the
36
+ `threads` pool, a browser project's specification runs in a Node worker,
37
+ `@orkestrel/test/browser` imports `vitest/browser` at module scope, and the browser setup file
38
+ throws before the case runs.
39
+ - **The project lookup.** The runtime stage looks a project up by the name it infers from the test
40
+ path, and a browser project is instantiated under its browser-expanded name, so the lookup finds
41
+ nothing and the claim is refused as missing.
42
+ - **The guide's statement.** Read what `@orkestrel/probe`'s own guide says the stages serve. Where
43
+ it names no browser project, no stage claims one.
44
+
45
+ Never route a rendered question to `prove` while any of them holds. The pinned-pool refusal arrives
46
+ as a case failure, which reads exactly like a broken claim. Record the version, the pool pin you
47
+ read, and what the guide says, so the next round re-reads rather than re-deriving.
39
48
 
40
49
  ## The rendered artifact
41
50
 
@@ -61,8 +70,18 @@ Rules the artifact obeys:
61
70
  old and part new.
62
71
  - Keep it out of version control.
63
72
 
64
- ## The harness link
65
-
66
- Send a look a person decides on to a statechart harness deep link rather than to a file. Name the
67
- exact link in the round, and let the person watch the widget move rather than read a still of it
68
- ([statechart.md](statechart.md) → Build the harness a person watches).
73
+ ## The harness run
74
+
75
+ Send a look a person decides on to what the harness run itself produced, rather than to a still of a
76
+ screen.
77
+
78
+ - Place a capture inside the harness run at each state the decision is about, and name the frames in
79
+ the round. The run drove the widget through the interface, so the frames are a record of movement
80
+ rather than a staged pose.
81
+ - Name the artifact beside them. Its accessible-tree lines and its journal say what the widget
82
+ announced while it moved, which a frame cannot carry.
83
+ - Give the harness a final row or a demo step that leaves the widget in its most legible state, so
84
+ the last frame is the one a person wants to look at.
85
+ - Name a deep link only where the workspace already ships a harness page
86
+ ([statechart.md](statechart.md) → A harness page is product). Never ask a round to open a link the
87
+ repository does not serve.