@crouton-kit/capture 1.3.7 → 1.3.9
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/bin/capture +32786 -9989
- package/package.json +9 -3
- package/src/capture.ts +86 -163
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -116
- package/src/cdp/bridge/protocol.ts +187 -1
- package/src/cdp/bridge/server.ts +211 -84
- package/src/cdp/bridge/spawn.ts +110 -1
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +35 -4
- package/src/cdp/commands/cdp.ts +266 -69
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +87 -0
- package/src/cdp/commands/measure/check.ts +109 -0
- package/src/cdp/commands/measure/diff.ts +120 -0
- package/src/cdp/commands/measure/explain.ts +133 -0
- package/src/cdp/commands/measure/index.ts +106 -0
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +52 -0
- package/src/cdp/commands/measure/map-layers.ts +59 -0
- package/src/cdp/commands/measure/map-scroll.ts +94 -0
- package/src/cdp/commands/measure/snap.ts +416 -0
- package/src/cdp/commands/measure/sweep.ts +227 -0
- package/src/cdp/commands/motion/index.ts +61 -0
- package/src/cdp/commands/motion/jank.ts +132 -0
- package/src/cdp/commands/motion/mask.ts +92 -0
- package/src/cdp/commands/motion/rec.ts +534 -0
- package/src/cdp/commands/motion/response.ts +76 -0
- package/src/cdp/commands/motion/timeline.ts +152 -0
- package/src/cdp/commands/page/click.ts +194 -0
- package/src/cdp/commands/page/elements.ts +154 -0
- package/src/cdp/commands/page/exec.ts +317 -0
- package/src/cdp/commands/page/index.ts +72 -0
- package/src/cdp/commands/page/navigate.ts +330 -0
- package/src/cdp/commands/page/scroll.ts +107 -0
- package/src/cdp/commands/page/shot.ts +208 -0
- package/src/cdp/commands/page/type.ts +104 -0
- package/src/cdp/commands/tab/index.ts +59 -0
- package/src/cdp/commands/tab/list.ts +167 -0
- package/src/cdp/commands/tab/network.ts +162 -0
- package/src/cdp/commands/tab/open.ts +90 -0
- package/src/cdp/commands/tab/reset.ts +160 -0
- package/src/cdp/connection.ts +368 -37
- package/src/cdp/coordinates.ts +158 -0
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +76 -26
- package/src/cdp/domains.ts +75 -0
- package/src/cdp/har-recorder.ts +648 -115
- package/src/cdp/leaf-grammar.ts +99 -0
- package/src/cdp/measure/census.ts +205 -0
- package/src/cdp/measure/check.ts +318 -0
- package/src/cdp/measure/collectors/animation.ts +385 -0
- package/src/cdp/measure/collectors/ax.ts +278 -0
- package/src/cdp/measure/collectors/focus.ts +1041 -0
- package/src/cdp/measure/collectors/forms.ts +905 -0
- package/src/cdp/measure/collectors/geometry.ts +1038 -0
- package/src/cdp/measure/collectors/hittest.ts +835 -0
- package/src/cdp/measure/collectors/layers.ts +697 -0
- package/src/cdp/measure/collectors/media.ts +522 -0
- package/src/cdp/measure/collectors/pixels.ts +1584 -0
- package/src/cdp/measure/collectors/queries.ts +464 -0
- package/src/cdp/measure/collectors/scroll.ts +599 -0
- package/src/cdp/measure/collectors/states.ts +992 -0
- package/src/cdp/measure/collectors/style-provenance.ts +643 -0
- package/src/cdp/measure/collectors/styles.ts +355 -0
- package/src/cdp/measure/collectors/text.ts +713 -0
- package/src/cdp/measure/diff.ts +470 -0
- package/src/cdp/measure/explain.ts +498 -0
- package/src/cdp/measure/map-ax.ts +633 -0
- package/src/cdp/measure/map-focus.ts +188 -0
- package/src/cdp/measure/map-layers.ts +177 -0
- package/src/cdp/measure/map-scroll.ts +317 -0
- package/src/cdp/measure/redaction.ts +51 -0
- package/src/cdp/measure/settle.ts +1051 -0
- package/src/cdp/measure/snapshot.ts +390 -0
- package/src/cdp/measure/sweep.ts +330 -0
- package/src/cdp/measure/types.ts +220 -0
- package/src/cdp/measure/v2/collectors.ts +19 -0
- package/src/cdp/measure/v2/orchestrator.ts +46 -0
- package/src/cdp/measure/v2/snap-selection.ts +33 -0
- package/src/cdp/measure/v2/target-lock.ts +15 -0
- package/src/cdp/measure/v2/target.ts +16 -0
- package/src/cdp/motion/jank.ts +433 -0
- package/src/cdp/motion/mask.ts +421 -0
- package/src/cdp/motion/recorder.ts +764 -0
- package/src/cdp/motion/response.ts +356 -0
- package/src/cdp/motion/timeline.ts +215 -0
- package/src/cdp/record.ts +5 -212
- package/src/cdp/recorder-bridge.ts +1894 -0
- package/src/cdp/recorder-client.ts +326 -0
- package/src/cdp/scope-lock.ts +73 -0
- package/src/cdp/screenshot.ts +199 -66
- package/src/cdp/source-map.ts +309 -0
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +87 -0
- package/src/cdp/types.ts +87 -6
- package/src/cdp/viewport.ts +21 -0
- package/src/cdp.ts +12 -58
- package/src/command-help.ts +29 -0
- package/src/command-resolution.ts +22 -0
- package/src/command-walker.ts +35 -0
- package/src/contracts/a11y.ts +33 -0
- package/src/contracts/cursor.ts +35 -0
- package/src/contracts/index.ts +15 -0
- package/src/contracts/motion.ts +19 -0
- package/src/contracts/neutral.ts +56 -0
- package/src/contracts/primitives.ts +205 -0
- package/src/contracts/release.ts +17 -0
- package/src/contracts/results.ts +208 -0
- package/src/contracts/routes.ts +140 -0
- package/src/contracts/snapshot.ts +92 -0
- package/src/cursor/index.ts +13 -0
- package/src/cursor/service.ts +310 -0
- package/src/errors.ts +71 -0
- package/src/har-manager.ts +992 -41
- package/src/interact.ts +410 -98
- package/src/output/artifact-lifecycle.ts +371 -0
- package/src/output/artifact.ts +526 -0
- package/src/output/diff.ts +111 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +987 -0
- package/src/output/selector.ts +185 -0
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +481 -0
- package/src/session/commands.ts +1237 -368
- package/src/session/coordinator.ts +251 -0
- package/src/session/har-filter.ts +57 -0
- package/src/session/log-tailer.ts +653 -0
- package/src/session-context.ts +302 -34
- package/src/vault/bundle.ts +32 -17
- package/src/cdp/commands/exec.ts +0 -105
- package/src/cdp/commands/har.ts +0 -128
- package/src/cdp/commands/tabs.ts +0 -183
- package/src/cdp/commands/traffic.ts +0 -121
- package/src/cdp/commands/ui.ts +0 -172
- package/src/cdp/execute.ts +0 -113
- package/src/cdp/har-output.ts +0 -47
- package/src/cdp/usage.ts +0 -77
- package/src/tab-session.ts +0 -91
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared timing helpers for `measure`/`motion`. See the design's "Recorder timing model":
|
|
3
|
+
* the authoritative clock for every reported timestamp is `performance.now()`, relative to
|
|
4
|
+
* the recorder-armed marker (`t=0`). Screencast frames carry a wall-clock `metadata.timestamp`
|
|
5
|
+
* and Tracing events carry a monotonic `ts` in their own domains; the recorder bridge (U13/U14)
|
|
6
|
+
* converts both into the `performance.now()` domain by baseline-offset subtraction using the
|
|
7
|
+
* baseline this module reads.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CDPClient } from './client.js';
|
|
11
|
+
|
|
12
|
+
export interface BracketedTiming<T> {
|
|
13
|
+
result: T;
|
|
14
|
+
/** `performance.now()` immediately before `fn` ran. */
|
|
15
|
+
startPerformanceNow: number;
|
|
16
|
+
/** `performance.now()` immediately after `fn` resolved. */
|
|
17
|
+
endPerformanceNow: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs `fn`, bracketed by two `performance.now()` reads taken in the page's document execution
|
|
22
|
+
* context. Used to attribute a discrete action (e.g. an `Input.dispatch*` call) to a window in
|
|
23
|
+
* the recorder's authoritative `performance.now()` clock domain. The action's landmark label
|
|
24
|
+
* (`mark`, when the caller supplied one) is recorded host-side only — appended straight to
|
|
25
|
+
* `events.jsonl` alongside these two bracket numbers (see `../recorder-bridge.ts`'s `handleCdp`)
|
|
26
|
+
* — never written into the page's own PerformanceTimeline via `performance.mark()`: that page-
|
|
27
|
+
* visible side channel was removed (a predictable, page-observable channel the injected
|
|
28
|
+
* PerformanceObserver would otherwise re-observe as its own event, in violation of the
|
|
29
|
+
* observational-collector invariant against page-side channels).
|
|
30
|
+
*/
|
|
31
|
+
export async function withDocumentPerformanceNow<T>(client: CDPClient, fn: () => Promise<T>): Promise<BracketedTiming<T>> {
|
|
32
|
+
const startPerformanceNow = await readPerformanceNow(client);
|
|
33
|
+
const result = await fn();
|
|
34
|
+
const endPerformanceNow = await readPerformanceNow(client);
|
|
35
|
+
return { result, startPerformanceNow, endPerformanceNow };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readPerformanceNow(client: CDPClient): Promise<number> {
|
|
39
|
+
const evaluation = (await client.send('Runtime.evaluate', {
|
|
40
|
+
expression: 'performance.now()',
|
|
41
|
+
returnByValue: true,
|
|
42
|
+
})) as { result: { value?: unknown }; exceptionDetails?: unknown };
|
|
43
|
+
|
|
44
|
+
if (evaluation.exceptionDetails) {
|
|
45
|
+
throw new Error(`Failed to read performance.now(): ${JSON.stringify(evaluation.exceptionDetails)}`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof evaluation.result.value !== 'number') {
|
|
48
|
+
throw new Error('performance.now() did not return a number');
|
|
49
|
+
}
|
|
50
|
+
return evaluation.result.value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TraceClockBaseline {
|
|
54
|
+
/** `performance.now()` (document-relative ms) at the moment the baseline was captured. */
|
|
55
|
+
performanceNowMs: number;
|
|
56
|
+
/** `Date.now()` (wall-clock epoch ms) at the same moment. */
|
|
57
|
+
wallClockMs: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reads one synchronized (`performance.now()`, wall-clock) pair from the page in a single
|
|
62
|
+
* round trip. The recorder bridge reads this once when it arms (`rec-start`) as the anchor half
|
|
63
|
+
* of the three-way `markers.json` baseline; the other two members — the first screencast frame's
|
|
64
|
+
* `metadata.timestamp` (wall-clock seconds) and the first Tracing batch's earliest event `ts`
|
|
65
|
+
* (trace-clock microseconds) — are captured separately by the recorder bridge itself as those
|
|
66
|
+
* events arrive (not by this function, since neither exists yet at arm time). Post-processing
|
|
67
|
+
* converts frame-time and trace-time into the `performance.now()` domain by subtracting this
|
|
68
|
+
* baseline's wall-clock offset.
|
|
69
|
+
*/
|
|
70
|
+
export async function readTraceClockBaseline(client: CDPClient): Promise<TraceClockBaseline> {
|
|
71
|
+
const evaluation = (await client.send('Runtime.evaluate', {
|
|
72
|
+
expression: '({ performanceNowMs: performance.now(), wallClockMs: Date.now() })',
|
|
73
|
+
returnByValue: true,
|
|
74
|
+
})) as {
|
|
75
|
+
result: { value?: { performanceNowMs?: unknown; wallClockMs?: unknown } };
|
|
76
|
+
exceptionDetails?: unknown;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (evaluation.exceptionDetails) {
|
|
80
|
+
throw new Error(`Failed to read trace clock baseline: ${JSON.stringify(evaluation.exceptionDetails)}`);
|
|
81
|
+
}
|
|
82
|
+
const value = evaluation.result.value;
|
|
83
|
+
if (!value || typeof value.performanceNowMs !== 'number' || typeof value.wallClockMs !== 'number') {
|
|
84
|
+
throw new Error('Trace clock baseline evaluation did not return the expected {performanceNowMs, wallClockMs} shape');
|
|
85
|
+
}
|
|
86
|
+
return { performanceNowMs: value.performanceNowMs, wallClockMs: value.wallClockMs };
|
|
87
|
+
}
|
package/src/cdp/types.ts
CHANGED
|
@@ -12,24 +12,37 @@ export interface ParsedArgs {
|
|
|
12
12
|
port?: number;
|
|
13
13
|
out?: string;
|
|
14
14
|
json?: boolean;
|
|
15
|
-
interactive?: boolean;
|
|
16
|
-
harOut?: string;
|
|
17
|
-
record?: boolean;
|
|
18
15
|
duration?: number;
|
|
19
16
|
settle?: number;
|
|
20
17
|
file?: string;
|
|
21
|
-
|
|
18
|
+
/** Session-filled internal slot: NOT CLI-settable (no --har flag, no env
|
|
19
|
+
* var). Auto-filled from the active session's harId so withConnection()
|
|
20
|
+
* can auto-append recorded traffic to the session HAR. */
|
|
22
21
|
har?: string;
|
|
23
22
|
new?: boolean;
|
|
24
23
|
target?: string;
|
|
24
|
+
/** Where `target` was assigned from: an actual `--target` flag, active-session
|
|
25
|
+
* autofill, or the ambient `CDP_TARGET` env var. Recorded at the assignment
|
|
26
|
+
* point — browser-scope CDP (`cdp --browser`) attaches a flattened target
|
|
27
|
+
* session only when this is `'flag'`; provenance is never inferred later by
|
|
28
|
+
* comparing final strings. */
|
|
29
|
+
targetSource?: 'flag' | 'session' | 'env';
|
|
25
30
|
url?: string;
|
|
26
|
-
role?: string;
|
|
27
31
|
into?: string;
|
|
28
32
|
noScreenshot?: boolean;
|
|
29
33
|
viewport?: string;
|
|
30
34
|
fullPage?: boolean;
|
|
31
|
-
height?: number;
|
|
32
35
|
help?: boolean;
|
|
36
|
+
/** `--all` — boolean scope-widener (per-leaf semantics). */
|
|
37
|
+
all?: boolean;
|
|
38
|
+
/** `--session <id>` — explicit session id override (per-leaf semantics). */
|
|
39
|
+
session?: string;
|
|
40
|
+
/** `session start --hold` — hold one CDP browser connection open for the session's lifetime. */
|
|
41
|
+
hold?: boolean;
|
|
42
|
+
/** `session view --filter <section>` — show only one bundle-manifest section. */
|
|
43
|
+
filter?: string;
|
|
44
|
+
/** `session log --name <label>` */
|
|
45
|
+
name?: string;
|
|
33
46
|
filterUrl?: string;
|
|
34
47
|
filterStatus?: string;
|
|
35
48
|
filterMethod?: string;
|
|
@@ -39,4 +52,72 @@ export interface ParsedArgs {
|
|
|
39
52
|
waitEvent?: string;
|
|
40
53
|
timeoutMs?: number;
|
|
41
54
|
socket?: string;
|
|
55
|
+
|
|
56
|
+
// -- measure/motion branch flags (settled in the capture measure+motion
|
|
57
|
+
// build plan's U02) -- every leaf under `capture measure`/`capture motion`
|
|
58
|
+
// parses these from the shared global parser instead of re-parsing argv
|
|
59
|
+
// itself.
|
|
60
|
+
/** `snap --freeze-animations` — pause CSS/WAAPI animation before capture. */
|
|
61
|
+
freezeAnimations?: boolean;
|
|
62
|
+
/** `snap --settle-timeout <ms>` — override the default 5000ms settle wait. */
|
|
63
|
+
settleTimeout?: number;
|
|
64
|
+
/** `snap --capture-unsettled` — write full substrate despite non-settlement. */
|
|
65
|
+
captureUnsettled?: boolean;
|
|
66
|
+
/** `snap|diff|mask --pixels` — include raster crop/diff facts. */
|
|
67
|
+
pixels?: boolean;
|
|
68
|
+
/** `snap --state <state[:selector]>` — repeatable; one entry per occurrence. */
|
|
69
|
+
state?: string[];
|
|
70
|
+
/** `check --for <list>` — comma-separated check names or categories. */
|
|
71
|
+
for?: string;
|
|
72
|
+
/** `diff --before <snap>` */
|
|
73
|
+
before?: string;
|
|
74
|
+
/** `diff --after <snap>` */
|
|
75
|
+
after?: string;
|
|
76
|
+
/** `diff --full` — expand to the complete state-matrix/per-element diff. */
|
|
77
|
+
full?: boolean;
|
|
78
|
+
/** `check|diff --gate` — exit 2 on findings/changes instead of 0. */
|
|
79
|
+
gate?: boolean;
|
|
80
|
+
/** `census --snap <id>` — repeatable; one entry per occurrence. */
|
|
81
|
+
snap?: string[];
|
|
82
|
+
/** `census --url <url>` — repeatable accumulator alongside the existing
|
|
83
|
+
* single-value `url` field above (which stays last-wins for every
|
|
84
|
+
* existing command's target-matching semantics). */
|
|
85
|
+
urls?: string[];
|
|
86
|
+
/** `census --set-file <path>` */
|
|
87
|
+
setFile?: string;
|
|
88
|
+
/** `census|sweep --axis <axis>` */
|
|
89
|
+
axis?: string;
|
|
90
|
+
/** `sweep --from <val>` — raw string; axis units vary (px/dpr/zoom). */
|
|
91
|
+
from?: string;
|
|
92
|
+
/** `sweep --to <val>` — raw string; axis units vary (px/dpr/zoom). */
|
|
93
|
+
to?: string;
|
|
94
|
+
/** `sweep --viewport-height <val>` — raw string for the viewport-height sweep axis. */
|
|
95
|
+
viewportHeight?: string;
|
|
96
|
+
/** `motion rec --stop --rec-id <id>` — explicit recording id override. */
|
|
97
|
+
recId?: string;
|
|
98
|
+
/** `check|snap|census --viewport WxH` — repeatable accumulator alongside
|
|
99
|
+
* the existing single-value `viewport` field above (last-wins, unchanged). */
|
|
100
|
+
viewports?: string[];
|
|
101
|
+
/** `explain|map focus|... --selector <sel>` */
|
|
102
|
+
selector?: string;
|
|
103
|
+
/** `explain --size` — include size/layout provenance detail. */
|
|
104
|
+
size?: boolean;
|
|
105
|
+
/** `explain --text` — include text/line-box detail. */
|
|
106
|
+
text?: boolean;
|
|
107
|
+
/** `explain --form` — include form/caret/autofill detail. */
|
|
108
|
+
form?: boolean;
|
|
109
|
+
/** `motion rec --start` — arm the composed recorder. */
|
|
110
|
+
start?: boolean;
|
|
111
|
+
/** `motion rec --stop` — finalize the composed recorder. */
|
|
112
|
+
stop?: boolean;
|
|
113
|
+
/** `motion rec --do <action>` — one-shot scripted action. */
|
|
114
|
+
do?: string;
|
|
115
|
+
/** `motion timeline --element <sel>` */
|
|
116
|
+
element?: string;
|
|
117
|
+
/** `motion timeline --prop <name>` */
|
|
118
|
+
prop?: string;
|
|
119
|
+
/** `motion response --action <action>` */
|
|
120
|
+
action?: string;
|
|
121
|
+
/** `motion response --occurrence <n>` — 1-based occurrence of a repeated action label. */
|
|
122
|
+
occurrence?: number;
|
|
42
123
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { invalidInput } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
export interface Viewport {
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const VIEWPORT_PATTERN = /^([1-9]\d*)x([1-9]\d*)$/;
|
|
9
|
+
|
|
10
|
+
/** Parses the one viewport grammar used by every live capture command. */
|
|
11
|
+
export function parseViewport(value: string): Viewport {
|
|
12
|
+
const match = VIEWPORT_PATTERN.exec(value);
|
|
13
|
+
if (match) {
|
|
14
|
+
const width = Number(match[1]);
|
|
15
|
+
const height = Number(match[2]);
|
|
16
|
+
if (Number.isSafeInteger(width) && Number.isSafeInteger(height)) {
|
|
17
|
+
return { width, height };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
throw invalidInput(`--viewport must match <positive-safe-int>x<positive-safe-int> using lowercase x with no whitespace; received "${value}"`, 'invalid_viewport');
|
|
21
|
+
}
|
package/src/cdp.ts
CHANGED
|
@@ -1,38 +1,16 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
1
|
/**
|
|
3
|
-
* CDP (Chrome DevTools Protocol) —
|
|
2
|
+
* CDP (Chrome DevTools Protocol) — the library barrel over the CDP substrate.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* capture screenshot Capture screenshot
|
|
16
|
-
* capture a11y Get accessibility tree
|
|
17
|
-
* capture record Passive HAR recording
|
|
18
|
-
* capture navigate <url> Navigate + record HAR
|
|
19
|
-
* capture har create|read|delete Manage HAR recordings
|
|
20
|
-
*
|
|
21
|
-
* Options:
|
|
22
|
-
* --port <port> Override CDP port (auto-detects if not specified)
|
|
23
|
-
* --target <tabId> Target tab by exact ID (preferred, parallel-safe)
|
|
24
|
-
* --new Force open a new tab (open command)
|
|
25
|
-
* --record Enable HAR recording (exec)
|
|
26
|
-
* --har <id> Append traffic to a HAR recording
|
|
27
|
-
* --har-out <path> HAR output path
|
|
28
|
-
* --file <path> Read JS from file (exec)
|
|
29
|
-
* --duration <secs> Recording duration (record, default: 10)
|
|
30
|
-
* --settle <ms> Settle time after navigation (navigate, default: 2000)
|
|
31
|
-
* --out <path> Output path (screenshot)
|
|
32
|
-
* --height <px> Override viewport height (screenshot)
|
|
33
|
-
* --full-page Capture entire scrollable page (screenshot)
|
|
34
|
-
* --json JSON output (a11y)
|
|
35
|
-
* --interactive Interactive elements only (a11y)
|
|
4
|
+
* Re-exports the client, target discovery, screenshot, a11y, console-recording,
|
|
5
|
+
* and tab-lock building blocks, plus `cdpMain` (the routed command dispatch).
|
|
6
|
+
* HAR ownership is NOT exported here: the session HAR manager
|
|
7
|
+
* (`src/har-manager.ts`) fed by the internal streaming recorder
|
|
8
|
+
* (`src/cdp/har-recorder.ts`, a direct import of the recorder bridge) is the
|
|
9
|
+
* sole HAR lane.
|
|
10
|
+
* The command surface itself lives under `src/cdp/commands/` and is routed by
|
|
11
|
+
* `src/capture.ts` → `src/cdp/dispatch.ts`. This module is a pure barrel:
|
|
12
|
+
* `src/capture.ts` is the ONLY entrypoint and the ONLY error/termination
|
|
13
|
+
* boundary — nothing here executes, renders, or exits.
|
|
36
14
|
*/
|
|
37
15
|
|
|
38
16
|
export { CDPClient } from './cdp/client.js';
|
|
@@ -42,32 +20,8 @@ export { listTargets, findTab, findTabById, openTab } from './cdp/targets.js';
|
|
|
42
20
|
export { captureScreenshot } from './cdp/screenshot.js';
|
|
43
21
|
export type { A11yNode } from './cdp/a11y.js';
|
|
44
22
|
export { getAccessibilityTree } from './cdp/a11y.js';
|
|
45
|
-
export {
|
|
46
|
-
export type { ExecuteOptions, ExecuteResult } from './cdp/execute.js';
|
|
47
|
-
export { recordTraffic, navigateAndRecord, navigateAndWait } from './cdp/record.js';
|
|
48
|
-
export type { RecordOptions, RecordResult, NavigateAndRecordOptions, NavigateAndRecordResult } from './cdp/record.js';
|
|
49
|
-
export { HARRecorder } from './cdp/har-recorder.js';
|
|
23
|
+
export { navigateAndWait } from './cdp/record.js';
|
|
50
24
|
export { ConsoleRecorder } from './cdp/console-recorder.js';
|
|
51
25
|
export type { ConsoleEntry } from './cdp/console-recorder.js';
|
|
52
26
|
export { acquireTabLock, isTabLocked, releaseTabLock, withTabLock } from './cdp/tab-lock.js';
|
|
53
27
|
export { cdpMain } from './cdp/dispatch.js';
|
|
54
|
-
|
|
55
|
-
// Only run CLI when executed directly (not imported via capture.ts)
|
|
56
|
-
const isMainModule =
|
|
57
|
-
process.argv[1]?.endsWith('cdp.ts') ||
|
|
58
|
-
process.argv[1]?.endsWith('cdp.mjs');
|
|
59
|
-
if (isMainModule) {
|
|
60
|
-
cdpMain()
|
|
61
|
-
.then(() => {
|
|
62
|
-
// Flush stdout before exiting — process.exit() doesn't wait for stream drain
|
|
63
|
-
if (process.stdout.writableNeedDrain) {
|
|
64
|
-
process.stdout.once('drain', () => process.exit(0));
|
|
65
|
-
} else {
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
.catch((err) => {
|
|
70
|
-
console.error(err.message);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Pure specification-help assembly from Capture's descriptor tree. */
|
|
2
|
+
import { type RegistryNode } from './registry.js';
|
|
3
|
+
|
|
4
|
+
const usage = (node: RegistryNode): string => `capture${node.path ? ` ${node.path}` : ''}`;
|
|
5
|
+
const parameter = (name: string, grammar: string, required: boolean, variadic = false): string => ` ${required ? '<' : '['}${name}${required ? '>' : ']'} ${grammar}${variadic ? ' (variadic)' : ''}`;
|
|
6
|
+
|
|
7
|
+
/** Renders only descriptor-owned content; help never parses inputs or performs effects. */
|
|
8
|
+
export function assembleHelp(node: RegistryNode): string {
|
|
9
|
+
const { help } = node;
|
|
10
|
+
const lines = [usage(node), '', help.description, '', `When to use: ${help.whenToUse}`, `Model: ${help.model ?? help.rubric}`];
|
|
11
|
+
if (help.globals) lines.push('', 'Globals:', help.globals);
|
|
12
|
+
if (help.ioContract) lines.push('', `I/O contract: ${help.ioContract}`);
|
|
13
|
+
if (node.kind === 'branch') {
|
|
14
|
+
const children = node.children.filter((child) => child.visibility === 'public');
|
|
15
|
+
if (children.length) {
|
|
16
|
+
lines.push('', 'Commands:');
|
|
17
|
+
for (const child of children) lines.push(` ${child.path.split(' ').at(-1)} ${child.help.description} When to use: ${child.help.whenToUse}`);
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
if (node.positionals.length || node.flags.length) {
|
|
21
|
+
lines.push('', 'Parameters:');
|
|
22
|
+
for (const positional of node.positionals) lines.push(parameter(positional.name, positional.grammar, positional.required, positional.variadic));
|
|
23
|
+
for (const flag of node.flags) lines.push(` ${flag.name}${flag.grammar === 'boolean' ? '' : ` <${flag.grammar}>`}${flag.required ? ' required' : ''}${flag.repeatable ? ' repeatable' : ''}${flag.default === undefined ? '' : ` default=${flag.default}`}${flag.values ? ` values=${flag.values.join('|')}` : ''}${flag.units ? ` units=${flag.units}` : ''}`);
|
|
24
|
+
}
|
|
25
|
+
lines.push('', `Constraints: ${help.constraints}`, `Output: ${help.orderedOutput}`, `Artifact ownership: ${help.artifactOwnership}`, `Effects: browser=${node.effects.browser}, session=${node.effects.session}, artifact=${node.effects.artifact}, environment=${node.effects.environment}`, `Recovery: ${node.recovery}`);
|
|
26
|
+
}
|
|
27
|
+
lines.push('', `Next action: ${help.followUp}`);
|
|
28
|
+
return `${lines.join('\n')}\n`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Pure pre-dispatch control precedence for U15's future launcher wiring. */
|
|
2
|
+
import { assembleHelp } from './command-help.js';
|
|
3
|
+
import { hasHelpFlag, hasRootVersionFlag, walkCommand } from './command-walker.js';
|
|
4
|
+
import { CAPTURE_REGISTRY, type RegistryBranch, type RegistryNode } from './registry.js';
|
|
5
|
+
|
|
6
|
+
export type CommandResolution =
|
|
7
|
+
| { readonly kind: 'version' }
|
|
8
|
+
| { readonly kind: 'help'; readonly node: RegistryNode; readonly text: string }
|
|
9
|
+
| { readonly kind: 'dispatch'; readonly node: RegistryNode; readonly argv: readonly string[] }
|
|
10
|
+
| { readonly kind: 'unknown-path'; readonly received: string; readonly expected: readonly string[]; readonly path: string };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Applies only control precedence. It deliberately does not parse leaf argv,
|
|
14
|
+
* validate `--json`, invoke a handler, or allocate any effectful dependency.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveCommand(argv: readonly string[], root: RegistryBranch = CAPTURE_REGISTRY): CommandResolution {
|
|
17
|
+
if (hasRootVersionFlag(argv)) return { kind: 'version' };
|
|
18
|
+
const walked = walkCommand(argv, root);
|
|
19
|
+
if (walked.kind === 'unknown-path') return { kind: 'unknown-path', received: walked.received, expected: walked.expected, path: walked.resolved.path };
|
|
20
|
+
if (hasHelpFlag(walked.remaining)) return { kind: 'help', node: walked.node, text: assembleHelp(walked.node) };
|
|
21
|
+
return { kind: 'dispatch', node: walked.node, argv: walked.remaining };
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Pure exact command-path resolution for the descriptor registry. */
|
|
2
|
+
import { CAPTURE_REGISTRY, type RegistryBranch, type RegistryNode } from './registry.js';
|
|
3
|
+
|
|
4
|
+
export type WalkResult =
|
|
5
|
+
| { readonly kind: 'resolved'; readonly node: RegistryNode; readonly remaining: readonly string[] }
|
|
6
|
+
| { readonly kind: 'unknown-path'; readonly received: string; readonly resolved: RegistryBranch; readonly expected: readonly string[]; readonly remaining: readonly string[] };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves only exact canonical path tokens. It stops before the first option,
|
|
10
|
+
* resolves the deepest known leaf/branch, and leaves all leaf argv untouched.
|
|
11
|
+
* Parsing, output compatibility, effects, and handler dispatch are intentionally
|
|
12
|
+
* outside this pure walker.
|
|
13
|
+
*/
|
|
14
|
+
export function walkCommand(argv: readonly string[], root: RegistryBranch = CAPTURE_REGISTRY): WalkResult {
|
|
15
|
+
let current: RegistryNode = root;
|
|
16
|
+
let index = 0;
|
|
17
|
+
while (current.kind === 'branch' && index < argv.length) {
|
|
18
|
+
const token = argv[index];
|
|
19
|
+
if (token.startsWith('-')) break;
|
|
20
|
+
const child: RegistryNode | undefined = current.children.find((candidate: RegistryNode) => candidate.path.split(' ').at(-1) === token);
|
|
21
|
+
if (!child) return { kind: 'unknown-path', received: token, resolved: current, expected: current.children.filter((candidate) => candidate.visibility === 'public').map((candidate) => candidate.path.split(' ').at(-1)!), remaining: argv.slice(index) };
|
|
22
|
+
current = child;
|
|
23
|
+
index += 1;
|
|
24
|
+
}
|
|
25
|
+
return { kind: 'resolved', node: current, remaining: argv.slice(index) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hasHelpFlag(argv: readonly string[]): boolean {
|
|
29
|
+
return argv.includes('-h') || argv.includes('--help');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Root version is the sole accepted version spelling and wins before walking. */
|
|
33
|
+
export function hasRootVersionFlag(argv: readonly string[]): boolean {
|
|
34
|
+
return argv[0] === '--version';
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Frozen retained accessibility tree contract (U1), with pure validators only. */
|
|
2
|
+
import { Availability, OK, ValidationResult, combine, fail, isObject } from './primitives.js';
|
|
3
|
+
|
|
4
|
+
export interface AxRectCoverage { readonly eligible: number; readonly attempted: number; readonly succeeded: number; readonly failed: number; readonly skipped: number; }
|
|
5
|
+
/** Every retained AX node is source ordered, including ignored/unnamed nodes, and preserves evidence verbatim. */
|
|
6
|
+
export interface AxTreeNode { readonly axId: string; readonly sourceOrdinal: number; readonly role?: string; readonly name?: string; readonly description?: string; readonly value?: unknown; readonly ignored: boolean; readonly cdpNodeId?: number; readonly backendNodeId?: number; readonly parentAxId?: string; readonly childAxIds: readonly string[]; readonly rect: Availability<unknown>; readonly raw: Record<string, unknown>; }
|
|
7
|
+
/** Exhaustive retained store: first <=5000 source-array nodes, no semantic filtering. */
|
|
8
|
+
export interface AxTreeManifest { readonly schemaVersion: 1; readonly treeId: string; readonly source: 'live' | 'snapshot'; readonly snapshotId?: string; readonly nodes: readonly AxTreeNode[]; readonly sourceTotal?: number; readonly retained: number; readonly dropped?: number; readonly droppedUnknown?: boolean; readonly rectCoverage: AxRectCoverage; readonly iframeExclusion: Availability<null>; }
|
|
9
|
+
/** Sidecar meta at `<owner>/a11y/trees/<tree-id>/meta.json`; tree lifetime equals file lifetime. */
|
|
10
|
+
export interface AxTreeReference { readonly schemaVersion: 1; readonly treeId: string; readonly treePath: string; readonly metaPath: string; readonly owner: string; readonly retained: number; readonly coverage: Pick<AxTreeManifest, 'sourceTotal' | 'dropped' | 'droppedUnknown' | 'rectCoverage'>; }
|
|
11
|
+
export function validateAxTreeManifest(value: unknown): ValidationResult {
|
|
12
|
+
if (!isObject(value)) return fail('a11y tree must be an object');
|
|
13
|
+
const errs: ValidationResult[] = [];
|
|
14
|
+
if (value.schemaVersion !== 1) errs.push(fail('a11y tree schemaVersion must be 1'));
|
|
15
|
+
if (typeof value.treeId !== 'string' || !value.treeId) errs.push(fail('a11y tree missing treeId'));
|
|
16
|
+
if (value.source !== 'live' && value.source !== 'snapshot') errs.push(fail('a11y tree source must be live or snapshot'));
|
|
17
|
+
if (!isObject(value.rectCoverage)) errs.push(fail('a11y tree missing rectCoverage'));
|
|
18
|
+
if (!isObject(value.iframeExclusion)) errs.push(fail('a11y tree missing iframeExclusion'));
|
|
19
|
+
if (!Array.isArray(value.nodes)) errs.push(fail('a11y tree requires nodes array'));
|
|
20
|
+
else {
|
|
21
|
+
if (value.nodes.length > 5000) errs.push(fail('a11y tree retains more than 5000 nodes'));
|
|
22
|
+
const ids = new Set<string>();
|
|
23
|
+
for (const node of value.nodes) {
|
|
24
|
+
if (!isObject(node) || typeof node.axId !== 'string') { errs.push(fail('a11y tree has malformed node')); continue; }
|
|
25
|
+
if (ids.has(node.axId)) errs.push(fail(`a11y tree duplicate retained AX ID: ${node.axId}`));
|
|
26
|
+
ids.add(node.axId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (typeof value.retained !== 'number') errs.push(fail('a11y tree missing retained count'));
|
|
30
|
+
else if (Array.isArray(value.nodes) && value.retained !== value.nodes.length) errs.push(fail('a11y tree retained count must equal node count'));
|
|
31
|
+
if (typeof value.sourceTotal === 'number' && typeof value.dropped === 'number' && typeof value.retained === 'number' && value.sourceTotal !== value.retained + value.dropped) errs.push(fail('a11y tree sourceTotal must equal retained + dropped'));
|
|
32
|
+
return combine(...errs);
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Frozen immutable index/cursor contract (U1), pure types/validators only. */
|
|
2
|
+
import { MAX_CURSOR_BYTES, OK, ValidationResult, combine, fail, isAscii, isObject, isSha256Hex } from './primitives.js';
|
|
3
|
+
|
|
4
|
+
/** Exactly the five public leaves permitted to materialize immutable cursor indexes. */
|
|
5
|
+
export type CursorLeaf = 'browser detect' | 'browser list' | 'library list' | 'library search' | 'library show';
|
|
6
|
+
export interface ImmutableIndex<Row = unknown> { readonly schemaVersion: 1; readonly leaf: CursorLeaf; readonly path: string; readonly digest: string; readonly createdAt: string; readonly expiresAt: string; readonly filter: Record<string, unknown>; readonly pageSize: number; readonly order: string; readonly coverage: unknown; readonly rows: readonly Row[]; }
|
|
7
|
+
/** Authenticated opaque cursor claims. The serialized token itself is <=2048 ASCII bytes. */
|
|
8
|
+
export interface CursorClaims { readonly schemaVersion: 1; readonly indexDigest: string; readonly leaf: CursorLeaf; readonly filter: Record<string, unknown>; readonly nextExclusiveOrdinal: number; readonly pageSize: number; readonly expiresAt: string; readonly mac: string; }
|
|
9
|
+
export function validateImmutableIndex(value: unknown): ValidationResult {
|
|
10
|
+
if (!isObject(value)) return fail('immutable index must be an object');
|
|
11
|
+
const errs: ValidationResult[] = [];
|
|
12
|
+
if (value.schemaVersion !== 1) errs.push(fail('index schemaVersion must be 1'));
|
|
13
|
+
if (typeof value.digest !== 'string' || !isSha256Hex(value.digest)) errs.push(fail('index digest must be lowercase SHA-256 hex'));
|
|
14
|
+
if (!Array.isArray(value.rows)) errs.push(fail('index requires rows array'));
|
|
15
|
+
if (!(typeof value.pageSize === 'number' && value.pageSize >= 1 && value.pageSize <= 20)) errs.push(fail('index pageSize must be 1..20'));
|
|
16
|
+
return combine(...errs);
|
|
17
|
+
}
|
|
18
|
+
export function validateCursorToken(token: string): ValidationResult {
|
|
19
|
+
if (!isAscii(token)) return fail('cursor must be ASCII');
|
|
20
|
+
if (Buffer.byteLength(token, 'ascii') > MAX_CURSOR_BYTES) return fail(`cursor exceeds ${MAX_CURSOR_BYTES} ASCII bytes`);
|
|
21
|
+
return OK;
|
|
22
|
+
}
|
|
23
|
+
export function validateCursorClaims(value: unknown): ValidationResult {
|
|
24
|
+
if (!isObject(value)) return fail('cursor claims must be an object');
|
|
25
|
+
const errs: ValidationResult[] = [];
|
|
26
|
+
if (value.schemaVersion !== 1) errs.push(fail('cursor schemaVersion must be 1'));
|
|
27
|
+
if (typeof value.indexDigest !== 'string' || !isSha256Hex(value.indexDigest)) errs.push(fail('cursor missing valid indexDigest'));
|
|
28
|
+
if (!['browser detect', 'browser list', 'library list', 'library search', 'library show'].includes(value.leaf as string)) errs.push(fail('cursor has invalid leaf'));
|
|
29
|
+
if (!isObject(value.filter)) errs.push(fail('cursor missing filter object'));
|
|
30
|
+
if (typeof value.expiresAt !== 'string' || Number.isNaN(Date.parse(value.expiresAt))) errs.push(fail('cursor missing valid expiresAt'));
|
|
31
|
+
if (!(typeof value.nextExclusiveOrdinal === 'number' && Number.isSafeInteger(value.nextExclusiveOrdinal) && value.nextExclusiveOrdinal >= 0)) errs.push(fail('cursor nextExclusiveOrdinal must be nonnegative safe integer'));
|
|
32
|
+
if (!(typeof value.pageSize === 'number' && value.pageSize >= 1 && value.pageSize <= 20)) errs.push(fail('cursor pageSize must be 1..20'));
|
|
33
|
+
if (typeof value.mac !== 'string' || !value.mac) errs.push(fail('cursor missing MAC'));
|
|
34
|
+
return combine(...errs);
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* U1 frozen Capture hard-cut contract surface. Import from this module for
|
|
3
|
+
* descriptor/result/schema types; it has no browser, CLI, output, or process
|
|
4
|
+
* side effects and remains intentionally unreachable from `src/capture.ts`
|
|
5
|
+
* until the atomic U15 public cutover.
|
|
6
|
+
*/
|
|
7
|
+
export * from './primitives.js';
|
|
8
|
+
export * from './results.js';
|
|
9
|
+
export * from './routes.js';
|
|
10
|
+
export * from './snapshot.js';
|
|
11
|
+
export * from './neutral.js';
|
|
12
|
+
export * from './a11y.js';
|
|
13
|
+
export * from './cursor.js';
|
|
14
|
+
export * from './motion.js';
|
|
15
|
+
export * from './release.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Frozen motion observations schema-2 contract (U1), pure types/validators only. */
|
|
2
|
+
import { Availability, OK, ValidationResult, combine, fail, isObject } from './primitives.js';
|
|
3
|
+
|
|
4
|
+
export const MOTION_OBSERVATION_FAMILIES = ['source-record', 'trace-event', 'frame-interval', 'frame-pixel-delta', 'element-rect-delta', 'observer-rect-delta'] as const;
|
|
5
|
+
export type MotionObservationFamily = (typeof MOTION_OBSERVATION_FAMILIES)[number];
|
|
6
|
+
/** Source ordinals are contiguous across the entire recording before final write. */
|
|
7
|
+
export interface MotionObservation { readonly id: string; readonly family: MotionObservationFamily; readonly sourceOrdinal: number; readonly kindRank: number; readonly childRank: number; readonly values: Record<string, unknown>; readonly availability: Availability<null>; readonly provenance: { readonly recordingId: string; readonly artifactPath: string; readonly jsonPointer: string }; }
|
|
8
|
+
export interface MotionObservationsManifest { readonly schemaVersion: 2; readonly recordingId: string; readonly sourceSchemaVersion: 2; readonly observations: readonly MotionObservation[]; readonly completeWithinRetainedCoverage: true; readonly ordering: '(sourceOrdinal,kind-rank,child-rank)'; }
|
|
9
|
+
export function validateMotionObservationsManifest(value: unknown): ValidationResult {
|
|
10
|
+
if (!isObject(value)) return fail('motion observations must be an object');
|
|
11
|
+
const errs: ValidationResult[] = [];
|
|
12
|
+
if (value.schemaVersion !== 2 || value.sourceSchemaVersion !== 2) errs.push(fail('motion observations requires recording/source schemaVersion 2'));
|
|
13
|
+
if (typeof value.recordingId !== 'string' || !value.recordingId) errs.push(fail('motion observations missing recordingId'));
|
|
14
|
+
if (value.ordering !== '(sourceOrdinal,kind-rank,child-rank)') errs.push(fail('motion observations has wrong ordering declaration'));
|
|
15
|
+
if (value.completeWithinRetainedCoverage !== true) errs.push(fail('motion observations must attest retained coverage completeness'));
|
|
16
|
+
if (!Array.isArray(value.observations)) errs.push(fail('motion observations requires array'));
|
|
17
|
+
else for (const observation of value.observations) if (!isObject(observation) || typeof observation.id !== 'string' || !MOTION_OBSERVATION_FAMILIES.includes(observation.family as MotionObservationFamily) || !Number.isSafeInteger(observation.sourceOrdinal)) errs.push(fail('motion observations contains malformed observation'));
|
|
18
|
+
return combine(...errs);
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Frozen neutral measurement derived-read contracts (U1), pure types/validators only. */
|
|
2
|
+
import { Availability, OK, ValidationResult, combine, fail, isObject } from './primitives.js';
|
|
3
|
+
|
|
4
|
+
/** The eleven canonical family order is semantic and deterministic. */
|
|
5
|
+
export const NEUTRAL_FAMILIES = [
|
|
6
|
+
'viewport-box-position', 'element-scroll-extent', 'native-interactive-box-size',
|
|
7
|
+
'computed-color-pair-contrast', 'sampled-hit-reception', 'text-inline-extent',
|
|
8
|
+
'form-control-state', 'media-state', 'animation-state',
|
|
9
|
+
'opaque-background-sibling-box-intersection',
|
|
10
|
+
'css-overflow-visible-descendant-peer-box-intersection',
|
|
11
|
+
] as const;
|
|
12
|
+
export type NeutralFamily = (typeof NEUTRAL_FAMILIES)[number];
|
|
13
|
+
|
|
14
|
+
export interface NumericSummary { readonly count: number; readonly unavailableCount: number; readonly min: number | null; readonly p25: number | null; readonly p50: number | null; readonly p75: number | null; readonly max: number | null; }
|
|
15
|
+
export interface FactProvenance { readonly snapshotId: string; readonly sources: readonly string[]; readonly artifactPath: string; readonly jsonPointer: string; }
|
|
16
|
+
export interface NeutralFact { readonly id: string; readonly family: NeutralFamily; readonly schemaVersion: 1; readonly subjects: readonly string[]; readonly values: Record<string, unknown>; readonly units: Record<string, string>; readonly availability: Availability<null>; readonly provenance: FactProvenance; }
|
|
17
|
+
export interface FamilyCoverage { readonly populationDefinition: string; readonly schemaVersion: 1; readonly sources: readonly string[]; readonly availability: Availability<null>; readonly populationCount: number; readonly measuredCount: number; readonly unavailableCount: number; readonly sourceTotal?: number; readonly kept: number; readonly dropped?: number; readonly droppedUnknown?: boolean; readonly caps: readonly string[]; readonly exclusions: readonly string[]; readonly unavailableReasons: Readonly<Record<string, number>>; }
|
|
18
|
+
export interface NeutralFamilyStore { readonly family: NeutralFamily; readonly coverage: FamilyCoverage; readonly numericSummaries: Readonly<Record<string, NumericSummary>>; readonly categoricalMaps: Readonly<Record<string, Readonly<Record<string, number>>>>; readonly relationCandidateAccounting?: Record<string, number>; readonly facts: readonly NeutralFact[]; }
|
|
19
|
+
/** Exhaustive retained measurement evidence, never a bounded display projection. */
|
|
20
|
+
export interface FactsManifest { readonly schemaVersion: 1; readonly snapshotIds: readonly string[]; readonly familyOrder: readonly NeutralFamily[]; readonly families: readonly NeutralFamilyStore[]; readonly completeWithinRetainedCoverage: true; }
|
|
21
|
+
|
|
22
|
+
/** One merged snapshot-local subject; collisions never merge subjects. */
|
|
23
|
+
export interface SelectorSubject { readonly subjectId: string; readonly snapshotId: string; readonly geometryOrdinal?: number; readonly axSourceOrdinal?: number; readonly textSourceOrdinal?: number; readonly backendNodeId?: number; readonly css?: readonly string[]; readonly axIds?: readonly string[]; readonly axNames?: readonly string[]; readonly text?: readonly string[]; }
|
|
24
|
+
export interface SelectorsManifest { readonly schemaVersion: 1; readonly snapshotIds: readonly string[]; readonly order: 'geometry-ordinal-then-ax-document-order-then-text-capture-order'; readonly subjects: readonly SelectorSubject[]; }
|
|
25
|
+
|
|
26
|
+
/** Source occurrence attested by a derived read. SHA-256 is over source `meta.json` bytes. */
|
|
27
|
+
export interface ReadSourceOccurrence { readonly snapshotId: string; readonly canonicalPath: string; readonly metaSchemaVersion: 1 | 2; readonly metaSha256: string; }
|
|
28
|
+
export interface DerivedReadManifest { readonly schemaVersion: 1; readonly readId: string; readonly owner: string; readonly operation: string; readonly sources: readonly ReadSourceOccurrence[]; readonly factsPath: string; readonly selectorsPath: string; readonly familyCounts: Readonly<Record<string, number>>; readonly factOrderVersion: 1; readonly selectorOrderVersion: 1; readonly completeWithinRetainedCoverage: true; readonly cropsPath?: string; }
|
|
29
|
+
|
|
30
|
+
export function validateFactsManifest(value: unknown): ValidationResult {
|
|
31
|
+
if (!isObject(value)) return fail('facts manifest must be an object');
|
|
32
|
+
const errs: ValidationResult[] = [];
|
|
33
|
+
if (value.schemaVersion !== 1) errs.push(fail('facts schemaVersion must be 1'));
|
|
34
|
+
if (!Array.isArray(value.snapshotIds) || !value.snapshotIds.every((x) => typeof x === 'string')) errs.push(fail('facts requires snapshotIds string array'));
|
|
35
|
+
if (!Array.isArray(value.familyOrder) || !Array.isArray(value.families)) errs.push(fail('facts requires familyOrder and families arrays'));
|
|
36
|
+
else for (const family of value.families) {
|
|
37
|
+
if (!isObject(family) || typeof family.family !== 'string' || !isObject(family.coverage) || !Array.isArray(family.facts)) errs.push(fail('facts contains malformed family store'));
|
|
38
|
+
}
|
|
39
|
+
if (value.completeWithinRetainedCoverage !== true) errs.push(fail('facts must attest completeWithinRetainedCoverage=true'));
|
|
40
|
+
return combine(...errs);
|
|
41
|
+
}
|
|
42
|
+
export function validateSelectorsManifest(value: unknown): ValidationResult {
|
|
43
|
+
if (!isObject(value)) return fail('selectors manifest must be an object');
|
|
44
|
+
if (value.schemaVersion !== 1) return fail('selectors schemaVersion must be 1');
|
|
45
|
+
if (!Array.isArray(value.snapshotIds) || !Array.isArray(value.subjects) || typeof value.order !== 'string') return fail('selectors requires snapshotIds, order, and subjects');
|
|
46
|
+
return OK;
|
|
47
|
+
}
|
|
48
|
+
export function validateDerivedReadManifest(value: unknown): ValidationResult {
|
|
49
|
+
if (!isObject(value)) return fail('derived read manifest must be an object');
|
|
50
|
+
const errs: ValidationResult[] = [];
|
|
51
|
+
if (value.schemaVersion !== 1) errs.push(fail('derived read manifest schemaVersion must be 1'));
|
|
52
|
+
for (const f of ['readId', 'owner', 'operation', 'factsPath', 'selectorsPath']) if (typeof value[f] !== 'string' || !value[f]) errs.push(fail(`missing ${f}`));
|
|
53
|
+
if (!Array.isArray(value.sources)) errs.push(fail('derived read manifest requires sources array'));
|
|
54
|
+
if (value.completeWithinRetainedCoverage !== true) errs.push(fail('derived read must attest completeWithinRetainedCoverage=true'));
|
|
55
|
+
return combine(...errs);
|
|
56
|
+
}
|