@reticlehq/server 3.2.0 → 3.3.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/SKILL.md +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { formatStepAddress } from 'open-verification';
|
|
2
|
+
import { assertsState, expectLabel, expectedElementTestid, withoutClauses } from '@reticlehq/core';
|
|
3
3
|
import { span } from '../../trace.js';
|
|
4
|
-
import { anchorLabel, expectElementDrift, resolveTestid, testidDrift } from './flow-anchor.js';
|
|
4
|
+
import { anchorLabel, expectElementDrift, resolveTestid, testidDrift, ambiguousAnchorDrift, } from './flow-anchor.js';
|
|
5
5
|
export { anchorLabel, componentLabel, componentQueryArgs, editDistance, expectElementDrift, nearestIsAmbiguous, nearestTestid, resolveQuery, testidDrift, } from './flow-anchor.js';
|
|
6
6
|
import { routeOfEvent } from '@reticlehq/engine/question/predicate/predicate-route.js';
|
|
7
7
|
import { stepEffect } from '@reticlehq/engine/evidence/step-effect.js';
|
|
8
|
-
import { AnchorKind, DriftReason, EventType, FlowStepTool,
|
|
8
|
+
import { AnchorKind, DriftReason, EventType, FlowStepTool, PredicateKind, } from '@reticlehq/core';
|
|
9
9
|
import { asString, isConsequenceDrift } from '@reticlehq/core';
|
|
10
|
-
import { replayActionArgs, ambiguousTestidNote } from './replay.js';
|
|
11
10
|
import { anchorFieldName } from './fields/flow-secret-field.js';
|
|
12
11
|
import { degradedStepResult, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
|
|
13
|
-
import { successToPredicate } from './flow-success.js';
|
|
14
12
|
import { inFlightRequestLabels } from '../../surface/tools/act/settle-in-flight.js';
|
|
15
13
|
import { namedNetIsInFlight } from '@reticlehq/engine/evidence/unsettled.js';
|
|
16
14
|
/** What a still-open named request means, in the drift's own words. */
|
|
17
15
|
const IN_FLIGHT_AT_BUDGET_END = 'the request this step declared had not come back when the budget ended — it is still in flight, ' +
|
|
18
16
|
'so nothing here says the app failed. Raise the step timeout, or look at the endpoint';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
17
|
+
import { isDocumentGoneError, SESSION_DISCONNECTED_REASON, } from '../../portal/session/facts/session-replaced.js';
|
|
18
|
+
import { actOnResolvedRef } from './flow-step-runners.js';
|
|
21
19
|
/**
|
|
22
20
|
* The document this replay was driving went away mid-run.
|
|
23
21
|
*
|
|
@@ -105,7 +103,7 @@ function summarizeConsequence(events) {
|
|
|
105
103
|
return parts.length > 0 ? parts.join('; ') : undefined;
|
|
106
104
|
}
|
|
107
105
|
/** Run one testid-anchored step: re-resolve via QUERY, then ACT on the live ref, else drift. */
|
|
108
|
-
async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
|
|
106
|
+
export async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
|
|
109
107
|
const { refs, hint } = await resolveTestid(session, value, sleep);
|
|
110
108
|
if (0 === refs.length) {
|
|
111
109
|
return {
|
|
@@ -116,35 +114,50 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
|
|
|
116
114
|
drift: testidDrift(value, hint),
|
|
117
115
|
};
|
|
118
116
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
117
|
+
/*
|
|
118
|
+
* More than one match is DRIFT, not a note on a passing step.
|
|
119
|
+
*
|
|
120
|
+
* This used to take refs[0], act, and return ok:true with `ambiguousTestidNote`. The verdict
|
|
121
|
+
* reads `drift` and `ok` and never reads a note, so "we guessed which element you meant" was
|
|
122
|
+
* indistinguishable from "it did what it did before" -- the one claim a replay makes.
|
|
123
|
+
*
|
|
124
|
+
* The action is NOT dispatched. Acting and then reporting drift would leave the app changed by
|
|
125
|
+
* a click nobody can attribute, which is worse than the ambiguity it reports.
|
|
126
|
+
*/
|
|
127
|
+
if (refs.length > 1) {
|
|
128
|
+
return {
|
|
129
|
+
step: index,
|
|
130
|
+
tool: step.tool,
|
|
131
|
+
anchor: value,
|
|
132
|
+
ok: false,
|
|
133
|
+
drift: ambiguousAnchorDrift(value, refs.length),
|
|
134
|
+
};
|
|
136
135
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
const ref = refs[0] ?? '';
|
|
137
|
+
/*
|
|
138
|
+
* The same dispatch its siblings use, including the one re-resolve on a stale ref (2.3).
|
|
139
|
+
*
|
|
140
|
+
* This runner had its own inline copy and no retry, so a flow died on a re-render purely because
|
|
141
|
+
* the step was anchored by testid rather than by role — the locator deciding how sturdy the replay
|
|
142
|
+
* is, which is exactly backwards. testid is also the anchor `reticle init` steers people towards,
|
|
143
|
+
* so the kind most likely to appear in a real flow was the kind without the cure.
|
|
144
|
+
*
|
|
145
|
+
* The re-resolve keeps the ambiguity rule above: more than one match is drift, never a guess, so
|
|
146
|
+
* it hands back a ref only when the locator still names exactly one element.
|
|
147
|
+
*/
|
|
148
|
+
const result = await actOnResolvedRef(session, step, index, value, ref, confirmDangerous,
|
|
149
|
+
// The field this step types into — from the anchor, so a redacted fill can be supplied from
|
|
150
|
+
// RETICLE_SECRET_<FIELD> without the flow carrying the secret.
|
|
151
|
+
anchorFieldName(step.anchor), async () => {
|
|
152
|
+
const again = await resolveTestid(session, value, sleep);
|
|
153
|
+
return 1 === again.refs.length ? again.refs[0] : undefined;
|
|
154
|
+
});
|
|
155
|
+
if (!result.ok)
|
|
142
156
|
return result;
|
|
143
|
-
}
|
|
144
157
|
// assert the step's expect.element testid is present AFTER the action —
|
|
145
158
|
// unless that testid was marked DYNAMIC (the LLM-output case), in which case its presence/content
|
|
146
159
|
// is NOT asserted (only the action ran). The skip is scoped strictly to the dynamic set.
|
|
147
|
-
const expectTestid = step.expect
|
|
160
|
+
const expectTestid = expectedElementTestid(step.expect);
|
|
148
161
|
if (expectTestid !== undefined && !dynamic.has(expectTestid)) {
|
|
149
162
|
const expectRefs = await resolveTestid(session, expectTestid, sleep);
|
|
150
163
|
if (0 === expectRefs.refs.length) {
|
|
@@ -160,8 +173,6 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
|
|
|
160
173
|
};
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
|
-
if (note !== undefined)
|
|
164
|
-
result.note = note;
|
|
165
176
|
return result;
|
|
166
177
|
}
|
|
167
178
|
/**
|
|
@@ -185,30 +196,52 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
|
|
|
185
196
|
* quietly stop agreeing, and the disagreement would land as a flow that replays green against a
|
|
186
197
|
* state it was never meant to run in.
|
|
187
198
|
*/
|
|
188
|
-
export async function assertStepExpect(session, expect,
|
|
199
|
+
export async function assertStepExpect(session, expect, waitForSignal, timeoutMs, since) {
|
|
189
200
|
// A testid is already asserted against the live DOM by the step runner. A role/name locator is
|
|
190
201
|
// not that path — stripping every element made a recorded `until` by button name a no-op, so a
|
|
191
202
|
// flow that proved the control at capture time could not go red when it was gone.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
/*
|
|
204
|
+
* Two clauses replay must not wait on, both for reasons that predate the tree.
|
|
205
|
+
*
|
|
206
|
+
* A testid element was already asserted against the live DOM by the step runner, so waiting on it
|
|
207
|
+
* again is a second read of a question already answered. A DYNAMIC testid is one whose content a
|
|
208
|
+
* model writes, and its presence is deliberately never asserted.
|
|
209
|
+
*
|
|
210
|
+
* A role/name locator is NEITHER, and stripping every element made a recorded `until` by button
|
|
211
|
+
* name a no-op — a flow that proved the control at capture time could not go red when it was gone.
|
|
212
|
+
*
|
|
213
|
+
* The DYNAMIC set used to be a parameter here and was dead the whole time: the testid was deleted
|
|
214
|
+
* from the expectation on the line above, and the only thing that read the set asked for the
|
|
215
|
+
* testid that had just been removed. One production caller was already passing an empty set. The
|
|
216
|
+
* rule is real and still applies to the flow's SUCCESS oracle, which is where it lives.
|
|
217
|
+
*/
|
|
218
|
+
const predicate = withoutClauses(expect,
|
|
219
|
+
// Only a PRESENT testid is the runner's to check; an absence stays here or nobody checks it.
|
|
220
|
+
(clause) => PredicateKind.ELEMENT === clause.kind &&
|
|
221
|
+
clause.query.testid !== undefined &&
|
|
222
|
+
true !== clause.absent);
|
|
197
223
|
if (predicate === undefined)
|
|
198
224
|
return undefined;
|
|
199
225
|
const verdict = await waitForSignal(session, predicate, timeoutMs, since);
|
|
200
226
|
if (verdict.pass)
|
|
201
227
|
return undefined;
|
|
228
|
+
// The wait ended because the page stopped being observable, not because the consequence failed:
|
|
229
|
+
// the same fact as a command the dying socket rejected, so it takes the same path (the replay loop
|
|
230
|
+
// reports a lost document). As a drift, a socket that blinked for 26ms read as the app's defect.
|
|
231
|
+
if (true === verdict.observationLost)
|
|
232
|
+
throw new Error(SESSION_DISCONNECTED_REASON);
|
|
202
233
|
// A request the step NAMED that had not come back yet is a blind spot, not an assertion the app
|
|
203
234
|
// failed. The live verdict path already draws this line (namedNetIsInFlight); replay read the
|
|
204
235
|
// same window and never asked, so a slow endpoint was reported as a consequence that never fired.
|
|
205
236
|
// An unrelated open request does not pardon a named URL that never started — matching decides it.
|
|
206
|
-
const openRequests = expect
|
|
237
|
+
const openRequests = assertsState(expect)
|
|
238
|
+
? []
|
|
239
|
+
: inFlightRequestLabels(session.eventsSince(since), session.url, session.background);
|
|
207
240
|
const namedInFlight = namedNetIsInFlight(predicate, openRequests);
|
|
208
241
|
return {
|
|
209
242
|
// The store case keeps its own kind because heal and the run report branch on it; everything
|
|
210
243
|
// else is a consequence that did not hold, and the reason carries observed-vs-expected.
|
|
211
|
-
reasonKind: expect
|
|
244
|
+
reasonKind: assertsState(expect)
|
|
212
245
|
? DriftReason.STATE_MISMATCH
|
|
213
246
|
: namedInFlight
|
|
214
247
|
? DriftReason.NET_STILL_IN_FLIGHT
|
|
@@ -220,21 +253,6 @@ export async function assertStepExpect(session, expect, dynamic, waitForSignal,
|
|
|
220
253
|
nearest: null,
|
|
221
254
|
};
|
|
222
255
|
}
|
|
223
|
-
/** Name the thing that was asserted, for the drift's `anchor` column. */
|
|
224
|
-
function expectLabel(expect) {
|
|
225
|
-
if (expect.signal !== undefined)
|
|
226
|
-
return `signal:${expect.signal}`;
|
|
227
|
-
if (expect.net !== undefined)
|
|
228
|
-
return `net:${expect.net.urlContains ?? expect.net.method ?? '*'}`;
|
|
229
|
-
if (expect.state !== undefined)
|
|
230
|
-
return `state:${expect.state.path}`;
|
|
231
|
-
if (expect.console !== undefined)
|
|
232
|
-
return `console:${expect.console.level ?? '*'}`;
|
|
233
|
-
if (undefined !== expect.element) {
|
|
234
|
-
return expect.element.testid ?? expect.element.name ?? expect.element.role ?? 'element';
|
|
235
|
-
}
|
|
236
|
-
return 'expect';
|
|
237
|
-
}
|
|
238
256
|
/** Run one signal-anchored step: wait for the signal predicate, else drift (no nearest for signals). */
|
|
239
257
|
async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs, since) {
|
|
240
258
|
// Scope to THIS replay's floor, not the whole buffer.
|
|
@@ -262,40 +280,6 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
|
|
|
262
280
|
},
|
|
263
281
|
};
|
|
264
282
|
}
|
|
265
|
-
/**
|
|
266
|
-
* Where to start REPORTING — and whether re-driving the steps before it is allowed at all.
|
|
267
|
-
*
|
|
268
|
-
* "Resume is nearly free, just re-run the prefix" is true of a browser and false on a subject that
|
|
269
|
-
* COMMITS. Re-driving a prefix on a service re-sends every request before step N; on a device it
|
|
270
|
-
* moves an arm again. Neither is a convenience, and a protocol that did it silently would be
|
|
271
|
-
* defective rather than helpful.
|
|
272
|
-
*
|
|
273
|
-
* So the surface's declared profile decides, through the specification's own `resumeStrategy` rather
|
|
274
|
-
* than a local reading of `replayPrefix`. A refusal resumes from 0 — the whole journey is reported,
|
|
275
|
-
* nothing is skipped and nothing is silently repeated beyond what a plain replay already does.
|
|
276
|
-
*/
|
|
277
|
-
function resumableFrom(options, steps) {
|
|
278
|
-
const asked = Math.max(0, options.from ?? 0);
|
|
279
|
-
if (0 === asked)
|
|
280
|
-
return 0;
|
|
281
|
-
if (!mayResumeByReplayingPrefix(options.surface ?? Surface.WEB))
|
|
282
|
-
return 0;
|
|
283
|
-
/*
|
|
284
|
-
* The surface answers for the SUBJECT; a step answers for itself.
|
|
285
|
-
*
|
|
286
|
-
* `web` is the permissive profile and it is right about a browser in general and wrong about the
|
|
287
|
-
* one click that charges a card. The surface check cannot see that, because the distinction is
|
|
288
|
-
* not a property of the realm — it is a property of the step. A flow that declares a prefix step
|
|
289
|
-
* as `commits` is saying: re-running me is not free, whatever the surface thinks.
|
|
290
|
-
*
|
|
291
|
-
* Refusing means resuming from 0, which is the same fail-safe the surface refusal already uses:
|
|
292
|
-
* the whole journey is reported and nothing is silently repeated beyond what a plain replay does.
|
|
293
|
-
* Absent effect is UNKNOWN and stays permissive — every flow recorded before this shipped has no
|
|
294
|
-
* effect on any step, and assuming the worst there would refuse every resume in existence.
|
|
295
|
-
*/
|
|
296
|
-
const commitsInPrefix = steps.slice(0, asked).some((step) => StepEffect.COMMITS === step.effect);
|
|
297
|
-
return commitsInPrefix ? 0 : asked;
|
|
298
|
-
}
|
|
299
283
|
/**
|
|
300
284
|
* Run an `invoke` step: load the named flow and replay it, or fail saying why.
|
|
301
285
|
*
|
|
@@ -329,10 +313,7 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
|
|
|
329
313
|
error: `cannot replay "${name}": it was not found, so this journey would report green having never run it`,
|
|
330
314
|
};
|
|
331
315
|
}
|
|
332
|
-
|
|
333
|
-
// caller's own step list, and applying it inside a sub-journey would silently hide that
|
|
334
|
-
// journey's first steps for a reason that has nothing to do with it.
|
|
335
|
-
const { from: _ignored, ...carried } = options;
|
|
316
|
+
const carried = options;
|
|
336
317
|
const nested = await replayFlow(session, sub, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, {
|
|
337
318
|
...carried,
|
|
338
319
|
via: [{ flow: flow.name, step: index }, ...via],
|
|
@@ -350,16 +331,6 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
|
|
|
350
331
|
}
|
|
351
332
|
export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false, sleep = realSleep, options = {}) {
|
|
352
333
|
const results = [];
|
|
353
|
-
/*
|
|
354
|
-
* Where to start REPORTING. Everything before it still runs.
|
|
355
|
-
*
|
|
356
|
-
* Resuming is re-driving the prefix, not restoring state: there is no way to put an app back
|
|
357
|
-
* where it was without driving it there, and at a measured ~27ms a step there is no reason to
|
|
358
|
-
* try. So the prefix executes silently and the caller sees the journey from the point it asked
|
|
359
|
-
* about -- which is what makes "fix the break, resume, find the next one" a loop rather than a
|
|
360
|
-
* full re-read each time.
|
|
361
|
-
*/
|
|
362
|
-
const from = resumableFrom(options, flow.steps);
|
|
363
334
|
// testids whose region is LLM-dynamic — their expect-presence is NOT asserted.
|
|
364
335
|
const dynamic = new Set((flow.dynamic ?? [])
|
|
365
336
|
.filter((a) => a.kind === AnchorKind.TESTID)
|
|
@@ -436,7 +407,7 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
436
407
|
for (const expectation of declared) {
|
|
437
408
|
if (!result.ok || result.drift !== undefined)
|
|
438
409
|
break;
|
|
439
|
-
const expectDrift = await assertStepExpect(session, expectation,
|
|
410
|
+
const expectDrift = await assertStepExpect(session, expectation, waitForSignal, waitFor(step), cursorBefore);
|
|
440
411
|
if (expectDrift !== undefined) {
|
|
441
412
|
result.ok = false;
|
|
442
413
|
result.drift = expectDrift;
|
|
@@ -466,8 +437,7 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
466
437
|
// forget the rule and quietly re-introduce the cost. Spelled out only when it is NOT the default.
|
|
467
438
|
if (FlowStepTool.ACT === result.tool)
|
|
468
439
|
delete result.tool;
|
|
469
|
-
|
|
470
|
-
results.push(result);
|
|
440
|
+
results.push(result);
|
|
471
441
|
// Under `sweep`, a failure whose action still RAN does not stop the run — the page is where the
|
|
472
442
|
// step left it, so the next step is as meaningful as it was going to be. Anything else halts.
|
|
473
443
|
const sweepPast = true === options.sweep &&
|
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
* How one anchored step is turned into a live action.
|
|
3
|
-
*
|
|
4
|
-
* Split out of flow-replay.ts when adding the role+name runner pushed that file past the size cap.
|
|
5
|
-
* These three share one shape — resolve an anchor to a ref, then dispatch — and the sharing is the
|
|
6
|
-
* point: the action window must open and close identically no matter which anchor found the element,
|
|
7
|
-
* or a step's events land in the wrong window depending on how it was addressed.
|
|
8
|
-
*/
|
|
9
|
-
import { AnchorKind, type FlowAnchor, type FlowStep, type FlowStepResult } from '@reticlehq/core';
|
|
1
|
+
import { AnchorKind, type FlowAnchor, type FlowStep, type FlowStepResult, type Predicate } from '@reticlehq/core';
|
|
10
2
|
import type { FlowReplaySession, Sleep } from './flow-replay-types.js';
|
|
11
3
|
export declare function runRoleStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
|
|
12
4
|
kind: typeof AnchorKind.ROLE;
|
|
13
5
|
}>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
|
|
14
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Dispatch one already-resolved step. Shared so every anchor kind runs the action the same way —
|
|
8
|
+
* including the action window, whose open/close must not depend on which anchor found the element.
|
|
9
|
+
*/
|
|
10
|
+
export declare function actOnResolvedRef(session: FlowReplaySession, step: FlowStep, index: number, label: string, ref: string, confirmDangerous: boolean,
|
|
11
|
+
/**
|
|
12
|
+
* The field name a redacted fill is supplied under, from `anchorFieldName` — the SAME function
|
|
13
|
+
* redaction uses to decide what to hide. Without it a role-anchored secret is redacted at save
|
|
14
|
+
* and looked up at replay under no name at all, so the flow types the placeholder into the form.
|
|
15
|
+
*/
|
|
16
|
+
field?: string,
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the anchor again. Supplied by the caller because only it knows how this step's anchor is
|
|
19
|
+
* found, and the retry below is worthless without it: re-dispatching the SAME dead ref fails
|
|
20
|
+
* identically. What changed between the two attempts is the DOM, so the locator has to be run
|
|
21
|
+
* against it again.
|
|
22
|
+
*/
|
|
23
|
+
reresolve?: () => Promise<string | undefined>): Promise<FlowStepResult>;
|
|
15
24
|
/** Run one component-anchored step: re-resolve via QUERY by:'component', ACT on the live ref, else drift. */
|
|
16
25
|
export declare function runComponentStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
|
|
17
26
|
kind: typeof AnchorKind.COMPONENT;
|
|
@@ -39,6 +48,13 @@ export declare function degradedStepResult(step: FlowStep, index: number, label:
|
|
|
39
48
|
* the step runner's own resolution asking the same thing of the same anchor.
|
|
40
49
|
*/
|
|
41
50
|
export declare function anchorQueryArgs(anchor: FlowAnchor): Record<string, unknown> | null;
|
|
51
|
+
/**
|
|
52
|
+
* The anchor as a PRECONDITION a flow file can store: "this element is on the page".
|
|
53
|
+
*
|
|
54
|
+
* What `requires` means for a flow that starts from state a page load discards, in the shape the
|
|
55
|
+
* flow's own `requires` holds. No tool writes `requires`, so the reset's hint hands over this line.
|
|
56
|
+
*/
|
|
57
|
+
export declare function anchorPrecondition(anchor: FlowAnchor): Predicate | undefined;
|
|
42
58
|
/**
|
|
43
59
|
* Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
|
|
44
60
|
* ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* point: the action window must open and close identically no matter which anchor found the element,
|
|
7
7
|
* or a step's events land in the wrong window depending on how it was addressed.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { expectedElementTestid } from '@reticlehq/core';
|
|
10
|
+
import { AnchorKind, DEGRADED_ANCHOR_ROLE, DriftReason, PredicateKind, QueryBy, ReticleCommand, } from '@reticlehq/core';
|
|
10
11
|
import { ReticleTool } from '@reticlehq/core';
|
|
11
12
|
import { replayActionArgs } from './replay.js';
|
|
12
13
|
import { isStaleRefError } from '../../surface/tools/act/act-sequence-retry.js';
|
|
@@ -102,7 +103,7 @@ export async function runRoleStep(session, step, index, anchor, confirmDangerous
|
|
|
102
103
|
* to recognise the raw refusal still finds it, plus the fix for this specific caller.
|
|
103
104
|
*/
|
|
104
105
|
const DESTRUCTIVE_ACTION_PATTERN = /potentially destructive (?:\w+ )*(?:action|tool) blocked/i;
|
|
105
|
-
|
|
106
|
+
function replayDestructiveActionHint(rawError) {
|
|
106
107
|
if (!DESTRUCTIVE_ACTION_PATTERN.test(rawError))
|
|
107
108
|
return rawError;
|
|
108
109
|
return (`${rawError} — this is a flow REPLAY: a step's own args.confirmDangerous is stripped before ` +
|
|
@@ -118,7 +119,7 @@ export function replayDestructiveActionHint(rawError) {
|
|
|
118
119
|
* Dispatch one already-resolved step. Shared so every anchor kind runs the action the same way —
|
|
119
120
|
* including the action window, whose open/close must not depend on which anchor found the element.
|
|
120
121
|
*/
|
|
121
|
-
async function actOnResolvedRef(session, step, index, label, ref, confirmDangerous,
|
|
122
|
+
export async function actOnResolvedRef(session, step, index, label, ref, confirmDangerous,
|
|
122
123
|
/**
|
|
123
124
|
* The field name a redacted fill is supplied under, from `anchorFieldName` — the SAME function
|
|
124
125
|
* redaction uses to decide what to hide. Without it a role-anchored secret is redacted at save
|
|
@@ -252,6 +253,21 @@ export function anchorQueryArgs(anchor) {
|
|
|
252
253
|
return roleQueryArgs(anchor);
|
|
253
254
|
return null;
|
|
254
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* The anchor as a PRECONDITION a flow file can store: "this element is on the page".
|
|
258
|
+
*
|
|
259
|
+
* What `requires` means for a flow that starts from state a page load discards, in the shape the
|
|
260
|
+
* flow's own `requires` holds. No tool writes `requires`, so the reset's hint hands over this line.
|
|
261
|
+
*/
|
|
262
|
+
export function anchorPrecondition(anchor) {
|
|
263
|
+
if (anchor.kind === AnchorKind.TESTID) {
|
|
264
|
+
return { kind: PredicateKind.ELEMENT, query: { testid: anchor.value } };
|
|
265
|
+
}
|
|
266
|
+
if (anchor.kind === AnchorKind.ROLE && !isDegradedAnchor(anchor)) {
|
|
267
|
+
return { kind: PredicateKind.ELEMENT, query: { role: anchor.role, name: anchor.name } };
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
255
271
|
/**
|
|
256
272
|
* Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
|
|
257
273
|
* ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
|
|
@@ -321,7 +337,7 @@ dynamic = new Set()) {
|
|
|
321
337
|
};
|
|
322
338
|
/** The sub-step's own expectation, unless its testid is deliberately unasserted. */
|
|
323
339
|
const declaredBy = (sub) => {
|
|
324
|
-
const testid = sub.expect
|
|
340
|
+
const testid = expectedElementTestid(sub.expect);
|
|
325
341
|
return testid === undefined || dynamic.has(testid) ? undefined : testid;
|
|
326
342
|
};
|
|
327
343
|
/**
|
|
@@ -12,21 +12,36 @@
|
|
|
12
12
|
* tool and the `@reticlehq/test` spec runner share one implementation — no divergent oracle.
|
|
13
13
|
* Pure: no IO, no clock.
|
|
14
14
|
*/
|
|
15
|
-
import { type FlowExpect, type FlowFile } from '@reticlehq/core';
|
|
16
|
-
import type { EvalResult
|
|
15
|
+
import { type FlowExpect, type FlowFile, type Predicate } from '@reticlehq/core';
|
|
16
|
+
import type { EvalResult } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
17
17
|
import type { FlowReplaySession, WaitForSignal } from './flow-replay-types.js';
|
|
18
18
|
/** The dynamic (LLM-output) testids whose presence is never asserted — same rule replay uses. */
|
|
19
19
|
export declare function dynamicTestids(flow: FlowFile): Set<string>;
|
|
20
20
|
/** The synthetic step.tool a replay appends when it asserts the flow's success oracle. */
|
|
21
21
|
export declare const SUCCESS_STEP_TOOL = "success";
|
|
22
22
|
/** A short human label for the success end-condition, for result rows. */
|
|
23
|
-
export declare function successLabel(success:
|
|
24
|
-
/**
|
|
25
|
-
|
|
23
|
+
export declare function successLabel(success: Predicate | undefined): string;
|
|
24
|
+
/**
|
|
25
|
+
* What a flow's success oracle actually asserts, once the file has been read and the dynamic
|
|
26
|
+
* (LLM-output) testids have been taken out of it.
|
|
27
|
+
*
|
|
28
|
+
* Most of what this function used to do is gone, and that is the migration working as intended: it
|
|
29
|
+
* compiled a flat `FlowExpect` into a `Predicate`, and a step's `expect` IS a predicate now, lifted
|
|
30
|
+
* by the schema when the file is read. It keeps the name and the signature because it is on
|
|
31
|
+
* `@reticlehq/test`'s published surface, and it still accepts the flat shape for the same reason a
|
|
32
|
+
* v1 file still loads — a spec written against the old spelling is not wrong, it is old.
|
|
33
|
+
*
|
|
34
|
+
* What remains is the DYNAMIC SKIP, and it is not cosmetic. A testid whose content a model writes is
|
|
35
|
+
* asserted for PRESENCE by the step layer and never for content; a success oracle bound to one would
|
|
36
|
+
* assert exactly what the flow has already declared unassertable. Dropping every such clause can
|
|
37
|
+
* leave nothing at all, and `undefined` is the honest answer — vacuously met, the same as declaring
|
|
38
|
+
* no success, because nothing here was checkable.
|
|
39
|
+
*/
|
|
40
|
+
export declare function successToPredicate(success: Predicate | FlowExpect | undefined, dynamic: ReadonlySet<string>): Predicate | undefined;
|
|
26
41
|
/**
|
|
27
42
|
* Assert a flow's success end-condition after replay. Delegates to the injected waitForSignal (the
|
|
28
43
|
* real waitForPredicate in production, a fake in unit tests). Passes when: no success was declared,
|
|
29
44
|
* OR every success field was dynamic-skipped (vacuously met), OR the compiled predicate held within
|
|
30
45
|
* the injected timeout. Never reads the wall clock.
|
|
31
46
|
*/
|
|
32
|
-
export declare function assertSuccess(session: FlowReplaySession, success:
|
|
47
|
+
export declare function assertSuccess(session: FlowReplaySession, success: Predicate | undefined, dynamic: ReadonlySet<string>, waitForSignal: WaitForSignal, timeoutMs: number, since?: number): Promise<EvalResult>;
|