@applitools/eyes-results 0.2.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +26 -0
  3. package/dist/api/api.js +17 -0
  4. package/dist/api/compare-step.js +20 -0
  5. package/dist/api/diff-fetch.js +13 -0
  6. package/dist/api/dom-diff.js +139 -0
  7. package/dist/api/dom-fetch.js +39 -0
  8. package/dist/api/find-regions.js +24 -0
  9. package/dist/api/fs-cache.js +19 -0
  10. package/dist/api/gzip.js +12 -0
  11. package/dist/api/image-fetch.js +31 -0
  12. package/dist/api/polling.js +117 -0
  13. package/dist/api/req-results.js +77 -0
  14. package/dist/api/same-diff-as.js +25 -0
  15. package/dist/api/selector-mapping.js +15 -0
  16. package/dist/api/session-fetch.js +35 -0
  17. package/dist/api/session-history-fetch.js +73 -0
  18. package/dist/api/session-updates.js +37 -0
  19. package/dist/api/tasks.js +48 -0
  20. package/dist/api/url-parsing.js +33 -0
  21. package/dist/common/common.js +2 -0
  22. package/dist/common/diff-rects.js +64 -0
  23. package/dist/common/region-mapping.js +50 -0
  24. package/dist/geometry/geometry.js +72 -0
  25. package/dist/index.js +14 -0
  26. package/dist/lib/api-key-provider.js +26 -0
  27. package/dist/lib/env.js +26 -0
  28. package/dist/lib/phase-tracker.js +223 -0
  29. package/dist/sessions/batch.js +98 -0
  30. package/dist/sessions/diffs.js +210 -0
  31. package/dist/sessions/dom-history.js +107 -0
  32. package/dist/sessions/dom-search.js +1150 -0
  33. package/dist/sessions/dom.js +513 -0
  34. package/dist/sessions/match.js +56 -0
  35. package/dist/sessions/regions.js +280 -0
  36. package/dist/sessions/session.js +115 -0
  37. package/llm-docs/dom-search.md +164 -0
  38. package/package.json +69 -0
  39. package/types/api/api.d.ts +17 -0
  40. package/types/api/compare-step.d.ts +52 -0
  41. package/types/api/diff-fetch.d.ts +21 -0
  42. package/types/api/dom-diff.d.ts +53 -0
  43. package/types/api/dom-fetch.d.ts +29 -0
  44. package/types/api/find-regions.d.ts +26 -0
  45. package/types/api/fs-cache.d.ts +12 -0
  46. package/types/api/gzip.d.ts +5 -0
  47. package/types/api/image-fetch.d.ts +29 -0
  48. package/types/api/polling.d.ts +41 -0
  49. package/types/api/req-results.d.ts +37 -0
  50. package/types/api/same-diff-as.d.ts +38 -0
  51. package/types/api/selector-mapping.d.ts +28 -0
  52. package/types/api/session-fetch.d.ts +34 -0
  53. package/types/api/session-history-fetch.d.ts +56 -0
  54. package/types/api/session-updates.d.ts +26 -0
  55. package/types/api/tasks.d.ts +33 -0
  56. package/types/api/url-parsing.d.ts +20 -0
  57. package/types/common/common.d.ts +2 -0
  58. package/types/common/diff-rects.d.ts +39 -0
  59. package/types/common/region-mapping.d.ts +26 -0
  60. package/types/geometry/geometry.d.ts +24 -0
  61. package/types/index.d.ts +14 -0
  62. package/types/lib/api-key-provider.d.ts +31 -0
  63. package/types/lib/env.d.ts +14 -0
  64. package/types/lib/phase-tracker.d.ts +146 -0
  65. package/types/sessions/batch.d.ts +67 -0
  66. package/types/sessions/diffs.d.ts +26 -0
  67. package/types/sessions/dom-history.d.ts +119 -0
  68. package/types/sessions/dom-search.d.ts +113 -0
  69. package/types/sessions/dom.d.ts +263 -0
  70. package/types/sessions/match.d.ts +72 -0
  71. package/types/sessions/regions.d.ts +171 -0
  72. package/types/sessions/session.d.ts +97 -0
@@ -0,0 +1,24 @@
1
+ export interface Rect {
2
+ left: number;
3
+ top: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export declare function parseRect(s: string): Rect;
8
+ /** Inverse of `parseRect` — formats a rect back into `"left,top,width,height"`. */
9
+ export declare function formatRect(rect: Rect): string;
10
+ /**
11
+ * Returns true if outer completely contains inner (all edges of inner are within outer).
12
+ * Equal rectangles are considered to contain each other.
13
+ */
14
+ export declare function rectContains(outer: Rect, inner: Rect): boolean;
15
+ export declare function rectsIntersect(a: Rect, b: Rect): boolean;
16
+ export declare function intersectionArea(a: Rect, b: Rect): number;
17
+ export declare function boundingRect(a: Rect, b: Rect): Rect;
18
+ /**
19
+ * Expands a rect by `padding` pixels on every side.
20
+ * When `imageWidth`/`imageHeight` are provided the result is clipped to the image bounds.
21
+ */
22
+ export declare function padRect(rect: Rect, padding: number, imageWidth?: number, imageHeight?: number): Rect;
23
+ /** Merges pairs of rects that overlap by more than 10% of the smaller rect's area. */
24
+ export declare function unifyRects(rects: Rect[]): Rect[];
@@ -0,0 +1,14 @@
1
+ export * from './lib/api-key-provider.js';
2
+ export * from './lib/env.js';
3
+ export * from './lib/phase-tracker.js';
4
+ export * from './geometry/geometry.js';
5
+ export * from './sessions/batch.js';
6
+ export * from './sessions/diffs.js';
7
+ export * from './sessions/dom-history.js';
8
+ export * from './sessions/dom-search.js';
9
+ export * from './sessions/dom.js';
10
+ export * from './sessions/match.js';
11
+ export * from './sessions/regions.js';
12
+ export * from './sessions/session.js';
13
+ export * from './api/api.js';
14
+ export * from './common/common.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A source of API keys, so a caller threads one object through a call chain instead of choosing
3
+ * and passing the right raw key string itself at every call site — the read/write (and, for a
4
+ * future run-triggering flow, run) distinction is made once, by whoever constructs the provider,
5
+ * not re-decided by every function that happens to need one. A function that only ever needs one
6
+ * specific kind of key calls that one getter and never sees the others, so passing a provider
7
+ * built for the wrong purpose fails at the point of use rather than silently substituting the
8
+ * wrong credential.
9
+ */
10
+ export interface ApiKeyProvider {
11
+ getReadKey(): string;
12
+ getWriteKey(): string;
13
+ getRunKey(): string;
14
+ }
15
+ /**
16
+ * Wraps already-resolved key strings. A getter throws a clear error if its key wasn't supplied —
17
+ * e.g. a write key deliberately withheld when building a provider for a read-only call — rather
18
+ * than returning `undefined` and letting a missing credential fail confusingly further down.
19
+ */
20
+ export declare class StaticApiKeyProvider implements ApiKeyProvider {
21
+ private readonly keys;
22
+ constructor(keys: {
23
+ readKey?: string;
24
+ writeKey?: string;
25
+ runKey?: string;
26
+ });
27
+ getReadKey(): string;
28
+ getWriteKey(): string;
29
+ getRunKey(): string;
30
+ private require;
31
+ }
@@ -0,0 +1,14 @@
1
+ import * as fs from 'fs';
2
+ export interface LoadEnvDeps {
3
+ existsSync: typeof fs.existsSync;
4
+ configDotenv: (options: {
5
+ path: string;
6
+ }) => unknown;
7
+ }
8
+ export declare const defaultLoadEnvDeps: LoadEnvDeps;
9
+ /**
10
+ * Walks up from `startDir` (default: the current working directory) looking for a `.env`
11
+ * file, loading the first one found via `dotenv`. Mirrors the behavior every skill's own
12
+ * `main.ts` used to inline directly.
13
+ */
14
+ export declare function loadEnvFromCwd(startDir?: string, deps?: LoadEnvDeps): void;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * A generic, eyes-agnostic completion registry backing eyes-inspect's workflow tools (and,
3
+ * eventually, eyes-resolve's own batch-review phases) — persisted to the filesystem so a
4
+ * restarted server picks an in-progress investigation back up with no special "resume"
5
+ * handling. Every id is an opaque string a caller constructs deterministically from its own
6
+ * natural identifiers (a batch/session URL, a step index, a rect) — this module has no
7
+ * knowledge of what an id represents, only that the same input always hashes to the same
8
+ * record.
9
+ *
10
+ * Alongside that registry sits a second, unrelated facility on the same filesystem substrate:
11
+ * `log`, a generic call-trace log any caller can append a labeled entry to — see its own doc
12
+ * comment.
13
+ */
14
+ export interface PhaseTrackerDeps {
15
+ readFile: (filePath: string) => string | null;
16
+ writeFile: (filePath: string, contents: string) => void;
17
+ /** Appends `contents` to `filePath`, creating it if absent — never reads first. */
18
+ appendFile: (filePath: string, contents: string) => void;
19
+ /** Removes `filePath` if it exists — a no-op if it doesn't. */
20
+ deleteFile: (filePath: string) => void;
21
+ mkdir: (dirPath: string) => void;
22
+ /** Blocks the current call for `ms` milliseconds — swapped out in tests to skip real delays. */
23
+ sleep: (ms: number) => void;
24
+ /** Root directory every record is written under, partitioned into completions/child-findings/invocations/modes/children/unready-children subdirectories, plus a debug subdirectory holding one log file per scope id a caller has logged under. */
25
+ stateDir: string;
26
+ }
27
+ export declare const defaultPhaseTrackerDeps: PhaseTrackerDeps;
28
+ export interface Completion<T = unknown> {
29
+ payload: T;
30
+ completedAt: string;
31
+ }
32
+ /** The recorded completion for `id`, or `undefined` if `<level>_end` was never (successfully) called for it. */
33
+ export declare function getCompletion<T = unknown>(id: string, deps?: PhaseTrackerDeps): Completion<T> | undefined;
34
+ export declare function isCompleted(id: string, deps?: PhaseTrackerDeps): boolean;
35
+ /**
36
+ * Persists `payload` as `id`'s completion, stamped with the current time — overwriting
37
+ * whatever was recorded for `id` before, so revising a summary (or refreshing a stale
38
+ * completion) is just calling this again.
39
+ */
40
+ export declare function recordCompletion<T>(id: string, payload: T, deps?: PhaseTrackerDeps): Completion<T>;
41
+ /** `parentId`'s full accumulated child-findings record, read as-is with no live-data filtering. `{}` if no child has ever reported into it. */
42
+ export declare function getChildFindings<T = unknown>(parentId: string, deps?: PhaseTrackerDeps): Record<string, T>;
43
+ /**
44
+ * Writes (or overwrites) `childId`'s entry in `parentId`'s accumulated findings record,
45
+ * creating that record the first time any child reports into it. Called once a child's own
46
+ * `_end` succeeds — independent of whether `childId` is still among `parentId`'s currently-live
47
+ * children, so a child that later drops out of the live set never loses its findings.
48
+ */
49
+ export declare function appendChildFinding<T>(parentId: string, childId: string, payload: T, deps?: PhaseTrackerDeps): void;
50
+ export interface InvocationLogEntry {
51
+ toolName: string;
52
+ args: unknown;
53
+ at: string;
54
+ }
55
+ /** Appends one entry to `scopeId`'s invocation log — called as a side effect of every eyes-inspect data tool. */
56
+ export declare function logInvocation(scopeId: string, toolName: string, args: unknown, deps?: PhaseTrackerDeps): void;
57
+ /** Whether `scopeId`'s invocation log contains a call to `toolName` whose `args` satisfy `matcher` — the per-item invocation-verification check backing `step_end`/`diff_end`'s hard gates. */
58
+ export declare function verifyInvocation(scopeId: string, toolName: string, matcher: (args: unknown) => boolean, deps?: PhaseTrackerDeps): boolean;
59
+ /** Persists `mode` as the mode `scopeId` is currently being reviewed in — overwriting whatever was recorded before, so re-entering with the same mode simply re-states it. */
60
+ export declare function recordScopeMode(scopeId: string, mode: string, deps?: PhaseTrackerDeps): void;
61
+ /** The mode last recorded for `scopeId` via `recordScopeMode`, or `undefined` if never recorded. */
62
+ export declare function getScopeMode(scopeId: string, deps?: PhaseTrackerDeps): string | undefined;
63
+ /** Discards `id`'s completion, as if `recordCompletion` had never been called for it. */
64
+ export declare function clearCompletion(id: string, deps?: PhaseTrackerDeps): void;
65
+ /** Discards `parentId`'s entire accumulated child-findings record — every child's entry, not just one. */
66
+ export declare function clearChildFindings(parentId: string, deps?: PhaseTrackerDeps): void;
67
+ /** Discards `scopeId`'s recorded mode, as if `recordScopeMode` had never been called for it. */
68
+ export declare function clearScopeMode(scopeId: string, deps?: PhaseTrackerDeps): void;
69
+ /** Persists the keys of `scopeId`'s children still unready to be finalized, as of its own latest live check — overwriting whatever was recorded before, so a child that has since become ready (or a newly-discovered unready one) is reflected exactly, never accumulated across checks. */
70
+ export declare function recordUnreadyChildren(scopeId: string, childKeys: string[], deps?: PhaseTrackerDeps): void;
71
+ /** The child keys `recordUnreadyChildren` last recorded as unready for `scopeId`, or `[]` if never recorded. */
72
+ export declare function getUnreadyChildren(scopeId: string, deps?: PhaseTrackerDeps): string[];
73
+ /** Discards `scopeId`'s recorded unready-children snapshot, as if `recordUnreadyChildren` had never been called for it. */
74
+ export declare function clearUnreadyChildren(scopeId: string, deps?: PhaseTrackerDeps): void;
75
+ /** `scopeId`'s queued child keys — the stable, append-only set of children discovered live so far, investigated from regardless of what a later live fetch would show. `[]` if none have ever been queued. */
76
+ export declare function getQueuedChildKeys(scopeId: string, deps?: PhaseTrackerDeps): string[];
77
+ /**
78
+ * Appends any of `liveKeys` not already queued, preserving the existing queue's order with
79
+ * newcomers appended in their given order — never removes a key, so a child that later drops out
80
+ * of a live fetch (resolved via propagation, or otherwise) stays queued and gets visited. Returns
81
+ * the full merged queue; writes only when something actually changed.
82
+ */
83
+ export declare function queueChildKeys(scopeId: string, liveKeys: string[], deps?: PhaseTrackerDeps): string[];
84
+ /** Discards `scopeId`'s entire queued-children record, as if nothing had ever been queued for it. */
85
+ export declare function clearQueuedChildKeys(scopeId: string, deps?: PhaseTrackerDeps): void;
86
+ /**
87
+ * Persists `payload` as everything `scopeId`'s own live fetch most recently discovered — e.g. a
88
+ * batch's full session list, expensive to fetch and shared by everything nested under it —
89
+ * overwriting whatever was recorded before, so a caller that decides to refresh simply calls this
90
+ * again. Unlike `queueChildKeys`/`getQueuedChildKeys` (a stable, append-only set of keys a caller
91
+ * has committed to investigating), this is the raw discovery result itself — including entries
92
+ * (e.g. still-`Running` sessions) that may not be ready to investigate at all — so a lower-level
93
+ * scope nested underneath `scopeId` can re-derive its own children from it without repeating the
94
+ * fetch.
95
+ */
96
+ export declare function recordDiscoveredChildren<T>(scopeId: string, payload: T, deps?: PhaseTrackerDeps): void;
97
+ /** What `recordDiscoveredChildren` last recorded for `scopeId`, or `undefined` if never recorded. */
98
+ export declare function getDiscoveredChildren<T>(scopeId: string, deps?: PhaseTrackerDeps): T | undefined;
99
+ /** Discards `scopeId`'s recorded discovery, as if `recordDiscoveredChildren` had never been called for it. */
100
+ export declare function clearDiscoveredChildren(scopeId: string, deps?: PhaseTrackerDeps): void;
101
+ /**
102
+ * The exact filesystem path `log` appends `scopeId`'s entries to — a caller (or a human trying to
103
+ * find the right file by hand) never has to reimplement this module's own hashing scheme just to
104
+ * locate it.
105
+ */
106
+ export declare function getLogFilePath(scopeId: string, deps?: PhaseTrackerDeps): string;
107
+ export interface DebugLogEntry {
108
+ at: string;
109
+ label: string;
110
+ input?: unknown;
111
+ output?: unknown;
112
+ error?: unknown;
113
+ message?: unknown;
114
+ }
115
+ /**
116
+ * A generic call-trace log: any caller can record one labeled entry, with no fixed vocabulary for
117
+ * what a call even is — `label` is whatever convention the caller adopts (today, resolution.ts
118
+ * labels each entry with the traced function's own name; a wholly different caller could start
119
+ * labeling its own entries tomorrow with no change here). Appends to `scopeId`'s own log file,
120
+ * partitioned by hashed id the same way every other record in this module is — `scopeId` is just
121
+ * as opaque to this module as everywhere else it appears, so it's entirely the caller's own choice
122
+ * whether to pass the same id (e.g. an investigation's root) on every call, consolidating every
123
+ * entry raised anywhere within it into one file, or a more granular one per call, splitting the
124
+ * trace up instead. `label` is the only other required field; `input`/`output`/`error`/`message`
125
+ * are all optional and independent — a call-and-response entry sets `input`/`output`, a failed one
126
+ * sets `error` (instead of stuffing it into `output`), and anything that isn't a call at all (a
127
+ * one-off event, a snapshot) just sets `message` to whatever shape fits. Every entry lands as one
128
+ * line (a JSON-encoded `DebugLogEntry`) in a single NDJSON file at `getLogFilePath(scopeId, deps)`.
129
+ * There is no reader function: this is meant to be opened directly (as a file on disk, or via
130
+ * `deps.readFile` in a test) rather than queried back through this module.
131
+ *
132
+ * Safe under concurrent callers by construction, not by locking: this never reads the file before
133
+ * appending (so there's no read-modify-write window for two callers to race), and each entry is
134
+ * written as a single `appendFile` call — opening for append and writing is one atomic
135
+ * seek-to-end-and-write at the OS level, so concurrent appends interleave whole lines and never
136
+ * corrupt or split one another's. The only remaining failure mode is a transient lock on the file
137
+ * itself (e.g. a concurrent antivirus scan) — `mkdir`/`appendFile` are wrapped in the same
138
+ * lock-retry backoff (`withRetry`) as every other record this module writes, so a caller never
139
+ * has to poll for the file to become available itself; a momentarily-busy append just retries.
140
+ */
141
+ export declare function log(scopeId: string, label: string, fields?: {
142
+ input?: unknown;
143
+ output?: unknown;
144
+ error?: unknown;
145
+ message?: unknown;
146
+ }, deps?: PhaseTrackerDeps): void;
@@ -0,0 +1,67 @@
1
+ import type { SimplifiedSession } from './session.js';
2
+ /**
3
+ * The slice of the raw batch entity's fields `toBatchStats` reads. The real response has
4
+ * many more fields (name, timing, `properties`, etc.) — irrelevant here, so left untyped.
5
+ */
6
+ export interface BatchInfo {
7
+ completedCount: number;
8
+ runningCount: number;
9
+ abortedCount: number;
10
+ passedCount: number;
11
+ failedCount: number;
12
+ unresolvedCount: number;
13
+ changedCount: number;
14
+ stepsCount: number;
15
+ unresolvedStepsCount: number;
16
+ changedStepsCount: number;
17
+ missingStepsCount: number;
18
+ newStepsCount: number;
19
+ }
20
+ export interface BatchStats {
21
+ total: number;
22
+ unresolved: number;
23
+ passed: number;
24
+ failed: number;
25
+ aborted: number;
26
+ unsaved: number;
27
+ totalSteps: number;
28
+ unresolvedSteps: number;
29
+ unsavedSteps: number;
30
+ missingSteps: number;
31
+ newSteps: number;
32
+ }
33
+ /** Shapes a raw batch entity's fields (see `BatchInfo` — fetched via `api`'s `fetchBatchInfo`) into `BatchStats`. */
34
+ export declare function toBatchStats(raw: Partial<BatchInfo>): BatchStats;
35
+ /**
36
+ * Computes the same `BatchStats` shape as `toBatchStats`, but by counting over a set of
37
+ * already-`simplifySession`-shaped sessions (`sessions/session`) instead of reading the
38
+ * (eventually consistent) batch aggregate — so it's always current, at the cost of only
39
+ * reflecting whatever subset of sessions `sessions` contains (the whole batch, or an
40
+ * already-filtered slice of it).
41
+ *
42
+ * Takes `SimplifiedSession[]`, not raw sessions, for two reasons: it reuses whatever
43
+ * `simplifySession` call the caller already made (e.g. to also build a session listing)
44
+ * instead of re-simplifying every session a second time just for this; and it counts a
45
+ * step's `resolution`/`isUnsaved` exactly as `simplifySession` derived them, with no second,
46
+ * possibly-diverging notion of what "resolved" means computed from the raw
47
+ * `appOutputResolution` fields directly. Every entry in `sessions` must have been simplified
48
+ * with `steps` included (the default) — one shaped with `{ includeSteps: false }` (`steps:
49
+ * null`) would silently contribute 0 to every step-level count.
50
+ */
51
+ export declare function computeBatchStats(sessions: SimplifiedSession[]): BatchStats;
52
+ export declare const BATCH_FILTERS: readonly ["unresolved", "unsaved", "mismatching"];
53
+ export type BatchFilter = (typeof BATCH_FILTERS)[number];
54
+ /**
55
+ * Narrows `sessions` to those matching `filter` — every session, unfiltered, when `filter` is
56
+ * omitted. `"mismatching"` keeps only sessions with at least one step that's `isMatching: false`,
57
+ * missing a checkpoint (`checkpoint.id === null` — the test stopped capturing this checkpoint),
58
+ * or new (`baseline.id === null` — the test started capturing a checkpoint with no prior baseline
59
+ * to compare against) — a missing/new step still needs a decision even though its own
60
+ * `isMatching` isn't a reliable signal either way, so it counts regardless of what `isMatching`
61
+ * reads. Also regardless of a step's own `resolution`: `isMatching` is fixed at comparison time
62
+ * and never changes afterward, including once a step is accepted/rejected and saved to the
63
+ * baseline, so it stays a reliable "did this ever differ" signal rather than one a decision
64
+ * clears. Requires `sessions` to have been simplified with `steps` included (the default); one
65
+ * shaped with `{ includeSteps: false }` never matches (`steps` is `null`).
66
+ */
67
+ export declare function filterSessions(sessions: SimplifiedSession[], filter?: BatchFilter): SimplifiedSession[];
@@ -0,0 +1,26 @@
1
+ import { type Rect } from '../geometry/geometry.js';
2
+ export interface DiffArea extends Rect {
3
+ diffCount: number;
4
+ }
5
+ /**
6
+ * Returns coarse page areas containing diffs. Each rect is expanded to at least
7
+ * `hPad`×`vPad` (centered) before checking for overlap; overlapping inflated rects
8
+ * are merged into the bounding rect of their originals (not the inflated ones).
9
+ */
10
+ export declare function diffAreas(rects: Rect[], hPad?: number, vPad?: number): DiffArea[];
11
+ /**
12
+ * Condenses a large number of DiffAreas into grouped ContainerDiffArea objects.
13
+ * When the number of areas is <= threshold, returns the original array unchanged.
14
+ *
15
+ * Each area is assigned to every grid cell it overlaps. A cell is dense when the
16
+ * total area of diff pixels covering it meets DENSE_COVERAGE (ratio to cell area).
17
+ * Dense cells are flood-filled into connected components, each producing a single
18
+ * ContainerDiffArea bounding box. Adjacent containers (gap ≤ 1 cell, or one contains
19
+ * the other) are merged. Finally, any unclaimed area whose rect falls completely inside
20
+ * a container is absorbed into it.
21
+ */
22
+ export declare function condenseDiffAreas(areas: DiffArea[], threshold?: number): DiffArea[];
23
+ export declare function condenseDiffAreasDetailed(areas: DiffArea[], threshold?: number): {
24
+ containers: DiffArea[];
25
+ individuals: DiffArea[];
26
+ };
@@ -0,0 +1,119 @@
1
+ import { type DomAlignment } from './dom.js';
2
+ /** One older DOM capture to compare a suspect node against — labeled and dated, but otherwise
3
+ * agnostic to where it came from (a past test session today; possibly a past baseline version
4
+ * later — see this package's README). Despite the name, this is always a *checkpoint* DOM in
5
+ * the Visual Testing sense (the actual rendering of some past run), never a "baseline" (the
6
+ * expected/reference image) in that sense — "historic" here is about *when* the capture
7
+ * happened, an entirely different axis. `step` is an opaque, caller-constructed reference back
8
+ * to where this capture came from — e.g. `eyes-inspect`'s `history.ts` builds it as
9
+ * `<batchId>/<sessionId>/<stepIndex>` so the reader can navigate back to that exact step; this
10
+ * module never inspects or parses it, only carries it through to the output. `domId` identifies
11
+ * this capture to `FetchAlignmentDeps.fetchAlignment` — always passed as its baseline-role
12
+ * argument — see `HistoricDomNodeDiffEntry`, whose own `baselineStep`/`baselineCapturedAt`
13
+ * fields are copied from here and named to match. */
14
+ export interface HistoricDom {
15
+ step: string;
16
+ capturedAt: string;
17
+ domId: string;
18
+ }
19
+ /** One non-empty diff for a suspect node against one older historic version. `checkpointStep`/
20
+ * `checkpointCapturedAt` and `baselineStep`/`baselineCapturedAt` identify and date the two sides
21
+ * actually compared for this entry — the tracker's anchor at the time (the current step, on the
22
+ * first hop that ever produces a diff; an intermediate historic version thereafter) and the
23
+ * `HistoricDom` it was diffed against, respectively. `baselineStep`/`baselineCapturedAt` are
24
+ * named to match the `diff` string's own embedded `"baseline"` key (`serializeDiff`
25
+ * always labels its first/older argument that way), so the two agree rather than
26
+ * describing the same referent with two different words — but neither side is a literal Visual
27
+ * Testing baseline; both are checkpoint DOMs, just from different points in time. */
28
+ export interface HistoricDomNodeDiffEntry {
29
+ checkpointStep: string;
30
+ checkpointCapturedAt: string;
31
+ baselineStep: string;
32
+ baselineCapturedAt: string;
33
+ diff: string;
34
+ }
35
+ export interface HistoricDomNodeEvidence {
36
+ nodeId: string;
37
+ diffs: HistoricDomNodeDiffEntry[];
38
+ }
39
+ /** Mutable per-node accumulator threaded through repeated `applyHistoricDom` calls. */
40
+ export interface HistoricDomNodeTracker {
41
+ nodeId: string;
42
+ /** The most recently matched node id/domId pair — the chain's current position. Seeded from
43
+ * the current step; only ever moves when a hop actually finds a counterpart for it. A hop
44
+ * that finds none leaves this untouched, so the same anchor is retried against the next
45
+ * (older) historic candidate rather than falling back to some other reference point. Kept as
46
+ * a plain id, not the node object itself — the object is never needed again once
47
+ * `extractNodeDiff` has used it to read the match/rect it needs off the alignment. */
48
+ anchorDomId: string;
49
+ anchorNodeId: string;
50
+ /** Identifies/dates whatever `anchorNodeId` currently refers to — the current step at first,
51
+ * then whichever historic version the anchor most recently advanced to. Copied onto a diff
52
+ * entry's `checkpointStep`/`checkpointCapturedAt` so the newer side of a comparison is
53
+ * traceable even when it isn't the current step (e.g. after the anchor has advanced past one
54
+ * or more hops, or held through a skipped miss). */
55
+ anchorStep: string;
56
+ anchorCapturedAt: string;
57
+ diffs: HistoricDomNodeDiffEntry[];
58
+ }
59
+ /**
60
+ * The one piece of I/O `applyHistoricDom` needs — aligning two already-identified DOM captures
61
+ * by `domId` — injected rather than imported directly, since this package has no network or
62
+ * filesystem awareness of its own. A caller wires this to whatever aligns-and-caches two domIds
63
+ * for it (e.g. `api`'s `fetchDomDiff`/`checkDomDiffTask` driven to a ready alignment, closed over
64
+ * its own `baseUrl`/`keys`); no default is provided here, since no such implementation is within
65
+ * this package's own reach.
66
+ */
67
+ export interface FetchAlignmentDeps {
68
+ fetchAlignment: (baselineDomId: string, checkpointDomId: string) => Promise<DomAlignment>;
69
+ }
70
+ /**
71
+ * Seeds one tracker per suspect nodeId, anchored at `currentDomId`. Doesn't verify the nodeId
72
+ * actually exists in the current DOM — that would need fetching the whole tree just to check one
73
+ * id — so a bad id is simply carried through and only surfaces once (if) a real historic
74
+ * candidate is actually walked (`applyHistoricDom` catches it there for free, off an alignment it
75
+ * already has to compute). `currentStep`/`currentCapturedAt` identify and date the tracker's
76
+ * initial anchor, so the very first diff entry (if any) correctly attributes its checkpoint side
77
+ * to the current step rather than leaving it implicit.
78
+ */
79
+ export declare function createHistoricDomNodeTrackers(currentDomId: string, nodeIds: string[], currentStep: string, currentCapturedAt: string): HistoricDomNodeTracker[];
80
+ /**
81
+ * Applies exactly one older `HistoricDom` to every tracker that still has fewer than
82
+ * `targetDiffCount` non-empty diffs, mutating each in place — the unit of lazy evaluation a
83
+ * caller feeds historic DOMs into one at a time as it discovers them (see `history.ts`'s
84
+ * `walkHistoricSources`), so it can stop pulling further history the moment every tracker has
85
+ * enough evidence, rather than pre-fetching a fixed batch before knowing how much is needed.
86
+ *
87
+ * Each hop diffs a tracker's current anchor against `historicDom.domId` via a *global* alignment
88
+ * between the two full trees (`deps.fetchAlignment`/`extractNodeDiff`), not a standalone search
89
+ * for the anchor alone — a single-node content search would chase a content lookalike elsewhere
90
+ * on the page (e.g. a rotating carousel slot, or a live counter) rather than the element that
91
+ * actually took its place. When the alignment finds a counterpart, that counterpart becomes the
92
+ * anchor for the next (older) hop, so the walk tracks the same structural slot across a long
93
+ * history one hop at a time. Trackers sharing the same anchor domId are aligned against
94
+ * `historicDom.domId` together, once, via `groupByAnchorDomId` — the alignment itself doesn't
95
+ * depend on which specific node is being asked about, only on the pair of domIds, so paying for
96
+ * it once per distinct pair rather than once per tracker avoids pure waste (and, since
97
+ * `deps.fetchAlignment` is typically itself cached by domId pair, this also means a hop already
98
+ * paid for elsewhere — e.g. by `eyes_inspect_dom_diff`'s own two-sided diff — is never
99
+ * recomputed here either).
100
+ *
101
+ * When a hop's alignment finds no counterpart for the current anchor, that doesn't mean the
102
+ * anchor itself is wrong — it may just mean this one historic candidate is a poor comparison
103
+ * point (an unrelated element elsewhere on the page stealing the alignment's matching capacity,
104
+ * a transient loading state, and so on). Rather than reverting to some other reference node,
105
+ * a miss is simply skipped: nothing is recorded, and the anchor is left untouched, so the very
106
+ * same anchor is retried against the next (older) candidate the next time this is called. The
107
+ * chain resumes forward from wherever it next finds a match.
108
+ *
109
+ * A tracker whose `anchorNodeId` doesn't exist at all in whichever tree just played the
110
+ * checkpoint role (`extractNodeDiff`'s `found: false`) is different from a poor-comparison miss —
111
+ * that's `createHistoricDomNodeTrackers` never having verified the caller's nodeId, surfacing now
112
+ * for the first time. Every such tracker across the whole call is collected and reported in one
113
+ * combined error, rather than silently left to retry forever.
114
+ */
115
+ export declare function applyHistoricDom(trackers: HistoricDomNodeTracker[], historicDom: HistoricDom, targetDiffCount: number, deps: FetchAlignmentDeps): Promise<void>;
116
+ /** Strips tracker-only bookkeeping (the chain anchor) down to the plain
117
+ * `{nodeId, diffs: [{checkpointStep, checkpointCapturedAt, baselineStep, baselineCapturedAt, diff}]}`
118
+ * output shape. */
119
+ export declare function finalizeHistoricDomNodeEvidence(trackers: HistoricDomNodeTracker[]): HistoricDomNodeEvidence[];
@@ -0,0 +1,113 @@
1
+ type RelKind = 'descendant' | 'ancestor' | 'child' | 'parent' | 'sibling' | 'nthChild';
2
+ type RelTarget = {
3
+ type: 'id';
4
+ id: string;
5
+ } | {
6
+ type: 'expr';
7
+ ast: PredicateAST;
8
+ };
9
+ type BoundsTarget = {
10
+ type: 'id';
11
+ id: string;
12
+ } | {
13
+ type: 'point';
14
+ x: number;
15
+ y: number;
16
+ } | {
17
+ type: 'rect';
18
+ rect: {
19
+ left: number;
20
+ top: number;
21
+ width: number;
22
+ height: number;
23
+ };
24
+ };
25
+ type SizeOpKind = 'larger' | 'smaller' | 'wider' | 'narrower' | 'taller' | 'shorter';
26
+ type SizeTarget = {
27
+ type: 'id';
28
+ id: string;
29
+ } | {
30
+ type: 'rect';
31
+ rect: {
32
+ left: number;
33
+ top: number;
34
+ width: number;
35
+ height: number;
36
+ };
37
+ } | {
38
+ type: 'size';
39
+ width: number;
40
+ height: number;
41
+ } | {
42
+ type: 'value';
43
+ value: number;
44
+ };
45
+ type DirOpKind = 'above' | 'below' | 'leftOf' | 'rightOf';
46
+ type DirTarget = {
47
+ type: 'id';
48
+ id: string;
49
+ } | {
50
+ type: 'rect';
51
+ rect: {
52
+ left: number;
53
+ top: number;
54
+ width: number;
55
+ height: number;
56
+ };
57
+ };
58
+ type CompareOpKey = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'startsWith' | 'endsWith' | 'contains' | 'in' | 'regex';
59
+ export type PredicateAST = {
60
+ type: 'or';
61
+ left: PredicateAST;
62
+ right: PredicateAST;
63
+ } | {
64
+ type: 'and';
65
+ left: PredicateAST;
66
+ right: PredicateAST;
67
+ } | {
68
+ type: 'not';
69
+ inner: PredicateAST;
70
+ } | {
71
+ type: 'bounds';
72
+ op: 'contains' | 'intersects' | 'containedBy';
73
+ target: BoundsTarget;
74
+ } | {
75
+ type: 'rel';
76
+ kind: RelKind;
77
+ ordinal?: number;
78
+ target: RelTarget;
79
+ } | {
80
+ type: 'size';
81
+ kind: SizeOpKind;
82
+ target: SizeTarget;
83
+ } | {
84
+ type: 'dir';
85
+ kind: DirOpKind;
86
+ target: DirTarget;
87
+ } | {
88
+ type: 'prop';
89
+ path: string[];
90
+ op: CompareOpKey;
91
+ value: string | string[] | RegExp;
92
+ } | {
93
+ type: 'nullary';
94
+ name: string;
95
+ } | {
96
+ type: 'hasAttr';
97
+ name: string;
98
+ };
99
+ export declare function parsePredicate(source: string): PredicateAST;
100
+ export declare function astNeedsIndex(ast: PredicateAST): boolean;
101
+ /**
102
+ * Searches a DOM capture for elements matching a boolean predicate. See `llm-docs/dom-search.md`
103
+ * in this package (shipped in the tarball) for the full predicate language documentation (grammar,
104
+ * operators, examples) — that file is the canonical, copy-paste-friendly source; don't re-describe
105
+ * the grammar here or anywhere else, keep it in sync by editing it and copying it where it's
106
+ * needed instead.
107
+ *
108
+ * @param domCaptureJson Raw JSON text of a DOM capture file (as written by eyes-inspect's `dom` command) — a single node, or an array of nodes.
109
+ * @param predicateSource The predicate string described in `llm-docs/dom-search.md`.
110
+ * @returns One line per matching element, most indented lines the deepest — each line is that element's own properties (minus `childNodes`) as JSON, with a trailing `# <text>` comment when it has non-empty text.
111
+ */
112
+ export declare function searchDom(domCaptureJson: string, predicateSource: string): string;
113
+ export {};