@hraness/direct 0.7.5

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +436 -0
  3. package/dist/core/index.js +162 -0
  4. package/dist/index-1csg00w4.js +1167 -0
  5. package/dist/index-6mdfd2ey.js +464 -0
  6. package/dist/index-7n1h75n6.js +616 -0
  7. package/dist/index.js +232 -0
  8. package/dist/react.js +32 -0
  9. package/dist/testing/index.js +1069 -0
  10. package/dist/tooling/bombadil.js +2117 -0
  11. package/dist/tooling/browser-verification-entry.js +1499 -0
  12. package/dist/tooling/bundle-boundary.js +119 -0
  13. package/dist/web.js +605 -0
  14. package/package.json +179 -0
  15. package/skills/direct/AGENTS.md +13 -0
  16. package/skills/direct/SKILL.md +49 -0
  17. package/skills/direct/agents/openai.yaml +4 -0
  18. package/skills/direct/references/adoption.md +131 -0
  19. package/skills/direct/references/install.md +91 -0
  20. package/skills/direct/references/verification.md +247 -0
  21. package/src/core/coverage.ts +336 -0
  22. package/src/core/definition.ts +378 -0
  23. package/src/core/effects.ts +88 -0
  24. package/src/core/fixture.ts +185 -0
  25. package/src/core/ids.ts +77 -0
  26. package/src/core/index.ts +13 -0
  27. package/src/core/json-value.ts +7 -0
  28. package/src/core/json.ts +593 -0
  29. package/src/core/query.ts +230 -0
  30. package/src/core/reason.ts +16 -0
  31. package/src/core/resource.ts +10 -0
  32. package/src/core/result.ts +19 -0
  33. package/src/core/runtime.ts +229 -0
  34. package/src/core/scenario.ts +149 -0
  35. package/src/core/store.ts +784 -0
  36. package/src/index.ts +51 -0
  37. package/src/react.ts +54 -0
  38. package/src/testing/activity.ts +228 -0
  39. package/src/testing/coverage-binding.ts +99 -0
  40. package/src/testing/evidence.ts +59 -0
  41. package/src/testing/index.ts +22 -0
  42. package/src/testing/manifest.ts +559 -0
  43. package/src/testing/probe.ts +446 -0
  44. package/src/testing/scripted-transport.ts +775 -0
  45. package/src/testing/session.ts +525 -0
  46. package/src/tooling/bombadil-campaign.ts +288 -0
  47. package/src/tooling/bombadil-internal.d.ts +46 -0
  48. package/src/tooling/bombadil-runner.ts +1424 -0
  49. package/src/tooling/bombadil.ts +27 -0
  50. package/src/tooling/browser-verification-entry.ts +32 -0
  51. package/src/tooling/browser-verification.ts +916 -0
  52. package/src/tooling/bundle-boundary.ts +159 -0
  53. package/src/web/browser-bridge.ts +296 -0
  54. package/src/web/browser.ts +277 -0
  55. package/src/web/fetch-firewall.ts +251 -0
  56. package/src/web.ts +27 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hraness contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,436 @@
1
+ # direct
2
+
3
+ [![skills.sh](https://skills.sh/b/hraness/direct)](https://skills.sh/hraness/direct)
4
+
5
+ a general harness for repeatable app states.
6
+
7
+ name signed-in, empty, error, and other hard-to-reach app states once, then let
8
+ coding agents open them by URL during development. your interface and feature
9
+ code run normally. direct replaces only the outside systems needed for that
10
+ state with predictable local stand-ins. it does not click through the browser
11
+ or test the systems it replaces.
12
+
13
+ ```sh
14
+ bun add --dev @hraness/direct@0.7.5
15
+ # or
16
+ npm install --save-dev @hraness/direct@0.7.5
17
+ ```
18
+
19
+ [overview](https://hraness.com/direct)
20
+
21
+ ```text
22
+ real interface and feature state
23
+
24
+ product-owned port
25
+ ┌───┴────┐
26
+ production direct
27
+ adapter harness
28
+ ```
29
+
30
+ ## Install
31
+
32
+ ### Install the Agent Skill
33
+
34
+ Install Direct's single bundled skill from the public repository:
35
+
36
+ ```sh
37
+ npx skills add hraness/direct
38
+ # or
39
+ bunx skills add hraness/direct
40
+ ```
41
+
42
+ The skill is invoked as `$direct`. It routes installation, adoption, and
43
+ verification work while keeping the development-only production boundary
44
+ visible. Restart or reload an agent runner that does not discover newly
45
+ installed skills during the current session.
46
+
47
+ ### Tell your coding agent to install it
48
+
49
+ Copy this prompt into Codex, Claude Code, or another coding agent:
50
+
51
+ ```text
52
+ Use $direct to install hraness/direct from
53
+ the npm registry at the exact 0.7.5 version. Follow the repository README, add
54
+ `@hraness/direct` to devDependencies only, and verify that the production
55
+ dependency graph excludes Direct. Do not add a fixture composition until I
56
+ ask.
57
+ ```
58
+
59
+ The repository and tagged package carry the same skill. Installing the skill
60
+ does not add Direct to a consumer project. The skills CLI remains the preferred
61
+ way to let runners discover it; the packaged copy supports runners and tools
62
+ that read skills from installed development dependencies.
63
+
64
+ Pin the public npm package to an exact immutable version:
65
+
66
+ ```json
67
+ {
68
+ "devDependencies": {
69
+ "@hraness/direct": "0.7.5"
70
+ }
71
+ }
72
+ ```
73
+
74
+ Then install with the package manager already used by the project:
75
+
76
+ ```sh
77
+ bun install
78
+ # or, in an npm project
79
+ npm install
80
+ ```
81
+
82
+ Keep Direct in `devDependencies`. A production entry must not import Direct, its fixture worlds, or its workbench.
83
+
84
+ ## Agent skills
85
+
86
+ Packages built from this source include one Agent Skill under
87
+ `node_modules/@hraness/direct/skills/direct/`. `$direct` guides a product-owned
88
+ port and deterministic composition, then audits scenario behavior,
89
+ quiescence, coverage claims, cleanup, and emitted production boundaries. The
90
+ package smoke test keeps that future packaged copy byte-identical to the
91
+ repository skill.
92
+
93
+ Prefer `npx skills add hraness/direct` or `bunx skills add hraness/direct` for
94
+ runner discovery. You can also copy or link that one skill directory into a
95
+ runner's configured location, then invoke `$direct`. Package installation
96
+ leaves the skill inert: it does not run a `postinstall` hook or edit repository
97
+ or user configuration.
98
+
99
+ ## A complete browser composition
100
+
101
+ Extract a strict world parser and product harness into product-owned files, then compose the lifecycle in one entry:
102
+
103
+ ```ts
104
+ import { defineDirect } from "@hraness/direct";
105
+ import { createDirectSession } from "@hraness/direct/testing";
106
+ import { installDirectBrowser } from "@hraness/direct/web";
107
+
108
+ import { parseGreetingWorld } from "./world.js";
109
+
110
+ const definition = defineDirect({
111
+ parseWorld: parseGreetingWorld,
112
+ defaultScenario: "greeting.ready",
113
+ scenarios: [{
114
+ id: "greeting.ready",
115
+ title: "Ready greeting",
116
+ route: "/",
117
+ world: { version: 1, greeting: "Hello" },
118
+ }],
119
+ coverage: [{
120
+ key: "greeting.render",
121
+ mode: "fixture",
122
+ claim: "The real greeting view renders a deterministic greeting.",
123
+ scenarios: ["greeting.ready"],
124
+ }],
125
+ });
126
+
127
+ const opened = createDirectSession({
128
+ definition,
129
+ activation: { kind: "query", source: globalThis.location.search },
130
+ create: ({ world }) => Object.freeze({ greeting: world.greeting }),
131
+ });
132
+ if (!opened.ok) throw new Error(opened.error.message);
133
+
134
+ const session = opened.value;
135
+ const installed = installDirectBrowser({ session });
136
+ if (!installed.ok) {
137
+ session.dispose();
138
+ throw new Error(installed.error.message);
139
+ }
140
+
141
+ renderGreeting(session.harness.greeting);
142
+ globalThis.addEventListener("pagehide", session.dispose, { once: true });
143
+ ```
144
+
145
+ `defineDirect` is the concise authored-config path; scenario defaults and coverage citations are checked against the same scenario tuple. Use `tryDefineDirect` for typed configuration assembled dynamically. Use `parseDirectDefinition` for a genuinely `unknown` value; it returns a `Result` with an intentionally broad JSON world and string route because foreign data cannot supply compile-time refinements.
146
+
147
+ One definition may contain at most 256 scenarios and 256 coverage entries.
148
+
149
+ The session supplies the parsed world, generation-safe store, logical clock,
150
+ activity scope, cancellation signal, world-free manifest, probe, coverage
151
+ value, and reverse-order cleanup. The browser installer publishes that
152
+ manifest with the live probe and reset action, blocks unmapped `fetch` calls
153
+ by default, rolls back partial installation, and registers teardown with
154
+ `session.dispose()`.
155
+
156
+ An external browser tool reads `window.__direct.schema`,
157
+ `window.__direct.manifest`, and `window.__direct.snapshot()` in one synchronous
158
+ evaluation. Require the exact `direct.browser-bridge/v2` schema, parse the
159
+ manifest and probe from `unknown`, confirm the expected activation source,
160
+ scenario, and product route, and require their activation hashes to match.
161
+ Retain one catalog hash across the run. Direct does not need a driver-specific
162
+ plugin: agent-browser, Playwright MCP, and other tools can read the same page
163
+ contract.
164
+
165
+ Direct's browser runtime remains driver-neutral and never launches a process.
166
+ The opt-in host tooling can invoke a consumer-installed agent-browser CLI; the
167
+ product verifier still owns its commands, process lifetime, and evidence. The
168
+ canonical
169
+ [verification workflow](./docs/verification.md#run-one-bounded-local-chromium-batch)
170
+ uses one task-owned local Chromium session and process for a sequential batch
171
+ of at most eight scenarios. It opens a fresh BrowserContext with `window new`
172
+ before every scenario and attempts to close scenario-owned tabs while
173
+ retaining the command results and tab inventories. It keeps the inert
174
+ no-URL bootstrap tab until the final whole-browser close, which is the
175
+ stronger disposal boundary. Semantic and visual evidence come from the same
176
+ exact Chromium context.
177
+
178
+ The product verifier declares exact `--allowed-domains` before navigation and
179
+ uses a bounded idle timeout. Direct's application-`fetch` firewall remains
180
+ instrumentation, not full egress containment. Runs stay serial unless a real
181
+ external coordinator enforces shared admission; Direct does not integrate or
182
+ enforce a process cap. Ordinary browser-wide `--cdp` attachment is forbidden
183
+ because named agent-browser sessions do not isolate contexts.
184
+
185
+ A nonzero final close fails the batch. Parallel-admission or crash-safe cleanup
186
+ claims require an external supervisor that owns both the agent-browser daemon
187
+ and Chromium roots, or one containing job; the roots can occupy different
188
+ process groups. Direct supplies neither that supervisor nor browser or
189
+ performance evidence.
190
+
191
+ See the [Todo example](https://github.com/hraness/direct/tree/main/examples/todos) for a strict parser, product-owned port, React workbench, and emitted-graph boundary verifier. The [React Native example](https://github.com/hraness/direct/tree/main/examples/react-native) uses the same session model in a platform-resolved Expo composition while keeping native production graphs Direct-free.
192
+
193
+ ## Keep evidence honest
194
+
195
+ Coverage entries have one proof mode:
196
+
197
+ | Mode | Meaning |
198
+ | --- | --- |
199
+ | `fixture` | The real interface and product logic ran through deterministic ports. Replaced adapters and platforms were not exercised. |
200
+ | `mixed` | Fixture evidence is paired with named direct adapter or service evidence. Neither half is sufficient alone. |
201
+ | `direct` | The claim requires the real host, service, runtime, filesystem, operating system, or device. |
202
+
203
+ Coverage cites scenario IDs rather than duplicating a singular route. Each scenario owns its route, so one claim may span scenarios on different routes without inventing a second routing abstraction.
204
+
205
+ A quiet probe means the declared deterministic work settled. It does not prove that the rendered result is correct. Pair quiescence with product-owned semantic assertions, visual inspection where relevant, and direct tests for every replaced boundary named by the coverage catalog.
206
+
207
+ ## Package surfaces
208
+
209
+ | Import | Purpose | Runtime boundary |
210
+ | --- | --- | --- |
211
+ | `@hraness/direct` | Authored definitions plus the scenario, coverage, fixture, JSON, activation, and logical-time types needed to describe them | Framework-free |
212
+ | `@hraness/direct/core` | Advanced catalog, parser, store, runtime, effect, resource, ID, and `Result` mechanics | Framework-free |
213
+ | `@hraness/direct/react` | Typed context, provider, and external-store hooks for React DOM or React Native | Optional React peer |
214
+ | `@hraness/direct/testing` | Sessions, manifest and probe parsers, evidence classification, activity scopes, and exact scripted transports | Development and verification |
215
+ | `@hraness/direct/web` | Atomic browser installation, with low-level bridge and firewall escape hatches | Browser only |
216
+ | `@hraness/direct/tooling/browser-verification` | Protocol-bound bridge reads, bounded agent-browser commands, local server leases, and artifact writes | Bun 1.3.14 with Node APIs |
217
+ | `@hraness/direct/tooling/bombadil-campaign` | Direct property and conservative action factories for a Bombadil specification | Bombadil 0.7.2 specification compiler |
218
+ | `@hraness/direct/tooling/bombadil` | Local server ownership, native Bombadil lifecycle, trace attestation, replay, and diagnostic artifacts | Bun 1.3.14 with Node APIs |
219
+ | `@hraness/direct/tooling/bundle-boundary` | Deterministic emitted-file scans and exact versioned-wire evidence | Bun 1.3.14 with Node APIs |
220
+
221
+ The tooling subpaths are development-only. They are built separately from the
222
+ browser runtime and never enter the default, core, React, testing, or web
223
+ graphs. Tooling type checks require Bun and Node type definitions. The
224
+ Bombadil subpaths require a consumer-installed exact
225
+ `@antithesishq/bombadil@0.7.2` development dependency. That peer stays
226
+ optional for Direct consumers that do not use fuzzing. The campaign export
227
+ points to its shipped TypeScript source because Bombadil 0.7.2 resolves package
228
+ exports without standard `import` or `types` conditions; use it only from a
229
+ Bombadil specification.
230
+
231
+ `readDirectBrowserContract` binds the exact package bridge schema and Direct's
232
+ manifest and probe parsers. Use `createDirectBrowserContractReader` when a
233
+ verifier supplies another compatible protocol. `createAgentBrowser` expects
234
+ agent-browser 0.32.3 at `node_modules/.bin/agent-browser` below the supplied
235
+ `repositoryRoot` and an empty task-owned config at
236
+ `scripts/direct/agent-browser.verify.json`. The product supplies its explicit
237
+ launch arguments, allowed domains, scenario commands, and final close policy.
238
+
239
+ ### Fuzz one Direct scenario
240
+
241
+ Bombadil can explore a rendered Direct scenario with four recurring bounded
242
+ health properties. The host then attests the complete trace for one stable
243
+ scenario, route, activation identity and catalog, exact contracts, zero
244
+ declared violation counters, and final quiescence. Install the supported release
245
+ directly in the consumer:
246
+
247
+ ```sh
248
+ bun add --dev @antithesishq/bombadil@0.7.2
249
+ ```
250
+
251
+ A product campaign re-exports Bombadil's browser properties, then names the
252
+ Direct formulas and conservative action generator:
253
+
254
+ ```ts
255
+ import {
256
+ createDirectBombadilActions,
257
+ createDirectBombadilProperties,
258
+ } from "@hraness/direct/tooling/bombadil-campaign";
259
+
260
+ export * from "@antithesishq/bombadil/browser/defaults/properties";
261
+
262
+ const direct = createDirectBombadilProperties();
263
+ export const direct_safe_actions = createDirectBombadilActions();
264
+ export const direct_exact_contract = direct.exactContract;
265
+ export const direct_stable_catalog = direct.stableCatalog;
266
+ export const direct_no_declared_violations = direct.noDeclaredViolations;
267
+ export const direct_eventual_quiescence = direct.eventualQuiescence;
268
+ ```
269
+
270
+ The product keeps its own scenario, semantic assertions, server command, entry
271
+ path, and any additional safe actions. Call `runDirectBombadilFuzz` from
272
+ `@hraness/direct/tooling/bombadil` in a small Bun wrapper. The runner accepts
273
+ only an explicit local HTTP origin, starts an argv-only server command, invokes
274
+ the exact native 0.7.2 binary, attests the bounded trace with Direct's canonical
275
+ parsers, writes pass or failure artifacts, and releases its owned processes.
276
+ See [Verification](./docs/verification.md#run-a-bounded-bombadil-campaign) for
277
+ the complete configuration and proof limits.
278
+
279
+ ## Activate scenarios
280
+
281
+ The browser query boundary reserves:
282
+
283
+ - `__direct_scenario=<id>` for a named catalog scenario.
284
+ - `__direct_fixture=<encoded-json>` for a portable `direct.fixture/v1` envelope.
285
+
286
+ Malformed encoding, duplicate activation, unknown reserved keys, unknown scenarios, route mismatches, invalid worlds, and oversized input fail closed. An empty activation selects the definition's validated default scenario.
287
+
288
+ ## Upgrade from v0.4.0
289
+
290
+ v0.5.0 replaces `direct.browser-bridge/v1` with the exact v2 shape:
291
+ `schema`, `manifest`, `snapshot()`, and `reset()`. Coverage moved from
292
+ `window.__direct.coverage` to `window.__direct.manifest.coverage`. Low-level
293
+ bridge callers now pass `manifest` instead of `coverage`, and probe activation
294
+ hashes use `fnv1a-64:<16 lowercase hexadecimal digits>`. The manifest parser
295
+ also recomputes `active.selectionHash`, which binds the public source,
296
+ scenario, and route to that activation identity without exposing world or
297
+ runtime data.
298
+
299
+ Migrate the browser installation and each verifier together. Keep v0.4.0
300
+ pinned until a consumer can accept the complete v2 contract; do not support a
301
+ hybrid bridge shape.
302
+
303
+ ## Repository scope
304
+
305
+ This repository contains the deterministic kernel, browser bridge, production-exclusion scanner, bounded host-verification helpers, agent skills, a small React example, and an Expo/React Native reference app. It does not bundle a browser driver, shared process coordinator, browser-worker pool, or browser benchmark. The optional agent-browser helper invokes the consumer's local installation. The optional Bombadil helper supervises one explicitly configured local server and native Bombadil process tree, but does not coordinate concurrent repositories or turn diagnostic fuzzing into product-specific proof. The product owns semantic assertions and evidence claims, and external proof remains required for replaced systems, browser custody, or performance.
306
+
307
+ <!-- article:direct-a-harness-for-your-frontend:start -->
308
+ ## [Direct gives browser agents deterministic app states](<https://hraness.com/direct>)
309
+
310
+ > Browser tools control a page. Direct makes the state behind it quick to reach and repeatable without claiming to test the external systems it replaces.
311
+
312
+ A browser agent can open a page, click a control, and inspect the result. It cannot make the state behind that page quick to reach. A signed-in account, a particular database record, a device permission, a model response, or a failure at the right moment may still take longer to arrange than the interface takes to review.
313
+
314
+ [Hraness Direct](<https://hraness.com/direct>) separates those two jobs. A browser tool controls the page. The product connects Direct's named, repeatable states to its existing interface and feature logic through deterministic adapters below a small product-owned boundary. Direct speeds up development and review; it does not drive the browser or prove that replaced systems work.
315
+
316
+ ### Browser control and app state are different jobs
317
+
318
+ [agent-browser](<https://agent-browser.dev/>) gives coding agents a compact command-line interface for opening pages, reading accessibility snapshots, and interacting with elements. Playwright and other browser drivers solve the same broad problem with different APIs. If the state you need is already fast and reliable to reach, a browser tool by itself is the smaller and better choice.
319
+
320
+ Direct becomes useful when setup dominates the loop: repeated sign-in, slow seed requests, hard-to-create empty or error states, unavailable native modules, paid model calls, or device permissions that automation cannot reset cleanly. Direct does not click the page. It gives the browser tool a stable page state to act on.
321
+
322
+ ### Replace setup below the behavior
323
+
324
+ A product-owned port is a small interface between product behavior and an external system. A task view might ask a task repository to read and update tasks. Production connects that port to a live service. A Direct composition connects the same port to a deterministic implementation. The interface, reducers, parsing, navigation, and feature decisions above the port stay on their normal code paths.
325
+
326
+ The boundary can be pictured without knowing the package API:
327
+
328
+ **Conceptual Direct boundary**
329
+
330
+ ```text
331
+ agent-browser or Playwright
332
+
333
+ real interface + feature state
334
+
335
+ product-owned port
336
+ ┌───┴────┐
337
+ live system Direct world
338
+ ```
339
+
340
+ A Direct world is validated JSON that describes one starting state. A scenario gives that world a name and route. It does not contain browser actions. The browser check still decides what to click and what outcome to assert.
341
+
342
+ The [public Todo example](<https://github.com/hraness/direct/tree/main/examples/todos>) uses one `TodoPort` in both compositions. The component receives whichever implementation the entry point owns:
343
+
344
+ **One product port, two compositions**
345
+
346
+ ```typescript
347
+ export interface TodoPort {
348
+ readTodos(): Promise<readonly TodoItem[]>;
349
+ setCompleted(id: string, completed: boolean):
350
+ Promise<readonly TodoItem[]>;
351
+ }
352
+
353
+ const port = isDirect
354
+ ? createDeterministicTodoPort(world)
355
+ : createLiveTodoPort();
356
+
357
+ <TodoApp port={port} />
358
+ ```
359
+
360
+ The interface speaks in product terms: todos and completion. It contains no Direct types and does not know whether storage is live or deterministic. Use the lowest port that preserves the behavior under review. If the Direct adapter must copy the logic named by the claim, the boundary is too high and the fixture would imitate its subject instead of testing it.
361
+
362
+ ### Direct owns one deterministic session
363
+
364
+ Direct gives the development composition one lifecycle instead of a collection of unrelated fixture helpers:
365
+
366
+ - A definition lists the named scenarios, their routes, and which systems each check claims to exercise, then validates that those declarations agree.
367
+ - A session activates one scenario and owns its deterministic state, controllable clock, pending work, reset, and cleanup.
368
+ - A small browser-facing manifest identifies the available and active scenarios, exposes readiness and reset controls, and blocks unmapped application requests by default.
369
+
370
+ That default network policy matters. A deterministic page should not silently call a live service when a fixture misses a case. The product can allow exact URLs when needed, but unknown application calls fail visibly. Direct and its fixture worlds also stay outside the production dependency graph.
371
+
372
+ The published manifest is a machine-readable description of the deterministic page. An agent can discover valid scenario IDs and routes, confirm that the active session matches the requested scenario and route, and inspect readiness without reading a product-specific source file. The browser tool still owns navigation and interaction; Direct does not turn scenarios into commands.
373
+
374
+ ### Wait for the app, not a guess
375
+
376
+ A fixed delay says, “wait 500 milliseconds and hope.” Direct exposes a readiness snapshot: no tracked operation is active, and each product-named pending counter is zero. The product's browser verifier polls until the active scenario, its tracked-work revision, and the counters remain unchanged for a bounded interval before checking the interface.
377
+
378
+ **Browser check using a named Direct scenario**
379
+
380
+ ```typescript
381
+ await page.goto(
382
+ "/direct/?__direct_scenario=todos.populated",
383
+ );
384
+ await waitForQuiescence(page);
385
+
386
+ await page.getByRole("checkbox", {
387
+ name: "Write the public guide",
388
+ }).check();
389
+
390
+ await waitForQuiescence(page);
391
+ await expect(page.getByRole("checkbox", {
392
+ name: "Write the public guide",
393
+ })).toBeChecked();
394
+ ```
395
+
396
+ Here, `waitForQuiescence` is product-owned verifier code around Direct's snapshot, not a Direct browser driver. A settled snapshot proves only that the work Direct knows about has stopped changing. It does not prove that the screen is correct. The verifier must still reject relevant console, runtime, and unhandled-request errors, then make product-specific assertions or visual checks.
397
+
398
+ ### Choose the smallest tool that covers the risk
399
+
400
+ - Use browser automation alone when the required state is already quick to reach, or when the live backend and browser assembly are part of the check.
401
+ - Pair Direct with agent-browser or Playwright when setup and reset dominate the loop and the substituted systems can sit behind a small product-owned port.
402
+ - Use unit or component tests when the subject is isolated logic or rendering that does not need the full application composition.
403
+ - Keep live integration and end-to-end tests when the backend, native host, browser assembly, filesystem, operating system, or device is the subject.
404
+
405
+ A coverage claim records which systems a check actually exercised. Direct uses three labels: a fixture claim stops at deterministic ports, a mixed claim combines fixture evidence with a named live check, and a direct claim requires the real system. The labels do not create evidence; they keep a fast development check from being reported as proof of a system it never touched.
406
+
407
+ Use Direct when the state behind the interface is the bottleneck and a small product-owned port can replace that setup without copying the behavior under review. Use the browser tool alone when it can already reach the state cheaply. In either case, the browser driver supplies the actions and assertions. Direct never exercises the systems behind replaced ports; cover those boundaries separately with live integration or end-to-end tests when their risk requires it.
408
+ <!-- article:direct-a-harness-for-your-frontend:end -->
409
+
410
+ ## Develop
411
+
412
+ ```sh
413
+ bun install --frozen-lockfile --ignore-scripts
414
+ bun run check
415
+ bun run example:test
416
+ bun run example:typecheck
417
+ bun run example:verify
418
+ bun run example:build
419
+ bun run example:check-boundary
420
+ bun run example:build:direct
421
+ bun run example:react-native:test
422
+ bun run example:react-native:typecheck
423
+ bun run example:react-native:verify
424
+ ```
425
+
426
+ Run the production app with `bun run example:dev`. Run the deterministic workbench with `bun run example:direct`, then select `empty`, `populated`, or `write failure` from its scenario navigation.
427
+
428
+ Run the Expo workbench with `bun run example:react-native`. Its verification command exports iOS and Android production bundles plus the deterministic React Native Web composition with paired source maps, proves the expected shared and platform-specific modules were selected, and rejects native/web cross-contamination. It does not replace browser-driven semantic assertions or direct device evidence.
429
+
430
+ See [Architecture](./docs/architecture.md), [Adoption](./docs/adoption.md), [Verification](./docs/verification.md), and [Wire formats](./docs/wire-formats.md) for durable contracts.
431
+
432
+ ## Contribute and report vulnerabilities
433
+
434
+ Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a pull request. Report suspected vulnerabilities privately as described in [SECURITY.md](./SECURITY.md).
435
+
436
+ Direct is available under the [MIT License](./LICENSE).
@@ -0,0 +1,162 @@
1
+ import {
2
+ DIRECT_STORE_MAX_REPLACEMENTS,
3
+ DIRECT_STORE_MAX_REPLACEMENT_PATH_DEPTH,
4
+ createDirectStore
5
+ } from "../index-6mdfd2ey.js";
6
+ import {
7
+ DEFAULT_JSON_LIMITS,
8
+ DEFAULT_LOGICAL_RUNTIME_SNAPSHOT,
9
+ DEFAULT_MAX_FIXTURE_BYTES,
10
+ DEFAULT_MAX_QUERY_BYTES,
11
+ DIRECT_COVERAGE_SCHEMA,
12
+ EMPTY_COVERAGE_CATALOG_SNAPSHOT,
13
+ FIXTURE_QUERY_KEY,
14
+ FIXTURE_SCHEMA,
15
+ LOGICAL_RUNTIME_SCHEMA,
16
+ MAX_DIRECT_COVERAGE_ENTRIES,
17
+ MAX_DIRECT_SCENARIOS,
18
+ MAX_HOST_TIMER_MILLISECONDS,
19
+ SCENARIO_QUERY_KEY,
20
+ STABLE_HASH_ALGORITHM,
21
+ activateDirectScenario,
22
+ canonicalJson,
23
+ cloneJson,
24
+ coverageKey,
25
+ createCoverageCatalog,
26
+ createCoverageCatalogSnapshot,
27
+ createFixtureEnvelope,
28
+ createLogicalRuntime,
29
+ createScenarioCatalog,
30
+ err,
31
+ freezeJson,
32
+ isRecord,
33
+ maximumFixtureQueryBytes,
34
+ ok,
35
+ operationId,
36
+ parseAndCloneWorld,
37
+ parseCoverageCatalogSnapshot,
38
+ parseCoverageKey,
39
+ parseDirectQuery,
40
+ parseExactJsonSource,
41
+ parseFixtureEnvelope,
42
+ parseFixtureJson,
43
+ parseJsonValue,
44
+ parseLogicalRuntimeSnapshot,
45
+ parseOperationId,
46
+ parseScenarioId,
47
+ parseTaggedStableHash,
48
+ renderUnknownReason,
49
+ scenarioId,
50
+ serializeFixtureJson,
51
+ stableHash,
52
+ tagStableHash,
53
+ utf8ByteLength
54
+ } from "../index-1csg00w4.js";
55
+ // src/core/effects.ts
56
+ function ownQueuedEffect(entry) {
57
+ const id = parseOperationId(entry.id);
58
+ if (!id.ok)
59
+ throw new Error(id.error.message);
60
+ if (!Number.isSafeInteger(entry.remaining) || entry.remaining < 1) {
61
+ throw new Error("Queued effect remaining uses must be a positive safe integer");
62
+ }
63
+ const cloned = cloneJson(entry.effect);
64
+ if (!cloned.ok)
65
+ throw new Error(cloned.error.message);
66
+ return Object.freeze({
67
+ id: id.value,
68
+ effect: freezeJson(cloned.value),
69
+ remaining: entry.remaining
70
+ });
71
+ }
72
+ function ownEffectQueue(queue) {
73
+ return Object.freeze(queue.map((entry) => ownQueuedEffect(entry)));
74
+ }
75
+ function enqueueEffect(queue, id, effect, uses = 1) {
76
+ if (!Number.isSafeInteger(uses) || uses < 1) {
77
+ throw new Error("Queued effect uses must be a positive safe integer");
78
+ }
79
+ const ownedQueue = ownEffectQueue(queue);
80
+ const appended = ownQueuedEffect({ id, effect, remaining: uses });
81
+ return Object.freeze([...ownedQueue, appended]);
82
+ }
83
+ function consumeEffect(queue, matches = () => true) {
84
+ const ownedQueue = ownEffectQueue(queue);
85
+ const index = ownedQueue.findIndex(matches);
86
+ if (index < 0) {
87
+ return Object.freeze({ kind: "empty", queue: ownedQueue });
88
+ }
89
+ const matched = ownedQueue[index];
90
+ if (matched === undefined) {
91
+ return Object.freeze({ kind: "empty", queue: ownedQueue });
92
+ }
93
+ const next = [...ownedQueue];
94
+ if (matched.remaining === 1) {
95
+ next.splice(index, 1);
96
+ } else {
97
+ next[index] = Object.freeze({ ...matched, remaining: matched.remaining - 1 });
98
+ }
99
+ return Object.freeze({
100
+ kind: "consumed",
101
+ effect: matched.effect,
102
+ queue: Object.freeze(next)
103
+ });
104
+ }
105
+ var enqueueFault = enqueueEffect;
106
+ var consumeFault = consumeEffect;
107
+ export {
108
+ utf8ByteLength,
109
+ tagStableHash,
110
+ stableHash,
111
+ serializeFixtureJson,
112
+ scenarioId,
113
+ renderUnknownReason,
114
+ parseTaggedStableHash,
115
+ parseScenarioId,
116
+ parseOperationId,
117
+ parseLogicalRuntimeSnapshot,
118
+ parseJsonValue,
119
+ parseFixtureJson,
120
+ parseFixtureEnvelope,
121
+ parseExactJsonSource,
122
+ parseDirectQuery,
123
+ parseCoverageKey,
124
+ parseCoverageCatalogSnapshot,
125
+ parseAndCloneWorld,
126
+ operationId,
127
+ ok,
128
+ maximumFixtureQueryBytes,
129
+ isRecord,
130
+ freezeJson,
131
+ err,
132
+ enqueueFault,
133
+ enqueueEffect,
134
+ createScenarioCatalog,
135
+ createLogicalRuntime,
136
+ createFixtureEnvelope,
137
+ createDirectStore,
138
+ createCoverageCatalogSnapshot,
139
+ createCoverageCatalog,
140
+ coverageKey,
141
+ consumeFault,
142
+ consumeEffect,
143
+ cloneJson,
144
+ canonicalJson,
145
+ activateDirectScenario,
146
+ STABLE_HASH_ALGORITHM,
147
+ SCENARIO_QUERY_KEY,
148
+ MAX_HOST_TIMER_MILLISECONDS,
149
+ MAX_DIRECT_SCENARIOS,
150
+ MAX_DIRECT_COVERAGE_ENTRIES,
151
+ LOGICAL_RUNTIME_SCHEMA,
152
+ FIXTURE_SCHEMA,
153
+ FIXTURE_QUERY_KEY,
154
+ EMPTY_COVERAGE_CATALOG_SNAPSHOT,
155
+ DIRECT_STORE_MAX_REPLACEMENT_PATH_DEPTH,
156
+ DIRECT_STORE_MAX_REPLACEMENTS,
157
+ DIRECT_COVERAGE_SCHEMA,
158
+ DEFAULT_MAX_QUERY_BYTES,
159
+ DEFAULT_MAX_FIXTURE_BYTES,
160
+ DEFAULT_LOGICAL_RUNTIME_SNAPSHOT,
161
+ DEFAULT_JSON_LIMITS
162
+ };