@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
@@ -0,0 +1,288 @@
1
+ /* eslint-disable @typescript-eslint/triple-slash-reference */
2
+ /// <reference path="./bombadil-internal.d.ts" />
3
+ /* eslint-enable @typescript-eslint/triple-slash-reference */
4
+
5
+ import {
6
+ actions,
7
+ always,
8
+ eventually,
9
+ extract,
10
+ weighted,
11
+ type ActionGenerator,
12
+ type Formula,
13
+ type JSON as BombadilJson,
14
+ type Tree,
15
+ } from "@antithesishq/bombadil";
16
+ import type {
17
+ ActionTemplate,
18
+ State as BombadilBrowserState,
19
+ } from "@antithesishq/bombadil/browser";
20
+ import {
21
+ clicks,
22
+ inputs,
23
+ scroll,
24
+ } from "@antithesishq/bombadil/browser/defaults/actions";
25
+
26
+ const DIRECT_BROWSER_BRIDGE_SCHEMA = "direct.browser-bridge/v2";
27
+ const DIRECT_SESSION_MANIFEST_SCHEMA = "direct.session-manifest/v1";
28
+ const DIRECT_PROBE_SCHEMA = "direct.probe/v1";
29
+ const MAX_RAW_CONTRACT_CHARACTERS = 2_000_000;
30
+ const BRIDGE_KEYS = new Set(["manifest", "reset", "schema", "snapshot"]);
31
+ const UNSAFE_CLICK_INPUT_TYPES = new Set(["image", "reset", "submit"]);
32
+
33
+ export interface DirectBombadilObservation {
34
+ readonly [key: string | number | symbol]: BombadilJson;
35
+ readonly activationHash: string;
36
+ readonly activeRoute: string;
37
+ readonly activeScenario: string;
38
+ readonly activeSource: string;
39
+ readonly bridgePresent: boolean;
40
+ readonly bridgeSchema: string;
41
+ readonly catalogHash: string;
42
+ readonly contractValid: boolean;
43
+ readonly isQuiescent: boolean;
44
+ readonly manifest: BombadilJson;
45
+ readonly probe: BombadilJson;
46
+ readonly violations: number[];
47
+ readonly violationsValid: boolean;
48
+ }
49
+
50
+ export interface DirectBombadilProperties {
51
+ readonly exactContract: Formula;
52
+ readonly stableCatalog: Formula;
53
+ readonly noDeclaredViolations: Formula;
54
+ readonly eventualQuiescence: Formula;
55
+ }
56
+
57
+ function safeClickAction(action: ActionTemplate): boolean {
58
+ if (typeof action !== "object" || action === null) return false;
59
+ const candidate = "Click" in action
60
+ ? action.Click
61
+ : "DoubleClick" in action
62
+ ? action.DoubleClick
63
+ : null;
64
+ if (candidate === null) return false;
65
+ const { fingerprint } = candidate;
66
+ const tag = fingerprint.tag.toLowerCase();
67
+ const inputType = fingerprint.inputType?.toLowerCase() ?? "";
68
+ const labels = [fingerprint.accessibleName, fingerprint.textContent]
69
+ .filter((label): label is string => label !== null)
70
+ .map((label) => label.trim().toLowerCase());
71
+ return fingerprint.href === null
72
+ && tag !== "a"
73
+ && fingerprint.role?.toLowerCase() !== "link"
74
+ && !labels.includes("reset")
75
+ && !UNSAFE_CLICK_INPUT_TYPES.has(inputType)
76
+ && (tag !== "button" || inputType === "button");
77
+ }
78
+
79
+ function safeInputAction(action: ActionTemplate): boolean {
80
+ return !(
81
+ typeof action === "object"
82
+ && action !== null
83
+ && "PressKey" in action
84
+ && action.PressKey.code === 13
85
+ );
86
+ }
87
+
88
+ function pruneActionTree<Action>(
89
+ tree: Tree<Action>,
90
+ keep: (action: Action) => boolean,
91
+ ): Tree<Action> | null {
92
+ if ("value" in tree) return keep(tree.value) ? tree : null;
93
+ const branches: [number, Tree<Action>][] = [];
94
+ for (const [weight, child] of tree.branches) {
95
+ const filtered = pruneActionTree(child, keep);
96
+ if (filtered !== null) branches.push([weight, filtered]);
97
+ }
98
+ return branches.length === 0 ? null : { branches };
99
+ }
100
+
101
+ /**
102
+ * Builds a browser action generator without reload/history actions or visible
103
+ * navigation and submission click targets, keeping Direct continuously bound.
104
+ */
105
+ export function createDirectBombadilActions(): ActionGenerator<ActionTemplate> {
106
+ const safeClicks = actions(() => pruneActionTree(clicks.generate(), safeClickAction) ?? []);
107
+ const safeInputs = actions(() => pruneActionTree(inputs.generate(), safeInputAction) ?? []);
108
+ const wait = actions<ActionTemplate>(() => ["Wait"]);
109
+ return weighted([
110
+ [4, safeClicks],
111
+ [3, safeInputs],
112
+ [2, scroll],
113
+ [1, wait],
114
+ ]);
115
+ }
116
+
117
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
118
+ return typeof value === "object" && value !== null && !Array.isArray(value);
119
+ }
120
+
121
+ function hasExactKeys(
122
+ value: Readonly<Record<string, unknown>>,
123
+ expected: ReadonlySet<string>,
124
+ ): boolean {
125
+ const keys = Object.keys(value);
126
+ return keys.length === expected.size && keys.every((key) => expected.has(key));
127
+ }
128
+
129
+ function invalidObservation(bridgePresent = false): DirectBombadilObservation {
130
+ return {
131
+ activationHash: "",
132
+ activeRoute: "",
133
+ activeScenario: "",
134
+ activeSource: "",
135
+ bridgePresent,
136
+ bridgeSchema: "",
137
+ catalogHash: "",
138
+ contractValid: false,
139
+ isQuiescent: false,
140
+ manifest: null,
141
+ probe: null,
142
+ violations: [],
143
+ violationsValid: false,
144
+ };
145
+ }
146
+
147
+ function boundedJsonClone(value: unknown): BombadilJson | null {
148
+ const source = JSON.stringify(value);
149
+ if (source === undefined || source.length > MAX_RAW_CONTRACT_CHARACTERS) return null;
150
+ return JSON.parse(source) as BombadilJson;
151
+ }
152
+
153
+ function readNonNegativeCounters(value: unknown): {
154
+ readonly valid: boolean;
155
+ readonly values: number[];
156
+ } {
157
+ if (!isRecord(value)) return { valid: false, values: [] };
158
+ const values = Object.values(value);
159
+ if (!values.every((candidate) =>
160
+ typeof candidate === "number"
161
+ && Number.isSafeInteger(candidate)
162
+ && candidate >= 0
163
+ )) {
164
+ return { valid: false, values: [] };
165
+ }
166
+ return { valid: true, values: values as number[] };
167
+ }
168
+
169
+ /**
170
+ * Reads the foreign Direct browser boundary without allowing a hostile getter,
171
+ * proxy, or snapshot callback to escape the extractor.
172
+ *
173
+ * The extractor intentionally retains bounded raw contracts. The host runner
174
+ * applies Direct's canonical manifest and probe parsers to every trace sample.
175
+ */
176
+ export function readDirectBombadilObservation(
177
+ windowValue: unknown,
178
+ ): DirectBombadilObservation {
179
+ let bridgePresent = false;
180
+ try {
181
+ if (!isRecord(windowValue)) return invalidObservation();
182
+ bridgePresent = Object.hasOwn(windowValue, "__direct");
183
+ if (!bridgePresent) return invalidObservation();
184
+ const bridge = Reflect.get(windowValue, "__direct");
185
+ if (!isRecord(bridge) || !hasExactKeys(bridge, BRIDGE_KEYS)) {
186
+ return invalidObservation(true);
187
+ }
188
+
189
+ if (
190
+ bridge.schema !== DIRECT_BROWSER_BRIDGE_SCHEMA
191
+ || typeof bridge.reset !== "function"
192
+ || typeof bridge.snapshot !== "function"
193
+ ) {
194
+ return invalidObservation(true);
195
+ }
196
+
197
+ const manifest = boundedJsonClone(Reflect.get(bridge, "manifest"));
198
+ const snapshot = Reflect.get(bridge, "snapshot");
199
+ if (manifest === null || typeof snapshot !== "function") return invalidObservation(true);
200
+ const probe: unknown = Reflect.apply(snapshot, bridge, []) as unknown;
201
+ const clonedProbe = boundedJsonClone(probe);
202
+ if (clonedProbe === null || !isRecord(manifest) || !isRecord(clonedProbe)) {
203
+ return invalidObservation(true);
204
+ }
205
+ const active: unknown = Reflect.get(manifest, "active");
206
+ if (!isRecord(active)) return invalidObservation(true);
207
+ const violations = readNonNegativeCounters(Reflect.get(clonedProbe, "violations"));
208
+ const activationHash: unknown = Reflect.get(active, "activationHash");
209
+ const probeActivationHash: unknown = Reflect.get(clonedProbe, "activationHash");
210
+ const activeRoute: unknown = Reflect.get(active, "route");
211
+ const activeScenario: unknown = Reflect.get(active, "scenario");
212
+ const activeSource: unknown = Reflect.get(active, "source");
213
+ const catalogHash: unknown = Reflect.get(manifest, "catalogHash");
214
+ const isQuiescent: unknown = Reflect.get(clonedProbe, "isQuiescent");
215
+ const contractValid = Reflect.get(manifest, "schema") === DIRECT_SESSION_MANIFEST_SCHEMA
216
+ && Reflect.get(clonedProbe, "schema") === DIRECT_PROBE_SCHEMA
217
+ && typeof activationHash === "string"
218
+ && activationHash.length > 0
219
+ && probeActivationHash === activationHash
220
+ && typeof activeRoute === "string"
221
+ && activeRoute.length > 0
222
+ && typeof activeScenario === "string"
223
+ && activeScenario.length > 0
224
+ && (activeSource === "scenario" || activeSource === "fixture")
225
+ && typeof catalogHash === "string"
226
+ && catalogHash.length > 0
227
+ && typeof isQuiescent === "boolean"
228
+ && violations.valid;
229
+
230
+ return {
231
+ activationHash: typeof activationHash === "string" ? activationHash : "",
232
+ activeRoute: typeof activeRoute === "string" ? activeRoute : "",
233
+ activeScenario: typeof activeScenario === "string" ? activeScenario : "",
234
+ activeSource: typeof activeSource === "string" ? activeSource : "",
235
+ bridgePresent: true,
236
+ bridgeSchema: DIRECT_BROWSER_BRIDGE_SCHEMA,
237
+ catalogHash: typeof catalogHash === "string" ? catalogHash : "",
238
+ contractValid,
239
+ isQuiescent: isQuiescent === true,
240
+ manifest,
241
+ probe: clonedProbe,
242
+ violations: violations.values,
243
+ violationsValid: violations.valid,
244
+ };
245
+ } catch {
246
+ return invalidObservation(bridgePresent);
247
+ }
248
+ }
249
+
250
+ /** Builds the four Direct invariants used by Bombadil browser campaigns. */
251
+ export function createDirectBombadilProperties(): DirectBombadilProperties {
252
+ const direct = extract<BombadilBrowserState, DirectBombadilObservation>((state) =>
253
+ readDirectBombadilObservation(state.window)
254
+ ).named("direct");
255
+
256
+ const exactContract = always(
257
+ eventually(() =>
258
+ direct.current.contractValid
259
+ && direct.current.activeSource === "scenario"
260
+ && direct.current.activeScenario.length > 0
261
+ && direct.current.activeRoute.length > 0
262
+ && direct.current.activationHash.length > 0
263
+ ).within(10, "seconds"),
264
+ );
265
+ const stableCatalog = always(
266
+ eventually(() =>
267
+ direct.current.contractValid
268
+ && direct.current.catalogHash.length > 0
269
+ ).within(10, "seconds"),
270
+ );
271
+ const noDeclaredViolations = always(
272
+ eventually(() =>
273
+ direct.current.contractValid
274
+ && direct.current.violationsValid
275
+ && direct.current.violations.every((value: number) => value === 0)
276
+ ).within(10, "seconds"),
277
+ );
278
+ const eventualQuiescence = always(
279
+ eventually(() => direct.current.isQuiescent).within(10, "seconds"),
280
+ );
281
+
282
+ return Object.freeze({
283
+ exactContract,
284
+ stableCatalog,
285
+ noDeclaredViolations,
286
+ eventualQuiescence,
287
+ });
288
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Bombadil 0.7.2's public declarations reference this subpath, but its npm
3
+ * export map omits it. Keep this ambient declaration pinned to that release's
4
+ * public type dependency until the package exports the declaration itself.
5
+ */
6
+ declare module "@antithesishq/bombadil/internal" {
7
+ export type TimeUnit = "milliseconds" | "seconds";
8
+
9
+ export interface Cell<T> {
10
+ readonly current: T;
11
+ update(snapshot: T): void;
12
+ }
13
+
14
+ export type JSON =
15
+ | string
16
+ | number
17
+ | boolean
18
+ | null
19
+ | JSON[]
20
+ | { [key: string | number | symbol]: JSON }
21
+ | { toJSON(): JSON };
22
+
23
+ export class ExtractorCell<T extends JSON, S> implements Cell<T> {
24
+ readonly current: T;
25
+ readonly index: number;
26
+ name: string | null;
27
+ constructor(runtime: Runtime<S>, extract: (state: S) => T);
28
+ named(name: string): this;
29
+ run(state: S): T;
30
+ update(snapshot: T): void;
31
+ }
32
+
33
+ export class Runtime<S> {
34
+ readonly extractors: readonly ExtractorCell<JSON, S>[];
35
+ checkNotExtracting(): void;
36
+ recordAccess(index: number): void;
37
+ registerExtractor(cell: ExtractorCell<JSON, S>): number;
38
+ runExtractors(state: S): readonly {
39
+ readonly index: number;
40
+ readonly name: string | null;
41
+ readonly value: JSON;
42
+ }[];
43
+ startTracking(): void;
44
+ stopTracking(): number[];
45
+ }
46
+ }