@crouton-kit/capture 1.4.1 → 1.4.10
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/README.md +178 -0
- package/bin/capture +31299 -7322
- package/package.json +3 -1
- package/src/capture.ts +21 -18
- package/src/cdp/args.ts +269 -49
- package/src/cdp/bridge/server.ts +6 -5
- package/src/cdp/bridge/spawn.ts +41 -97
- package/src/cdp/commands/bridge-serve.ts +11 -21
- package/src/cdp/commands/cdp.ts +44 -22
- package/src/cdp/commands/heap/census.ts +61 -0
- package/src/cdp/commands/heap/common.ts +77 -0
- package/src/cdp/commands/heap/diff.ts +92 -0
- package/src/cdp/commands/heap/index.ts +38 -0
- package/src/cdp/commands/heap/objects.ts +42 -0
- package/src/cdp/commands/heap/retainers.ts +52 -0
- package/src/cdp/commands/heap/snapshot.ts +94 -0
- package/src/cdp/commands/lib.ts +9 -17
- package/src/cdp/commands/lighthouse.ts +414 -0
- package/src/cdp/commands/measure/census.ts +2 -1
- package/src/cdp/commands/measure/check.ts +70 -16
- package/src/cdp/commands/measure/diff.ts +1 -2
- package/src/cdp/commands/measure/explain.ts +44 -7
- package/src/cdp/commands/measure/index.ts +24 -32
- package/src/cdp/commands/measure/map-ax.ts +2 -1
- package/src/cdp/commands/measure/map-focus.ts +2 -1
- package/src/cdp/commands/measure/map-layers.ts +5 -3
- package/src/cdp/commands/measure/map-scroll.ts +2 -1
- package/src/cdp/commands/measure/snap.ts +5 -4
- package/src/cdp/commands/measure/sweep.ts +16 -11
- package/src/cdp/commands/measure/text.ts +147 -0
- package/src/cdp/commands/motion/index.ts +10 -18
- package/src/cdp/commands/motion/jank.ts +13 -22
- package/src/cdp/commands/motion/mask.ts +10 -9
- package/src/cdp/commands/motion/rec.ts +230 -28
- package/src/cdp/commands/motion/timeline.ts +1 -1
- package/src/cdp/commands/page/click.ts +18 -8
- package/src/cdp/commands/page/elements.ts +43 -11
- package/src/cdp/commands/page/exec.ts +53 -18
- package/src/cdp/commands/page/index.ts +20 -20
- package/src/cdp/commands/page/inspect.ts +338 -0
- package/src/cdp/commands/page/navigate.ts +83 -38
- package/src/cdp/commands/page/repeat.ts +176 -0
- package/src/cdp/commands/page/scroll.ts +11 -6
- package/src/cdp/commands/page/shot.ts +68 -33
- package/src/cdp/commands/page/type.ts +7 -3
- package/src/cdp/commands/perf/index.ts +32 -0
- package/src/cdp/commands/perf/insights.ts +214 -0
- package/src/cdp/commands/perf/trace.ts +235 -0
- package/src/cdp/commands/perf/vitals.ts +76 -0
- package/src/cdp/commands/session/collectors.ts +54 -0
- package/src/cdp/commands/stub.ts +10 -0
- package/src/cdp/commands/tab/close.ts +2 -2
- package/src/cdp/commands/tab/index.ts +16 -23
- package/src/cdp/commands/tab/launch.ts +1 -1
- package/src/cdp/commands/tab/mock/index.ts +24 -0
- package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
- package/src/cdp/commands/tab/mock/start.ts +65 -0
- package/src/cdp/commands/tab/mock/stop.ts +39 -0
- package/src/cdp/commands/tab/network.ts +22 -13
- package/src/cdp/commands/tab/reset.ts +11 -12
- package/src/cdp/connection.ts +19 -39
- package/src/cdp/detect.ts +27 -7
- package/src/cdp/dispatch.ts +25 -4
- package/src/cdp/domains.ts +2 -2
- package/src/cdp/har-recorder.ts +0 -1
- package/src/cdp/heap-snapshot.ts +647 -0
- package/src/cdp/host/client.ts +22 -0
- package/src/cdp/host/collector.ts +74 -0
- package/src/cdp/host/collectors/heap.ts +79 -0
- package/src/cdp/host/collectors/intercept.ts +247 -0
- package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
- package/src/cdp/host/collectors/trace.ts +108 -0
- package/src/cdp/host/core.ts +217 -0
- package/src/cdp/host/handle.ts +47 -0
- package/src/cdp/host/held-client.ts +91 -0
- package/src/cdp/host/kinds.ts +97 -0
- package/src/cdp/host/lifecycle.ts +123 -0
- package/src/cdp/host/server.ts +88 -0
- package/src/cdp/leaf-grammar.ts +2 -2
- package/src/cdp/measure/check.ts +181 -16
- package/src/cdp/measure/collectors/hittest.ts +16 -10
- package/src/cdp/measure/collectors/scroll.ts +15 -7
- package/src/cdp/measure/collectors/style-provenance.ts +11 -0
- package/src/cdp/measure/collectors/styles.ts +4 -4
- package/src/cdp/measure/explain.ts +14 -2
- package/src/cdp/measure/map-layers.ts +52 -8
- package/src/cdp/measure/snapshot.ts +5 -3
- package/src/cdp/measure/sweep.ts +29 -6
- package/src/cdp/measure/text.ts +227 -0
- package/src/cdp/motion/jank.ts +29 -83
- package/src/cdp/motion/mask.ts +18 -9
- package/src/cdp/motion/recorder.ts +86 -731
- package/src/cdp/scope-lock.ts +9 -9
- package/src/cdp/screenshot.ts +51 -21
- package/src/cdp/timing.ts +5 -5
- package/src/cdp/trace-analysis.ts +320 -0
- package/src/cdp/types.ts +31 -1
- package/src/cdp.ts +4 -6
- package/src/har-manager.ts +2 -4
- package/src/interact.ts +78 -25
- package/src/output/artifact.ts +49 -5
- package/src/output/render.ts +60 -9
- package/src/output/selection.ts +6 -0
- package/src/session/artifacts.ts +122 -12
- package/src/session/commands.ts +348 -113
- package/src/session/coordinator.ts +0 -93
- package/src/session/har-redact.ts +29 -23
- package/src/session-context.ts +13 -9
- package/src/cdp/bridge/protocol.ts +0 -224
- package/src/cdp/recorder-client.ts +0 -326
- package/src/cdp/tab-lock.ts +0 -132
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ParsedArgs } from '../../types.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { selectRecords } from '../../../output/selection.js';
|
|
3
|
+
import { resolveSnapRef, readGeometry, readMeta, type ArtifactResolutionError } from '../../../output/artifact.js';
|
|
4
|
+
import { resolveSelectorInput } from '../../../output/selector.js';
|
|
5
|
+
import { capped, data, emitResult, fact, line, text, formatCoordinate, formatFindings, type FactLine, type FindingInput, type RenderableResult } from '../../../output/render.js';
|
|
4
6
|
import { captureMeasureSnap } from './snap.js';
|
|
5
7
|
import { checkSnapshot, parseChecks, writeFindingCrop } from '../../measure/check.js';
|
|
6
8
|
|
|
@@ -9,10 +11,13 @@ const USAGE = `capture measure check [url|snap] — read threshold/fact measurem
|
|
|
9
11
|
input:
|
|
10
12
|
[url|snap] a URL creates one one-shot snapshot first; a snap id or absolute path is read without re-driving the browser
|
|
11
13
|
--for <checks> geometry|content|targetability|forms|animation|all, or comma-separated overlap,offscreen,overflow,tap-targets,contrast,hit-test,truncation,forms,media,animation
|
|
12
|
-
--limit <n>
|
|
14
|
+
--limit <n> return at most n representative findings in prose and JSON (default: 20)
|
|
15
|
+
--selector <s> limit measurements to recorded CSS/text/AX selector input or backend:<id>
|
|
16
|
+
--artifact-dir <path> root for the one-shot snapshot bundle when the target is a URL
|
|
13
17
|
--gate exit 2 when the report contains findings (default: exit 0)
|
|
14
18
|
output: <checks result=… findings=…> — findings with coordinates and collection provenance; measurements, not a pass/fail judgment; --json mirrors
|
|
15
|
-
|
|
19
|
+
boundary: measurements are local to the selected snapshot. \`capture lighthouse\` runs the independent audited sweep.
|
|
20
|
+
effects: read-only over an existing snapshot artifact; a URL target writes one one-shot snapshot bundle under --artifact-dir when supplied`;
|
|
16
21
|
|
|
17
22
|
const DEFAULT_FINDING_LIMIT = 20;
|
|
18
23
|
|
|
@@ -49,6 +54,34 @@ function caveatLine(caveats: readonly { regionId: string; selector?: string; rea
|
|
|
49
54
|
return fact`Nondeterminism caveat: unstable region ${caveats.map((c) => `${c.regionId}${c.selector ? ` (${c.selector})` : ''}${c.reason ? `: ${c.reason}` : ''}`).join('; ')}.`;
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/** Contrast colors are the measurement's subject, not incidental selector text. Keep each color as its own uncapped, escaped field so a long selector cannot elide either one. */
|
|
58
|
+
function contrastHeadline(detail: string): FactLine {
|
|
59
|
+
const ratioPrefix = 'contrast ratio ';
|
|
60
|
+
const divider = ' — ';
|
|
61
|
+
const foregroundMarker = ' foreground ';
|
|
62
|
+
const backgroundMarker = ' against composited background ';
|
|
63
|
+
const dividerAt = detail.indexOf(divider);
|
|
64
|
+
if (!detail.startsWith(ratioPrefix) || dividerAt < 0) return fact`${detail}`;
|
|
65
|
+
const ratio = detail.slice(ratioPrefix.length, dividerAt);
|
|
66
|
+
const subject = detail.slice(dividerAt + divider.length);
|
|
67
|
+
const foregroundAt = subject.lastIndexOf(foregroundMarker);
|
|
68
|
+
const backgroundAt = subject.indexOf(backgroundMarker, foregroundAt + foregroundMarker.length);
|
|
69
|
+
if (foregroundAt < 0 || backgroundAt < 0) return fact`${detail}`;
|
|
70
|
+
const selector = subject.slice(0, foregroundAt);
|
|
71
|
+
const foreground = subject.slice(foregroundAt + foregroundMarker.length, backgroundAt);
|
|
72
|
+
const background = subject.slice(backgroundAt + backgroundMarker.length);
|
|
73
|
+
return line(
|
|
74
|
+
text`contrast ratio `,
|
|
75
|
+
data(ratio),
|
|
76
|
+
text` — `,
|
|
77
|
+
data(selector),
|
|
78
|
+
text` foreground `,
|
|
79
|
+
data(capped(foreground, Number.MAX_SAFE_INTEGER)),
|
|
80
|
+
text` against composited background `,
|
|
81
|
+
data(capped(background, Number.MAX_SAFE_INTEGER)),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
52
85
|
export async function cmdMeasureCheck(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
53
86
|
if (parsed.help) { console.log(USAGE); return; }
|
|
54
87
|
if (parsed.positional.length > 1) {
|
|
@@ -62,14 +95,34 @@ export async function cmdMeasureCheck(parsed: ParsedArgs, _args: string[]): Prom
|
|
|
62
95
|
const checks = parseChecks(parsed.for);
|
|
63
96
|
const ref = await resolveSnapRef(target, { onUrl: async (url) => captureMeasureSnap({ ...parsed, positional: [url] }, url) });
|
|
64
97
|
const report = checkSnapshot(ref, checks);
|
|
65
|
-
const
|
|
66
|
-
|
|
98
|
+
const scopedElements = parsed.selector
|
|
99
|
+
? resolveSelectorInput(readGeometry<{ elements?: Array<{ id: string; selector?: string; backendNodeId?: number | null }> }>(ref).elements ?? [], parsed.selector)
|
|
100
|
+
: undefined;
|
|
101
|
+
if (scopedElements && !scopedElements.length) {
|
|
102
|
+
emitResult({
|
|
103
|
+
tag: 'error',
|
|
104
|
+
attrs: { command: 'measure check', status: 'missing_selector', selector: parsed.selector! },
|
|
105
|
+
summary: fact`No geometry record matched selector input ${parsed.selector}.`,
|
|
106
|
+
followUp: text`Use a recorded selector or backend:<id> from geometry.json.`,
|
|
107
|
+
}, { json: parsed.json });
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const scopedIds = scopedElements ? new Set(scopedElements.map((element) => element.id)) : undefined;
|
|
112
|
+
const scopedBackendNodeIds = scopedElements ? new Set(scopedElements.map((element) => element.backendNodeId).filter((backendNodeId): backendNodeId is number => backendNodeId !== null && backendNodeId !== undefined)) : undefined;
|
|
113
|
+
const selectorCounts = new Map<string, number>();
|
|
114
|
+
for (const element of readGeometry<{ elements?: Array<{ selector?: string }> }>(ref).elements ?? []) if (element.selector !== undefined) selectorCounts.set(element.selector, (selectorCounts.get(element.selector) ?? 0) + 1);
|
|
115
|
+
const scopedSelectors = scopedElements ? new Set(scopedElements.map((element) => element.selector).filter((selector): selector is string => selector !== undefined && selectorCounts.get(selector) === 1)) : undefined;
|
|
116
|
+
const findings = scopedIds ? report.findings.filter((finding) => (finding.elementId !== undefined && scopedIds.has(finding.elementId)) || (finding.backendNodeId !== undefined && scopedBackendNodeIds!.has(finding.backendNodeId)) || (finding.selector !== undefined && scopedSelectors!.has(finding.selector))) : report.findings;
|
|
117
|
+
const findingsForOutput = selectRecords(representativeFindings(findings, parsed.limit ?? DEFAULT_FINDING_LIMIT), parsed, parsed.limit ?? DEFAULT_FINDING_LIMIT);
|
|
67
118
|
const withCrops = findingsForOutput.map((finding, index) => ({ ...finding, crop: writeFindingCrop(ref, finding, index) }));
|
|
68
119
|
const meta = readMeta<{ settled: boolean; capturedAt?: string }> (ref);
|
|
69
120
|
const findingSections: FindingInput[] = withCrops.map((finding) => ({
|
|
70
121
|
kind: finding.kind,
|
|
71
|
-
headline: fact`${finding.detail}`,
|
|
122
|
+
headline: finding.kind === 'contrast' ? contrastHeadline(finding.detail) : fact`${finding.detail}`,
|
|
72
123
|
detail: [
|
|
124
|
+
...(finding.rect ? [line(text`Rect: `, formatCoordinate(finding.rect))] : []),
|
|
125
|
+
...(finding.backendNodeId !== undefined ? [fact`Selector input: backend:${finding.backendNodeId}`] : []),
|
|
73
126
|
...(finding.provenance ? [fact`Provenance: ${finding.provenance}`] : []),
|
|
74
127
|
...(caveatLine(finding.caveats) ? [caveatLine(finding.caveats)!] : []),
|
|
75
128
|
],
|
|
@@ -81,20 +134,21 @@ export async function cmdMeasureCheck(parsed: ParsedArgs, _args: string[]): Prom
|
|
|
81
134
|
kind: 'snapshot', id: ref.id, path: ref.dir,
|
|
82
135
|
note: meta.settled ? text`Measurements read from a settled snapshot.` : text`Measurements read from a queryable unsettled snapshot; affected facts carry per-region nondeterminism caveats.`,
|
|
83
136
|
},
|
|
84
|
-
attrs: { result:
|
|
85
|
-
summary:
|
|
86
|
-
? fact`${
|
|
87
|
-
: fact`No selected measurement threshold was crossed among ${report.elementCount} recorded elements in viewport ${report.viewport.width}×${report.viewport.height}.`,
|
|
137
|
+
attrs: { result: findings.length ? 'findings' : 'clean', checks: checks.join(','), elements: scopedElements?.length ?? report.elementCount, findings: findings.length, displayed: withCrops.length, settled: report.settled, ...(parsed.selector ? { selector: parsed.selector } : {}) },
|
|
138
|
+
summary: findings.length
|
|
139
|
+
? fact`${findings.length} measured fact(s) matched the selected thresholds in viewport ${report.viewport.width}×${report.viewport.height}; ${withCrops.length} representative record(s) rendered.`
|
|
140
|
+
: fact`No selected measurement threshold was crossed among ${scopedElements?.length ?? report.elementCount} recorded elements in viewport ${report.viewport.width}×${report.viewport.height}.`,
|
|
88
141
|
sections: [
|
|
89
|
-
...(
|
|
142
|
+
...(findings.length ? [line(text`Finding counts: `, rollup(findings)!)] : []),
|
|
90
143
|
...formatFindings(findingSections),
|
|
144
|
+
fact`Measurements are local to snapshot ${ref.id}; \`capture lighthouse\` runs the independent audited sweep.`,
|
|
91
145
|
],
|
|
92
|
-
followUp:
|
|
93
|
-
? fact`
|
|
94
|
-
:
|
|
146
|
+
followUp: findings.length > withCrops.length
|
|
147
|
+
? fact`The full snapshot artifact at ${ref.dir} retains every measured finding input; this result is bounded by --limit.`
|
|
148
|
+
: undefined,
|
|
95
149
|
};
|
|
96
150
|
emitResult(result, { json: parsed.json });
|
|
97
|
-
if (parsed.gate &&
|
|
151
|
+
if (parsed.gate && findings.length) process.exitCode = 2;
|
|
98
152
|
} catch (err) {
|
|
99
153
|
const resolution = err as Partial<ArtifactResolutionError>;
|
|
100
154
|
const detail = err instanceof Error ? err.message : 'unknown artifact read failure';
|
|
@@ -50,8 +50,7 @@ export async function cmdMeasureDiff(parsed: ParsedArgs, _args: string[]): Promi
|
|
|
50
50
|
const result: RenderableResult = {
|
|
51
51
|
tag: 'error',
|
|
52
52
|
attrs: { command: 'measure diff', status: 'invalid_input' },
|
|
53
|
-
summary:
|
|
54
|
-
followUp: text`Use capture measure diff --before SNAP --after SNAP.`,
|
|
53
|
+
summary: fact`received: ${parsed.positional.length} positional argument(s), --before=${parsed.before ?? '(missing)'}, --after=${parsed.after ?? '(missing)'}\nexpected: no positional target and both settled snapshot references\nfield: <positional>, --before, --after\nNext: Run \`capture measure diff -h\` and read the schema before re-issuing.`,
|
|
55
54
|
};
|
|
56
55
|
emitResult(result, { json: parsed.json });
|
|
57
56
|
process.exitCode = 1;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ParsedArgs } from '../../types.js';
|
|
2
|
-
import { explainSnapshot, type ExplainMissingSelector } from '../../measure/explain.js';
|
|
2
|
+
import { explainSnapshot, type ExplainAmbiguousSelector, type ExplainMissingSelector } from '../../measure/explain.js';
|
|
3
3
|
import { ArtifactResolutionError, resolveSnapRef } from '../../../output/artifact.js';
|
|
4
4
|
import { data, emitResult, fact, line, lineList, text, type FactLine, type RenderableResult } from '../../../output/render.js';
|
|
5
5
|
|
|
@@ -9,10 +9,11 @@ input:
|
|
|
9
9
|
<snap> snapshot id in the active session or absolute artifact path (required)
|
|
10
10
|
--selector <sel> element selector (required): CSS tag/#id/.class/[attr]/[attr="value"] compounds with descendant or child relations (or a verbatim recorded path), backend:<id>, axid:<id>, ax:<name>, or text:<text>
|
|
11
11
|
--state <name> one recorded forced state; derives border/ink and clipping comparisons from its post-force target values
|
|
12
|
-
--size include size/layout provenance (box/flex/grid/constraints)
|
|
13
|
-
--text include text line/baseline/font/wrap metrics
|
|
14
|
-
--form include form value/geometry/caret/selection/autofill facts
|
|
15
|
-
output: <explain selector=… matches=…> —
|
|
12
|
+
--size include size/layout provenance (box/flex/grid/constraints) before general rendering context
|
|
13
|
+
--text include text line/baseline/font/wrap metrics before general rendering context
|
|
14
|
+
--form include form value/geometry/caret/selection/autofill facts before general rendering context
|
|
15
|
+
output: <explain selector=… matches=…> — requested size/text/form measurements lead after target identity; full cascade, border/ink, stacking, clipping, focus, scroll, query, and state context remains in the result; --json mirrors
|
|
16
|
+
boundary: measurements are local to the selected snapshot. \`capture lighthouse\` runs the independent audited sweep.
|
|
16
17
|
effects: read-only — reads existing snapshot artifacts, never drives the browser`;
|
|
17
18
|
|
|
18
19
|
function attestation(ref: { id: string; dir: string }, meta: { settled?: boolean; settleMs?: number }) {
|
|
@@ -60,6 +61,14 @@ function recoverySections(missing: ExplainMissingSelector): FactLine[] {
|
|
|
60
61
|
];
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function ambiguitySections(ambiguous: ExplainAmbiguousSelector): FactLine[] {
|
|
65
|
+
const rows = ambiguous.candidates.map((candidate, index) => fact`${index + 1}. ${candidate.selector ?? candidate.id}${typeof candidate.backendNodeId === 'number' ? ` — backend:${candidate.backendNodeId}` : ''}${candidate.axId ? ` — axid:${candidate.axId}` : ''}${candidate.axName ? ` — ax:${candidate.axName}` : ''}`);
|
|
66
|
+
return [lineList([
|
|
67
|
+
...(ambiguous.matchCount > ambiguous.candidates.length ? [fact`candidates (first ${ambiguous.candidates.length} of ${ambiguous.matchCount}):`] : [text`candidates:`]),
|
|
68
|
+
...rows,
|
|
69
|
+
])];
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
function invalidInput(message: FactLine): RenderableResult {
|
|
64
73
|
return {
|
|
65
74
|
tag: 'error',
|
|
@@ -69,6 +78,19 @@ function invalidInput(message: FactLine): RenderableResult {
|
|
|
69
78
|
};
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
function requestedSectionsFirst<T extends { kind: string }>(sections: readonly T[], parsed: ParsedArgs): readonly T[] {
|
|
82
|
+
const requested = new Set([
|
|
83
|
+
...(parsed.size ? ['size'] : []),
|
|
84
|
+
...(parsed.text ? ['text'] : []),
|
|
85
|
+
...(parsed.form ? ['form'] : []),
|
|
86
|
+
]);
|
|
87
|
+
if (!requested.size) return sections;
|
|
88
|
+
const element = sections.filter((section) => section.kind === 'element');
|
|
89
|
+
const detail = sections.filter((section) => requested.has(section.kind));
|
|
90
|
+
const context = sections.filter((section) => section.kind !== 'element' && !requested.has(section.kind));
|
|
91
|
+
return [...element, ...detail, ...context];
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
export async function cmdMeasureExplain(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
73
95
|
if (parsed.help) {
|
|
74
96
|
console.log(USAGE);
|
|
@@ -110,8 +132,20 @@ export async function cmdMeasureExplain(parsed: ParsedArgs, _args: string[]): Pr
|
|
|
110
132
|
process.exitCode = 1;
|
|
111
133
|
return;
|
|
112
134
|
}
|
|
135
|
+
if (report.kind === 'ambiguous-selector') {
|
|
136
|
+
emitResult({
|
|
137
|
+
tag: 'error',
|
|
138
|
+
attestation: attestation(report.ref, report.meta),
|
|
139
|
+
attrs: { command: 'measure explain', status: 'ambiguous_selector', selector: report.selector, matches: report.matchCount, displayed: report.candidates.length },
|
|
140
|
+
summary: fact`Selector input ${report.selector} matched ${report.matchCount} recorded elements; no element was selected.`,
|
|
141
|
+
sections: ambiguitySections(report),
|
|
142
|
+
followUp: text`Retry with an exact CSS selector, axid, or numeric backend id from the candidate identity facts.`,
|
|
143
|
+
}, { json: parsed.json });
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
113
147
|
|
|
114
|
-
const sections = report.sections.map((section) => lineList(section.facts.map(({ fact: item, caveats }) => {
|
|
148
|
+
const sections = requestedSectionsFirst(report.sections, parsed).map((section) => lineList(section.facts.map(({ fact: item, caveats }) => {
|
|
115
149
|
const suffix = caveatSuffix(caveats);
|
|
116
150
|
return suffix ? line(item.line, suffix) : item.line;
|
|
117
151
|
})));
|
|
@@ -129,7 +163,10 @@ export async function cmdMeasureExplain(parsed: ParsedArgs, _args: string[]): Pr
|
|
|
129
163
|
state: parsed.state?.[0] ?? 'base',
|
|
130
164
|
},
|
|
131
165
|
summary: fact`Recorded cascade, border/ink geometry, stacking, clipping, focus, scroll, query, and state context for ${targetSelector}${parsed.state?.[0] ? ` under requested state ${parsed.state[0]}` : ' from base-state artifacts'}.`,
|
|
132
|
-
sections
|
|
166
|
+
sections: [
|
|
167
|
+
...sections,
|
|
168
|
+
fact`Measurements are local to snapshot ${report.ref.id}; \`capture lighthouse\` runs the independent audited sweep.`,
|
|
169
|
+
],
|
|
133
170
|
followUp: parsed.size || parsed.text || parsed.form
|
|
134
171
|
? text`Re-run against a new snapshot to compare the same recorded measurements after a page change.`
|
|
135
172
|
: text`Add --size, --text, or --form to include only those optional detail sections.`,
|
|
@@ -14,6 +14,7 @@ import { cmdMeasureCheck } from './check.js';
|
|
|
14
14
|
import { cmdMeasureDiff } from './diff.js';
|
|
15
15
|
import { cmdMeasureCensus } from './census.js';
|
|
16
16
|
import { cmdMeasureExplain } from './explain.js';
|
|
17
|
+
import { cmdMeasureText } from './text.js';
|
|
17
18
|
import { cmdMeasureSweep } from './sweep.js';
|
|
18
19
|
import { cmdMeasureMapFocus } from './map-focus.js';
|
|
19
20
|
import { cmdMeasureMapScroll } from './map-scroll.js';
|
|
@@ -23,41 +24,30 @@ import { cmdMeasureMapPaint } from './map-paint.js';
|
|
|
23
24
|
|
|
24
25
|
/** Root-help representation of this branch, assembled by `src/capture.ts`. */
|
|
25
26
|
export const COMMAND_BLOCK = `<command name="measure">
|
|
26
|
-
static facts
|
|
27
|
-
use
|
|
28
|
-
snap · check · diff · census · explain · sweep · map (focus|scroll|layers|ax|paint) — \`capture measure -h\`
|
|
27
|
+
the settled-snapshot substrate — static facts at one moment; \`snap\` writes the substrate, every other leaf is a read-only query over it
|
|
28
|
+
use for static rendered-page facts from a settled snapshot: layout, content, cascade provenance, targetability, and snapshot diffs
|
|
29
29
|
</command>`;
|
|
30
30
|
|
|
31
|
-
export const MEASURE_USAGE =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<subcommand name="
|
|
41
|
-
|
|
42
|
-
<subcommand name="diff" args="--before <snap> --after <snap> [--pixels] [--full] [--gate]" whenToUse="structured before/after delta between two snapshots"/>
|
|
43
|
-
<subcommand name="census" args="[--snap <id>]... [--url <url>]... [--set-file <path>] --axis <axis>" whenToUse="value distributions across one or more snapshots"/>
|
|
44
|
-
<subcommand name="explain" args="<snap> --selector <sel> [--size] [--text] [--form]" whenToUse="per-element cascade/stacking/clipping/size/text/form explanation"/>
|
|
45
|
-
<subcommand name="sweep" args="[url] --axis <axis> [--from <val>] [--to <val>] [--viewport-height <val>]" whenToUse="responsive/environment sampling across an axis"/>
|
|
46
|
-
<subcommand name="map" args="focus|scroll|layers|ax [url|snap] | paint <snap> --selector <target> [--state <name>]" whenToUse="read one facet of a snapshot's substrate — capture measure map -h"/>
|
|
47
|
-
|
|
48
|
-
capture measure <leaf> -h Per-leaf usage`;
|
|
49
|
-
|
|
50
|
-
export const MEASURE_MAP_USAGE = `capture measure map — read one facet of a snapshot's substrate (no browser re-drive).
|
|
51
|
-
|
|
52
|
-
Focus, scroll, layers, and ax accept a URL target and create a snap first; paint requires an existing snapshot and reads it without browser driving.
|
|
53
|
-
|
|
54
|
-
<subcommand name="focus" args="[url|snap]" whenToUse="keyboard traversal order (focus.json)"/>
|
|
55
|
-
<subcommand name="scroll" args="[url|snap]" whenToUse="scroll-container topology (scroll.json)"/>
|
|
56
|
-
<subcommand name="layers" args="[url|snap]" whenToUse="paint/compositor layer map (layers.json)"/>
|
|
57
|
-
<subcommand name="ax" args="[url|snap]" whenToUse="AX-tree ↔ layout map (ax.json + geometry.json)"/>
|
|
58
|
-
<subcommand name="paint" args="<snap> --selector <target> [--state <name>]" whenToUse="elements painted above one target and their recorded AABB coverage (geometry.json + layers.json)"/>
|
|
31
|
+
export const MEASURE_USAGE = `<command name="measure" description="enriched snapshot substrate and read-only queries">
|
|
32
|
+
<model>\`snap\` drives the page (or a base snapshot) and writes one settled artifact directory; every other leaf below reads that artifact and never re-drives the browser unless its target is a URL, which first creates a snapshot. Findings exit 0 — a report, not a failure. \`--gate\` (exit 2 on findings or changes) is accepted only by check and diff.</model>
|
|
33
|
+
<subcommand name="snap" description="settled snapshot artifact" whenToUse="Use to capture the static rendered-page substrate that measure queries read."/>
|
|
34
|
+
<subcommand name="check" description="snapshot threshold measurements" whenToUse="Use to measure selected layout and content thresholds from one snapshot."/>
|
|
35
|
+
<subcommand name="diff" description="snapshot delta" whenToUse="Use to compare the measured facts from two snapshots."/>
|
|
36
|
+
<subcommand name="census" description="snapshot value distributions" whenToUse="Use to count values across one or more snapshots."/>
|
|
37
|
+
<subcommand name="explain" description="element rendering explanation" whenToUse="Use to inspect cascade, stacking, clipping, size, text, or form facts for one element."/>
|
|
38
|
+
<subcommand name="text" description="targeted DOM-content text measurement" whenToUse="Use to read one element's direct DOM content text, font, recorded color-model values, contrast ratio, and provenance from a snapshot."/>
|
|
39
|
+
<subcommand name="sweep" description="responsive environment sampling" whenToUse="Use to compare snapshot facts across values on one environment axis."/>
|
|
40
|
+
<subcommand name="map" description="one snapshot facet" whenToUse="Use to read focus, scroll, layer, accessibility, or paint facts from a snapshot."/>
|
|
41
|
+
</command>`;
|
|
59
42
|
|
|
60
|
-
|
|
43
|
+
export const MEASURE_MAP_USAGE = `<command name="map" description="one facet of a snapshot's substrate">
|
|
44
|
+
<model>Focus, scroll, layers, and ax accept a URL target and create a snapshot first; paint requires an existing snapshot and reads it without browser driving.</model>
|
|
45
|
+
<subcommand name="focus" description="keyboard traversal order" whenToUse="Use to inspect focus order from a snapshot."/>
|
|
46
|
+
<subcommand name="scroll" description="scroll-container topology" whenToUse="Use to inspect scroll containers and their relationships."/>
|
|
47
|
+
<subcommand name="layers" description="paint and compositor layers" whenToUse="Use to inspect layer allocation and paint order."/>
|
|
48
|
+
<subcommand name="ax" description="accessibility and layout map" whenToUse="Use to relate accessibility-tree nodes to layout geometry."/>
|
|
49
|
+
<subcommand name="paint" description="coverage above one target" whenToUse="Use to inspect elements painted above a target and their recorded coverage."/>
|
|
50
|
+
</command>`;
|
|
61
51
|
|
|
62
52
|
export async function measureMain(parsed: ParsedArgs, args: string[]): Promise<void> {
|
|
63
53
|
const leaf = parsed.positional[0];
|
|
@@ -74,6 +64,8 @@ export async function measureMain(parsed: ParsedArgs, args: string[]): Promise<v
|
|
|
74
64
|
return cmdMeasureCensus(rest, args);
|
|
75
65
|
case 'explain':
|
|
76
66
|
return cmdMeasureExplain(rest, args);
|
|
67
|
+
case 'text':
|
|
68
|
+
return cmdMeasureText(rest, args);
|
|
77
69
|
case 'sweep':
|
|
78
70
|
return cmdMeasureSweep(rest, args);
|
|
79
71
|
case 'map':
|
|
@@ -8,8 +8,9 @@ const USAGE = `capture measure map ax [url|snap] — accessibility-tree facts re
|
|
|
8
8
|
|
|
9
9
|
input:
|
|
10
10
|
[url|snap] required target: a URL creates a settled snapshot first; a snapshot id or absolute path is read without re-driving the browser
|
|
11
|
+
--artifact-dir <path> root for the one-shot snapshot bundle when the target is a URL
|
|
11
12
|
output: <ax-map …> — non-ignored AX nodes with role, name, states, backendNodeId, and top-viewport rect; ignored AX nodes with their ignored-reasons; DOM elements with rendered boxes but no non-ignored AX node; and AX nodes whose rect is offscreen, clipped, or zero-size; --json mirrors
|
|
12
|
-
effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot
|
|
13
|
+
effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot bundle under --artifact-dir when supplied`;
|
|
13
14
|
|
|
14
15
|
function recoveryResult(err: unknown): RenderableResult {
|
|
15
16
|
const detail = err instanceof ArtifactResolutionError || err instanceof Error
|
|
@@ -8,8 +8,9 @@ const USAGE = `capture measure map focus [url|snap] — keyboard traversal facts
|
|
|
8
8
|
|
|
9
9
|
input:
|
|
10
10
|
[url|snap] required target: a URL creates a settled snapshot first; a snapshot id or absolute path is read without re-driving the browser
|
|
11
|
+
--artifact-dir <path> root for the one-shot snapshot bundle when the target is a URL
|
|
11
12
|
output: <focus-map …> — forward and reverse Tab sequences, top-viewport rects, scroll jumps, focus-visible style facts, and unreached focusable elements; --json mirrors
|
|
12
|
-
effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot
|
|
13
|
+
effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot bundle under --artifact-dir when supplied`;
|
|
13
14
|
|
|
14
15
|
function recoveryResult(err: unknown): RenderableResult {
|
|
15
16
|
const detail = err instanceof ArtifactResolutionError || err instanceof Error
|
|
@@ -8,8 +8,10 @@ const USAGE = `capture measure map layers [url|snap] — paint/compositor facts
|
|
|
8
8
|
|
|
9
9
|
input:
|
|
10
10
|
[url|snap] required target: a URL creates a settled snapshot first; a snapshot id or absolute path is read without re-driving the browser
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
--artifact-dir <path> root for the one-shot snapshot bundle when the target is a URL
|
|
12
|
+
--limit <n> maximum final layer records in prose and JSON (default 25)
|
|
13
|
+
output: <layer-map …> — layer bounds, compositing reasons, painted-node counts, per-node membership, and available source provenance for layer-affecting declarations; backend-id corpora are limited to 50 per list with omitted counts, and --json mirrors the same cap
|
|
14
|
+
effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot bundle under --artifact-dir when supplied`;
|
|
13
15
|
|
|
14
16
|
function errorResult(err: unknown): RenderableResult {
|
|
15
17
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -51,7 +53,7 @@ export async function cmdMeasureMapLayers(parsed: ParsedArgs, _args: string[]):
|
|
|
51
53
|
const ref = await resolveSnapRef(target, {
|
|
52
54
|
onUrl: async (url) => captureMeasureSnap(parsed, url),
|
|
53
55
|
});
|
|
54
|
-
emitResult(buildMeasureMapLayersResult(ref), { json: parsed.json });
|
|
56
|
+
emitResult(buildMeasureMapLayersResult(ref, parsed.limit), { json: parsed.json });
|
|
55
57
|
} catch (err) {
|
|
56
58
|
emitResult(errorResult(err), { json: parsed.json });
|
|
57
59
|
process.exitCode = 1;
|
|
@@ -8,8 +8,9 @@ const USAGE = `capture measure map scroll [url|snap] — scroll-container topolo
|
|
|
8
8
|
|
|
9
9
|
input:
|
|
10
10
|
[url|snap] required target: a URL creates a settled snapshot first; a snapshot id from the active session or an absolute snapshot artifact path is read without re-driving the browser
|
|
11
|
+
--artifact-dir <path> root for the one-shot snapshot bundle when the target is a URL
|
|
11
12
|
output: <scroll-map …> — containers, ranges, current/max offsets, sticky/fixed occupancy, snap points, visual/layout viewport facts, and reachable-content samples; --json mirrors
|
|
12
|
-
effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot
|
|
13
|
+
effects: read-only over an existing snapshot artifact; a URL target writes one settled snapshot bundle under --artifact-dir when supplied`;
|
|
13
14
|
|
|
14
15
|
type CaptureSnap = (parsed: ParsedArgs, target: string) => Promise<SnapUrlResult>;
|
|
15
16
|
type Emit = typeof emitResult;
|
|
@@ -4,7 +4,7 @@ import { randomBytes } from 'crypto';
|
|
|
4
4
|
|
|
5
5
|
import { type ParsedArgs } from '../../types.js';
|
|
6
6
|
import { withConnection } from '../../connection.js';
|
|
7
|
-
import {
|
|
7
|
+
import { isMotionHeldClient } from '../../host/held-client.js';
|
|
8
8
|
import { parseViewport, type Viewport } from '../../viewport.js';
|
|
9
9
|
import { parseColorScheme, withAppliedColorScheme, type ColorScheme } from '../../color-scheme.js';
|
|
10
10
|
import { captureSnapshotSubstrate, COLLECTOR_NAMES, SnapshotCaptureTimeout } from '../../measure/snapshot.js';
|
|
@@ -40,8 +40,9 @@ input:
|
|
|
40
40
|
--state-padding <px> CSS-pixel padding around the target border box in each forced-state diff crop (default 8)
|
|
41
41
|
--viewport <WxH> temporarily capture at a CSS-pixel viewport (repeatable); exact <positive-safe-int>x<positive-safe-int> grammar with lowercase x and no whitespace
|
|
42
42
|
--color-scheme <dark|light> temporarily capture under this prefers-color-scheme value; cleared afterward
|
|
43
|
+
--artifact-dir <path> root for this sessionless snapshot bundle; ignored when an active session owns the snapshot
|
|
43
44
|
output: <snapshot id=… path=…> — the settled artifact directory (geometry, styles, hit-test, text, forms, animation, ax, queries, focus, scroll, layers; per-element crops with --pixels) every other measure/motion query leaf reads instead of re-driving the browser; --json mirrors
|
|
44
|
-
effects: drives the target page to capture; writes one snapshot artifact directory under the active
|
|
45
|
+
effects: drives the target page to capture; writes one snapshot artifact directory under the active session or, when sessionless, a one-shot bundle under --artifact-dir. --color-scheme transiently applies Emulation.setEmulatedMedia and clears it after capture.`;
|
|
45
46
|
|
|
46
47
|
interface SnapshotMetaForBase {
|
|
47
48
|
readonly url?: string | null;
|
|
@@ -132,7 +133,7 @@ interface ScreenInfo {
|
|
|
132
133
|
* never clears an override it cannot restore.
|
|
133
134
|
*/
|
|
134
135
|
async function viewportOwnership(client: ViewportClient): Promise<ViewportOwnership> {
|
|
135
|
-
if (
|
|
136
|
+
if (isMotionHeldClient(client)) return { owner: 'recorder' };
|
|
136
137
|
try {
|
|
137
138
|
const [page, screens] = await Promise.all([
|
|
138
139
|
client.send('Runtime.evaluate', {
|
|
@@ -285,7 +286,7 @@ export async function captureMeasureSnap(parsed: ParsedArgs, targetRef = parsed.
|
|
|
285
286
|
let artifacts: readonly string[] = [];
|
|
286
287
|
await withConnection(connectionArgs, async (client, tab) => {
|
|
287
288
|
await withAppliedColorScheme(client, colorScheme, async () => withAppliedViewport(client, viewport, async () => {
|
|
288
|
-
const oneshot = active ? undefined : createOneshotSession('measure');
|
|
289
|
+
const oneshot = active ? undefined : createOneshotSession('measure', parsed.artifactDir);
|
|
289
290
|
const destination = active ? path.join(active.dir, 'measure', 'snaps') : oneshot!.artifactsDir;
|
|
290
291
|
snapDir = path.join(destination, snapId);
|
|
291
292
|
allocatedRoot = active ? snapDir : oneshot!.dir;
|
|
@@ -7,7 +7,8 @@ import { withConnection } from '../../connection.js';
|
|
|
7
7
|
import { captureSnapshotSubstrate } from '../../measure/snapshot.js';
|
|
8
8
|
import { createOneshotSession } from '../../../session/commands.js';
|
|
9
9
|
import { getActiveSession } from '../../../session-context.js';
|
|
10
|
-
import { emitResult, fact, line, lineList, renderResult, text, type FactLine, type RenderableResult } from '../../../output/render.js';
|
|
10
|
+
import { capped, emitResult, fact, line, lineList, renderResult, text, type FactLine, type RenderableResult } from '../../../output/render.js';
|
|
11
|
+
import { normalizeFailure } from '../../../errors.js';
|
|
11
12
|
import { assertSweepBounds, parsePositiveNumber } from '../../leaf-grammar.js';
|
|
12
13
|
import {
|
|
13
14
|
SWEEP_AXES,
|
|
@@ -30,13 +31,14 @@ import {
|
|
|
30
31
|
const USAGE = `capture measure sweep [url] --axis <axis> — responsive/environment sampling with recursive bracketing of observed state changes
|
|
31
32
|
|
|
32
33
|
input:
|
|
33
|
-
[url] page
|
|
34
|
+
[url] match an existing page tab by URL; this does not open a URL — run inside \`capture session start --url …\` when no matching tab exists
|
|
34
35
|
--axis <axis> width|dpr|zoom|color-scheme|reduced-motion (required)
|
|
35
36
|
--from <val> numeric range start, or a color-scheme/reduced-motion value
|
|
36
37
|
--to <val> numeric range end, or a color-scheme/reduced-motion value
|
|
37
38
|
--viewport-height <val> fixed viewport height for width/dpr sweeps
|
|
39
|
+
--artifact-dir <path> root for this sessionless sweep bundle; ignored when an active session owns the sweep
|
|
38
40
|
output: <sweep axis=… samples=… transitions=…> — observed state facts per sampled point, each sampled snapshot path included; --json mirrors
|
|
39
|
-
effects: drives the page repeatedly under CDP Emulation settings; writes one snapshot substrate per sampled point plus sweep.json`;
|
|
41
|
+
effects: drives the page repeatedly under CDP Emulation settings; writes one snapshot substrate per sampled point plus sweep.json under the active session or, when sessionless, a one-shot bundle under --artifact-dir`;
|
|
40
42
|
|
|
41
43
|
const SAMPLE_LIMIT = 96;
|
|
42
44
|
|
|
@@ -80,9 +82,11 @@ function valuesForAxis(axis: SweepAxis, parsed: ParsedArgs): Array<number | stri
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export function transitionSections(artifact: SweepArtifact): FactLine[] {
|
|
83
|
-
const ranges = artifact.ranges.map((range) =>
|
|
85
|
+
const ranges = artifact.ranges.map((range) => fact` observed matching fingerprint ${range.from}–${range.to} → ${range.snapId} (${range.snapDir})`);
|
|
84
86
|
const transitions = artifact.transitions.flatMap((transition, index) => {
|
|
85
|
-
const header =
|
|
87
|
+
const header = transition.uncertainty === 'sampling_limit'
|
|
88
|
+
? fact`${index + 1}. observed difference unresolved between ${transition.bracket.from} and ${transition.bracket.to}: the ${artifact.sampleLimit} sample limit stopped refinement — ${transition.before} → ${transition.after}`
|
|
89
|
+
: fact`${index + 1}. bracketed between ${transition.bracket.from} and ${transition.bracket.to} — ${transition.before} → ${transition.after}`;
|
|
86
90
|
const changes = transition.changes.slice(0, 12).map((change) => {
|
|
87
91
|
const provenance = change.provenance?.source ? fact` (${change.provenance.selector ?? 'winning declaration'}${change.provenance.specificity ? `, specificity ${change.provenance.specificity}` : ''}, ${change.provenance.source})` : change.provenance?.selector ? fact` (${change.provenance.selector}${change.provenance.specificity ? `, specificity ${change.provenance.specificity}` : ''})` : text``;
|
|
88
92
|
return line(fact` ${change.selector} ${change.property}: ${change.before ?? 'unset'} → ${change.after ?? 'unset'}`, provenance);
|
|
@@ -136,7 +140,7 @@ export async function runMeasureSweep(parsed: ParsedArgs, _args: string[], overr
|
|
|
136
140
|
|
|
137
141
|
const axis = parsed.axis;
|
|
138
142
|
const active = dependencies.getActiveSession();
|
|
139
|
-
const oneShot = active ? undefined : dependencies.createOneshotSession('measure');
|
|
143
|
+
const oneShot = active ? undefined : dependencies.createOneshotSession('measure', parsed.artifactDir);
|
|
140
144
|
const snapsDir = active ? path.join(active.dir, 'measure', 'snaps') : oneShot!.artifactsDir;
|
|
141
145
|
const artifactDir = active ? path.join(active.dir, 'measure', 'sweeps', sweepId()) : path.join(oneShot!.dir, 'measure', 'sweeps', sweepId());
|
|
142
146
|
const recoverySamples: Array<SweepRecovery['samples'][number]> = [];
|
|
@@ -204,7 +208,7 @@ export async function runMeasureSweep(parsed: ParsedArgs, _args: string[], overr
|
|
|
204
208
|
const samples = (axis === 'width' || axis === 'dpr' || axis === 'zoom') ? [...samplesByValue.values()].sort((a, b) => Number(a.value) - Number(b.value)) : values.map((value) => samplesByValue.get(String(value))!).filter(Boolean);
|
|
205
209
|
const from = values[0];
|
|
206
210
|
const to = values[values.length - 1];
|
|
207
|
-
const analyzed = analyzeSweepSamples(axis, from, to, samples);
|
|
211
|
+
const analyzed = analyzeSweepSamples(axis, from, to, samples, uncertainties);
|
|
208
212
|
const artifact: SweepArtifact = { axis, from, to, capturedAt: new Date().toISOString(), samples, ...analyzed, uncertainties, sampleLimit: SAMPLE_LIMIT, environmentRestoration: environmentFacts };
|
|
209
213
|
const artifactPath = writeSweepArtifact(artifactDir, artifact);
|
|
210
214
|
const result: RenderableResult = {
|
|
@@ -214,13 +218,14 @@ export async function runMeasureSweep(parsed: ParsedArgs, _args: string[], overr
|
|
|
214
218
|
followUp: samples.length > 1 ? fact`Compare two sampled substrates with \`capture measure diff --before ${samples[0].snapDir} --after ${samples[1].snapDir}\`.` : undefined,
|
|
215
219
|
};
|
|
216
220
|
dependencies.emitResult(result, { json: parsed.json });
|
|
217
|
-
} catch {
|
|
221
|
+
} catch (error) {
|
|
218
222
|
const evidenceOnly = recoverySamples.some((sample) => sample.status === 'evidence_only');
|
|
223
|
+
const failure = normalizeFailure(error).descriptor;
|
|
219
224
|
const recoveryPath = writeSweepRecoveryArtifact(artifactDir, { axis, capturedAt: new Date().toISOString(), reason: evidenceOnly ? 'evidence_only' : 'capture_failed', environmentRestoration, samples: recoverySamples });
|
|
220
225
|
dependencies.emitResult({
|
|
221
|
-
tag: 'error', attrs: { command: 'measure sweep', status: evidenceOnly ? 'evidence_only' :
|
|
222
|
-
summary: fact`Sweep did not produce a queryable sample set. Recovery artifact: ${recoveryPath}.`,
|
|
223
|
-
sections: recoverySamples.length ? [text`Partial snapshot/recovery provenance:`, ...recoverySamples.map((sample) => fact`${sample.value}: ${sample.snapDir} status=${sample.status} captured=${sample.captured} settled=${sample.settled} artifacts=${sample.artifacts.join(', ') || 'none'} unstable=${sample.unstableRegions.map((region) => `${region.id}${region.reason ? `:${region.reason}` : ''}`).join(', ') || 'none'}`), fact`environment restoration: ${environmentRestoration}`] : undefined,
|
|
226
|
+
tag: 'error', attrs: { command: 'measure sweep', status: evidenceOnly ? 'evidence_only' : failure.code, path: recoveryPath },
|
|
227
|
+
summary: fact`Sweep did not produce a queryable sample set: ${capped(failure.message, 600)} Recovery artifact: ${recoveryPath}.`,
|
|
228
|
+
sections: recoverySamples.length ? [text`Partial snapshot/recovery provenance:`, ...recoverySamples.map((sample) => fact`${sample.value}: ${sample.snapDir} status=${sample.status} captured=${String(sample.captured)} settled=${String(sample.settled)} artifacts=${sample.artifacts.join(', ') || 'none'} unstable=${sample.unstableRegions.map((region) => `${region.id}${region.reason ? `:${region.reason}` : ''}`).join(', ') || 'none'}`), fact`environment restoration: ${environmentRestoration}`] : undefined,
|
|
224
229
|
}, { json: parsed.json });
|
|
225
230
|
process.exitCode = 1;
|
|
226
231
|
}
|