@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
package/src/registry.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/** Static, pure descriptor tree; unreachable from the legacy launcher until U15. */
|
|
2
|
+
import {
|
|
3
|
+
EXACT_RAW_LEAF_PAYLOADS, EXPECTED_LEAF_PATHS, MAX_BOUNDED_BYTES, NO_EFFECTS,
|
|
4
|
+
PAGINATED_LEAF_PATHS, type BranchDescriptor, type FlagSpec, type LeafDescriptor,
|
|
5
|
+
type PositionalSpec, type RouteDescriptor, type RouteEffects, combine, fail,
|
|
6
|
+
validateBranchDescriptor, validateLeafDescriptor, validateRegistry, type ValidationResult,
|
|
7
|
+
} from './contracts/index.js';
|
|
8
|
+
import {
|
|
9
|
+
exactRawJsonRejection, type LeafOutputMode, type LeafOutputOwner, validateLeafOutputOwners,
|
|
10
|
+
} from './output/exact-raw.js';
|
|
11
|
+
|
|
12
|
+
export interface HelpContent {
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly whenToUse: string;
|
|
15
|
+
readonly rubric: string;
|
|
16
|
+
readonly followUp: string;
|
|
17
|
+
readonly constraints: string;
|
|
18
|
+
readonly orderedOutput: string;
|
|
19
|
+
readonly artifactOwnership: string;
|
|
20
|
+
readonly model?: string;
|
|
21
|
+
readonly globals?: string;
|
|
22
|
+
readonly ioContract?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RegistryBranch extends BranchDescriptor {
|
|
25
|
+
readonly visibility: 'public' | 'internal'; readonly help: HelpContent;
|
|
26
|
+
readonly children: readonly RegistryNode[]; readonly aliases?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
export interface RegistryLeaf extends LeafDescriptor {
|
|
29
|
+
readonly visibility: 'public' | 'internal'; readonly help: HelpContent;
|
|
30
|
+
readonly recovery: string; readonly outputMode: LeafOutputMode; readonly aliases?: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
export type RegistryNode = RegistryBranch | RegistryLeaf;
|
|
33
|
+
|
|
34
|
+
type LeafOptions = {
|
|
35
|
+
readonly positionals?: readonly PositionalSpec[]; readonly flags?: readonly FlagSpec[];
|
|
36
|
+
readonly mutualExclusions?: readonly (readonly string[])[]; readonly effects?: RouteEffects;
|
|
37
|
+
readonly growing?: boolean; readonly stderr?: 'empty-on-success' | 'declared-progress';
|
|
38
|
+
readonly constraints?: string; readonly orderedOutput?: string; readonly artifactOwnership?: string;
|
|
39
|
+
readonly recovery?: string; readonly whenToUse?: string;
|
|
40
|
+
};
|
|
41
|
+
const effects = (browser = false, session = false, artifact = false, environment = false): RouteEffects => ({ browser, session, artifact, environment });
|
|
42
|
+
const positional = (name: string, grammar: string, required = true): PositionalSpec => ({ name, grammar, required });
|
|
43
|
+
const bool = (name: string): FlagSpec => ({ name, grammar: 'boolean' });
|
|
44
|
+
const value = (name: string, grammar: string, values?: readonly string[], defaultValue?: string | number, units?: string): FlagSpec => ({ name, grammar, ...(values ? { values } : {}), ...(defaultValue !== undefined ? { default: defaultValue } : {}), ...(units ? { units } : {}) });
|
|
45
|
+
const requiredValue = (name: string, grammar: string, values?: readonly string[]): FlagSpec => ({ name, grammar, required: true, ...(values ? { values } : {}) });
|
|
46
|
+
const repeatableValue = (name: string, grammar: string, required = false): FlagSpec => ({ name, grammar, repeatable: true, ...(required ? { required: true } : {}) });
|
|
47
|
+
const port = (required = false) => ({ ...value('--port', '1..65535', undefined, undefined, 'tcp-port'), ...(required ? { required: true } : {}) });
|
|
48
|
+
const timeout = (name: '--navigation-timeout' | '--settle-timeout') => value(name, '1..60000', undefined, name === '--navigation-timeout' ? 10000 : 5000, 'ms');
|
|
49
|
+
const limit = () => value('--limit', '1..20', undefined, 20, 'records');
|
|
50
|
+
const bounded = (path: string) => ({ kind: 'bounded' as const, domain: path.replaceAll(' ', '-'), schema: `${path.replaceAll(' ', '-')}-v1` });
|
|
51
|
+
|
|
52
|
+
function leaf(path: string, description: string, options: LeafOptions = {}): RegistryLeaf {
|
|
53
|
+
const payload = EXACT_RAW_LEAF_PAYLOADS[path];
|
|
54
|
+
const raw = payload !== undefined;
|
|
55
|
+
const paginated = PAGINATED_LEAF_PATHS.includes(path);
|
|
56
|
+
const constraints = options.constraints ?? 'Singleton value flags accept one grammar-valid value; unknown options and unexpected positionals fail before effects.';
|
|
57
|
+
return {
|
|
58
|
+
kind: 'leaf', path, summary: description, visibility: 'public', positionals: options.positionals ?? [], flags: options.flags ?? [], mutualExclusions: options.mutualExclusions ?? [], effects: options.effects ?? NO_EFFECTS,
|
|
59
|
+
result: raw ? { kind: 'exact-raw', payload } : bounded(path), outputMode: raw ? 'exact-raw-json-rejected' : 'structured-json-capable',
|
|
60
|
+
...(raw ? {} : { bounds: { maxBytes: MAX_BOUNDED_BYTES, maxRecords: 20, growing: options.growing ?? paginated, paginated } }),
|
|
61
|
+
stderr: options.stderr ?? 'empty-on-success', exits: [0, 2, 3], handler: path.replaceAll(' ', '.'),
|
|
62
|
+
help: {
|
|
63
|
+
description, whenToUse: options.whenToUse ?? `Choose this when you need ${description.toLowerCase()}`,
|
|
64
|
+
rubric: raw ? `Exact raw ${payload}; size unbounded. --json rejects before effects with code=output_mode_unsupported, field=--json, expected=omit --json for exact raw output, and next_action=run ${path} -h.` : `Structured ${path.replaceAll(' ', '-')}-v1 result, bounded independently to ${MAX_BOUNDED_BYTES} UTF-8 bytes in prose and JSON.`,
|
|
65
|
+
constraints,
|
|
66
|
+
orderedOutput: options.orderedOutput ?? (raw ? `Handler stdout is ${payload} exactly as produced, including final-newline presence; it is never parsed, wrapped, normalized, or reserialized.` : 'Identity and attestation; fixed summary and scope; source coverage; leaf-owned evidence access; omission metadata; bounded records; static follow-up.'),
|
|
67
|
+
artifactOwnership: options.artifactOwnership ?? (options.effects?.artifact ? 'Creates or reuses only this leaf’s declared artifact; stdout carries its absolute access locator.' : 'Reads declared evidence only and creates no projection-owned artifact.'),
|
|
68
|
+
followUp: `run capture ${path} -h`,
|
|
69
|
+
},
|
|
70
|
+
recovery: options.recovery ?? `Correct the reported input and run capture ${path} -h.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function branch(path: string, description: string, children: readonly RegistryNode[], model?: string, root = false, whenToUse?: string): RegistryBranch {
|
|
74
|
+
return { kind: 'branch', path, summary: description, visibility: 'public', children, help: {
|
|
75
|
+
description, whenToUse: whenToUse ?? `Choose this when working with ${description.toLowerCase()}`,
|
|
76
|
+
rubric: model ?? `${path || 'Capture'} command family.`, constraints: 'Choose an exact canonical child; old names and aliases are not accepted.', orderedOutput: 'Branch help only; branches perform no effects.', artifactOwnership: 'Branch help creates no artifact.', followUp: `run capture${path ? ` ${path}` : ''} -h`,
|
|
77
|
+
...(root ? {
|
|
78
|
+
model: 'Commands divide browser lifecycle, current-page work, rendered structure, time, traffic, endpoint control, and site-service APIs. Select evidence by modality: page owns semantics and pixels; measure owns rendered structure; motion owns time; traffic owns requests.',
|
|
79
|
+
globals: '-h resolves the deepest command then renders help with zero effects; --json requests JSON only from structured leaves; --version is root-only and precedes path resolution.',
|
|
80
|
+
ioContract: 'Leaf-declared flags and positionals are input. Structured leaves emit factual prose or JSON; exact-raw leaves emit declared bytes/text. Diagnostics are stderr; exit 0 is success and nonzero is failure.',
|
|
81
|
+
} : {}),
|
|
82
|
+
}};
|
|
83
|
+
}
|
|
84
|
+
const snapStates = ['normal', 'hover', 'focus', 'active', 'checked', 'open', 'disabled', 'invalid', 'all'];
|
|
85
|
+
const families = ['viewport-box-position', 'element-scroll-extent', 'native-control-and-anchor-box-size', 'computed-color-pair-contrast', 'sampled-hit-reception', 'text-inline-extent', 'form-control-state', 'media-state', 'animation-state', 'opaque-background-sibling-box-intersection', 'css-overflow-visible-box-intersection'];
|
|
86
|
+
const ref = 'css:<selector> | backend:<id> | axid:<id> | ax:<name> | text:<text>';
|
|
87
|
+
|
|
88
|
+
export const CAPTURE_REGISTRY: RegistryBranch = branch('', 'Capture — browser evidence and automation over CDP.', [
|
|
89
|
+
branch('session', 'persistent browser-work lifecycle and artifact bundles.', [
|
|
90
|
+
leaf('session start', 'start a fresh persistent capture session.', { flags: [requiredValue('--url', 'absolute-http(s)-url'), port()], effects: effects(true, true, true), constraints: 'Required singleton --url <absolute-http(s)-url>; optional singleton --port <1..65535>. No target, prefix, positional URL, or ambient CDP selection.', orderedOutput: 'Exact session ID; open lifecycle; full target ID; endpoint; observed URL; target_live=true; scope key; active generation.', artifactOwnership: 'Exclusively allocates the session directory and atomically publishes the canonical session record before its scoped pointer.' }),
|
|
91
|
+
leaf('session stop', 'stop and bundle one exact capture session.', { positionals: [positional('session-id', 'exact-session-id')], effects: effects(true, true, true), constraints: 'One exact case-sensitive session ID; no prefixes. Only matching open/stopping/completed lifecycle records are accepted before bundle effects.', orderedOutput: 'Exact ID; stop-operation ID; stopped lifecycle; final bundle access; pointer outcome deleted|absent|superseded.', artifactOwnership: 'Resumes the recorded operation and owns bundle staging/publication; never rolls lifecycle back to open.', recovery: 'session_stop_state_invalid: inspect capture session view <exact-session-id> and preserve the session directory; never delete or allocate a new stop. session_stop_staging_cleanup_failed: correct access at the reported staging path and retry the identical capture session stop <exact-session-id>. session_stop_bundle_failed: correct the reported bundle/filesystem cause and retry that identical command. Both retries resume the recorded operation.' }),
|
|
92
|
+
leaf('session list', 'list same-user host capture sessions.', { effects: effects(false, true), constraints: 'No target/session filters or ambient defaults.', orderedOutput: 'Rows sorted by exact session ID: ID, lifecycle, active_in_current_scope, full target ID or null, endpoint or null, target_live.' }),
|
|
93
|
+
leaf('session view', 'view one exact capture session.', { positionals: [positional('session-id', 'exact-session-id')], effects: effects(false, true), constraints: 'One exact case-sensitive session ID; no prefix.' }),
|
|
94
|
+
leaf('session log', 'read recorded session log bytes.', { positionals: [positional('path', 'session-log-path')], effects: effects(false, true), constraints: 'One declared session-log path; no global JSON.', recovery: 'Use the exact session log path and run capture session log -h.' }),
|
|
95
|
+
], undefined, false, 'Choose this when work spans multiple invocations or must bundle the selected tab and artifacts; use an evidence branch for work performed inside the session.'),
|
|
96
|
+
branch('page', 'semantic/pixel orientation and actions on the selected page.', [
|
|
97
|
+
leaf('page a11y', 'read the full accessibility tree.', { effects: effects(true), constraints: 'No global JSON; reads the selected page accessibility tree.' }),
|
|
98
|
+
leaf('page screenshot', 'capture screenshot pixel evidence.', { flags: [value('--out', 'absolute-output-path'), bool('--full-page')], effects: effects(true, false, true), constraints: 'Optional singleton --out path and --full-page; pixel evidence does not establish structural causality.' }),
|
|
99
|
+
leaf('page click', 'click an accessible page element.', { positionals: [positional('name', 'accessible-name')], flags: [value('--role', 'aria-role')], effects: effects(true), constraints: 'One accessible-name positional; optional singleton --role.' }),
|
|
100
|
+
leaf('page type', 'type text into a page control.', { positionals: [positional('text', 'text')], flags: [value('--into', 'accessible-name-or-selector')], effects: effects(true), constraints: 'One text positional and optional singleton --into target.' }),
|
|
101
|
+
leaf('page navigate', 'navigate the selected page.', { positionals: [positional('url', 'absolute-http(s)-url')], flags: [timeout('--settle-timeout')], effects: effects(true), constraints: 'One absolute HTTP(S) URL; optional settle timeout.' }),
|
|
102
|
+
leaf('page exec', 'evaluate handler-owned page code.', { positionals: [positional('code', 'javascript-source')], flags: [value('--file', 'absolute-source-path')], effects: effects(true), constraints: 'One source positional or declared --file source; no global JSON.' }),
|
|
103
|
+
], undefined, false, 'Choose this when reading accessibility roles and names, capturing pixels, clicking, typing, navigating, or executing page JavaScript. For what pixels visibly appear covered, occluded, clipped, cropped, masked, or absent, screenshot is the pixel evidence; use measure only for separately named nominal geometry, captured CSS/ancestor clipping or overflow structure, stacking, paint order, computed style, or distance evidence. Capture does not infer rendered-pixel coverage.'),
|
|
104
|
+
branch('measure', 'immutable snapshots and factual rendered-structure reads.', [
|
|
105
|
+
leaf('measure snap', 'acquire one immutable structural snapshot.', { positionals: [positional('url', 'absolute-http(s)-url', false)], flags: [value('--session', 'exact-session-id'), value('--target', 'target-token'), port(), timeout('--navigation-timeout'), timeout('--settle-timeout'), bool('--freeze-animations'), bool('--capture-unsettled'), bool('--pixels'), repeatableValue('--state', `name[:css-selector], name ∈ ${snapStates.join('|')}`), value('--viewport', '<width>x<height>')], mutualExclusions: [['--session', '--target'], ['--session', '--port']], effects: effects(true, true, true), whenToUse: 'Choose this when no suitable snapshot exists or a later page state must be recorded; every structural read takes the returned snapshot explicitly.', constraints: 'Exactly one target mode from the four-row matrix: fresh-url = one absolute http(s) URL with optional --port; named-session = --session <exact-id> with no --port; explicit-target = --target <token> with optional --port; active-session = none of url/--session/--target. --port alone fails port_requires_url_or_target; --port with either session route fails port_conflicts_with_session; conflicting modes fail snapshot_target_conflict; more than one positional fails unexpected_positional. --navigation-timeout <1..60000> integer ms default 10000 is valid only in fresh-url mode (else navigation_timeout_requires_fresh_url). --settle-timeout <1..60000> integer ms default 5000. State and target tokens are case-sensitive. --state <name[:css-selector]> repeats; the nine names are normal, hover, focus, active, checked, open, disabled, invalid, all; all expands in that fixed order excluding normal and conflicts with every other state; the first colon splits name from a nonempty selector and later colons belong to the selector; duplicate normalized (state,selector) requests are rejected while distinct selectors for one state stay distinct. --viewport <width>x<height> with each dimension a base-10 safe integer 1..16384. CDP_PORT, CDP_TARGET, CDP_HAR_ID are ignored. Every syntax/mode error fails before endpoint probes, navigation, mutation, lock acquisition, or durable allocation.', orderedOutput: 'Snapshot ID and complete absolute directory; fixed target attestation {mode,session_id,session_source,target_id,endpoint,observed_url}; request metadata; settledness/timing; viewport provenance; content extent; source coverage; fixed facets; aggregate pixels/crops; immutable source-manifest access.', artifactOwnership: 'Creates one immutable snapshot source tree and atomically publishes its global ID only after restoration and closure gates.', recovery: 'snapshot_target_required: run capture measure snap <url> [--port <port>]; intentionally persistent work may instead run capture session start --url <url> [--port <port>] and retry without target arguments, or run capture browser list and use the exact target retry below. snapshot_target_conflict: keep exactly one row from the snap mode matrix; port may qualify only fresh URL or explicit target. duplicate_option: remove the repeated occurrence and rerun the same invocation. unexpected_positional: keep exactly one absolute HTTP(S) URL or no positional. port_requires_url_or_target: add one URL or --target <token>, or remove port to use the scoped active session. port_conflicts_with_session: remove port; session routes always use their recorded endpoint. navigation_timeout_requires_fresh_url: remove the option and reacquire the existing page state, or select fresh-url mode with one absolute HTTP(S) URL when navigation timing is required. session_unavailable: run capture session list; retry only with an exact row whose lifecycle is open and target_live=true, or run capture session start --url <url> [--port <port>]. target_unavailable: run capture browser list; retry exactly capture measure snap --port <displayed-port> --target <full-displayed-id>. browser_endpoint_unavailable: run capture browser detect or capture browser list, then rerun the URL with an available displayed port. navigation_failed: correct the URL/connectivity or raise the bounded navigation timeout and rerun the same URL invocation; no snapshot was retained. temporary_target_cleanup_unconfirmed: run capture browser list --port <reported-port>; if the full reported target remains, run capture browser cdp --port <reported-port> Target.closeTarget \'{"targetId":"<full-reported-id>"}\', then reacquire. No snapshot or ID index entry was published. capture_restoration_unconfirmed: re-establish the intended page state and reacquire explicitly; the failed snapshot is unavailable, incomplete artifacts were removed, and no ID index entry was published. artifact_path_too_long: use a shorter Capture root or snapshot path and rerun; no durable artifact was created for the failed invocation. snapshot_publication_owner_live: let the reported live publication finish or terminate that exact owner before retrying; no index entry was published. snapshot_index_recovery_failed: restore filesystem access for the reported final/quarantine paths and retry; no index entry was published.' }),
|
|
106
|
+
leaf('measure check', 'inventory one measurement family or detail.', { positionals: [positional('snapshot', 'snapshot-id-or-absolute-directory')], flags: [value('--family', 'canonical-family', families), value('--detail', 'fact-id'), limit()], mutualExclusions: [['--family', '--detail'], ['--detail', '--limit']], effects: effects(false, false, true), whenToUse: 'Choose this for a known family\'s recorded population and distributions or a bounded all-family orientation; use geometry for an exact relation between two selected subjects, map for page-wide topology, and explain for one subject\'s causal provenance.', constraints: `One snapshot; optional --family is one of ${families.join(', ')}; --detail and --family conflict; --limit applies only to summary/family mode.`, orderedOutput: 'Attestation; scope; viewport/content facts; canonical family directory or bounded detail; collector coverage; one derived manifest and exhaustive fact locator; omission metadata; static follow-up.', artifactOwnership: 'Always creates or reuses the deterministic derived read and its exhaustive fact artifact.', recovery: 'Correct snapshot/family/detail/limit input before effects; broad reads return to capture measure check -h, while focused facts use their returned fact ID detail form.' }),
|
|
107
|
+
leaf('measure geometry', 'measure nominal relation between two subjects.', { positionals: [positional('snapshot', 'snapshot-id-or-absolute-directory')], flags: [requiredValue('--first', ref), requiredValue('--second', ref)], effects: NO_EFFECTS, whenToUse: 'Choose this for two-subject rectangles, edge/center deltas, nominal intersection, and pairwise distance; these are nominal boxes, not painted-pixel coverage. Use explain for one subject\'s captured clipping/style provenance, map layers for page-wide stacking/paint evidence, and page screenshot for what pixels visibly appear covered, occluded, clipped, cropped, masked, or absent.', constraints: 'One explicit snapshot and required singleton --first and --second ordered element references.', orderedOutput: 'Explicit snapshot; ordered --first and --second subject refs (css:, backend:, axid:, ax:, text:); attestation and ordered identities; page-css-v1 coordinate authority/comparability; each subject border_aabb availability/provenance; available border AABBs; corresponding-edge and center deltas (second - first); signed separations (positive gap, zero touch, negative overlap); separately named center distance as the Euclidean center-to-center distance; minimum border-AABB distance as the Euclidean norm of nonnegative gaps (zero for touching/intersecting boxes); exact intersection relation/AABB/area: positive-area, edge-touching, or disjoint with null intersection_aabb and zero area; source coverage; exactly one immutable snapshot root, fixed meta.json locator, bounded manifest keys; caveats. It makes no paint, clipping, or pixel-coverage claim; content_quads_union_aabb is separately named provenance and never satisfies a border field.', artifactOwnership: 'Reads immutable snapshot artifacts only and creates no artifact, derived read, crop, browser mutation, or session mutation.', recovery: 'If the explicit snapshot or either ordered subject reference is unavailable, inspect its exact resolver reason and source availability, then retry with one available css:, backend:, axid:, ax:, or text: reference from that immutable snapshot. If either border_aabb is unavailable, pair scalars remain unavailable; reacquire a snapshot only when the required source evidence was not captured.' }),
|
|
108
|
+
branch('measure map', 'page-wide focus, scroll/container, or paint/layer topology.', [
|
|
109
|
+
leaf('measure map focus', 'map keyboard traversal and focus movement.', { positionals: [positional('snapshot', 'snapshot-id-or-absolute-directory')], flags: [value('--detail', 'focus-id'), limit()], mutualExclusions: [['--detail', '--limit']], effects: effects(false, false, true), whenToUse: 'Choose this for forward/reverse focus order, unreached focusable subjects, focus-visible facts, and scroll movement caused by traversal; use scroll for container reachability independent of focus.', constraints: 'One snapshot; --detail or summary --limit <1..20>, never both.', orderedOutput: 'Attestation; traversal/source coverage; exact counts; derived manifest and focus artifact; bounded forward/reverse topology or one detail; omissions and follow-up.', artifactOwnership: 'Always creates or reuses one deterministic focus-topology derived read.', recovery: 'Correct snapshot, detail, or limit selection and rerun capture measure map focus -h.' }),
|
|
110
|
+
leaf('measure map scroll', 'map scroll topology and container extents.', { positionals: [positional('snapshot', 'snapshot-id-or-absolute-directory')], flags: [value('--detail', 'container-id'), limit()], mutualExclusions: [['--detail', '--limit']], effects: effects(false, false, true), whenToUse: 'Choose this for page-wide bounds, scroll containers, current/maximum offsets, client-versus-content size, overflow behavior on both axes, recorded ancestor overflow boundaries, sticky/fixed occupancy, snap points, and reachable content; use layers for stacking or paint order.', constraints: 'One snapshot; --detail or summary --limit <1..20>, never both.', orderedOutput: 'Attestation; authoritative viewport and separate content extent; source coverage; exact complete relation counts/reasons and relation object key; derived manifest/scroll artifact; root then source-ordered container rows with both-axis client/content/excess/overflow; detail ancestry, boundaries, reachability, relations, provenance, and omissions.', artifactOwnership: 'Always creates or reuses one deterministic scroll-topology derived read.', recovery: 'Correct the snapshot, --detail container id, or summary --limit (mutually exclusive) and rerun capture measure map scroll -h; scroll owns viewport, containers, client/content extent, both-axis overflow, recorded overflow boundaries, and reachability from one deterministic scroll-topology derived read.' }),
|
|
111
|
+
leaf('measure map layers', 'map paint, stacking, and compositor topology.', { positionals: [positional('snapshot', 'snapshot-id-or-absolute-directory')], flags: [value('--detail', 'subject-or-layer-id'), limit()], mutualExclusions: [['--detail', '--limit']], effects: effects(false, false, true), whenToUse: 'Choose this for page-wide DOMSnapshot paint rank, stacking-context/layer membership, compositor bounds/reasons, and layer-affecting declaration provenance. These facts do not establish which rendered pixels occlude another subject; use page screenshot for observed pixel coverage, geometry for one pair\'s nominal box relation, and scroll for overflow containment.', constraints: 'One snapshot; --detail or summary --limit <1..20>, never both.', orderedOutput: 'Attestation; paint/layer coverage and counts; derived manifest/layer artifact; bounded topology with nominal border bounds or exact unavailability, paint rank, membership, compositing reasons, provenance, and omissions.', artifactOwnership: 'Always creates or reuses one deterministic layer-topology derived read.', recovery: 'Correct snapshot, detail, or limit selection and rerun capture measure map layers -h.' }),
|
|
112
|
+
], 'Map children partition one snapshot by keyboard traversal, overflow/scroll containment, and paint/compositor structure; they never infer quality, intent, or pixel coverage.', false, 'Choose this when the relationship is page-wide: traversal, container bounds and client/content extents, recorded overflow clipping and reachability, or stacking/paint/layer membership and order. Use geometry for one selected pair, explain for one selected subject, and page screenshot for what pixels visibly appear covered, occluded, clipped, cropped, masked, or absent; no map leaf infers rendered-pixel coverage.'),
|
|
113
|
+
leaf('measure explain', 'read one subject’s structural provenance.', { positionals: [positional('snapshot', 'snapshot-id-or-absolute-directory')], flags: [requiredValue('--subject', ref), bool('--size'), bool('--text'), bool('--form')], effects: NO_EFFECTS, whenToUse: 'Choose this for one subject\'s cascade, containing/overflow-clip/scroll chain, stacking context, size, text, or form provenance; use geometry for distance or nominal intersection between two subjects and map for topology across the page.', constraints: 'One explicit snapshot and required singleton --subject element reference; optional bounded --size, --text, and --form sections.', orderedOutput: 'Explicit snapshot and --subject; attestation/identity; cascade winners; containing block; stacking context; scroll/overflow chain; independently named normalized border_aabb, content_quads_union_aabb, and requested box/source bounds with availability; requested sections; source coverage; one immutable snapshot root, fixed meta.json locator, bounded manifest keys covering consulted and omitted retained arrays; omission metadata.', artifactOwnership: 'Reads immutable snapshot artifacts only and creates no artifact, derived read, crop, browser mutation, or session mutation.', recovery: 'If the explicit snapshot or --subject is unavailable, inspect its exact resolver reason and source availability, then retry with one available css:, backend:, axid:, ax:, or text: reference from that immutable snapshot. Requested size, text, or form evidence remains unavailable when its captured source was omitted; reacquire only when that source evidence is required.' }),
|
|
114
|
+
branch('measure variation', 'compare states, distributions, or controlled environments.', [
|
|
115
|
+
leaf('measure variation diff', 'compare two immutable snapshots.', { flags: [requiredValue('--before', 'snapshot-id-or-absolute-directory'), requiredValue('--after', 'snapshot-id-or-absolute-directory'), bool('--pixels'), limit()], effects: effects(false, false, true), constraints: 'Exactly one ordered --before and --after; snapshots differ; optional --pixels and --limit <1..20>; no positionals.', orderedOutput: 'Ordered pair attestation; normalized diff scope; exact source/change coverage; derived manifest and exhaustive diff artifact; requested raster artifact/coverage; bounded changed records and omissions.', artifactOwnership: 'Creates or reuses one deterministic ordered-pair diff read and exhaustive diff/raster artifacts.', recovery: 'Correct the ordered before/after pair, pixels mode, or limit and rerun capture measure variation diff -h.' }),
|
|
116
|
+
leaf('measure variation census', 'summarize variation across snapshots.', { flags: [repeatableValue('--snapshot', 'snapshot-id-or-absolute-directory', true), requiredValue('--axis', 'viewport|geometry|styles|scroll|focus|layers', ['viewport', 'geometry', 'styles', 'scroll', 'focus', 'layers']), limit()], effects: effects(false, false, true), constraints: 'Repeat --snapshot 2..64 times in caller order; exactly one case-sensitive --axis; no duplicates or positionals.', orderedOutput: 'Ordered-set attestation; axis population definition; exact per-source coverage and nearest-rank distributions/state counts; derived manifest/census artifact; bounded records and omissions.', artifactOwnership: 'Creates or reuses one deterministic census read and exhaustive census artifact.', recovery: 'Correct ordered snapshot membership, axis, or limit and rerun capture measure variation census -h.' }),
|
|
117
|
+
leaf('measure variation sweep', 'acquire a controlled variation sweep.', { flags: [value('--session', 'exact-session-id'), value('--target', 'target-token'), port(), value('--navigation-timeout', '1..60000', undefined, undefined, 'ms'), requiredValue('--axis', 'viewport-width|viewport-height', ['viewport-width', 'viewport-height']), requiredValue('--from', '1..16384'), requiredValue('--to', '1..16384'), value('--samples', '2..64', undefined, 5), value('--viewport-width', '1..16384'), value('--viewport-height', '1..16384'), bool('--freeze-animations'), timeout('--settle-timeout'), bool('--capture-unsettled'), bool('--pixels')], mutualExclusions: [['--session', '--target'], ['--session', '--port']], effects: effects(true, true, true), constraints: 'Selects one existing target: --session <exact-id>, or --target <token> with optional --port, or scoped active-session omission. --port is accepted only with --target (with --session it fails port_conflicts_with_session; alone it fails sweep_port_requires_target). A positional URL is forbidden. --navigation-timeout is recognized but always fails sweep_navigation_timeout_forbidden (no fresh-url mode here). Raw selection ignores ambient CDP_PORT, CDP_TARGET, and CDP_HAR_ID; all selection failures precede probes, locks, mutation, and allocation; resolution/locking matches snap. Axis is exactly viewport-width or viewport-height. --from and --to are safe integers 1..16384 with from < to. --samples <2..64> default 5, and samples must be <= to-from+1. A width sweep requires singleton --viewport-height <1..16384>; a height sweep requires singleton --viewport-width <1..16384>. Snap acquisition flags allowed are --freeze-animations, --settle-timeout <1..60000> default 5000, --capture-unsettled, --pixels; state-mutation flags, legacy ranges/set-files/gates are rejected before probes, locks, mutation, allocation.', orderedOutput: 'Endpoint/session attestation; exact axis/range/sample values; immutable observation manifest; ascending sampled snapshot IDs/directories; transitions/uncertainties; coverage; bounded rows/omissions; restoration state.', artifactOwnership: 'Creates immutable sampled snapshots and one observation/recovery manifest, never a derived read.', recovery: 'sweep_port_requires_target: add --target <token>, or remove --port to use the scoped active session. port_conflicts_with_session: remove --port; session routes always use their recorded endpoint. sweep_navigation_timeout_forbidden: remove --navigation-timeout and retry the same existing-target sweep. Correct axis, --from, --to, --samples, or the orthogonal --viewport dimension through capture measure variation sweep -h. A failed sample points to its recovery manifest with every finalized partial snapshot. snapshot_publication_owner_live: let the reported live publication finish or terminate that exact owner before retrying; no index entry was published. snapshot_index_recovery_failed: restore filesystem access for the reported final/quarantine paths and retry; no index entry was published.' }),
|
|
118
|
+
], 'Variation children select ordered pair comparison, ordered-set distribution, or controlled target sampling; direct reads own facts within one snapshot.', false, 'Choose this when the question spans snapshots or controlled environments rather than one settled state; use the direct read children for facts within one snapshot.'),
|
|
119
|
+
], 'A snapshot is an immutable explicit observation. snap acquires it; check, geometry, map, and explain read one explicit snapshot; variation owns cross-state work. Choose by fact resolution, not fixed sequence.', false, 'Choose this when a claim depends on nominal geometry or intersection, captured CSS/ancestor clipping or overflow structure, scroll/container extents, stacking or paint order, computed style, sampled hit reception, or distance; screenshots and accessibility trees do not establish these structural facts. Measurement does not establish what pixels visibly appear covered, occluded, clipped, cropped, masked, or absent; use page screenshot for that separate pixel evidence.'),
|
|
120
|
+
branch('motion', 'recorded temporal behavior and frame-change measurements.', [
|
|
121
|
+
leaf('motion rec', 'record page motion.', { effects: effects(true, false, true) }),
|
|
122
|
+
leaf('motion mask', 'record motion masking evidence.', { positionals: [positional('recording-id', 'recording-id')], effects: effects(false, false, true) }),
|
|
123
|
+
leaf('motion timeline', 'read a motion timeline.', { positionals: [positional('recording-id', 'recording-id')] }),
|
|
124
|
+
leaf('motion jank', 'read frame timing measurements.', { positionals: [positional('recording-id', 'recording-id')] }),
|
|
125
|
+
leaf('motion response', 'read motion response measurements.', { positionals: [positional('recording-id', 'recording-id')] }),
|
|
126
|
+
], undefined, false, 'Choose this when measuring response timing, changed pixels over time, animation timelines, or frame/jank facts; use measure for one settled structural state.'),
|
|
127
|
+
branch('traffic', 'network recording and HAR reads.', [
|
|
128
|
+
leaf('traffic record', 'record network traffic.', { flags: [value('--duration', 'positive-ms', undefined, undefined, 'ms')], effects: effects(true, false, true) }),
|
|
129
|
+
branch('traffic har', 'HAR artifact lifecycle.', [
|
|
130
|
+
leaf('traffic har create', 'create a HAR recording.', { effects: effects(true, false, true) }),
|
|
131
|
+
leaf('traffic har read', 'read stored HAR bytes.', { positionals: [positional('har-id', 'exact-har-id')], constraints: 'One exact HAR ID; no global JSON.' }),
|
|
132
|
+
leaf('traffic har delete', 'delete a HAR recording.', { positionals: [positional('har-id', 'exact-har-id')], effects: effects(false, false, true) }),
|
|
133
|
+
]),
|
|
134
|
+
], undefined, false, 'Choose this when measuring requests, responses, status, timing, or payload metadata; use page for page state and browser for endpoint/connectivity control.'),
|
|
135
|
+
branch('browser', 'CDP endpoints, tabs, connectivity, and raw protocol access.', [
|
|
136
|
+
leaf('browser detect', 'detect browser endpoints.', { growing: true, effects: effects(true, false, false, true), constraints: 'No target/port input or ambient CDP selection.', orderedOutput: 'Selected endpoint; preference class; deterministically numeric-port-sorted endpoint rows; availability.' }),
|
|
137
|
+
leaf('browser list', 'list attachable browser page targets.', { flags: [port()], growing: true, effects: effects(true), constraints: 'Optional singleton --port <1..65535>; duplicate or invalid port fails before probes.', orderedOutput: 'Only attachable page rows with full target ID, endpoint, observed URL, title, attachability, sorted by (port, fullTargetId).' }),
|
|
138
|
+
leaf('browser open', 'open a fresh browser page.', { flags: [requiredValue('--url', 'absolute-http(s)-url'), port(), timeout('--navigation-timeout')], effects: effects(true), constraints: 'Required singleton --url; optional --port and navigation timeout; no positional URL, target, or --new.', orderedOutput: 'Full endpoint-qualified target identity and observed URL after navigation.' }),
|
|
139
|
+
leaf('browser reset', 'reset a browser page.', { positionals: [positional('url', 'absolute-http(s)-url')], effects: effects(true) }),
|
|
140
|
+
leaf('browser network', 'set selected page network state.', { positionals: [positional('state', 'network-state')], effects: effects(true) }),
|
|
141
|
+
leaf('browser cdp', 'send one raw CDP request.', { positionals: [positional('method', '^[A-Za-z][A-Za-z0-9]*\\.[A-Za-z][A-Za-z0-9]*$'), positional('params-json-object', 'JSON-object', false)], flags: [port(true)], effects: effects(true), constraints: 'Required singleton --port <1..65535>, one required method, and at most one JSON-object params positional (omitted means {}). No ambient endpoint, session, target, URL, stdin, or duplicate options.', orderedOutput: 'Exactly the matching CDP response text-frame bytes, including byte order and final-newline absence; unrelated events are omitted.', artifactOwnership: 'Creates no artifact. Exactly one CDP request is sent only after all validation and endpoint connection succeed.', recovery: 'browser_endpoint_unavailable and cdp_transport_failed: check the exact port with capture browser list --port <port> before retry. cdp_command_failed: use the exact stdout error response to correct the method or params before retry. Stable input failures (browser_cdp_port_required, invalid_port, browser_cdp_method_required, browser_cdp_method_invalid, browser_cdp_params_invalid, duplicate_option, unknown_option, unexpected_positional) recover through capture browser cdp -h.' }),
|
|
142
|
+
], undefined, false, 'Choose this when selecting or repairing browser/tab infrastructure, opening a tab, changing network state, or invoking an otherwise-unwrapped CDP method; use a domain branch when Capture already exposes the needed evidence or action.'),
|
|
143
|
+
branch('library', 'catalog and schemas for bundled site-service functions.', [
|
|
144
|
+
leaf('library list', 'list bundled libraries.', { growing: true }),
|
|
145
|
+
leaf('library search', 'search bundled library functions.', { positionals: [positional('query', 'text')], growing: true }),
|
|
146
|
+
leaf('library show', 'show a bundled library.', { positionals: [positional('library', 'library-name')], growing: true }),
|
|
147
|
+
leaf('library read', 'read bundled function source bytes.', { positionals: [positional('library', 'library-name')], effects: effects(false, false, false, true), constraints: 'One declared library name; no global JSON.' }),
|
|
148
|
+
], undefined, false, 'Choose this when selecting or reading a bundled site API function before executing it through page exec; use traffic to inspect browser network evidence.'),
|
|
149
|
+
{ kind: 'leaf', path: '__bridge-serve', summary: 'internal bridge server.', visibility: 'internal', positionals: [], flags: [], mutualExclusions: [], effects: effects(false, false, false, true), result: bounded('__bridge-serve'), outputMode: 'structured-json-capable', bounds: { maxBytes: MAX_BOUNDED_BYTES, maxRecords: 0, growing: false, paginated: false }, stderr: 'empty-on-success', exits: [0, 2, 3], handler: '__bridge.serve', help: { description: 'internal bridge server.', whenToUse: 'Internal runtime bridge only.', rubric: 'Not a public command.', constraints: 'Exact internal path only.', orderedOutput: 'Internal structured status.', artifactOwnership: 'No public artifact contract.', followUp: 'internal only.' }, recovery: 'Internal runtime recovery only.' },
|
|
150
|
+
], undefined, true);
|
|
151
|
+
|
|
152
|
+
export function flattenRegistry(node: RegistryNode = CAPTURE_REGISTRY): readonly RouteDescriptor[] {
|
|
153
|
+
if (node.kind === 'leaf') return [node];
|
|
154
|
+
return [...(node.path ? [node] : []), ...node.children.flatMap((child) => flattenRegistry(child))];
|
|
155
|
+
}
|
|
156
|
+
function outputOwner(leaf: RegistryLeaf): LeafOutputOwner {
|
|
157
|
+
if (leaf.outputMode === 'structured-json-capable') return { mode: leaf.outputMode, canonicalPath: leaf.path };
|
|
158
|
+
return { mode: leaf.outputMode, canonicalPath: leaf.path, payloadType: (leaf.result.kind === 'exact-raw' ? leaf.result.payload : ''), size: 'unbounded' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Validates public census/output projection and internal entries independently. */
|
|
162
|
+
export function validateCaptureRegistry(root: RegistryBranch = CAPTURE_REGISTRY): ValidationResult {
|
|
163
|
+
const all = flattenRegistry(root) as readonly RegistryNode[];
|
|
164
|
+
const publicNodes = all.filter((node) => node.visibility === 'public');
|
|
165
|
+
const internalNodes = all.filter((node) => node.visibility === 'internal');
|
|
166
|
+
const errors: ValidationResult[] = [validateRegistry(publicNodes), validateLeafOutputOwners(publicNodes.filter((node): node is RegistryLeaf => node.kind === 'leaf').map(outputOwner))];
|
|
167
|
+
const publicPaths = new Set(['', ...publicNodes.map((node) => node.path)]);
|
|
168
|
+
const validateFollowUp = (node: RegistryNode): void => {
|
|
169
|
+
if (node.visibility !== 'public') return;
|
|
170
|
+
const match = /^run capture(?: (.*))? -h$/.exec(node.help.followUp);
|
|
171
|
+
if (!match || !publicPaths.has(match[1] ?? '')) errors.push(fail(`route ${node.path || 'root'} follow-up does not resolve to a public canonical help path`));
|
|
172
|
+
};
|
|
173
|
+
const visit = (node: RegistryNode, parent?: RegistryBranch): void => {
|
|
174
|
+
const h = node.help;
|
|
175
|
+
if (!h.description || !h.whenToUse || !h.rubric || !h.followUp || !h.constraints || !h.orderedOutput || !h.artifactOwnership) errors.push(fail(`route ${node.path || 'root'} missing help contract`));
|
|
176
|
+
if (node.aliases?.length) errors.push(fail(`route ${node.path || 'root'} declares aliases; canonical paths have no aliases`));
|
|
177
|
+
validateFollowUp(node);
|
|
178
|
+
if (node.kind === 'leaf') {
|
|
179
|
+
errors.push(validateLeafDescriptor(node));
|
|
180
|
+
if (!node.recovery) errors.push(fail(`leaf ${node.path} missing recovery`));
|
|
181
|
+
if (node.outputMode === 'exact-raw-json-rejected') {
|
|
182
|
+
if (node.result.kind !== 'exact-raw') errors.push(fail(`leaf ${node.path} exact-raw output mode needs exact-raw result`));
|
|
183
|
+
const rejection = exactRawJsonRejection(node.path);
|
|
184
|
+
if (node.visibility === 'public' && ![rejection.code, rejection.field, rejection.expected, rejection.next_action].every((field) => node.help.rubric.includes(field))) {
|
|
185
|
+
errors.push(fail(`exact-raw leaf ${node.path} help must declare the full ${rejection.code} diagnostic`));
|
|
186
|
+
}
|
|
187
|
+
} else if (node.result.kind !== 'bounded') errors.push(fail(`leaf ${node.path} structured output mode needs bounded result`));
|
|
188
|
+
} else {
|
|
189
|
+
if (node.path) errors.push(validateBranchDescriptor(node));
|
|
190
|
+
const publicChildren = node.children.filter((child) => child.visibility === 'public');
|
|
191
|
+
if (publicChildren.length > 7) errors.push(fail(`branch ${node.path || 'root'} has ${publicChildren.length} public children; maximum is 7`));
|
|
192
|
+
const names = new Set<string>();
|
|
193
|
+
for (const child of node.children) {
|
|
194
|
+
const name = child.path.split(' ').at(-1)!;
|
|
195
|
+
if (names.has(name)) errors.push(fail(`branch ${node.path || 'root'} has duplicate child ${name}`));
|
|
196
|
+
names.add(name);
|
|
197
|
+
if (child.path !== `${node.path}${node.path ? ' ' : ''}${name}`) errors.push(fail(`child ${child.path} is not canonical beneath ${node.path || 'root'}`));
|
|
198
|
+
visit(child, node);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (parent && !node.path) errors.push(fail('child missing canonical path'));
|
|
202
|
+
};
|
|
203
|
+
visit(root);
|
|
204
|
+
for (const internal of internalNodes) if (internal.visibility !== 'internal') errors.push(fail(`internal route ${internal.path} leaked public`));
|
|
205
|
+
const expected = new Set(EXPECTED_LEAF_PATHS);
|
|
206
|
+
if (publicNodes.filter((node): node is RegistryLeaf => node.kind === 'leaf').some((leaf) => !expected.has(leaf.path))) errors.push(fail('registry contains a public leaf outside the canonical census'));
|
|
207
|
+
return combine(...errors);
|
|
208
|
+
}
|