@reticlehq/server 3.9.0 → 3.10.1
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 +13 -12
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -187
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/cli-doctor.js +1 -1
- package/dist/command/cli/cli-flow-commands.js +2 -9
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +17 -15
- package/dist/command/cli/cli-parse.js +44 -27
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +58 -29
- package/dist/command/cli/cli-verify.d.ts +11 -2
- package/dist/command/cli/cli-verify.js +27 -8
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor-repairs.js +1 -1
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
- package/dist/command/cli/ports/port-holder.d.ts +9 -0
- package/dist/command/cli/ports/port-holder.js +27 -3
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +22 -0
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +7 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +46 -41
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +12 -1
- package/dist/command/daemon/daemon.js +35 -4
- package/dist/command/daemon/lifetime/death-cause.js +21 -2
- package/dist/command/daemon/lifetime/heartbeat.js +7 -2
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-runtime.js +43 -2
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +6 -3
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/version-skew.js +1 -1
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/visual/visual-tools.js +66 -19
- package/dist/index.js +7 -4
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +44 -0
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
- package/dist/language/flows/recording/tape/recordings.js +71 -5
- package/dist/language/flows/replay.d.ts +19 -5
- package/dist/language/flows/replay.js +99 -7
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +28 -9
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +22 -20
- package/dist/portal/input/real-input.js +41 -31
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +75 -18
- package/dist/portal/pool/browser-pool.d.ts +11 -29
- package/dist/portal/pool/browser-pool.js +35 -17
- package/dist/portal/pool/context-permissions.d.ts +17 -3
- package/dist/portal/pool/context-permissions.js +36 -5
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +44 -17
- package/dist/portal/pool/lease-page-error.js +147 -5
- package/dist/portal/pool/page-error-journal.d.ts +7 -1
- package/dist/portal/pool/page-error-journal.js +27 -0
- package/dist/portal/pool/playwright-launcher.js +51 -8
- package/dist/portal/pool/pool-contract.d.ts +27 -1
- package/dist/portal/pool/pool-lease.d.ts +36 -1
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +23 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
- package/dist/portal/session/dev-server/dev-command.js +61 -15
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -25
- package/dist/portal/session/no-session-next-action.d.ts +2 -0
- package/dist/portal/session/no-session-next-action.js +12 -3
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +38 -3
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +174 -63
- package/dist/start-wiring.d.ts +2 -1
- package/dist/start-wiring.js +2 -1
- package/dist/surface/mcp/mcp.js +3 -2
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +1 -1
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/after-match.d.ts +4 -1
- package/dist/surface/tools/act/after-match.js +8 -1
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +15 -1
- package/dist/surface/tools/act/resolve-target.js +24 -1
- package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
- package/dist/surface/tools/act/sequence-grade.js +37 -26
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +78 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +74 -86
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +16 -37
- package/dist/surface/tools/browser-tools.js +12 -6
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +9 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +91 -21
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +3 -2
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +35 -1
- package/dist/surface/tools/lease-hint.js +42 -0
- package/dist/surface/tools/lease-readiness.d.ts +26 -0
- package/dist/surface/tools/lease-readiness.js +79 -0
- package/dist/surface/tools/lease-seed.d.ts +14 -1
- package/dist/surface/tools/lease-seed.js +35 -3
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +62 -110
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +6 -0
- package/dist/surface/tools/tool-kit.js +2 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +28 -5
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +1 -0
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +8 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +1 -1
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/init.mdx +2 -2
- package/docs/cli/license.mdx +9 -7
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +33 -23
- package/docs/deploy-checks.md +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +2 -2
- package/docs/environment.mdx +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +2 -2
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +30 -1
- package/docs/usage.md +17 -14
- package/package.json +11 -14
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
* used from the two points where observation can be lost: a write rejected at dispatch, and a wait
|
|
11
11
|
* cut off mid-predicate.
|
|
12
12
|
*/
|
|
13
|
-
import { EventType, asRecord, asString, isDevToolingUrl } from '@reticlehq/core';
|
|
13
|
+
import { DriftReason, EventType, NetInitiator, asRecord, asString, isDevToolingUrl, } from '@reticlehq/core';
|
|
14
|
+
import { sdkBehindNote } from '../../../portal/session/session-info.js';
|
|
14
15
|
import { awaitDocumentSuccessor, } from '../../../portal/session/session-successor.js';
|
|
16
|
+
/** How long an act whose page navigated away waits for the lease's successor to connect. */
|
|
17
|
+
export const NAVIGATION_FOLLOW_GRACE_MS = 8_000;
|
|
18
|
+
const REAL_CLOCK = {
|
|
19
|
+
now: () => Date.now(),
|
|
20
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
21
|
+
};
|
|
15
22
|
/**
|
|
16
23
|
* Follow the unique same-origin successor and re-ask there, or leave the verdict untouched.
|
|
17
24
|
*
|
|
@@ -22,18 +29,65 @@ import { awaitDocumentSuccessor, } from '../../../portal/session/session-success
|
|
|
22
29
|
*/
|
|
23
30
|
export async function followLostObservation(input) {
|
|
24
31
|
const { sessions, session, verdict, timeout, predicateStarted, reevaluate } = input;
|
|
25
|
-
|
|
32
|
+
const clock = input.clock ?? REAL_CLOCK;
|
|
33
|
+
const spent = session.elapsed() - predicateStarted;
|
|
34
|
+
const remaining = timeout - spent;
|
|
35
|
+
// Phase 1 — the wait was CUT OFF mid-predicate. The page re-announces on the new document;
|
|
36
|
+
// re-ask there with whatever budget is left.
|
|
37
|
+
if (true === verdict.observationLost && remaining > 0) {
|
|
38
|
+
const next = await awaitDocumentSuccessor(sessions, session, remaining);
|
|
39
|
+
if (null !== next) {
|
|
40
|
+
// Computed on the DEPARTED session, before the caller points at the successor: the new
|
|
41
|
+
// session's elapsed() starts at 0 and would silently refund the whole budget the caller had
|
|
42
|
+
// already spent.
|
|
43
|
+
const leftover = timeout - (session.elapsed() - predicateStarted);
|
|
44
|
+
if (leftover <= 0)
|
|
45
|
+
return { session: next, verdict, followed: true };
|
|
46
|
+
return { session: next, verdict: await reevaluate(next, leftover), followed: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Phase 2 — gated on NAVIGATION EVIDENCE ALREADY IN THE WINDOW (#2009): a FAILURE graded while
|
|
50
|
+
// the action's own full-page navigation was still in flight reads "still on /login" for a
|
|
51
|
+
// sign-in that worked. The departure observer records the leaving as a NET_PENDING at dispatch
|
|
52
|
+
// time — while the old document's socket is still alive to deliver it — so the evidence is
|
|
53
|
+
// already in the buffer when the verdict is graded. Its presence unlocks the successor wait
|
|
54
|
+
// (bounded by the grace); its absence means the failure is about a document that never moved,
|
|
55
|
+
// and it stands UNCHANGED and AT ONCE — no wait, and the grace is never spent on it.
|
|
56
|
+
const grace = input.failedFollowGraceMs;
|
|
57
|
+
const registryCapable = 'function' === typeof sessions.get && 'function' === typeof sessions.all;
|
|
58
|
+
if (true === verdict.observationLost ||
|
|
59
|
+
true === verdict.pass ||
|
|
60
|
+
grace === undefined ||
|
|
61
|
+
input.since === undefined ||
|
|
62
|
+
!registryCapable) {
|
|
26
63
|
return { session, verdict, followed: false };
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
64
|
+
}
|
|
65
|
+
const departurePending = session
|
|
66
|
+
.eventsSince(input.since)
|
|
67
|
+
.some((e) => e.type === EventType.NET_PENDING && e.data['initiator'] === NetInitiator.NAVIGATION);
|
|
68
|
+
if (!departurePending) {
|
|
30
69
|
return { session, verdict, followed: false };
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
70
|
+
}
|
|
71
|
+
const next = await awaitDocumentSuccessor(sessions, session, grace, clock);
|
|
72
|
+
if (null !== next) {
|
|
73
|
+
return {
|
|
74
|
+
session: next,
|
|
75
|
+
verdict: await reevaluate(next, grace),
|
|
76
|
+
followed: true,
|
|
77
|
+
budgetExtendedMs: grace,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
session,
|
|
82
|
+
followed: false,
|
|
83
|
+
verdict: {
|
|
84
|
+
...verdict,
|
|
85
|
+
observationLost: true,
|
|
86
|
+
failureReason: `the act's page left its document and no successor session connected within ` +
|
|
87
|
+
`${String(grace)} ms. If this action navigated, the outcome after the navigation was ` +
|
|
88
|
+
'never observed — re-run reticle_session { action: "list" } and re-drive on the live session.',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
37
91
|
}
|
|
38
92
|
/** Why a consequence that did not follow a click on a disabled control is not the app's failure. */
|
|
39
93
|
export const DISABLED_TARGET_NOTE = 'the control was disabled when it was acted on, so nothing a user could do there would have ' +
|
|
@@ -52,7 +106,9 @@ export function disabledTargetNote(pass, actResult) {
|
|
|
52
106
|
const UNREACHABLE = {
|
|
53
107
|
covered: (by) => `a person could not have clicked the target: it is covered by ${by}. Dismiss or move what ` +
|
|
54
108
|
'covers it, then act again',
|
|
55
|
-
|
|
109
|
+
HIDDEN: 'a person could not have clicked the target: it is not visible (for example, it is inside a ' +
|
|
110
|
+
'closed <details> or has visibility: hidden). Show it the way a person would, then act again',
|
|
111
|
+
NO_SIZE: 'a person could not have clicked the target: it has no size (zero width or zero height). Give it a size, or act on ' +
|
|
56
112
|
'the control a person actually clicks',
|
|
57
113
|
OFF_VIEWPORT: 'a person could not have clicked the target: it is outside the viewport even after scrolling ' +
|
|
58
114
|
'it into view. Bring it on screen, then act again',
|
|
@@ -68,6 +124,11 @@ const UNREACHABLE = {
|
|
|
68
124
|
export function unreachableTargetNote(actResult) {
|
|
69
125
|
const effect = asRecord(asRecord(actResult)['effect']);
|
|
70
126
|
if (true === effect['occluded']) {
|
|
127
|
+
// A hidden target keeps its box, so the hit test finds whatever is under it. That element
|
|
128
|
+
// covers nothing: the target is not shown.
|
|
129
|
+
if (false === effect['visible']) {
|
|
130
|
+
return UNREACHABLE.HIDDEN;
|
|
131
|
+
}
|
|
71
132
|
const by = asString(effect['occluderName']) ?? asString(effect['occludedBy']);
|
|
72
133
|
return UNREACHABLE.covered(by ?? 'another element');
|
|
73
134
|
}
|
|
@@ -77,10 +138,55 @@ export function unreachableTargetNote(actResult) {
|
|
|
77
138
|
return UNREACHABLE.OFF_VIEWPORT;
|
|
78
139
|
return undefined;
|
|
79
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The drift for the first sub-step of an `ACT_SEQUENCE` result whose target no person could have
|
|
143
|
+
* clicked, or undefined. The page reports one effect for each sub-step it ran, in order. Synthetic
|
|
144
|
+
* dispatch reaches an unreachable target all the same, so a sequence is read sub-step by sub-step, as
|
|
145
|
+
* a single act is. `labelOf` names the sub-step, by its index in this result.
|
|
146
|
+
*/
|
|
147
|
+
export function unreachableSubStepDrift(sequenceResult, labelOf) {
|
|
148
|
+
const effects = asRecord(sequenceResult)['effects'];
|
|
149
|
+
const list = Array.isArray(effects) ? effects : [];
|
|
150
|
+
for (let at = 0; at < list.length; at += 1) {
|
|
151
|
+
const reason = unreachableTargetNote({ effect: list[at] });
|
|
152
|
+
if (reason !== undefined) {
|
|
153
|
+
return {
|
|
154
|
+
reasonKind: DriftReason.TARGET_UNREACHABLE,
|
|
155
|
+
reason,
|
|
156
|
+
anchor: labelOf(at),
|
|
157
|
+
nearest: null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
80
163
|
/** Why the acted target leaves the verdict undecided: no person could reach it, or it was disabled. */
|
|
81
164
|
export function actTargetNote(pass, actResult) {
|
|
82
165
|
return unreachableTargetNote(actResult) ?? disabledTargetNote(pass, actResult);
|
|
83
166
|
}
|
|
167
|
+
// Unanchored: the engine prefixes the reason with "the page did not answer:".
|
|
168
|
+
const COMMAND_TIMEOUT = /command '[^']*' timed out after (\d+)ms/;
|
|
169
|
+
/**
|
|
170
|
+
* The cause and the one fix for a read that came back `unknown`, in place of a bare fact (#1990).
|
|
171
|
+
* A page whose SDK is behind this daemon may not serve `match` at all, so the fix is the same
|
|
172
|
+
* SDK-behind sentence the listing shows. On a current page the read ran out of the assertion's own
|
|
173
|
+
* budget, and the fix is more of it. Any reason that is not a command timeout is returned as it came.
|
|
174
|
+
*/
|
|
175
|
+
export function diagnoseTimeout(reason, session) {
|
|
176
|
+
const granted = reason === undefined ? undefined : COMMAND_TIMEOUT.exec(reason)?.[1];
|
|
177
|
+
if (reason === undefined || granted === undefined) {
|
|
178
|
+
return reason;
|
|
179
|
+
}
|
|
180
|
+
const behind = sdkBehindNote(session);
|
|
181
|
+
if (behind !== undefined) {
|
|
182
|
+
return `${reason} ${behind}`;
|
|
183
|
+
}
|
|
184
|
+
return (`${reason} This read was bounded by what the assertion's budget had left ` +
|
|
185
|
+
`(${granted}ms): the budget ran out while the page was still working. ` +
|
|
186
|
+
'Give the assertion more time with `timeout_ms`.');
|
|
187
|
+
}
|
|
188
|
+
/** Why an act's verdict is `unknown`: a read that timed out, else an unreachable or disabled target. */
|
|
189
|
+
export const actInconclusive = (verdict, actResult, session) => diagnoseTimeout(verdict.inconclusive, session) ?? actTargetNote(verdict.pass, actResult);
|
|
84
190
|
/** Calls quoted on a failed check, and how much of each response body. */
|
|
85
191
|
const MAX_NET_CALLS = 5;
|
|
86
192
|
const MAX_BODY_CHARS = 300;
|
|
@@ -169,7 +169,7 @@ export function assertSequenceSteps(steps) {
|
|
|
169
169
|
throw new Error(`step ${String(i)} has no \`ref\` or \`target\`. ` +
|
|
170
170
|
'Sequence steps take `ref` (from reticle_query/reticle_snapshot) or `target` ' +
|
|
171
171
|
'(e.g. { testid } or { label }), the same locator reticle_act accepts. ' +
|
|
172
|
-
'A press of Escape, Tab, or a modifier shortcut is a document key and needs neither. ' +
|
|
172
|
+
'A press of Escape, Tab, an arrow key or a modifier shortcut, or any press with `document: true`, is a document key and needs neither. ' +
|
|
173
173
|
'Nothing was acted on — the whole sequence is refused so a bad step cannot leave the earlier ones half-applied.');
|
|
174
174
|
});
|
|
175
175
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* re-render,
|
|
2
|
+
* The things a sequence step needs beyond a plain act: a retry when the ref goes stale under a
|
|
3
|
+
* re-render, a result shaped for the per-step report, and the `burst` pace (at the end).
|
|
4
4
|
*
|
|
5
5
|
* ── THE RETRY ─────────────────────────────────────────────────────────────────────────────────
|
|
6
6
|
*
|
|
@@ -42,4 +42,25 @@ export declare function runStepWithStaleRetry(attempt: () => Promise<StepOutcome
|
|
|
42
42
|
* nothing to look for".
|
|
43
43
|
*/
|
|
44
44
|
export declare function describeStepResult(step: Record<string, unknown>, result: Record<string, unknown>): Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* Ceiling for a burst gap. Past this it is not a burst, it is a paced walk.
|
|
47
|
+
*
|
|
48
|
+
* HOW TO PICK A GAP, because both edges are real and neither is guessable. It must outlast the
|
|
49
|
+
* handler committing and dispatching its request, and undercut the response it is racing.
|
|
50
|
+
* Measured against a superseded-filter race (the superseded query 700ms, the one replacing it 90ms):
|
|
51
|
+
* at 0ms the first request was never issued at all, and at the ordinary settle (~500ms in a
|
|
52
|
+
* throttled tab, where rAF never fires and the frame budget always times out) the two clicks
|
|
53
|
+
* serialised and never overlapped. 50ms sat in the window. A different app needs a different
|
|
54
|
+
* number: read its own slow endpoint.
|
|
55
|
+
*/
|
|
56
|
+
export declare const BURST_MAX_GAP_MS = 500;
|
|
57
|
+
/** A recorded gap as the live tool would apply it: clamped, or absent. */
|
|
58
|
+
export declare function burstGap(burstMs: number | undefined): number | undefined;
|
|
59
|
+
/** A burst step returns as soon as it is dispatched: the gap, not the settle, paces the sequence. */
|
|
60
|
+
export declare function pacedArgs(args: Record<string, unknown>, gap: number | undefined): Record<string, unknown>;
|
|
61
|
+
/**
|
|
62
|
+
* Wait out the rest of the gap that began when the previous step was dispatched, if there is one. The gap is between
|
|
63
|
+
* ACTIONS, as live, so time the previous step already took counts towards it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function holdGap(elapsed: () => number, since: number | undefined, gap: number | undefined, sleep: (ms: number) => Promise<void>): Promise<void>;
|
|
45
66
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* re-render,
|
|
2
|
+
* The things a sequence step needs beyond a plain act: a retry when the ref goes stale under a
|
|
3
|
+
* re-render, a result shaped for the per-step report, and the `burst` pace (at the end).
|
|
4
4
|
*
|
|
5
5
|
* ── THE RETRY ─────────────────────────────────────────────────────────────────────────────────
|
|
6
6
|
*
|
|
@@ -30,8 +30,9 @@ const realSleep = {
|
|
|
30
30
|
*/
|
|
31
31
|
export async function runStepWithStaleRetry(attempt, session, since, budgetMs, opts = realSleep) {
|
|
32
32
|
const first = await attempt();
|
|
33
|
-
if (first.ok || !isStaleRefError(first.error))
|
|
33
|
+
if (first.ok || !isStaleRefError(first.error)) {
|
|
34
34
|
return first;
|
|
35
|
+
}
|
|
35
36
|
await waitForReaction(session, since, budgetMs, opts);
|
|
36
37
|
return attempt();
|
|
37
38
|
}
|
|
@@ -51,9 +52,64 @@ export function describeStepResult(step, result) {
|
|
|
51
52
|
settleReason: result['settleReason'] ?? null,
|
|
52
53
|
};
|
|
53
54
|
// `reticleUi`: the step hit Reticle's own HUD, so the recorder leaves it out of the app's flow.
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// `secret`: the SDK says the target holds a secret. Without it a sequence saved a password in clear.
|
|
56
|
+
// `nth`/`of`/`testidOf`: the control's position among namesakes, which the recorder anchors on.
|
|
57
|
+
// `componentNth`/`componentOf`: its position among the elements of its component anchor.
|
|
58
|
+
for (const key of [
|
|
59
|
+
'testid',
|
|
60
|
+
'testidOf',
|
|
61
|
+
'testidNth',
|
|
62
|
+
'nth',
|
|
63
|
+
'of',
|
|
64
|
+
'component',
|
|
65
|
+
'componentNth',
|
|
66
|
+
'componentOf',
|
|
67
|
+
'ownText',
|
|
68
|
+
'role',
|
|
69
|
+
'name',
|
|
70
|
+
'within',
|
|
71
|
+
'source',
|
|
72
|
+
'warning',
|
|
73
|
+
'reticleUi',
|
|
74
|
+
'secret',
|
|
75
|
+
]) {
|
|
76
|
+
if (result[key] !== undefined) {
|
|
56
77
|
out[key] = result[key];
|
|
78
|
+
}
|
|
57
79
|
}
|
|
58
80
|
return out;
|
|
59
81
|
}
|
|
82
|
+
// ── THE BURST PACE ── shared by the live tool and the replay of a saved sequence (#2130). ──────
|
|
83
|
+
/**
|
|
84
|
+
* Ceiling for a burst gap. Past this it is not a burst, it is a paced walk.
|
|
85
|
+
*
|
|
86
|
+
* HOW TO PICK A GAP, because both edges are real and neither is guessable. It must outlast the
|
|
87
|
+
* handler committing and dispatching its request, and undercut the response it is racing.
|
|
88
|
+
* Measured against a superseded-filter race (the superseded query 700ms, the one replacing it 90ms):
|
|
89
|
+
* at 0ms the first request was never issued at all, and at the ordinary settle (~500ms in a
|
|
90
|
+
* throttled tab, where rAF never fires and the frame budget always times out) the two clicks
|
|
91
|
+
* serialised and never overlapped. 50ms sat in the window. A different app needs a different
|
|
92
|
+
* number: read its own slow endpoint.
|
|
93
|
+
*/
|
|
94
|
+
export const BURST_MAX_GAP_MS = 500;
|
|
95
|
+
/** A recorded gap as the live tool would apply it: clamped, or absent. */
|
|
96
|
+
export function burstGap(burstMs) {
|
|
97
|
+
return burstMs === undefined ? undefined : Math.max(0, Math.min(burstMs, BURST_MAX_GAP_MS));
|
|
98
|
+
}
|
|
99
|
+
/** A burst step returns as soon as it is dispatched: the gap, not the settle, paces the sequence. */
|
|
100
|
+
export function pacedArgs(args, gap) {
|
|
101
|
+
return gap === undefined ? args : { ...args, settleMs: 0 };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Wait out the rest of the gap that began when the previous step was dispatched, if there is one. The gap is between
|
|
105
|
+
* ACTIONS, as live, so time the previous step already took counts towards it.
|
|
106
|
+
*/
|
|
107
|
+
export async function holdGap(elapsed, since, gap, sleep) {
|
|
108
|
+
if (since === undefined || gap === undefined) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const remaining = gap - (elapsed() - since);
|
|
112
|
+
if (remaining > 0) {
|
|
113
|
+
await sleep(remaining);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* nothing on its own.
|
|
10
10
|
*/
|
|
11
11
|
import type { Predicate } from '@reticlehq/core';
|
|
12
|
-
import { evaluatePredicate, type EvalResult } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
12
|
+
import { evaluatePredicate, waitForPredicate, type EvalResult } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
13
13
|
import { waitForInFlight } from './settle-in-flight.js';
|
|
14
14
|
import { revertedAfterMatch } from './durable.js';
|
|
15
|
+
type WaitArgs = Parameters<typeof waitForPredicate>;
|
|
16
|
+
/** Wait for the predicate inside the budget, or read it once when there is no budget. */
|
|
17
|
+
export declare function evaluateAfterAct(session: WaitArgs[0], until: Predicate, timeout: number, since: number, baselines: WaitArgs[4]): Promise<EvalResult>;
|
|
15
18
|
type Waited = Parameters<typeof waitForInFlight>[0] & Parameters<typeof revertedAfterMatch>[0];
|
|
16
19
|
/** Returns the sentence to fail with when the consequence reverted, else undefined. */
|
|
17
20
|
export declare function finishAfterMatch(session: Waited, until: Predicate, since: number, baselines: Parameters<typeof revertedAfterMatch>[3], remaining: () => number): Promise<string | undefined>;
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* nothing on its own.
|
|
10
10
|
*/
|
|
11
11
|
import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
|
|
12
|
-
import { evaluatePredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
12
|
+
import { evaluatePredicate, waitForPredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
13
13
|
import { waitForInFlight } from './settle-in-flight.js';
|
|
14
14
|
import { waitForReaction } from './react-grace.js';
|
|
15
15
|
import { revertedAfterMatch } from './durable.js';
|
|
16
|
+
/** Wait for the predicate inside the budget, or read it once when there is no budget. */
|
|
17
|
+
export function evaluateAfterAct(session, until, timeout, since, baselines) {
|
|
18
|
+
if (timeout > 0) {
|
|
19
|
+
return waitForPredicate(session, until, timeout, since, baselines);
|
|
20
|
+
}
|
|
21
|
+
return evaluatePredicate(session, until, since, true, baselines);
|
|
22
|
+
}
|
|
16
23
|
/** Returns the sentence to fail with when the consequence reverted, else undefined. */
|
|
17
24
|
export async function finishAfterMatch(session, until, since, baselines, remaining) {
|
|
18
25
|
const sleep = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
|
|
@@ -18,7 +18,7 @@ export async function readAlreadyTrue(session, until, since) {
|
|
|
18
18
|
// baseline yet would report `inconclusive` — reading the app twice to answer a question we had
|
|
19
19
|
// not taken the reading for.
|
|
20
20
|
const baselines = await captureBaselines(session, until);
|
|
21
|
-
const precheck = until !== undefined && readsDomState(until)
|
|
21
|
+
const precheck = until !== undefined && readsDomState(until, since)
|
|
22
22
|
? await evaluatePredicate(session, until, since, false, baselines)
|
|
23
23
|
: undefined;
|
|
24
24
|
const alreadyTrue = precheck?.pass ?? false;
|
|
@@ -16,4 +16,15 @@
|
|
|
16
16
|
* branch reports it (navigate-result.ts): so "Reticle stopped waiting" is distinguishable from "the
|
|
17
17
|
* page never came back", and the fix — a larger `timeout_ms` — is visible in the result.
|
|
18
18
|
*/
|
|
19
|
-
export declare function reloadResult(reconnected: boolean, waitedMs: number): Record<string, unknown>;
|
|
19
|
+
export declare function reloadResult(reconnected: boolean, waitedMs: number, hard?: HardReload): Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* How a `hard` reload reached the page (#1785). A leased tab reloads through the browser with the
|
|
22
|
+
* cache ignored. Any other tab can only fetch its document again, so its modules can be stale.
|
|
23
|
+
*/
|
|
24
|
+
export declare const HardReload: {
|
|
25
|
+
readonly CACHE_IGNORED: "cache_ignored";
|
|
26
|
+
readonly DOCUMENT_ONLY: "document_only";
|
|
27
|
+
};
|
|
28
|
+
export type HardReload = (typeof HardReload)[keyof typeof HardReload];
|
|
29
|
+
/** The `hardReload` field for a reload, or nothing when the call did not ask for `hard`. */
|
|
30
|
+
export declare function hardReloadOf(hard: boolean, throughBrowser: boolean): HardReload | undefined;
|
|
@@ -27,8 +27,31 @@ const RELOAD_RECONNECTED_NOTE = 'the page came back and re-announced itself unde
|
|
|
27
27
|
* branch reports it (navigate-result.ts): so "Reticle stopped waiting" is distinguishable from "the
|
|
28
28
|
* page never came back", and the fix — a larger `timeout_ms` — is visible in the result.
|
|
29
29
|
*/
|
|
30
|
-
export function reloadResult(reconnected, waitedMs) {
|
|
31
|
-
|
|
30
|
+
export function reloadResult(reconnected, waitedMs, hard) {
|
|
31
|
+
const base = reconnected
|
|
32
32
|
? { ok: true, confirmed: true, note: RELOAD_RECONNECTED_NOTE }
|
|
33
33
|
: { ok: true, confirmed: false, waitedMs, note: RELOAD_NOTE };
|
|
34
|
+
if (hard === undefined) {
|
|
35
|
+
return base;
|
|
36
|
+
}
|
|
37
|
+
return { ...base, hardReload: hard, note: `${base.note} ${HARD_RELOAD_NOTE[hard]}` };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* How a `hard` reload reached the page (#1785). A leased tab reloads through the browser with the
|
|
41
|
+
* cache ignored. Any other tab can only fetch its document again, so its modules can be stale.
|
|
42
|
+
*/
|
|
43
|
+
export const HardReload = {
|
|
44
|
+
CACHE_IGNORED: 'cache_ignored',
|
|
45
|
+
DOCUMENT_ONLY: 'document_only',
|
|
46
|
+
};
|
|
47
|
+
const HARD_RELOAD_NOTE = {
|
|
48
|
+
[HardReload.CACHE_IGNORED]: 'The browser reloaded with its cache ignored, so every script and module was fetched again.',
|
|
49
|
+
[HardReload.DOCUMENT_ONLY]: 'This tab is not a lease, so only the document was fetched again. Scripts and modules can still come from the browser cache. To fetch them again, reload the tab in the browser with Cmd+Shift+R, or take a lease with reticle_lease.',
|
|
50
|
+
};
|
|
51
|
+
/** The `hardReload` field for a reload, or nothing when the call did not ask for `hard`. */
|
|
52
|
+
export function hardReloadOf(hard, throughBrowser) {
|
|
53
|
+
if (!hard) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return throughBrowser ? HardReload.CACHE_IGNORED : HardReload.DOCUMENT_ONLY;
|
|
34
57
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export type TargetResolution = {
|
|
3
3
|
readonly kind: 'ref';
|
|
4
4
|
readonly ref: string;
|
|
5
|
+
/** Set when every match was hidden and the action uses the hidden one. Says so to the caller. */
|
|
6
|
+
readonly hiddenTarget?: string;
|
|
5
7
|
} | {
|
|
6
8
|
readonly kind: 'global';
|
|
7
9
|
readonly ref: '';
|
|
@@ -9,6 +11,16 @@ export type TargetResolution = {
|
|
|
9
11
|
readonly kind: 'error';
|
|
10
12
|
readonly message: string;
|
|
11
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* What a result says when every match was hidden and the action used the hidden one. It does not
|
|
16
|
+
* say that a person could not reach it: a hidden input with a visible label is reached through the
|
|
17
|
+
* label, and the reach check after the action decides that.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hiddenTargetNote(ref: string): string;
|
|
20
|
+
/** The `hiddenTarget` field for a tool result, or nothing when the action used a visible match. */
|
|
21
|
+
export declare function hiddenTargetField(resolution: TargetResolution): {
|
|
22
|
+
hiddenTarget?: string;
|
|
23
|
+
};
|
|
12
24
|
/** The words before the nearest-target list in a no_match refusal. The repeat hint reads after it. */
|
|
13
25
|
export declare const NEAREST_TARGETS_LEAD = " The nearest targets on the page, most similar first: ";
|
|
14
26
|
/**
|
|
@@ -26,7 +38,9 @@ export declare const NEAREST_TARGETS_LEAD = " The nearest targets on the page, m
|
|
|
26
38
|
*
|
|
27
39
|
* Invisible matches are filtered before counting: a hidden duplicate is a common way for a
|
|
28
40
|
* legitimately unambiguous query to look ambiguous, and refusing on it would push callers back to
|
|
29
|
-
* the two-turn path for no gain.
|
|
41
|
+
* the two-turn path for no gain. When every match is hidden, the one hidden match is used and the
|
|
42
|
+
* resolution names it in `hiddenTarget`. It is not refused: a hidden input is often clicked through
|
|
43
|
+
* its visible label.
|
|
30
44
|
*
|
|
31
45
|
* When several remain, they are ranked (in-viewport, then role) inside the refusal only — so the
|
|
32
46
|
* agent can pass the top ref on the next turn without a snapshot. Ranking is never acted on here.
|
|
@@ -83,6 +83,23 @@ function nearNamesClause(hint) {
|
|
|
83
83
|
'Use that name, or — when the name carries a part that changes, like a time — target it by ' +
|
|
84
84
|
'`testid` or by `text` (a contained match).');
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* What a result says when every match was hidden and the action used the hidden one. It does not
|
|
88
|
+
* say that a person could not reach it: a hidden input with a visible label is reached through the
|
|
89
|
+
* label, and the reach check after the action decides that.
|
|
90
|
+
*/
|
|
91
|
+
export function hiddenTargetNote(ref) {
|
|
92
|
+
return (`every element that matched the target is hidden, so the action used the hidden match ${ref}. ` +
|
|
93
|
+
'If a person reaches it through a visible label, this is correct. If not, target the control ' +
|
|
94
|
+
'a person can see.');
|
|
95
|
+
}
|
|
96
|
+
/** The `hiddenTarget` field for a tool result, or nothing when the action used a visible match. */
|
|
97
|
+
export function hiddenTargetField(resolution) {
|
|
98
|
+
if ('ref' !== resolution.kind || resolution.hiddenTarget === undefined) {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
return { hiddenTarget: resolution.hiddenTarget };
|
|
102
|
+
}
|
|
86
103
|
const NearestTargetsSchema = z.array(NearestTargetSchema).min(1);
|
|
87
104
|
/** The words before the nearest-target list in a no_match refusal. The repeat hint reads after it. */
|
|
88
105
|
export const NEAREST_TARGETS_LEAD = ' The nearest targets on the page, most similar first: ';
|
|
@@ -117,7 +134,9 @@ function nearestClause(hint) {
|
|
|
117
134
|
*
|
|
118
135
|
* Invisible matches are filtered before counting: a hidden duplicate is a common way for a
|
|
119
136
|
* legitimately unambiguous query to look ambiguous, and refusing on it would push callers back to
|
|
120
|
-
* the two-turn path for no gain.
|
|
137
|
+
* the two-turn path for no gain. When every match is hidden, the one hidden match is used and the
|
|
138
|
+
* resolution names it in `hiddenTarget`. It is not refused: a hidden input is often clicked through
|
|
139
|
+
* its visible label.
|
|
121
140
|
*
|
|
122
141
|
* When several remain, they are ranked (in-viewport, then role) inside the refusal only — so the
|
|
123
142
|
* agent can pass the top ref on the next turn without a snapshot. Ranking is never acted on here.
|
|
@@ -153,5 +172,9 @@ export function resolveTargetRef(candidates, hint) {
|
|
|
153
172
|
if ('string' !== typeof ref || 0 === ref.length) {
|
|
154
173
|
return { kind: 'error', message: 'target matched an element with no usable ref.' };
|
|
155
174
|
}
|
|
175
|
+
// Every match was hidden, so the action uses a hidden one. Rule 11: say so, never silently.
|
|
176
|
+
if (0 === visible.length) {
|
|
177
|
+
return { kind: 'ref', ref, hiddenTarget: hiddenTargetNote(ref) };
|
|
178
|
+
}
|
|
156
179
|
return { kind: 'ref', ref };
|
|
157
180
|
}
|
|
@@ -46,6 +46,16 @@ export interface StepExpectation {
|
|
|
46
46
|
* step is undecided, never a miss — `unknown`, as act_and_wait says.
|
|
47
47
|
*/
|
|
48
48
|
undecided?: string;
|
|
49
|
+
/** The step's action (`click`, `fill`), so a plan that could not judge a step can name it. */
|
|
50
|
+
action?: string;
|
|
51
|
+
/** A real frame flushed after this step's action, as `settled` on its act result says. */
|
|
52
|
+
settled?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Absence-derived contradiction kinds seen in this step's window (a request that never settled,
|
|
55
|
+
* for example). They do not decide a miss, but they stop a plan from claiming no channel saw a
|
|
56
|
+
* problem.
|
|
57
|
+
*/
|
|
58
|
+
doubted?: readonly string[];
|
|
49
59
|
}
|
|
50
60
|
export interface SequenceGrade {
|
|
51
61
|
verified: Verified;
|
|
@@ -69,6 +79,8 @@ interface SequencePlan {
|
|
|
69
79
|
readonly planned: number;
|
|
70
80
|
/** Whether anything actually reached the page. */
|
|
71
81
|
readonly dispatched: boolean;
|
|
82
|
+
/** The capture lost events inside the plan's window, so no channel saw all of it. */
|
|
83
|
+
readonly captureLost?: boolean;
|
|
72
84
|
}
|
|
73
85
|
export declare function gradeSequence(steps: readonly StepExpectation[], plan?: SequencePlan): SequenceGrade;
|
|
74
86
|
/**
|
|
@@ -23,6 +23,23 @@ import { Verified, VerifiedReason } from '@reticlehq/core';
|
|
|
23
23
|
* sweep of independent controls, where stopping at the first would hide the other thirteen.
|
|
24
24
|
*/
|
|
25
25
|
export const DeviationMode = { HALT: 'halt', CONTINUE: 'continue' };
|
|
26
|
+
/** The sentence for a plan whose steps declared nothing. */
|
|
27
|
+
function undeclaredBecause(total, dispatched) {
|
|
28
|
+
if (0 === total) {
|
|
29
|
+
return 'the plan had no steps, so nothing was driven and nothing was proved';
|
|
30
|
+
}
|
|
31
|
+
// A step refused before dispatch is a malformed call: telling its author to add an `expect`
|
|
32
|
+
// sends them to the wrong thing.
|
|
33
|
+
if (!dispatched) {
|
|
34
|
+
return `nothing was dispatched, so none of the ${String(total)} step(s) ran — read the per-step \`error\` for the one that was refused, and fix that before adding an \`expect\``;
|
|
35
|
+
}
|
|
36
|
+
return `all ${String(total)} step(s) declared nothing, so the app was driven but not verified — give each step an \`expect\` naming the consequence it causes`;
|
|
37
|
+
}
|
|
38
|
+
/** A step as a sentence names it: its number, counted from 1, and its action when it has one. */
|
|
39
|
+
function stepName(index, action) {
|
|
40
|
+
const number = `step ${String(index + 1)}`;
|
|
41
|
+
return action === undefined ? number : `${number} (${action})`;
|
|
42
|
+
}
|
|
26
43
|
export function gradeSequence(steps, plan = { planned: steps.length, dispatched: true }) {
|
|
27
44
|
const total = Math.max(plan.planned, steps.length);
|
|
28
45
|
const declaring = steps.filter((step) => step.declared);
|
|
@@ -54,44 +71,37 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
|
|
|
54
71
|
because: `step ${String(contradictedAt + 1)}: channels disagree about this action (${kinds}) even though it went on`,
|
|
55
72
|
};
|
|
56
73
|
}
|
|
57
|
-
// Nothing failed that was seen: a step nobody could judge leaves the plan undecided
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
// Nothing failed that was seen: a step nobody could judge leaves the plan undecided. The sentence
|
|
75
|
+
// names the step and what was missing, so the agent knows which step to change (#2204).
|
|
76
|
+
const unjudgedAt = steps.findIndex((step) => step.undecided !== undefined);
|
|
77
|
+
if (unjudgedAt >= 0) {
|
|
78
|
+
const unjudged = steps[unjudgedAt];
|
|
60
79
|
return {
|
|
61
80
|
verified: Verified.UNKNOWN,
|
|
62
81
|
reason: VerifiedReason.INCONCLUSIVE,
|
|
63
82
|
declared,
|
|
64
83
|
total,
|
|
65
|
-
because:
|
|
84
|
+
because: `${stepName(unjudgedAt, unjudged?.action)} could not be judged: ${unjudged?.undecided ?? ''}`,
|
|
66
85
|
};
|
|
86
|
+
}
|
|
67
87
|
/*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* `no-fault` here would claim evidence nobody collected, in the one place this product cannot
|
|
74
|
-
* afford to overclaim.
|
|
75
|
-
*
|
|
76
|
-
* So the enum stays conservative and the SENTENCE carries the whole answer: it names declaring a
|
|
77
|
-
* consequence as the next move, which is what an agent reading `no-fault` would have done.
|
|
78
|
-
* Upgrading means plumbing `settled` from the act results through `SequencePlan`.
|
|
88
|
+
* Nothing declared is `no-fault` over a quiet plan and `unknown` otherwise. This is the rule a
|
|
89
|
+
* single action follows (`engine/src/evidence/verified.ts`): no-fault claims the whole window was
|
|
90
|
+
* observed, so every step must report a settled frame, no step may carry an absence-derived
|
|
91
|
+
* finding, and the capture must be whole. `unknown` sends an agent to look again with better
|
|
92
|
+
* coverage. When the plan was quiet, that cannot help: the fix is to declare a consequence.
|
|
79
93
|
*/
|
|
80
94
|
if (0 === declared) {
|
|
95
|
+
const quiet = plan.dispatched &&
|
|
96
|
+
true !== plan.captureLost &&
|
|
97
|
+
steps.length > 0 &&
|
|
98
|
+
steps.every((step) => true === step.settled && 0 === (step.doubted ?? []).length);
|
|
81
99
|
return {
|
|
82
|
-
verified: Verified.UNKNOWN,
|
|
100
|
+
verified: quiet ? Verified.NO_FAULT : Verified.UNKNOWN,
|
|
83
101
|
reason: VerifiedReason.NOTHING_DECLARED,
|
|
84
102
|
declared,
|
|
85
103
|
total,
|
|
86
|
-
because:
|
|
87
|
-
? 'the plan had no steps, so nothing was driven and nothing was proved'
|
|
88
|
-
: // "the app was driven" was said unconditionally, including on a call whose own
|
|
89
|
-
// `dispatched` was false in the same object. A step refused before dispatch is a
|
|
90
|
-
// malformed call, and telling its author to add an `expect` sends them at the wrong
|
|
91
|
-
// thing entirely.
|
|
92
|
-
plan.dispatched
|
|
93
|
-
? `all ${String(total)} step(s) declared nothing, so the app was driven but not verified — give each step an \`expect\` naming the consequence it causes`
|
|
94
|
-
: `nothing was dispatched, so none of the ${String(total)} step(s) ran — read the per-step \`error\` for the one that was refused, and fix that before adding an \`expect\``,
|
|
104
|
+
because: undeclaredBecause(total, plan.dispatched),
|
|
95
105
|
};
|
|
96
106
|
}
|
|
97
107
|
// A journey is proved at its END. Declaring only early steps checks that it started, and a yes
|
|
@@ -144,8 +154,9 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
|
|
|
144
154
|
/** The stopped recording a proved journey is held under, so the `keep` offer can be taken. */
|
|
145
155
|
export const JOURNEY_RECORDING = 'last-journey';
|
|
146
156
|
export function offerToKeep(grade) {
|
|
147
|
-
if (grade.verified !== Verified.YES || 0 === grade.declared)
|
|
157
|
+
if (grade.verified !== Verified.YES || 0 === grade.declared) {
|
|
148
158
|
return undefined;
|
|
159
|
+
}
|
|
149
160
|
return (`this plan proved something (${String(grade.declared)} of ${String(grade.total)} steps declared a ` +
|
|
150
161
|
'consequence and every one held) — keep it as a regression flow with ' +
|
|
151
162
|
`reticle_flow_save { flowName: "${JOURNEY_RECORDING}", saveAs: "<name>" }, and the steps that declared nothing will replay ` +
|