@crouton-kit/capture 1.3.21 → 1.4.0
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 +2830 -1730
- package/package.json +1 -1
- package/src/capture.ts +3 -2
- package/src/cdp/args.ts +18 -3
- package/src/cdp/commands/measure/explain.ts +22 -7
- package/src/cdp/commands/measure/index.ts +8 -4
- package/src/cdp/commands/measure/map-paint.ts +273 -0
- package/src/cdp/commands/measure/snap.ts +34 -7
- package/src/cdp/commands/page/exec.ts +1 -1
- package/src/cdp/commands/page/index.ts +2 -2
- package/src/cdp/commands/page/shot.ts +118 -6
- package/src/cdp/connection.ts +19 -12
- package/src/cdp/measure/collectors/geometry.ts +66 -9
- package/src/cdp/measure/collectors/pixels.ts +9 -1
- package/src/cdp/measure/collectors/states.ts +243 -4
- package/src/cdp/measure/explain.ts +123 -23
- package/src/cdp/measure/ink-source.ts +116 -0
- package/src/cdp/measure/ink.ts +180 -0
- package/src/cdp/measure/snapshot.ts +3 -0
- package/src/cdp/screenshot.ts +43 -11
- package/src/cdp/targets.ts +24 -0
- package/src/cdp/types.ts +15 -0
- package/src/contracts/index.ts +0 -2
- package/src/output/render.ts +1 -0
- package/src/output/selector.ts +238 -5
- package/src/session/commands.ts +47 -32
- package/src/command-help.ts +0 -29
- package/src/command-resolution.ts +0 -22
- package/src/command-walker.ts +0 -35
- package/src/contracts/results.ts +0 -208
- package/src/contracts/routes.ts +0 -140
- package/src/output/exact-raw.ts +0 -114
- package/src/registry.ts +0 -208
package/src/contracts/results.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Frozen result-lane contracts (U1). Two lanes only:
|
|
3
|
-
*
|
|
4
|
-
* - `bounded:<domain>` — one typed projection through the bounded output
|
|
5
|
-
* kernel: at most {@link MAX_BOUNDED_BYTES} UTF-8 bytes independently in
|
|
6
|
-
* prose and JSON, with fixed identity/summary/scope/coverage/count/artifact
|
|
7
|
-
* metadata that is NEVER dropped, and optional records removed only at
|
|
8
|
-
* record boundaries (never byte-sliced) until both encodings fit.
|
|
9
|
-
* - `exact-raw:<payload>` — handler-produced bytes/text for one named payload,
|
|
10
|
-
* unbounded and preserved exactly (including final-newline presence); empty
|
|
11
|
-
* stderr on success. Global `--json` is rejected before effects.
|
|
12
|
-
*
|
|
13
|
-
* This module is type/interface + pure validators only. It imports no print or
|
|
14
|
-
* exit API. The "no direct stdout" rule is a contract on the handlers that
|
|
15
|
-
* produce these values, encoded in the descriptor layer (see `routes.ts`).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
MAX_BOUNDED_BYTES,
|
|
20
|
-
OK,
|
|
21
|
-
ValidationResult,
|
|
22
|
-
combine,
|
|
23
|
-
contextualize,
|
|
24
|
-
fail,
|
|
25
|
-
isObject,
|
|
26
|
-
utf8ByteLength,
|
|
27
|
-
} from './primitives.js';
|
|
28
|
-
|
|
29
|
-
export { MAX_BOUNDED_BYTES };
|
|
30
|
-
|
|
31
|
-
/** Discriminator for the two public result lanes plus the effect-only branch/launcher rows. */
|
|
32
|
-
export type ResultLaneKind = 'bounded' | 'exact-raw' | 'branch' | 'launcher-metadata';
|
|
33
|
-
|
|
34
|
-
/** A `bounded:<domain>` lane naming exactly one bounded projection domain. */
|
|
35
|
-
export interface BoundedLane {
|
|
36
|
-
readonly kind: 'bounded';
|
|
37
|
-
readonly domain: string;
|
|
38
|
-
/** The JSON/prose schema name the bounded kernel serializes. */
|
|
39
|
-
readonly schema: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** An exact raw lane naming its handler-owned unbounded payload. */
|
|
43
|
-
export interface ExactRawLane {
|
|
44
|
-
readonly kind: 'exact-raw';
|
|
45
|
-
/** The payload type, e.g. `recorded HAR bytes`. */
|
|
46
|
-
readonly payload: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** A branch row: help + child assembly, no result. */
|
|
50
|
-
export interface BranchLane {
|
|
51
|
-
readonly kind: 'branch';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The launcher `--version` metadata row. */
|
|
55
|
-
export interface LauncherMetadataLane {
|
|
56
|
-
readonly kind: 'launcher-metadata';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export type ResultLane = BoundedLane | ExactRawLane | BranchLane | LauncherMetadataLane;
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Byte/list bounds descriptor a bounded leaf declares.
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Every bounded leaf declares its collection and byte bounds up front so the
|
|
67
|
-
* registry can reject a growing collection that lacks an omission contract.
|
|
68
|
-
*/
|
|
69
|
-
export interface BoundedBounds {
|
|
70
|
-
/** Independent max UTF-8 bytes of prose and JSON. Always {@link MAX_BOUNDED_BYTES}. */
|
|
71
|
-
readonly maxBytes: number;
|
|
72
|
-
/** Max displayed records for the primary collection (e.g. 20). */
|
|
73
|
-
readonly maxRecords: number;
|
|
74
|
-
/**
|
|
75
|
-
* Whether the primary collection can grow with the underlying source. A
|
|
76
|
-
* growing collection MUST declare `paginated` so a cursor covers every row.
|
|
77
|
-
*/
|
|
78
|
-
readonly growing: boolean;
|
|
79
|
-
/** True iff this leaf adopts the immutable-cursor pagination service. */
|
|
80
|
-
readonly paginated: boolean;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Bounded projection payload shape (the kernel's typed input, pre-encoding).
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
/** Fixed metadata every bounded projection carries and never drops when bounding. */
|
|
88
|
-
export interface BoundedFixedMeta {
|
|
89
|
-
readonly domain: string;
|
|
90
|
-
readonly schema: string;
|
|
91
|
-
/** Identity of the subject the projection describes (snapshot id, session id, tree id, …). */
|
|
92
|
-
readonly identity: Record<string, string>;
|
|
93
|
-
/** Scope and source coverage (populations/caps/availability), preserved verbatim. */
|
|
94
|
-
readonly coverage: unknown;
|
|
95
|
-
/** total/displayed/omitted/limit for the primary collection. */
|
|
96
|
-
readonly counts: BoundedCollectionCounts;
|
|
97
|
-
/** Absolute artifact paths a caller can re-read for the exhaustive evidence. */
|
|
98
|
-
readonly artifacts: readonly string[];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** total/displayed/omitted/limit accounting for one bounded collection. */
|
|
102
|
-
export interface BoundedCollectionCounts {
|
|
103
|
-
readonly total: number;
|
|
104
|
-
readonly displayed: number;
|
|
105
|
-
readonly omitted: number;
|
|
106
|
-
readonly limit: number;
|
|
107
|
-
/** Machine-stable reasons the omitted records were dropped (e.g. `byte-bound`, `record-limit`). */
|
|
108
|
-
readonly omissionCauses: readonly string[];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* A capped string reports byte accounting and an exact RFC-6901 pointer into
|
|
113
|
-
* the exhaustive artifact rather than silently truncating.
|
|
114
|
-
*/
|
|
115
|
-
export interface CappedStringMeta {
|
|
116
|
-
readonly sourceBytes: number;
|
|
117
|
-
readonly displayedBytes: number;
|
|
118
|
-
readonly omittedBytes: number;
|
|
119
|
-
/** Absolute artifact path + RFC-6901 JSON Pointer to the full value. */
|
|
120
|
-
readonly artifactPath: string;
|
|
121
|
-
readonly jsonPointer: string;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* A structured public error (bounded, factual). Written to stderr by the result
|
|
126
|
-
* owner, never by a handler. Exit 2 = invalid route/grammar/selection/cursor
|
|
127
|
-
* input; exit 3 = source/runtime/publication failure.
|
|
128
|
-
*/
|
|
129
|
-
export interface BoundedError {
|
|
130
|
-
readonly message: string;
|
|
131
|
-
readonly exit: 2 | 3;
|
|
132
|
-
readonly details?: Record<string, unknown>;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// Validators.
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
export function validateResultLane(lane: ResultLane): ValidationResult {
|
|
140
|
-
switch (lane.kind) {
|
|
141
|
-
case 'bounded':
|
|
142
|
-
if (!lane.domain) return fail('bounded lane missing domain');
|
|
143
|
-
if (!lane.schema) return fail('bounded lane missing schema');
|
|
144
|
-
return OK;
|
|
145
|
-
case 'exact-raw':
|
|
146
|
-
if (!lane.payload) return fail('exact-raw lane missing payload type');
|
|
147
|
-
return OK;
|
|
148
|
-
case 'branch':
|
|
149
|
-
case 'launcher-metadata':
|
|
150
|
-
return OK;
|
|
151
|
-
default:
|
|
152
|
-
return fail(`unknown result lane kind: ${(lane as { kind: string }).kind}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function validateBoundedBounds(bounds: BoundedBounds): ValidationResult {
|
|
157
|
-
const errs: ValidationResult[] = [];
|
|
158
|
-
if (bounds.maxBytes !== MAX_BOUNDED_BYTES) {
|
|
159
|
-
errs.push(fail(`bounded maxBytes must be ${MAX_BOUNDED_BYTES}, got ${bounds.maxBytes}`));
|
|
160
|
-
}
|
|
161
|
-
if (!(bounds.maxRecords >= 0)) errs.push(fail('bounded maxRecords must be >= 0'));
|
|
162
|
-
if (bounds.growing && !bounds.paginated) {
|
|
163
|
-
errs.push(fail('a growing bounded collection must declare pagination (cursor coverage)'));
|
|
164
|
-
}
|
|
165
|
-
return combine(...errs);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** True iff both encodings fit the independent byte bound. */
|
|
169
|
-
export function fitsBoundedBounds(prose: string, json: string): ValidationResult {
|
|
170
|
-
const errs: ValidationResult[] = [];
|
|
171
|
-
const p = utf8ByteLength(prose);
|
|
172
|
-
const j = utf8ByteLength(json);
|
|
173
|
-
if (p > MAX_BOUNDED_BYTES) errs.push(fail(`prose is ${p} bytes, exceeds ${MAX_BOUNDED_BYTES}`));
|
|
174
|
-
if (j > MAX_BOUNDED_BYTES) errs.push(fail(`json is ${j} bytes, exceeds ${MAX_BOUNDED_BYTES}`));
|
|
175
|
-
return combine(...errs);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Validate collection counts are internally consistent: displayed+omitted==total, displayed<=limit. */
|
|
179
|
-
export function validateCollectionCounts(counts: BoundedCollectionCounts): ValidationResult {
|
|
180
|
-
const errs: ValidationResult[] = [];
|
|
181
|
-
if (counts.displayed + counts.omitted !== counts.total) {
|
|
182
|
-
errs.push(fail(`displayed(${counts.displayed}) + omitted(${counts.omitted}) != total(${counts.total})`));
|
|
183
|
-
}
|
|
184
|
-
if (counts.displayed > counts.limit) {
|
|
185
|
-
errs.push(fail(`displayed(${counts.displayed}) exceeds limit(${counts.limit})`));
|
|
186
|
-
}
|
|
187
|
-
if (counts.omitted > 0 && counts.omissionCauses.length === 0) {
|
|
188
|
-
errs.push(fail('omitted records present but no omissionCauses declared'));
|
|
189
|
-
}
|
|
190
|
-
return combine(...errs);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Validate the fixed metadata block that must precede any bounded records. */
|
|
194
|
-
export function validateBoundedFixedMeta(meta: unknown): ValidationResult {
|
|
195
|
-
if (!isObject(meta)) return fail('bounded fixed meta must be an object');
|
|
196
|
-
const errs: ValidationResult[] = [];
|
|
197
|
-
if (typeof meta.domain !== 'string' || !meta.domain) errs.push(fail('missing domain'));
|
|
198
|
-
if (typeof meta.schema !== 'string' || !meta.schema) errs.push(fail('missing schema'));
|
|
199
|
-
if (!isObject(meta.identity)) errs.push(fail('missing identity object'));
|
|
200
|
-
if (!('coverage' in meta)) errs.push(fail('missing coverage'));
|
|
201
|
-
if (!Array.isArray(meta.artifacts)) errs.push(fail('artifacts must be an array of absolute paths'));
|
|
202
|
-
if (isObject(meta.counts)) {
|
|
203
|
-
errs.push(contextualize('counts', validateCollectionCounts(meta.counts as BoundedCollectionCounts)));
|
|
204
|
-
} else {
|
|
205
|
-
errs.push(fail('missing counts'));
|
|
206
|
-
}
|
|
207
|
-
return combine(...errs);
|
|
208
|
-
}
|
package/src/contracts/routes.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Frozen public route/leaf descriptor contract. A descriptor is the sole truth
|
|
3
|
-
* for one canonical public path. It names inputs, effects, output lane, bounds,
|
|
4
|
-
* stderr, exits, and an opaque handler slot; it never imports a handler.
|
|
5
|
-
*/
|
|
6
|
-
import { OK, ValidationResult, combine, contextualize, fail } from './primitives.js';
|
|
7
|
-
import { BoundedBounds, ResultLane, validateBoundedBounds, validateResultLane } from './results.js';
|
|
8
|
-
|
|
9
|
-
export interface RouteEffects {
|
|
10
|
-
readonly browser: boolean;
|
|
11
|
-
readonly session: boolean;
|
|
12
|
-
readonly artifact: boolean;
|
|
13
|
-
readonly environment: boolean;
|
|
14
|
-
}
|
|
15
|
-
export const NO_EFFECTS: RouteEffects = { browser: false, session: false, artifact: false, environment: false };
|
|
16
|
-
|
|
17
|
-
export interface PositionalSpec { readonly name: string; readonly grammar: string; readonly required: boolean; readonly variadic?: boolean; }
|
|
18
|
-
export interface FlagSpec {
|
|
19
|
-
readonly name: string;
|
|
20
|
-
readonly grammar: string | 'boolean';
|
|
21
|
-
/** Required singleton unless repeatable is explicitly declared. */
|
|
22
|
-
readonly required?: boolean;
|
|
23
|
-
/** Repetition is accepted; required means at least one occurrence. */
|
|
24
|
-
readonly repeatable?: boolean;
|
|
25
|
-
readonly values?: readonly string[];
|
|
26
|
-
readonly default?: string | number | boolean;
|
|
27
|
-
readonly units?: string;
|
|
28
|
-
}
|
|
29
|
-
export type StderrPolicy = 'empty-on-success' | 'declared-progress';
|
|
30
|
-
export type LeafExit = 0 | 2 | 3 | 10 | 11 | 130 | 143;
|
|
31
|
-
|
|
32
|
-
export interface BranchDescriptor { readonly kind: 'branch'; readonly path: string; readonly summary: string; }
|
|
33
|
-
export interface LeafDescriptor {
|
|
34
|
-
readonly kind: 'leaf'; readonly path: string; readonly summary: string;
|
|
35
|
-
readonly positionals: readonly PositionalSpec[]; readonly flags: readonly FlagSpec[];
|
|
36
|
-
readonly mutualExclusions: readonly (readonly string[])[]; readonly effects: RouteEffects;
|
|
37
|
-
readonly result: ResultLane; readonly bounds?: BoundedBounds; readonly stderr: StderrPolicy;
|
|
38
|
-
readonly exits: readonly LeafExit[]; readonly handler: string;
|
|
39
|
-
}
|
|
40
|
-
export type RouteDescriptor = BranchDescriptor | LeafDescriptor;
|
|
41
|
-
|
|
42
|
-
/** All public non-root branches in the seven-noun Capture topology. */
|
|
43
|
-
export const EXPECTED_BRANCH_PATHS: readonly string[] = [
|
|
44
|
-
'session', 'page', 'measure', 'measure map', 'measure variation', 'motion',
|
|
45
|
-
'traffic', 'traffic har', 'browser', 'library',
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
/** Every public runnable path, exactly once. There are no aliases. */
|
|
49
|
-
export const EXPECTED_LEAF_PATHS: readonly string[] = [
|
|
50
|
-
'session start', 'session stop', 'session list', 'session view', 'session log',
|
|
51
|
-
'page a11y', 'page screenshot', 'page click', 'page type', 'page navigate', 'page exec',
|
|
52
|
-
'measure snap', 'measure check', 'measure geometry', 'measure map focus', 'measure map scroll', 'measure map layers', 'measure explain', 'measure variation diff', 'measure variation census', 'measure variation sweep',
|
|
53
|
-
'motion rec', 'motion mask', 'motion timeline', 'motion jank', 'motion response',
|
|
54
|
-
'traffic record', 'traffic har create', 'traffic har read', 'traffic har delete',
|
|
55
|
-
'browser detect', 'browser list', 'browser open', 'browser reset', 'browser network', 'browser cdp',
|
|
56
|
-
'library list', 'library search', 'library show', 'library read',
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
export const PAGINATED_LEAF_PATHS: readonly string[] = [
|
|
60
|
-
'browser detect', 'browser list', 'library list', 'library search', 'library show',
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
/** Exact raw leaves are handler-owned bytes/text, never global JSON envelopes. */
|
|
64
|
-
export const EXACT_RAW_LEAF_PAYLOADS: Readonly<Record<string, string>> = {
|
|
65
|
-
'session log': 'recorded session log bytes',
|
|
66
|
-
'page a11y': 'full accessibility-tree text',
|
|
67
|
-
'page exec': 'handler evaluation bytes/text',
|
|
68
|
-
'traffic har read': 'stored HAR bytes',
|
|
69
|
-
'browser cdp': 'raw protocol response bytes',
|
|
70
|
-
'library read': 'bundled function source bytes',
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/** Legacy spellings are deliberately unreachable after the U15 cutover. */
|
|
74
|
-
export const FORBIDDEN_PUBLIC_PATHS: readonly string[] = [
|
|
75
|
-
'-v', 'version', 'log', 'detect', 'list', 'open', 'reset-tab', 'screenshot',
|
|
76
|
-
'click', 'type', 'a11y', 'record', 'navigate', 'network', 'har', 'lib', 'cdp',
|
|
77
|
-
'exec', 'motion observations', '__bridge-serve',
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
function flagNames(leaf: LeafDescriptor): Set<string> { return new Set(leaf.flags.map((flag) => flag.name)); }
|
|
81
|
-
|
|
82
|
-
export function validateLeafDescriptor(leaf: LeafDescriptor): ValidationResult {
|
|
83
|
-
const errors: ValidationResult[] = [];
|
|
84
|
-
if (!leaf.path) errors.push(fail('leaf has empty path'));
|
|
85
|
-
const fixed = leaf.positionals.filter((positional) => !positional.variadic);
|
|
86
|
-
const required = fixed.filter((positional) => positional.required);
|
|
87
|
-
const optional = fixed.filter((positional) => !positional.required);
|
|
88
|
-
if (required.length > 1 || optional.length > 1 || fixed.length > 2 || (fixed.length === 2 && (!fixed[0].required || fixed[1].required))) {
|
|
89
|
-
errors.push(fail(`leaf ${leaf.path} permits only one required positional followed by one optional trailing positional`));
|
|
90
|
-
}
|
|
91
|
-
const names = flagNames(leaf);
|
|
92
|
-
if (names.size !== leaf.flags.length) errors.push(fail(`leaf ${leaf.path} declares duplicate flag names`));
|
|
93
|
-
for (const flag of leaf.flags) {
|
|
94
|
-
if (!flag.name.startsWith('--')) errors.push(fail(`leaf ${leaf.path} flag ${flag.name} is not a long option`));
|
|
95
|
-
if (flag.grammar === 'boolean' && (flag.values || flag.default !== undefined || flag.units)) errors.push(fail(`boolean flag ${flag.name} must not declare value metadata`));
|
|
96
|
-
if (flag.repeatable && flag.default !== undefined) errors.push(fail(`repeatable flag ${flag.name} must not declare a default`));
|
|
97
|
-
}
|
|
98
|
-
for (const group of leaf.mutualExclusions) for (const name of group) if (!names.has(name)) errors.push(fail(`leaf ${leaf.path} mutual-exclusion references undeclared flag ${name}`));
|
|
99
|
-
errors.push(contextualize(`leaf ${leaf.path} result`, validateResultLane(leaf.result)));
|
|
100
|
-
if (leaf.result.kind === 'bounded') {
|
|
101
|
-
if (!leaf.bounds) errors.push(fail(`bounded leaf ${leaf.path} missing byte/list bounds`));
|
|
102
|
-
else errors.push(contextualize(`leaf ${leaf.path} bounds`, validateBoundedBounds(leaf.bounds)));
|
|
103
|
-
} else if (leaf.result.kind === 'exact-raw') {
|
|
104
|
-
if (leaf.bounds) errors.push(fail(`exact raw leaf ${leaf.path} must not declare bounded bounds`));
|
|
105
|
-
if (leaf.stderr !== 'empty-on-success') errors.push(fail(`exact raw leaf ${leaf.path} must have empty-on-success stderr`));
|
|
106
|
-
} else errors.push(fail(`leaf ${leaf.path} must use bounded or exact-raw result lane`));
|
|
107
|
-
if (!leaf.handler) errors.push(fail(`leaf ${leaf.path} missing handler slot`));
|
|
108
|
-
if (!leaf.exits.length) errors.push(fail(`leaf ${leaf.path} declares no exits`));
|
|
109
|
-
return combine(...errors);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function validateBranchDescriptor(branch: BranchDescriptor): ValidationResult {
|
|
113
|
-
if (!branch.path) return fail('branch has empty path');
|
|
114
|
-
if ((branch as unknown as { handler?: unknown }).handler !== undefined) return fail(`branch ${branch.path} must not declare a handler`);
|
|
115
|
-
return OK;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Validate an assembled public registry against the canonical seven-noun census. */
|
|
119
|
-
export function validateRegistry(descriptors: readonly RouteDescriptor[]): ValidationResult {
|
|
120
|
-
const errors: ValidationResult[] = [];
|
|
121
|
-
const seen = new Set<string>(); const leaves = new Set<string>(); const branches = new Set<string>();
|
|
122
|
-
for (const descriptor of descriptors) {
|
|
123
|
-
if (seen.has(descriptor.path)) errors.push(fail(`duplicate route path: ${descriptor.path}`));
|
|
124
|
-
seen.add(descriptor.path);
|
|
125
|
-
if (FORBIDDEN_PUBLIC_PATHS.includes(descriptor.path)) errors.push(fail(`forbidden public path present: ${descriptor.path}`));
|
|
126
|
-
if (descriptor.kind === 'leaf') {
|
|
127
|
-
leaves.add(descriptor.path); errors.push(validateLeafDescriptor(descriptor));
|
|
128
|
-
const expectedPayload = EXACT_RAW_LEAF_PAYLOADS[descriptor.path];
|
|
129
|
-
if (expectedPayload && (descriptor.result.kind !== 'exact-raw' || descriptor.result.payload !== expectedPayload)) errors.push(fail(`leaf ${descriptor.path} must be exact-raw:${expectedPayload}`));
|
|
130
|
-
if (!expectedPayload && descriptor.result.kind === 'exact-raw') errors.push(fail(`leaf ${descriptor.path} is not declared exact-raw by the public output contract`));
|
|
131
|
-
const paginated = descriptor.result.kind === 'bounded' && descriptor.bounds?.paginated === true;
|
|
132
|
-
if (PAGINATED_LEAF_PATHS.includes(descriptor.path) !== paginated) errors.push(fail(`leaf ${descriptor.path} pagination declaration disagrees with census`));
|
|
133
|
-
} else { branches.add(descriptor.path); errors.push(validateBranchDescriptor(descriptor)); }
|
|
134
|
-
}
|
|
135
|
-
for (const path of EXPECTED_LEAF_PATHS) if (!leaves.has(path)) errors.push(fail(`missing expected leaf path: ${path}`));
|
|
136
|
-
for (const path of EXPECTED_BRANCH_PATHS) if (!branches.has(path)) errors.push(fail(`missing expected branch path: ${path}`));
|
|
137
|
-
for (const path of leaves) if (!EXPECTED_LEAF_PATHS.includes(path)) errors.push(fail(`unexpected leaf path (not in census): ${path}`));
|
|
138
|
-
for (const path of branches) if (!EXPECTED_BRANCH_PATHS.includes(path)) errors.push(fail(`unexpected branch path (not in census): ${path}`));
|
|
139
|
-
return combine(...errors);
|
|
140
|
-
}
|
package/src/output/exact-raw.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Leaf-owned exact-raw output foundation. It deliberately has no structured
|
|
3
|
-
* fallback: successful payloads are written exactly as their handler supplied
|
|
4
|
-
* them, and global --json is rejected before the handler can perform effects.
|
|
5
|
-
*/
|
|
6
|
-
import { OK, type ValidationResult, fail } from '../contracts/primitives.js';
|
|
7
|
-
|
|
8
|
-
export type ExactRawPayload = string | Uint8Array;
|
|
9
|
-
|
|
10
|
-
/** The only two public leaf output modes at the hard cut. */
|
|
11
|
-
export type LeafOutputMode = 'structured-json-capable' | 'exact-raw-json-rejected';
|
|
12
|
-
|
|
13
|
-
export interface StructuredOutputOwner {
|
|
14
|
-
readonly mode: 'structured-json-capable';
|
|
15
|
-
readonly canonicalPath: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ExactRawOutputOwner {
|
|
19
|
-
readonly mode: 'exact-raw-json-rejected';
|
|
20
|
-
readonly canonicalPath: string;
|
|
21
|
-
readonly payloadType: string;
|
|
22
|
-
readonly size: 'unbounded';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type LeafOutputOwner = StructuredOutputOwner | ExactRawOutputOwner;
|
|
26
|
-
|
|
27
|
-
/** The fixed exhaustive public output-mode matrix. */
|
|
28
|
-
export const PUBLIC_OUTPUT_OWNERS: readonly LeafOutputOwner[] = [
|
|
29
|
-
...['session start', 'session stop', 'session list', 'session view'].map((canonicalPath) => ({ mode: 'structured-json-capable' as const, canonicalPath })),
|
|
30
|
-
...['page screenshot', 'page click', 'page type', 'page navigate'].map((canonicalPath) => ({ mode: 'structured-json-capable' as const, canonicalPath })),
|
|
31
|
-
...['measure snap', 'measure check', 'measure geometry', 'measure map focus', 'measure map scroll', 'measure map layers', 'measure explain', 'measure variation diff', 'measure variation census', 'measure variation sweep'].map((canonicalPath) => ({ mode: 'structured-json-capable' as const, canonicalPath })),
|
|
32
|
-
...['motion rec', 'motion mask', 'motion timeline', 'motion jank', 'motion response'].map((canonicalPath) => ({ mode: 'structured-json-capable' as const, canonicalPath })),
|
|
33
|
-
...['traffic record', 'traffic har create', 'traffic har delete'].map((canonicalPath) => ({ mode: 'structured-json-capable' as const, canonicalPath })),
|
|
34
|
-
...['browser detect', 'browser list', 'browser open', 'browser reset', 'browser network'].map((canonicalPath) => ({ mode: 'structured-json-capable' as const, canonicalPath })),
|
|
35
|
-
...['library list', 'library search', 'library show'].map((canonicalPath) => ({ mode: 'structured-json-capable' as const, canonicalPath })),
|
|
36
|
-
{ mode: 'exact-raw-json-rejected', canonicalPath: 'session log', payloadType: 'recorded session log bytes', size: 'unbounded' },
|
|
37
|
-
{ mode: 'exact-raw-json-rejected', canonicalPath: 'page a11y', payloadType: 'full accessibility-tree text', size: 'unbounded' },
|
|
38
|
-
{ mode: 'exact-raw-json-rejected', canonicalPath: 'page exec', payloadType: 'handler evaluation bytes/text', size: 'unbounded' },
|
|
39
|
-
{ mode: 'exact-raw-json-rejected', canonicalPath: 'traffic har read', payloadType: 'stored HAR bytes', size: 'unbounded' },
|
|
40
|
-
{ mode: 'exact-raw-json-rejected', canonicalPath: 'browser cdp', payloadType: 'raw protocol response bytes', size: 'unbounded' },
|
|
41
|
-
{ mode: 'exact-raw-json-rejected', canonicalPath: 'library read', payloadType: 'bundled function source bytes', size: 'unbounded' },
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
/** Validate a registry's exhaustive output-mode partition before route effects are bound. */
|
|
45
|
-
export function validateLeafOutputOwners(owners: readonly LeafOutputOwner[]): ValidationResult {
|
|
46
|
-
const expected = new Map(PUBLIC_OUTPUT_OWNERS.map((owner) => [owner.canonicalPath, owner]));
|
|
47
|
-
const paths = new Set<string>();
|
|
48
|
-
const errors: string[] = [];
|
|
49
|
-
for (const owner of owners) {
|
|
50
|
-
if (!owner.canonicalPath) errors.push('output owner has empty canonical path');
|
|
51
|
-
if (paths.has(owner.canonicalPath)) errors.push(`duplicate output owner path: ${owner.canonicalPath}`);
|
|
52
|
-
paths.add(owner.canonicalPath);
|
|
53
|
-
const required = expected.get(owner.canonicalPath);
|
|
54
|
-
if (!required) { errors.push(`unexpected output owner path: ${owner.canonicalPath}`); continue; }
|
|
55
|
-
if (owner.mode !== required.mode) { errors.push(`wrong output mode for ${owner.canonicalPath}`); continue; }
|
|
56
|
-
if (owner.mode === 'exact-raw-json-rejected' && (owner.payloadType !== required.payloadType || owner.size !== 'unbounded')) {
|
|
57
|
-
errors.push(`wrong exact raw declaration for ${owner.canonicalPath}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
for (const path of expected.keys()) if (!paths.has(path)) errors.push(`missing output owner path: ${path}`);
|
|
61
|
-
return errors.length === 0 ? OK : fail(...errors);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface ExactRawJsonRejection {
|
|
65
|
-
readonly code: 'output_mode_unsupported';
|
|
66
|
-
readonly field: '--json';
|
|
67
|
-
readonly expected: 'omit --json for exact raw output';
|
|
68
|
-
readonly next_action: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ExactRawLeaf {
|
|
72
|
-
readonly canonicalPath: string;
|
|
73
|
-
readonly argv: readonly string[];
|
|
74
|
-
/** The leaf handler; callers must put all effects inside this callback. */
|
|
75
|
-
readonly produce: () => ExactRawPayload | Promise<ExactRawPayload>;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export type ExactRawRunResult =
|
|
79
|
-
| { readonly ok: true }
|
|
80
|
-
| { readonly ok: false; readonly error: ExactRawJsonRejection };
|
|
81
|
-
|
|
82
|
-
export function exactRawJsonRejection(canonicalPath: string): ExactRawJsonRejection {
|
|
83
|
-
if (!canonicalPath) throw new Error('canonical exact-raw path must not be empty');
|
|
84
|
-
return {
|
|
85
|
-
code: 'output_mode_unsupported',
|
|
86
|
-
field: '--json',
|
|
87
|
-
expected: 'omit --json for exact raw output',
|
|
88
|
-
next_action: `run ${canonicalPath} -h`,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** True only for the global JSON switch, never for payload text that resembles it. */
|
|
93
|
-
export function rejectsGlobalJson(argv: readonly string[]): boolean {
|
|
94
|
-
for (const token of argv) {
|
|
95
|
-
if (token === '--') return false;
|
|
96
|
-
if (token === '--json') return true;
|
|
97
|
-
}
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Writes a supplied payload with no newline insertion or any transformation. */
|
|
102
|
-
export function emitExactRaw(payload: ExactRawPayload, write: (chunk: ExactRawPayload) => void = process.stdout.write.bind(process.stdout)): void {
|
|
103
|
-
write(payload);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Execute an exact-raw leaf. `produce` is intentionally unreachable whenever
|
|
108
|
-
* --json is present, giving route adapters a pre-effect validation seam.
|
|
109
|
-
*/
|
|
110
|
-
export async function runExactRaw(leaf: ExactRawLeaf, write: (chunk: ExactRawPayload) => void = process.stdout.write.bind(process.stdout)): Promise<ExactRawRunResult> {
|
|
111
|
-
if (rejectsGlobalJson(leaf.argv)) return { ok: false, error: exactRawJsonRejection(leaf.canonicalPath) };
|
|
112
|
-
emitExactRaw(await leaf.produce(), write);
|
|
113
|
-
return { ok: true };
|
|
114
|
-
}
|