@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.
- package/LICENSE +21 -0
- package/README.md +436 -0
- package/dist/core/index.js +162 -0
- package/dist/index-1csg00w4.js +1167 -0
- package/dist/index-6mdfd2ey.js +464 -0
- package/dist/index-7n1h75n6.js +616 -0
- package/dist/index.js +232 -0
- package/dist/react.js +32 -0
- package/dist/testing/index.js +1069 -0
- package/dist/tooling/bombadil.js +2117 -0
- package/dist/tooling/browser-verification-entry.js +1499 -0
- package/dist/tooling/bundle-boundary.js +119 -0
- package/dist/web.js +605 -0
- package/package.json +179 -0
- package/skills/direct/AGENTS.md +13 -0
- package/skills/direct/SKILL.md +49 -0
- package/skills/direct/agents/openai.yaml +4 -0
- package/skills/direct/references/adoption.md +131 -0
- package/skills/direct/references/install.md +91 -0
- package/skills/direct/references/verification.md +247 -0
- package/src/core/coverage.ts +336 -0
- package/src/core/definition.ts +378 -0
- package/src/core/effects.ts +88 -0
- package/src/core/fixture.ts +185 -0
- package/src/core/ids.ts +77 -0
- package/src/core/index.ts +13 -0
- package/src/core/json-value.ts +7 -0
- package/src/core/json.ts +593 -0
- package/src/core/query.ts +230 -0
- package/src/core/reason.ts +16 -0
- package/src/core/resource.ts +10 -0
- package/src/core/result.ts +19 -0
- package/src/core/runtime.ts +229 -0
- package/src/core/scenario.ts +149 -0
- package/src/core/store.ts +784 -0
- package/src/index.ts +51 -0
- package/src/react.ts +54 -0
- package/src/testing/activity.ts +228 -0
- package/src/testing/coverage-binding.ts +99 -0
- package/src/testing/evidence.ts +59 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/manifest.ts +559 -0
- package/src/testing/probe.ts +446 -0
- package/src/testing/scripted-transport.ts +775 -0
- package/src/testing/session.ts +525 -0
- package/src/tooling/bombadil-campaign.ts +288 -0
- package/src/tooling/bombadil-internal.d.ts +46 -0
- package/src/tooling/bombadil-runner.ts +1424 -0
- package/src/tooling/bombadil.ts +27 -0
- package/src/tooling/browser-verification-entry.ts +32 -0
- package/src/tooling/browser-verification.ts +916 -0
- package/src/tooling/bundle-boundary.ts +159 -0
- package/src/web/browser-bridge.ts +296 -0
- package/src/web/browser.ts +277 -0
- package/src/web/fetch-firewall.ts +251 -0
- 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
|
+
[](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
|
+
};
|