my-frontend-observer 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/README.md +51 -13
- package/dist/application/comparisonService.d.ts +56 -0
- package/dist/application/comparisonService.js +77 -0
- package/dist/application/comparisonService.js.map +1 -0
- package/dist/application/observationPersistence.js +1 -0
- package/dist/application/observationPersistence.js.map +1 -1
- package/dist/artifacts/artifactReader.d.ts +19 -0
- package/dist/artifacts/artifactReader.js +36 -0
- package/dist/artifacts/artifactReader.js.map +1 -0
- package/dist/artifacts/comparisonArtifactWriter.d.ts +41 -0
- package/dist/artifacts/comparisonArtifactWriter.js +67 -0
- package/dist/artifacts/comparisonArtifactWriter.js.map +1 -0
- package/dist/browser/chromiumAdapter.js +58 -4
- package/dist/browser/chromiumAdapter.js.map +1 -1
- package/dist/browser/evidenceCapture.d.ts +29 -2
- package/dist/browser/evidenceCapture.js +43 -19
- package/dist/browser/evidenceCapture.js.map +1 -1
- package/dist/browser/scrollCapture.d.ts +32 -0
- package/dist/browser/scrollCapture.js +163 -0
- package/dist/browser/scrollCapture.js.map +1 -0
- package/dist/browser/types.d.ts +3 -1
- package/dist/cli.js +299 -2
- package/dist/cli.js.map +1 -1
- package/dist/domain/comparison.d.ts +198 -0
- package/dist/domain/comparison.js +324 -0
- package/dist/domain/comparison.js.map +1 -0
- package/dist/domain/comparisonEngine.d.ts +48 -0
- package/dist/domain/comparisonEngine.js +694 -0
- package/dist/domain/comparisonEngine.js.map +1 -0
- package/dist/domain/comparisonIdentity.d.ts +13 -0
- package/dist/domain/comparisonIdentity.js +46 -0
- package/dist/domain/comparisonIdentity.js.map +1 -0
- package/dist/domain/evidence.d.ts +2 -0
- package/dist/domain/evidence.js +4 -0
- package/dist/domain/evidence.js.map +1 -1
- package/dist/domain/identity.d.ts +8 -3
- package/dist/domain/identity.js +9 -3
- package/dist/domain/identity.js.map +1 -1
- package/dist/domain/relationships.d.ts +168 -0
- package/dist/domain/relationships.js +343 -0
- package/dist/domain/relationships.js.map +1 -0
- package/dist/domain/schema.d.ts +116 -2
- package/dist/domain/schema.js +186 -2
- package/dist/domain/schema.js.map +1 -1
- package/dist/domain/scrollEvidence.d.ts +51 -0
- package/dist/domain/scrollEvidence.js +134 -0
- package/dist/domain/scrollEvidence.js.map +1 -0
- package/dist/index.d.ts +17 -4
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/request/request.d.ts +29 -0
- package/dist/request/request.js +108 -0
- package/dist/request/request.js.map +1 -1
- package/docs/ARCHITECTURE.md +110 -4
- package/docs/CI_CD.md +62 -6
- package/docs/COMMANDS.md +247 -6
- package/docs/CONTRACTS.md +172 -7
- package/docs/CURRENT_STATE.md +148 -10
- package/docs/DEVELOPMENT.md +52 -12
- package/docs/PROJECT_OVERVIEW.md +18 -11
- package/docs/QUICKSTART.md +7 -1
- package/docs/RELEASE.md +14 -7
- package/docs/ROADMAP.md +9 -0
- package/docs/SECURITY.md +14 -1
- package/docs/WORKFLOWS.md +90 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,107 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-08-12
|
|
4
|
+
|
|
5
|
+
Layout Relationships, Dependency Evidence, and Before/After Comparison.
|
|
6
|
+
|
|
7
|
+
- New comparison artifact kind `my-frontend-observer/comparison`, schema
|
|
8
|
+
`1.0.0` - independent of and never reused for the observation schema.
|
|
9
|
+
- Canonical layout-relationship derivation from a single observation:
|
|
10
|
+
horizontal order (`left-of`/`right-of`/`horizontally-overlapping`),
|
|
11
|
+
vertical order (`above`/`below`/`vertically-overlapping`), area overlap,
|
|
12
|
+
relative width, geometric fit (kept explicitly distinct from DOM
|
|
13
|
+
containment), vertical sequencing (`follows-vertically`), and document-
|
|
14
|
+
width fit/exceeds-viewport - bounded to configured targets, with explicit
|
|
15
|
+
evidence-path provenance and honest unresolved-target handling.
|
|
16
|
+
- Comparability analysis, evaluated before any rendered difference:
|
|
17
|
+
`comparable` / `comparable-with-warnings` / `incomparable`, with
|
|
18
|
+
structured reasons (hard mismatches on page URL, viewport, browser
|
|
19
|
+
engine, or scroll-scenario configuration; warnings for producer/browser
|
|
20
|
+
version and target-configuration differences; theme/authenticated-state/
|
|
21
|
+
application-state recorded as unassessed, never silently equal).
|
|
22
|
+
- Before/after target and page differences: appeared/disappeared (never
|
|
23
|
+
confused with a target added/removed from configuration), moved, resized,
|
|
24
|
+
visibility changes, clipping changes (reusing one canonical clipping
|
|
25
|
+
derivation), actual horizontal/vertical dimensional-overflow changes, DOM
|
|
26
|
+
containment changes, page-size changes, and scroll-owner changes - each a
|
|
27
|
+
structured record with before/after values, deltas where meaningful, and
|
|
28
|
+
supporting evidence references.
|
|
29
|
+
- Relationship-change detection between two observations, matched by
|
|
30
|
+
relationship family and subject/related target (never array position),
|
|
31
|
+
including a `relative-position-changed` distinction from plain absolute
|
|
32
|
+
target movement.
|
|
33
|
+
- Explicit, non-causal expected-dependency evidence: a caller may declare an
|
|
34
|
+
expected relationship between two targets' `x`/`y`/`width`/`height`
|
|
35
|
+
properties and `increase`/`decrease`/`change`/`unchanged` directions; each
|
|
36
|
+
declaration evaluates independently to `consistent` / `not-observed` /
|
|
37
|
+
`contradictory-to-declaration` / `unavailable`. The observer never infers
|
|
38
|
+
a dependency from observed co-change and never produces a causal claim.
|
|
39
|
+
- Deterministic, direction-sensitive comparison identity
|
|
40
|
+
(`comparisonRequestId`) plus a fresh `comparisonId` per execution;
|
|
41
|
+
operational filesystem paths never affect identity and are never written
|
|
42
|
+
into the persisted manifest.
|
|
43
|
+
- Atomic comparison-artifact persistence: `<outputLocation>/<comparisonId>/
|
|
44
|
+
manifest.json` only - no screenshot bytes are copied; the manifest
|
|
45
|
+
retains logical references to the source observations' own
|
|
46
|
+
`screenshot.path`. Source observations are never modified.
|
|
47
|
+
- New public `compare` command: `my-frontend-observer compare --before
|
|
48
|
+
<observation-artifact-root> --after <observation-artifact-root> --output
|
|
49
|
+
<directory> [--config-file <json-file>]`. Reads two already-persisted
|
|
50
|
+
observation artifacts and never launches a browser. `comparable`,
|
|
51
|
+
`comparable-with-warnings`, and `incomparable` all persist successfully
|
|
52
|
+
and exit `0`; only invalid syntax, an unreadable/invalid source artifact,
|
|
53
|
+
invalid configuration, or a failed write exits nonzero.
|
|
54
|
+
|
|
55
|
+
## 0.3.0 - 2026-08-12
|
|
56
|
+
|
|
57
|
+
Runtime Scrolling, Overflow, and Visibility Behavior.
|
|
58
|
+
|
|
59
|
+
- Bounded runtime scroll scenarios: an observation may configure zero or
|
|
60
|
+
one scroll action, `window-scroll-by` or `target-scroll-by` (signed
|
|
61
|
+
integer `deltaX`/`deltaY`, bounded to `[-20000, 20000]`, at least one
|
|
62
|
+
non-zero). Not a generic interaction recorder or browser automation
|
|
63
|
+
framework - exactly one bounded action per observation.
|
|
64
|
+
- Real `window-scroll-by` execution: vertical and horizontal document
|
|
65
|
+
scrolling, with browser-authoritative (not calculated) final position,
|
|
66
|
+
including natural boundary clamping and valid no-movement scenarios.
|
|
67
|
+
- Real `target-scroll-by` execution against the existing stable configured
|
|
68
|
+
target identity and the same canonical target-resolution path every
|
|
69
|
+
locator kind already uses: real nested vertical/horizontal element
|
|
70
|
+
scrolling, boundary clamping, and non-scrollable/no-movement targets. An
|
|
71
|
+
action target that cannot be uniquely resolved at runtime is never
|
|
72
|
+
scrolled and never fabricated as moved - the existing target-missing/
|
|
73
|
+
target-ambiguous/target-hidden diagnostics explain it honestly.
|
|
74
|
+
- Initial/final bounded runtime snapshots (window scroll position, the
|
|
75
|
+
browser's own scrolling-root/`documentElement`/`body` metrics, and
|
|
76
|
+
per-configured-target scroll metrics) around an immediate, non-smooth
|
|
77
|
+
scroll action and an exact two-`requestAnimationFrame` stabilization
|
|
78
|
+
wait.
|
|
79
|
+
- Actual dimensional overflow (`scrollWidth`/`scrollHeight` vs.
|
|
80
|
+
`clientWidth`/`clientHeight`) kept explicitly distinct from the computed
|
|
81
|
+
`overflow-x`/`overflow-y` CSS declaration.
|
|
82
|
+
- Real viewport-relation evidence (`above`/`intersecting`/`below`,
|
|
83
|
+
`intersectsViewport`, `fullyWithinViewport`) and `enteredViewport`/
|
|
84
|
+
`leftViewport` scenario transitions; a hidden/non-rendered target's
|
|
85
|
+
viewport relation is honestly `not-applicable`, never fabricated
|
|
86
|
+
geometry - hidden and offscreen remain distinct.
|
|
87
|
+
- Bounded before/after scenario transition evidence for window and
|
|
88
|
+
per-target scroll position, geometry, and viewport relation - not a
|
|
89
|
+
generic comparison/diff engine.
|
|
90
|
+
- Derived scroll-owner interpretation (`document` /
|
|
91
|
+
`target:<stable-target-name>` / `none` / `indeterminate`), always
|
|
92
|
+
traceable (`derivedFrom`) to the underlying observed scroll-position
|
|
93
|
+
measurements only - never from CSS overflow, bounding-rectangle movement
|
|
94
|
+
alone, target name, or DOM hierarchy.
|
|
95
|
+
- New `--scroll-scenario-file <json-file>` CLI input, usable together with
|
|
96
|
+
either `--target` or `--targets-file`; the file path is operational input
|
|
97
|
+
only, never persisted and never part of request identity, exactly like
|
|
98
|
+
`--targets-file`'s path.
|
|
99
|
+
- Observation schema `1.2.0` (additive over `1.1.0`).
|
|
100
|
+
- Cross-platform packed-candidate validation: one hash-verified npm
|
|
101
|
+
candidate tarball proven on Windows, Linux, and macOS, covering the
|
|
102
|
+
legacy `--target` CSS shorthand, the structured `--targets-file`
|
|
103
|
+
semantic-target path, and both `--scroll-scenario-file` action kinds.
|
|
104
|
+
|
|
3
105
|
## 0.2.0 - 2026-08-11
|
|
4
106
|
|
|
5
107
|
Stable Semantic Targets and Region Identity.
|
package/README.md
CHANGED
|
@@ -6,12 +6,15 @@ in [docs/PROJECT_DESCRIPTION.md](docs/PROJECT_DESCRIPTION.md).
|
|
|
6
6
|
|
|
7
7
|
## Current status
|
|
8
8
|
|
|
9
|
-
`v0.
|
|
10
|
-
published release: `my-frontend-observer observe`
|
|
11
|
-
sandboxed Chromium browser, enforces a loopback-only safety
|
|
12
|
-
captures a viewport screenshot plus bounded page/target evidence,
|
|
13
|
-
persists it as one portable `manifest.json` + `screenshot.png` artifact
|
|
14
|
-
(observation schema `1.
|
|
9
|
+
`v0.4.0`, Layout Relationships, Dependency Evidence, and Before/After
|
|
10
|
+
Comparison, is the current published release: `my-frontend-observer observe`
|
|
11
|
+
launches a real, sandboxed Chromium browser, enforces a loopback-only safety
|
|
12
|
+
policy, captures a viewport screenshot plus bounded page/target evidence,
|
|
13
|
+
and persists it as one portable `manifest.json` + `screenshot.png` artifact
|
|
14
|
+
(observation schema `1.2.0`). `my-frontend-observer compare` reads two
|
|
15
|
+
already-persisted observation artifacts and derives before/after evidence
|
|
16
|
+
purely from their existing content, persisting a comparison artifact
|
|
17
|
+
(comparison schema `1.0.0`).
|
|
15
18
|
|
|
16
19
|
Install:
|
|
17
20
|
|
|
@@ -45,14 +48,49 @@ This prints a concise result (`Observation:`/`State:`/`Artifact:`/`Targets:`/
|
|
|
45
48
|
`Diagnostics:`) and exits `0` on a successfully persisted observation. See
|
|
46
49
|
[docs/COMMANDS.md](docs/COMMANDS.md) for the full flag reference.
|
|
47
50
|
|
|
48
|
-
`--target <id=css-selector>` remains the simple CSS shorthand. A
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
`--target <id=css-selector>` remains the simple CSS shorthand. A structured
|
|
52
|
+
`--targets-file <json-file>` input mode - supporting a role and accessible
|
|
53
|
+
name, a stable `id`, a `data-*` attribute, a semantic landmark element,
|
|
54
|
+
exact text, or an ordered fallback between several of those - ships
|
|
55
|
+
alongside it. See "Structured semantic targets" in
|
|
53
56
|
[docs/COMMANDS.md](docs/COMMANDS.md#structured-semantic-targets-targets-file)
|
|
54
57
|
for the exact JSON format.
|
|
55
58
|
|
|
59
|
+
### Runtime scroll scenarios
|
|
60
|
+
|
|
61
|
+
`--scroll-scenario-file <json-file>` ships in this release: a real, bounded
|
|
62
|
+
`window-scroll-by` or `target-scroll-by` action performs one immediate,
|
|
63
|
+
non-smooth scroll and captures initial/final runtime evidence - window and
|
|
64
|
+
configured-target scroll position, actual overflow, viewport relation,
|
|
65
|
+
entered/left-viewport transitions, and a derived scroll-owner
|
|
66
|
+
interpretation (`document`, `target:<stable-target-name>`, `none`, or
|
|
67
|
+
`indeterminate`), all persisted in the same `manifest.json`. It may be
|
|
68
|
+
combined with either `--target` or `--targets-file`. See "Scroll scenario"
|
|
69
|
+
in
|
|
70
|
+
[docs/COMMANDS.md](docs/COMMANDS.md#scroll-scenario---scroll-scenario-file)
|
|
71
|
+
for the exact JSON format and flag reference.
|
|
72
|
+
|
|
73
|
+
### Comparison
|
|
74
|
+
|
|
75
|
+
`my-frontend-observer compare` reads two already-persisted observation
|
|
76
|
+
artifacts and derives before/after evidence purely from their existing
|
|
77
|
+
content - it never launches a browser:
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
my-frontend-observer compare `
|
|
81
|
+
--before observations/<before-observation-id> `
|
|
82
|
+
--after observations/<after-observation-id> `
|
|
83
|
+
--output comparisons
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
(From a source checkout, use `node dist/cli.js compare ...` instead.)
|
|
87
|
+
|
|
88
|
+
This prints a concise result (`Comparison:`/`State:`/`Artifact:`/
|
|
89
|
+
`Differences:`/`Relationship changes:`/`Diagnostics:`) and exits `0` -
|
|
90
|
+
including when the two observations turn out to be `incomparable`, which is
|
|
91
|
+
itself a successful comparison outcome. See
|
|
92
|
+
[docs/COMMANDS.md](docs/COMMANDS.md) for the full flag reference.
|
|
93
|
+
|
|
56
94
|
Validation:
|
|
57
95
|
|
|
58
96
|
```powershell
|
|
@@ -70,8 +108,8 @@ Planning authorities:
|
|
|
70
108
|
intent and responsibility boundaries.
|
|
71
109
|
- [Project Milestones](docs/PROJECT_MILESTONES.md): complete ordered capability
|
|
72
110
|
design and cross-milestone rules.
|
|
73
|
-
- [ROADMAP](docs/ROADMAP.md): version-level requirements; v0.1
|
|
74
|
-
released, v0.
|
|
111
|
+
- [ROADMAP](docs/ROADMAP.md): version-level requirements; v0.1-v0.4 are
|
|
112
|
+
released, v0.5 is next.
|
|
75
113
|
- [Current State](docs/CURRENT_STATE.md): retained scaffold and release state.
|
|
76
114
|
|
|
77
115
|
No sibling ecosystem repository is a runtime dependency of the retained
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ObservationArtifact } from '../domain/schema.js';
|
|
2
|
+
import type { ComparabilityState, ComparisonConfig } from '../domain/comparison.js';
|
|
3
|
+
import type { Diagnostic } from '../domain/diagnostics.js';
|
|
4
|
+
import type { WriteComparisonArtifactOptions } from '../artifacts/comparisonArtifactWriter.js';
|
|
5
|
+
export type { PersistedComparisonResult } from '../artifacts/comparisonArtifactWriter.js';
|
|
6
|
+
/** Default comparison output location, mirroring `request.ts`'s `DEFAULT_OUTPUT_LOCATION` for observations - distinct directories so comparisons never collide with source observations. */
|
|
7
|
+
export declare const DEFAULT_COMPARISON_OUTPUT_LOCATION = "comparisons";
|
|
8
|
+
export interface CompareAndPersistOptions {
|
|
9
|
+
config?: Partial<ComparisonConfig>;
|
|
10
|
+
/** Portable, relative output location (validated the same way as an observation's `outputLocation`). Defaults to `DEFAULT_COMPARISON_OUTPUT_LOCATION`. */
|
|
11
|
+
outputLocation?: string;
|
|
12
|
+
cwd?: WriteComparisonArtifactOptions['cwd'];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Observer-owned summary of one full compare-and-persist attempt, with just
|
|
16
|
+
* enough information for a presentation layer (a future Batch 4 CLI) to
|
|
17
|
+
* report a result without seeing a raw filesystem exception or persistence
|
|
18
|
+
* implementation detail.
|
|
19
|
+
*/
|
|
20
|
+
export type ApplicationComparisonResult = {
|
|
21
|
+
ok: true;
|
|
22
|
+
comparisonId: string;
|
|
23
|
+
comparisonRequestId: string;
|
|
24
|
+
comparability: ComparabilityState;
|
|
25
|
+
artifactRoot: string;
|
|
26
|
+
manifestPath: string;
|
|
27
|
+
differenceCount: number;
|
|
28
|
+
relationshipChangeCount: number;
|
|
29
|
+
diagnosticsCount: number;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
diagnostics: Diagnostic[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The one canonical application-level comparison use case: takes two
|
|
36
|
+
* already-validated/in-memory `ObservationArtifact`s, runs the existing pure
|
|
37
|
+
* `compareObservations` exactly once, and - only for a structurally valid
|
|
38
|
+
* result - persists it exactly once through the existing comparison
|
|
39
|
+
* writer. Never launches Chromium, never re-resolves targets, never mutates
|
|
40
|
+
* either source observation.
|
|
41
|
+
*/
|
|
42
|
+
export declare function compareAndPersist(before: ObservationArtifact, after: ObservationArtifact, options?: CompareAndPersistOptions): Promise<ApplicationComparisonResult>;
|
|
43
|
+
/**
|
|
44
|
+
* The canonical Batch 4 CLI-facing orchestration: reads two already-persisted
|
|
45
|
+
* observation artifacts by their root directory (each expected to contain
|
|
46
|
+
* `manifest.json`, mirroring `writeObservationArtifact`'s own layout) through
|
|
47
|
+
* the existing `readObservationArtifact` reader, then delegates to
|
|
48
|
+
* `compareAndPersist` exactly once. A thin wrapper only - artifact reading
|
|
49
|
+
* stays owned by `artifacts/artifactReader.ts`, comparison stays owned by
|
|
50
|
+
* `domain/comparisonEngine.ts`, and persistence stays owned by
|
|
51
|
+
* `artifacts/comparisonArtifactWriter.ts`. Kept in `application/` (not
|
|
52
|
+
* `artifacts/`) so `src/cli.ts` can depend on it without importing the
|
|
53
|
+
* artifacts layer directly - see `tests/unit/cli.test.ts`'s import-boundary
|
|
54
|
+
* assertion. Never launches a browser, never re-resolves targets.
|
|
55
|
+
*/
|
|
56
|
+
export declare function compareAndPersistFromArtifactRoots(beforeRoot: string, afterRoot: string, options?: CompareAndPersistOptions): Promise<ApplicationComparisonResult>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { compareObservations } from '../domain/comparisonEngine.js';
|
|
3
|
+
import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
|
|
4
|
+
import { normalizeOutputLocation } from '../request/paths.js';
|
|
5
|
+
import { writeComparisonArtifact } from '../artifacts/comparisonArtifactWriter.js';
|
|
6
|
+
import { readObservationArtifact } from '../artifacts/artifactReader.js';
|
|
7
|
+
import { MANIFEST_FILENAME as OBSERVATION_MANIFEST_FILENAME } from '../artifacts/artifactWriter.js';
|
|
8
|
+
/** Default comparison output location, mirroring `request.ts`'s `DEFAULT_OUTPUT_LOCATION` for observations - distinct directories so comparisons never collide with source observations. */
|
|
9
|
+
export const DEFAULT_COMPARISON_OUTPUT_LOCATION = 'comparisons';
|
|
10
|
+
/**
|
|
11
|
+
* The one canonical application-level comparison use case: takes two
|
|
12
|
+
* already-validated/in-memory `ObservationArtifact`s, runs the existing pure
|
|
13
|
+
* `compareObservations` exactly once, and - only for a structurally valid
|
|
14
|
+
* result - persists it exactly once through the existing comparison
|
|
15
|
+
* writer. Never launches Chromium, never re-resolves targets, never mutates
|
|
16
|
+
* either source observation.
|
|
17
|
+
*/
|
|
18
|
+
export async function compareAndPersist(before, after, options = {}) {
|
|
19
|
+
const result = compareObservations(before, after, options.config ?? {});
|
|
20
|
+
if (!result.ok) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
diagnostics: [{ code: 'invalid-request', severity: DIAGNOSTIC_SEVERITY['invalid-request'], message: result.reason }],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const rawOutputLocation = options.outputLocation ?? DEFAULT_COMPARISON_OUTPUT_LOCATION;
|
|
27
|
+
const normalized = normalizeOutputLocation(rawOutputLocation);
|
|
28
|
+
if (!normalized.ok) {
|
|
29
|
+
return { ok: false, diagnostics: [normalized.diagnostic] };
|
|
30
|
+
}
|
|
31
|
+
const persisted = await writeComparisonArtifact(result.artifact, normalized.value, options.cwd === undefined ? {} : { cwd: options.cwd });
|
|
32
|
+
if (!persisted.ok) {
|
|
33
|
+
return { ok: false, diagnostics: persisted.diagnostics };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
comparisonId: result.artifact.comparisonId,
|
|
38
|
+
comparisonRequestId: result.artifact.comparisonRequestId,
|
|
39
|
+
comparability: result.artifact.comparability.state,
|
|
40
|
+
artifactRoot: persisted.artifactRoot,
|
|
41
|
+
manifestPath: persisted.manifestPath,
|
|
42
|
+
differenceCount: result.artifact.differences.length,
|
|
43
|
+
relationshipChangeCount: result.artifact.relationshipChanges.length,
|
|
44
|
+
diagnosticsCount: result.artifact.diagnostics.length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The canonical Batch 4 CLI-facing orchestration: reads two already-persisted
|
|
49
|
+
* observation artifacts by their root directory (each expected to contain
|
|
50
|
+
* `manifest.json`, mirroring `writeObservationArtifact`'s own layout) through
|
|
51
|
+
* the existing `readObservationArtifact` reader, then delegates to
|
|
52
|
+
* `compareAndPersist` exactly once. A thin wrapper only - artifact reading
|
|
53
|
+
* stays owned by `artifacts/artifactReader.ts`, comparison stays owned by
|
|
54
|
+
* `domain/comparisonEngine.ts`, and persistence stays owned by
|
|
55
|
+
* `artifacts/comparisonArtifactWriter.ts`. Kept in `application/` (not
|
|
56
|
+
* `artifacts/`) so `src/cli.ts` can depend on it without importing the
|
|
57
|
+
* artifacts layer directly - see `tests/unit/cli.test.ts`'s import-boundary
|
|
58
|
+
* assertion. Never launches a browser, never re-resolves targets.
|
|
59
|
+
*/
|
|
60
|
+
export async function compareAndPersistFromArtifactRoots(beforeRoot, afterRoot, options = {}) {
|
|
61
|
+
const beforeRead = await readObservationArtifact(path.join(beforeRoot, OBSERVATION_MANIFEST_FILENAME));
|
|
62
|
+
if (!beforeRead.ok) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
diagnostics: [{ code: 'invalid-request', severity: DIAGNOSTIC_SEVERITY['invalid-request'], message: `--before observation artifact: ${beforeRead.reason}` }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const afterRead = await readObservationArtifact(path.join(afterRoot, OBSERVATION_MANIFEST_FILENAME));
|
|
69
|
+
if (!afterRead.ok) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
diagnostics: [{ code: 'invalid-request', severity: DIAGNOSTIC_SEVERITY['invalid-request'], message: `--after observation artifact: ${afterRead.reason}` }],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return compareAndPersist(beforeRead.artifact, afterRead.artifact, options);
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=comparisonService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparisonService.js","sourceRoot":"","sources":["../../src/application/comparisonService.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,6 +26,7 @@ export function buildObservationArtifact(capture, request) {
|
|
|
26
26
|
provenance: { capturedAt: new Date().toISOString(), observationMethod: 'chromium-navigate-and-capture' },
|
|
27
27
|
pageEvidence: capture.pageEvidence,
|
|
28
28
|
targetEvidence: capture.targetEvidence,
|
|
29
|
+
...(capture.scrollScenarioEvidence ? { scrollScenarioEvidence: capture.scrollScenarioEvidence } : {}),
|
|
29
30
|
screenshot: { state: 'available', source: 'browser', value: { path: SCREENSHOT_FILENAME } },
|
|
30
31
|
completion,
|
|
31
32
|
diagnostics: capture.diagnostics,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observationPersistence.js","sourceRoot":"","sources":["../../src/application/observationPersistence.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"observationPersistence.js","sourceRoot":"","sources":["../../src/application/observationPersistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ObservationArtifact } from '../domain/schema.js';
|
|
2
|
+
export type ReadObservationArtifactResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
artifact: ObservationArtifact;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Narrow canonical reader counterpart to `artifactWriter.ts#writeObservationArtifact`:
|
|
11
|
+
* reads one `manifest.json`, parses it, and validates it through the same
|
|
12
|
+
* `isValidObservationArtifact` structural gate the writer itself uses -
|
|
13
|
+
* never a second validator. Read-only: never mutates the file, never
|
|
14
|
+
* launches a browser, never resolves the sibling screenshot's bytes (the
|
|
15
|
+
* manifest's `screenshot.path` field is returned as-is for the caller to
|
|
16
|
+
* resolve if it needs to). Established here as the canonical reader ahead of
|
|
17
|
+
* the Batch 4 `compare` CLI's `--before`/`--after` path arguments.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readObservationArtifact(manifestPath: string): Promise<ReadObservationArtifactResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isValidObservationArtifact } from '../domain/schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* Narrow canonical reader counterpart to `artifactWriter.ts#writeObservationArtifact`:
|
|
5
|
+
* reads one `manifest.json`, parses it, and validates it through the same
|
|
6
|
+
* `isValidObservationArtifact` structural gate the writer itself uses -
|
|
7
|
+
* never a second validator. Read-only: never mutates the file, never
|
|
8
|
+
* launches a browser, never resolves the sibling screenshot's bytes (the
|
|
9
|
+
* manifest's `screenshot.path` field is returned as-is for the caller to
|
|
10
|
+
* resolve if it needs to). Established here as the canonical reader ahead of
|
|
11
|
+
* the Batch 4 `compare` CLI's `--before`/`--after` path arguments.
|
|
12
|
+
*/
|
|
13
|
+
export async function readObservationArtifact(manifestPath) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = await readFile(manifestPath, 'utf8');
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20
|
+
return { ok: false, reason: `failed to read observation artifact manifest at "${manifestPath}": ${message}` };
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28
|
+
return { ok: false, reason: `observation artifact manifest at "${manifestPath}" is not valid JSON: ${message}` };
|
|
29
|
+
}
|
|
30
|
+
const validation = isValidObservationArtifact(parsed);
|
|
31
|
+
if (!validation.valid) {
|
|
32
|
+
return { ok: false, reason: `observation artifact manifest at "${manifestPath}" failed structural validation: ${validation.reason}` };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, artifact: parsed };
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=artifactReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifactReader.js","sourceRoot":"","sources":["../../src/artifacts/artifactReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAIjE;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,YAAoB;IAChE,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oDAAoD,YAAY,MAAM,OAAO,EAAE,EAAE,CAAC;IAChH,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qCAAqC,YAAY,wBAAwB,OAAO,EAAE,EAAE,CAAC;IACnH,CAAC;IAED,MAAM,UAAU,GAAG,0BAA0B,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qCAAqC,YAAY,mCAAmC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;IACxI,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAA6B,EAAE,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ComparisonArtifact } from '../domain/comparison.js';
|
|
2
|
+
import type { Diagnostic } from '../domain/diagnostics.js';
|
|
3
|
+
export declare const COMPARISON_MANIFEST_FILENAME = "manifest.json";
|
|
4
|
+
/**
|
|
5
|
+
* Observer-owned result of persisting one ComparisonArtifact. Mirrors
|
|
6
|
+
* `PersistedObservationResult`'s shape/semantics but is its own type: a
|
|
7
|
+
* comparison directory has no screenshot file, and this writer never touches
|
|
8
|
+
* `src/artifacts/artifactWriter.ts` or its observation-persistence
|
|
9
|
+
* guarantees.
|
|
10
|
+
*/
|
|
11
|
+
export type PersistedComparisonResult = {
|
|
12
|
+
ok: true;
|
|
13
|
+
artifactRoot: string;
|
|
14
|
+
manifestPath: string;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
diagnostics: Diagnostic[];
|
|
18
|
+
};
|
|
19
|
+
export interface WriteComparisonArtifactOptions {
|
|
20
|
+
/** Base directory that `outputLocation` is resolved against. Defaults to `process.cwd()`. */
|
|
21
|
+
cwd?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Persists one already-assembled, already-validated `ComparisonArtifact` as
|
|
25
|
+
* one portable artifact directory: `<outputLocation>/<comparisonId>/manifest.json`
|
|
26
|
+
* only - no screenshot is copied (the manifest's `before`/`after.screenshot`
|
|
27
|
+
* fields already point back to the source observations; see
|
|
28
|
+
* `domain/comparisonEngine.ts#sourceReference`), and no `relationships.json`/
|
|
29
|
+
* `differences.json`/`diff.png` side files are produced.
|
|
30
|
+
*
|
|
31
|
+
* Follows the same atomic-write discipline as
|
|
32
|
+
* `artifacts/artifactWriter.ts#writeObservationArtifact` (sibling temporary
|
|
33
|
+
* directory, then one atomic rename onto the final name) without sharing its
|
|
34
|
+
* implementation - a comparison directory has a different, smaller content
|
|
35
|
+
* shape, and this function never touches observation persistence. A fresh
|
|
36
|
+
* `comparisonId` per execution normally prevents collisions; an existing
|
|
37
|
+
* directory at the final path is treated as a genuine collision and
|
|
38
|
+
* rejected rather than overwritten, so prior comparison evidence is never
|
|
39
|
+
* destructively replaced.
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeComparisonArtifact(artifact: ComparisonArtifact, outputLocation: string, options?: WriteComparisonArtifactOptions): Promise<PersistedComparisonResult>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdir, writeFile, rename, rm, access } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isValidComparisonArtifact } from '../domain/comparison.js';
|
|
4
|
+
import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
|
|
5
|
+
export const COMPARISON_MANIFEST_FILENAME = 'manifest.json';
|
|
6
|
+
function writeFailure(message, details) {
|
|
7
|
+
const diagnostic = {
|
|
8
|
+
code: 'artifact-write-failure',
|
|
9
|
+
severity: DIAGNOSTIC_SEVERITY['artifact-write-failure'],
|
|
10
|
+
message,
|
|
11
|
+
...(details === undefined ? {} : { details }),
|
|
12
|
+
};
|
|
13
|
+
return { ok: false, diagnostics: [diagnostic] };
|
|
14
|
+
}
|
|
15
|
+
async function pathExists(candidate) {
|
|
16
|
+
return access(candidate).then(() => true, () => false);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Persists one already-assembled, already-validated `ComparisonArtifact` as
|
|
20
|
+
* one portable artifact directory: `<outputLocation>/<comparisonId>/manifest.json`
|
|
21
|
+
* only - no screenshot is copied (the manifest's `before`/`after.screenshot`
|
|
22
|
+
* fields already point back to the source observations; see
|
|
23
|
+
* `domain/comparisonEngine.ts#sourceReference`), and no `relationships.json`/
|
|
24
|
+
* `differences.json`/`diff.png` side files are produced.
|
|
25
|
+
*
|
|
26
|
+
* Follows the same atomic-write discipline as
|
|
27
|
+
* `artifacts/artifactWriter.ts#writeObservationArtifact` (sibling temporary
|
|
28
|
+
* directory, then one atomic rename onto the final name) without sharing its
|
|
29
|
+
* implementation - a comparison directory has a different, smaller content
|
|
30
|
+
* shape, and this function never touches observation persistence. A fresh
|
|
31
|
+
* `comparisonId` per execution normally prevents collisions; an existing
|
|
32
|
+
* directory at the final path is treated as a genuine collision and
|
|
33
|
+
* rejected rather than overwritten, so prior comparison evidence is never
|
|
34
|
+
* destructively replaced.
|
|
35
|
+
*/
|
|
36
|
+
export async function writeComparisonArtifact(artifact, outputLocation, options = {}) {
|
|
37
|
+
const validation = isValidComparisonArtifact(artifact);
|
|
38
|
+
if (!validation.valid) {
|
|
39
|
+
return writeFailure(`refusing to persist an invalid ComparisonArtifact: ${validation.reason}`);
|
|
40
|
+
}
|
|
41
|
+
const cwd = options.cwd ?? process.cwd();
|
|
42
|
+
const outputRoot = path.resolve(cwd, outputLocation);
|
|
43
|
+
const finalRoot = path.join(outputRoot, artifact.comparisonId);
|
|
44
|
+
const tempRoot = path.join(outputRoot, `.tmp-${artifact.comparisonId}`);
|
|
45
|
+
if (await pathExists(finalRoot)) {
|
|
46
|
+
return writeFailure(`a comparison artifact already exists at "${artifact.comparisonId}"; refusing to overwrite it`, {
|
|
47
|
+
artifactRoot: finalRoot,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
52
|
+
await mkdir(tempRoot, { recursive: true });
|
|
53
|
+
await writeFile(path.join(tempRoot, COMPARISON_MANIFEST_FILENAME), JSON.stringify(artifact, null, 2), 'utf8');
|
|
54
|
+
await rename(tempRoot, finalRoot);
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
artifactRoot: finalRoot,
|
|
58
|
+
manifestPath: path.join(finalRoot, COMPARISON_MANIFEST_FILENAME),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
await rm(tempRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
return writeFailure(`failed to persist comparison artifact: ${message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=comparisonArtifactWriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparisonArtifactWriter.js","sourceRoot":"","sources":["../../src/artifacts/comparisonArtifactWriter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
2
|
import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
|
|
3
3
|
import { classifyUrl, classifyRedirect, classifySubresource, classifyPopup, classifyDownload } from '../safety/policy.js';
|
|
4
|
-
import { capturePageEvidence, captureTargetEvidence } from './evidenceCapture.js';
|
|
4
|
+
import { capturePageEvidence, captureTargetEvidence, resolveConfiguredTargets, findResolvedTarget } from './evidenceCapture.js';
|
|
5
|
+
import { captureScrollRuntimeSnapshot, performWindowScrollBy, performTargetScrollBy, waitTwoAnimationFrames } from './scrollCapture.js';
|
|
6
|
+
import { deriveScrollScenarioTransition, deriveScrollOwner } from '../domain/scrollEvidence.js';
|
|
5
7
|
function diagnostic(code, message) {
|
|
6
8
|
return { code, severity: DIAGNOSTIC_SEVERITY[code], message };
|
|
7
9
|
}
|
|
@@ -104,8 +106,52 @@ export async function captureViewportInternal(request) {
|
|
|
104
106
|
result = { ok: false, diagnostics: [...diagnostics, classifyNavigationError(navigationError, request.readiness.timeoutMs)] };
|
|
105
107
|
}
|
|
106
108
|
else {
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
+
// v0.3 Batch 2/3: for a scenario request, resolve configured targets
|
|
110
|
+
// exactly once (reusing the same canonical resolveConfiguredTargets
|
|
111
|
+
// algorithm captureTargetEvidence uses below), capture the initial
|
|
112
|
+
// runtime snapshot, perform the one immediate scroll action (window or
|
|
113
|
+
// the uniquely-resolved action target - same two-animation-frame
|
|
114
|
+
// stabilization either way), and capture the final runtime snapshot -
|
|
115
|
+
// all before the screenshot and ordinary page/target evidence below,
|
|
116
|
+
// so every one of those downstream captures describes only the final
|
|
117
|
+
// post-action state. No second navigation, page, or observation ever
|
|
118
|
+
// happens.
|
|
119
|
+
let scrollScenarioEvidence;
|
|
120
|
+
if (request.scrollScenario) {
|
|
121
|
+
const action = request.scrollScenario.action;
|
|
122
|
+
const resolvedForScenario = await resolveConfiguredTargets(page, request.targets);
|
|
123
|
+
try {
|
|
124
|
+
const initial = await captureScrollRuntimeSnapshot(page, resolvedForScenario, request.viewport);
|
|
125
|
+
if (action.kind === 'window-scroll-by') {
|
|
126
|
+
await performWindowScrollBy(page, action.deltaX, action.deltaY);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const actionTarget = findResolvedTarget(resolvedForScenario, action.target);
|
|
130
|
+
if (actionTarget?.status === 'matched' && actionTarget.handle) {
|
|
131
|
+
await performTargetScrollBy(actionTarget.handle, action.deltaX, action.deltaY);
|
|
132
|
+
}
|
|
133
|
+
// else: the configured action target could not be uniquely
|
|
134
|
+
// resolved at runtime (not-found/ambiguous/unavailable) - no
|
|
135
|
+
// scroll is performed and no movement is fabricated. The
|
|
136
|
+
// existing target-missing/target-ambiguous/target-hidden/
|
|
137
|
+
// browser-evidence-unavailable diagnostic for this same target
|
|
138
|
+
// name still surfaces below from the ordinary
|
|
139
|
+
// captureTargetEvidence pass, so the unresolved action target
|
|
140
|
+
// remains honestly explained rather than silently ignored.
|
|
141
|
+
}
|
|
142
|
+
await waitTwoAnimationFrames(page);
|
|
143
|
+
const final = await captureScrollRuntimeSnapshot(page, resolvedForScenario, request.viewport);
|
|
144
|
+
const transition = deriveScrollScenarioTransition(initial, final);
|
|
145
|
+
const scrollOwner = deriveScrollOwner(transition);
|
|
146
|
+
scrollScenarioEvidence = { initial, final, transition, scrollOwner };
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await Promise.all(resolvedForScenario.filter((info) => info.handle).map((info) => info.handle.dispose()));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Captured from the same live page, after the same readiness point (and,
|
|
153
|
+
// for a scenario request, after the scroll action above), as required by
|
|
154
|
+
// Batch 3/v0.3 Batch 2: no second browser/page is ever opened.
|
|
109
155
|
const screenshotBuffer = await page.screenshot({ type: 'png' });
|
|
110
156
|
const provenance = { engine: 'chromium', version: browser.version() };
|
|
111
157
|
let pageEvidence;
|
|
@@ -128,7 +174,15 @@ export async function captureViewportInternal(request) {
|
|
|
128
174
|
diagnostics.push(diagnostic('browser-evidence-unavailable', `target evidence collection failed: ${message}`));
|
|
129
175
|
targetEvidence = {};
|
|
130
176
|
}
|
|
131
|
-
result = {
|
|
177
|
+
result = {
|
|
178
|
+
ok: true,
|
|
179
|
+
provenance,
|
|
180
|
+
screenshot: new Uint8Array(screenshotBuffer),
|
|
181
|
+
pageEvidence,
|
|
182
|
+
targetEvidence,
|
|
183
|
+
diagnostics,
|
|
184
|
+
...(scrollScenarioEvidence ? { scrollScenarioEvidence } : {}),
|
|
185
|
+
};
|
|
132
186
|
}
|
|
133
187
|
}
|
|
134
188
|
catch (err) {
|