@reticlehq/server 2.13.1 → 2.14.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 +8 -18
- package/bin/reticle.js +3 -0
- package/dist/bridge/auth-failure-reason.js +12 -1
- package/dist/bridge/bridge-security.d.ts +8 -0
- package/dist/bridge/bridge-security.js +6 -1
- package/dist/bridge/bridge.d.ts +3 -0
- package/dist/bridge/bridge.js +23 -1
- package/dist/bridge/bridge.test-harness.d.ts +10 -1
- package/dist/bridge/bridge.test-harness.js +7 -1
- package/dist/bridge/pairing-token.d.ts +17 -0
- package/dist/bridge/pairing-token.js +26 -0
- package/dist/bridge/token-auth.d.ts +7 -0
- package/dist/bridge/token-auth.js +7 -1
- package/dist/capsule/capsule-store.d.ts +4 -4
- package/dist/capsule/capsule.d.ts +1 -1
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +21 -1
- package/dist/capsule/causal-summary.js +8 -6
- package/dist/capsule/divergence.d.ts +1 -1
- package/dist/capsule/divergence.js +26 -10
- package/dist/chromium-launch-options.d.ts +2 -1
- package/dist/cli/adhoc-verdict.d.ts +58 -0
- package/dist/cli/adhoc-verdict.js +115 -0
- package/dist/cli/chromium-hint.d.ts +18 -4
- package/dist/cli/chromium-hint.js +54 -6
- package/dist/cli/cli-doctor.js +1 -2
- package/dist/cli/cli-flow-commands.d.ts +16 -2
- package/dist/cli/cli-flow-commands.js +28 -9
- package/dist/cli/cli-parse-feedback.js +1 -1
- package/dist/cli/cli-parse.d.ts +2 -1
- package/dist/cli/cli-parse.js +38 -0
- package/dist/cli/cli-port.d.ts +5 -13
- package/dist/cli/cli-port.js +5 -38
- package/dist/cli/cli-update-commands.js +4 -4
- package/dist/cli/cli-verify.d.ts +24 -7
- package/dist/cli/cli-verify.js +87 -13
- package/dist/cli/cloud-keystore.d.ts +2 -1
- package/dist/cli/cloud-login.d.ts +2 -1
- package/dist/cli/cloud-session.d.ts +2 -1
- package/dist/cli/cloud-session.js +1 -1
- package/dist/cli/daemon-start-options.d.ts +2 -1
- package/dist/cli/device-flow.d.ts +2 -2
- package/dist/cli/doctor-daemon-line.d.ts +2 -1
- package/dist/cli/doctor-project-line.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +2 -1
- package/dist/cli/drive-attach.d.ts +4 -3
- package/dist/cli/gate-hook-message.d.ts +2 -1
- package/dist/cli/status-next-action.js +10 -11
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +21 -9
- package/dist/cloud/account-state.d.ts +20 -0
- package/dist/cloud/account-state.js +65 -0
- package/dist/cloud/cloud-sync.d.ts +46 -3
- package/dist/cloud/cloud-sync.js +116 -0
- package/dist/cloud/sync-cycle.d.ts +5 -4
- package/dist/cloud/sync-cycle.js +1 -1
- package/dist/cloud/sync-daemon.d.ts +3 -19
- package/dist/cloud/sync-daemon.js +2 -2
- package/dist/crawl/crawl-tools.js +1 -1
- package/dist/crawl/crawl.d.ts +5 -5
- package/dist/crawl/crawl.js +138 -4
- package/dist/daemon/daemon-resilience.d.ts +3 -2
- package/dist/daemon/daemon-resilience.js +43 -4
- package/dist/daemon/daemon-resolve.d.ts +0 -9
- package/dist/daemon/daemon-resolve.js +2 -2
- package/dist/daemon/dev-servers.d.ts +1 -1
- package/dist/daemon/dev-servers.js +1 -1
- package/dist/daemon/loopback-alias.d.ts +2 -1
- package/dist/daemon/startup-failure.d.ts +11 -0
- package/dist/daemon/startup-failure.js +46 -0
- package/dist/events/contradictions.d.ts +10 -16
- package/dist/events/contradictions.js +149 -17
- package/dist/events/declared.d.ts +13 -1
- package/dist/events/declared.js +59 -3
- package/dist/events/echo-mismatch.js +75 -6
- package/dist/events/lineage.d.ts +52 -0
- package/dist/events/lineage.js +122 -0
- package/dist/events/net-evidence.d.ts +21 -0
- package/dist/events/net-evidence.js +81 -0
- package/dist/events/predicate-console.d.ts +13 -0
- package/dist/events/predicate-console.js +138 -0
- package/dist/events/predicate-element.d.ts +13 -0
- package/dist/events/predicate-element.js +214 -0
- package/dist/events/predicate-eval.d.ts +15 -3
- package/dist/events/predicate-eval.js +128 -110
- package/dist/events/predicate-parse.js +34 -1
- package/dist/events/predicate-request-body.d.ts +50 -0
- package/dist/events/predicate-request-body.js +120 -0
- package/dist/events/predicate-route.d.ts +21 -1
- package/dist/events/predicate-route.js +149 -21
- package/dist/events/predicate-schema.d.ts +8 -1
- package/dist/events/predicate-schema.js +34 -0
- package/dist/events/predicate.d.ts +2 -0
- package/dist/events/predicate.js +52 -159
- package/dist/events/split-text-miss.d.ts +6 -4
- package/dist/events/split-text-miss.js +12 -5
- package/dist/flows/affected-tools.js +22 -9
- package/dist/flows/annotate.d.ts +2 -1
- package/dist/flows/annotate.js +19 -1
- package/dist/flows/coverage.d.ts +2 -2
- package/dist/flows/flake.d.ts +3 -3
- package/dist/flows/flow-expect-grammar.d.ts +33 -0
- package/dist/flows/flow-expect-grammar.js +155 -0
- package/dist/flows/flow-replay-run.d.ts +1 -1
- package/dist/flows/flow-replay-run.js +28 -15
- package/dist/flows/flow-replay.d.ts +10 -0
- package/dist/flows/flow-replay.js +57 -22
- package/dist/flows/flow-secret-field.d.ts +14 -0
- package/dist/flows/flow-secret-field.js +22 -0
- package/dist/flows/flow-step-runners.d.ts +1 -0
- package/dist/flows/flow-step-runners.js +91 -34
- package/dist/flows/flow-success.js +22 -0
- package/dist/flows/flow-tools.js +27 -8
- package/dist/flows/flow-upload-resolve.d.ts +23 -0
- package/dist/flows/flow-upload-resolve.js +46 -0
- package/dist/flows/flows.d.ts +15 -0
- package/dist/flows/flows.js +17 -14
- package/dist/flows/git-changed.d.ts +23 -4
- package/dist/flows/git-changed.js +32 -6
- package/dist/flows/heal-run.js +2 -2
- package/dist/flows/predicate-to-expect.d.ts +3 -2
- package/dist/flows/predicate-to-expect.js +11 -5
- package/dist/flows/recording-backtrack.d.ts +24 -0
- package/dist/flows/recording-backtrack.js +73 -0
- package/dist/flows/recordings.d.ts +6 -0
- package/dist/flows/replay-halt.d.ts +26 -0
- package/dist/flows/replay-halt.js +30 -0
- package/dist/flows/replay.d.ts +0 -8
- package/dist/flows/replay.js +15 -3
- package/dist/flows/suite-flakes.d.ts +2 -1
- package/dist/flows/verify-change-tools.js +16 -4
- package/dist/honesty/already-true.d.ts +14 -0
- package/dist/honesty/already-true.js +47 -4
- package/dist/honesty/blind-spots.d.ts +3 -1
- package/dist/honesty/blind-spots.js +24 -1
- package/dist/honesty/body-capture-remedy.d.ts +42 -0
- package/dist/honesty/body-capture-remedy.js +95 -0
- package/dist/honesty/feature-capture.d.ts +3 -2
- package/dist/honesty/feature-capture.js +1 -1
- package/dist/honesty/gap-ledger.d.ts +1 -1
- package/dist/honesty/gap-ledger.js +1 -1
- package/dist/honesty/instrumentation-gaps.d.ts +8 -1
- package/dist/honesty/instrumentation-gaps.js +12 -2
- package/dist/honesty/observability.d.ts +4 -3
- package/dist/honesty/tool-hit-rate.d.ts +3 -2
- package/dist/honesty/uncaptured-bodies.d.ts +3 -1
- package/dist/honesty/uncaptured-bodies.js +6 -2
- package/dist/honesty/unsettled.d.ts +9 -0
- package/dist/honesty/unsettled.js +45 -0
- package/dist/honesty/verified.d.ts +32 -1
- package/dist/honesty/verified.js +37 -10
- package/dist/impact/impact-store.d.ts +5 -14
- package/dist/impact/impact-store.js +49 -6
- package/dist/index.js +1 -1
- package/dist/input/network-detail.d.ts +26 -1
- package/dist/input/network-detail.js +44 -12
- package/dist/input/network-mock-tools.js +24 -17
- package/dist/input/real-input.js +1 -1
- package/dist/input/viewport-tools.js +25 -13
- package/dist/intent/inline-intent.d.ts +0 -10
- package/dist/intent/inline-intent.js +1 -1
- package/dist/intent/intent-shard-store.d.ts +3 -2
- package/dist/intent/intent-shard-store.js +1 -1
- package/dist/intent/intent-shard.d.ts +14 -14
- package/dist/intent/intent-shard.js +4 -4
- package/dist/intent/intent-store.d.ts +1 -1
- package/dist/intent/intent-store.js +1 -1
- package/dist/intent/intent-subject.d.ts +2 -1
- package/dist/intent/open-intents.d.ts +3 -2
- package/dist/license/license.d.ts +4 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/attach-memory.d.ts +2 -1
- package/dist/mcp/client-identity.d.ts +2 -1
- package/dist/mcp/mcp-outage.d.ts +1 -1
- package/dist/mcp/mcp-outage.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +3 -1
- package/dist/mcp/mcp-proxy.js +32 -7
- package/dist/mcp/mcp.js +3 -1
- package/dist/mcp/proxy-handshake.d.ts +2 -1
- package/dist/mcp/proxy-idle-exit.d.ts +45 -0
- package/dist/mcp/proxy-idle-exit.js +94 -0
- package/dist/mcp/server-instructions.d.ts +2 -1
- package/dist/memory/project-memory.d.ts +2 -3
- package/dist/memory/project-memory.js +1 -1
- package/dist/oracles/propose-consequences.js +5 -5
- package/dist/oracles/self-instrument.d.ts +2 -2
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/platform.d.ts +3 -5
- package/dist/platform.js +3 -5
- package/dist/pool/browser-pool.d.ts +92 -0
- package/dist/pool/browser-pool.js +119 -5
- package/dist/pool/playwright-launcher.js +29 -2
- package/dist/pool/storage-seed.d.ts +23 -0
- package/dist/pool/storage-seed.js +198 -0
- package/dist/project/artifact-root.d.ts +2 -1
- package/dist/project/memory-fs.d.ts +2 -1
- package/dist/project/reticle-dir.js +1 -1
- package/dist/project/session-root.d.ts +8 -0
- package/dist/project/session-root.js +9 -2
- package/dist/runs/reticle-runner.d.ts +14 -1
- package/dist/runs/reticle-runner.js +39 -2
- package/dist/session/attachment-history.d.ts +2 -1
- package/dist/session/fake-session.d.ts +37 -0
- package/dist/session/fake-session.js +81 -0
- package/dist/session/lease-visibility.d.ts +2 -1
- package/dist/session/live-control-tools.js +6 -0
- package/dist/session/no-session-diagnosis.d.ts +17 -1
- package/dist/session/no-session-diagnosis.js +32 -6
- package/dist/session/no-session-next-action.d.ts +14 -0
- package/dist/session/no-session-next-action.js +18 -0
- package/dist/session/no-session-watch.js +46 -3
- package/dist/session/registered-projects.d.ts +54 -0
- package/dist/session/registered-projects.js +73 -0
- package/dist/session/review-store.d.ts +2 -1
- package/dist/session/session-health.d.ts +62 -1
- package/dist/session/session-health.js +82 -2
- package/dist/session/session-manager.d.ts +11 -1
- package/dist/session/session-manager.js +18 -3
- package/dist/session/session-replaced.d.ts +24 -0
- package/dist/session/session-replaced.js +29 -0
- package/dist/session/session-successor.js +20 -4
- package/dist/session/session.d.ts +13 -5
- package/dist/session/session.js +31 -23
- package/dist/setup/agent-configs.d.ts +3 -2
- package/dist/setup/agent-writer.d.ts +3 -2
- package/dist/setup/approval-migration.d.ts +2 -1
- package/dist/setup/approval-migration.js +1 -1
- package/dist/setup/auto-approve.d.ts +11 -19
- package/dist/setup/auto-approve.js +15 -12
- package/dist/{init → setup}/confirm.js +1 -1
- package/dist/setup/desktop-shape.d.ts +3 -2
- package/dist/setup/drive-agent.d.ts +2 -3
- package/dist/setup/drive-agent.js +1 -1
- package/dist/setup/drive-plan.d.ts +3 -2
- package/dist/setup/ensure-daemon.d.ts +2 -1
- package/dist/setup/existing-app.d.ts +12 -0
- package/dist/setup/existing-app.js +13 -0
- package/dist/setup/init-host.d.ts +11 -0
- package/dist/setup/init-host.js +26 -0
- package/dist/setup/init-runtime.d.ts +4 -10
- package/dist/setup/init-runtime.js +23 -16
- package/dist/setup/license-key.d.ts +2 -1
- package/dist/setup/listeners.d.ts +2 -1
- package/dist/setup/loopback-probe-urls.d.ts +18 -0
- package/dist/setup/loopback-probe-urls.js +52 -0
- package/dist/setup/node-effects.d.ts +3 -0
- package/dist/setup/node-effects.js +26 -11
- package/dist/setup/page-probe.d.ts +5 -0
- package/dist/setup/relaunch.d.ts +2 -1
- package/dist/setup/run-setup.d.ts +5 -1
- package/dist/setup/run-setup.js +141 -50
- package/dist/setup/setup-command.d.ts +3 -2
- package/dist/setup/setup-command.js +8 -0
- package/dist/setup/setup-options.d.ts +5 -3
- package/dist/setup/setup-options.js +3 -2
- package/dist/setup/wait-progress.d.ts +28 -0
- package/dist/setup/wait-progress.js +43 -0
- package/dist/telemetry/app-instrumented.d.ts +2 -1
- package/dist/telemetry/app-instrumented.js +1 -1
- package/dist/telemetry/automation-hint.d.ts +1 -1
- package/dist/telemetry/automation-hint.js +1 -1
- package/dist/telemetry/cli-telemetry.js +1 -1
- package/dist/telemetry/connect-failure.d.ts +1 -1
- package/dist/telemetry/connect-failure.js +1 -1
- package/dist/telemetry/crash-cause.d.ts +3 -2
- package/dist/telemetry/crash-cause.js +1 -1
- package/dist/telemetry/daemon-telemetry.d.ts +3 -2
- package/dist/telemetry/daemon-telemetry.js +1 -1
- package/dist/telemetry/error-class.d.ts +3 -2
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-cli.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +3 -2
- package/dist/telemetry/feedback-context.js +5 -4
- package/dist/telemetry/feedback-local.d.ts +3 -2
- package/dist/telemetry/feedback.d.ts +4 -3
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/git-facts.d.ts +2 -2
- package/dist/telemetry/git-facts.js +1 -1
- package/dist/telemetry/identify.d.ts +1 -1
- package/dist/telemetry/identify.js +1 -1
- package/dist/telemetry/init-telemetry.d.ts +2 -14
- package/dist/telemetry/init-telemetry.js +4 -13
- package/dist/telemetry/install-source.d.ts +7 -13
- package/dist/telemetry/install-source.js +7 -29
- package/dist/telemetry/license-activation.d.ts +3 -2
- package/dist/telemetry/license-activation.js +1 -1
- package/dist/telemetry/machine-snapshot.d.ts +1 -1
- package/dist/telemetry/mcp-connection.js +1 -1
- package/dist/telemetry/project-profile.d.ts +0 -14
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/proxy-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +1 -17
- package/dist/telemetry/telemetry.js +4 -4
- package/dist/telemetry/tool-refused.d.ts +1 -1
- package/dist/telemetry/tool-refused.js +1 -1
- package/dist/tools/act-observation.d.ts +3 -2
- package/dist/tools/act-preflight.js +8 -3
- package/dist/tools/act-target.d.ts +4 -0
- package/dist/tools/act-target.js +14 -5
- package/dist/tools/act-tools.d.ts +0 -2
- package/dist/tools/act-tools.js +45 -15
- package/dist/tools/assert-source.d.ts +30 -0
- package/dist/tools/assert-source.js +21 -0
- package/dist/tools/assert-verdict.d.ts +1 -1
- package/dist/tools/assert-verdict.js +14 -4
- package/dist/tools/browser-tools.js +28 -8
- package/dist/tools/complete-snapshot.d.ts +3 -2
- package/dist/tools/dynamic-tools.js +8 -0
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +23 -0
- package/dist/tools/feedback-tools.js +1 -1
- package/dist/tools/invoke-tool.js +2 -0
- package/dist/tools/lease-tools.d.ts +48 -20
- package/dist/tools/lease-tools.js +311 -76
- package/dist/tools/lineage-tools.d.ts +14 -0
- package/dist/tools/lineage-tools.js +64 -0
- package/dist/tools/merge-tools.js +1 -3
- package/dist/tools/navigate-arrival.d.ts +45 -1
- package/dist/tools/navigate-arrival.js +39 -6
- package/dist/tools/navigate-result.d.ts +8 -1
- package/dist/tools/navigate-result.js +14 -4
- package/dist/tools/numeric-bounds.d.ts +0 -11
- package/dist/tools/numeric-bounds.js +4 -4
- package/dist/tools/observe-tools.js +22 -26
- package/dist/tools/observed-nothing.d.ts +9 -0
- package/dist/tools/observed-nothing.js +2 -1
- package/dist/tools/read-tools.js +12 -17
- package/dist/tools/real-input-attempt.d.ts +14 -6
- package/dist/tools/real-input-attempt.js +75 -18
- package/dist/tools/reconcile-tools.js +2 -1
- package/dist/tools/reload-result.d.ts +6 -1
- package/dist/tools/reload-result.js +7 -2
- package/dist/tools/resolve-target.d.ts +7 -1
- package/dist/tools/resolve-target.js +72 -9
- package/dist/tools/settle-in-flight.d.ts +1 -1
- package/dist/tools/settle-in-flight.js +9 -2
- package/dist/tools/snapshot-delta.d.ts +24 -1
- package/dist/tools/snapshot-delta.js +66 -7
- package/dist/tools/tool-names.d.ts +1 -0
- package/dist/tools/tool-names.js +1 -0
- package/dist/tools/tool-surface.js +20 -2
- package/dist/tools/tools.js +120 -11
- package/dist/tools/verify-next-baton.d.ts +2 -1
- package/dist/update/sdk-sync.d.ts +1 -1
- package/dist/update/sdk-sync.js +1 -1
- package/dist/update/updater.js +1 -1
- package/dist/version/sdk-fix.d.ts +2 -1
- package/dist/version/version-skew.d.ts +0 -8
- package/dist/version/version-skew.js +1 -1
- package/docs/README.md +8 -6
- package/docs/actions.mdx +1 -1
- package/docs/agent-cheatsheet.md +4 -2
- package/docs/architecture.md +1 -1
- package/docs/cli/mcp.mdx +3 -1
- package/docs/cli/watch.mdx +1 -1
- package/docs/cli.mdx +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +8 -3
- package/docs/faq.mdx +9 -0
- package/docs/first-drive.md +136 -0
- package/docs/frameworks.mdx +29 -7
- package/docs/gate-plan.md +2 -2
- package/docs/gates.md +19 -11
- package/docs/getting-started.md +7 -1
- package/docs/http-transport.md +164 -0
- package/docs/index.mdx +5 -3
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +15 -3
- package/docs/platform-integration.md +1 -1
- package/docs/predicates.mdx +2 -0
- package/docs/system-map.md +2 -2
- package/docs/telemetry-contract.md +4 -4
- package/docs/telemetry-events.mdx +1 -1
- package/docs/telemetry.md +3 -3
- package/docs/tools-navigate.mdx +14 -6
- package/docs/usage.md +4 -4
- package/package.json +7 -4
- package/dist/init/agent-rules.d.ts +0 -116
- package/dist/init/agent-rules.js +0 -279
- package/dist/init/app-choice.d.ts +0 -20
- package/dist/init/app-choice.js +0 -25
- package/dist/init/astro-host.d.ts +0 -21
- package/dist/init/astro-host.js +0 -27
- package/dist/init/astro-patch.d.ts +0 -33
- package/dist/init/astro-patch.js +0 -300
- package/dist/init/capabilities.d.ts +0 -40
- package/dist/init/capabilities.js +0 -150
- package/dist/init/closing-hint.d.ts +0 -36
- package/dist/init/closing-hint.js +0 -120
- package/dist/init/connect-steps.d.ts +0 -2
- package/dist/init/connect-steps.js +0 -50
- package/dist/init/cra.d.ts +0 -79
- package/dist/init/cra.js +0 -178
- package/dist/init/csp-check.d.ts +0 -38
- package/dist/init/csp-check.js +0 -107
- package/dist/init/csp-doctor.d.ts +0 -32
- package/dist/init/csp-doctor.js +0 -54
- package/dist/init/desktop-doctor.d.ts +0 -41
- package/dist/init/desktop-doctor.js +0 -198
- package/dist/init/detect.d.ts +0 -96
- package/dist/init/detect.js +0 -209
- package/dist/init/dev-script.d.ts +0 -50
- package/dist/init/dev-script.js +0 -66
- package/dist/init/format-generated.d.ts +0 -18
- package/dist/init/format-generated.js +0 -39
- package/dist/init/install-hint.d.ts +0 -9
- package/dist/init/install-hint.js +0 -29
- package/dist/init/mcp-clients.d.ts +0 -89
- package/dist/init/mcp-clients.js +0 -299
- package/dist/init/mcp-registered.d.ts +0 -12
- package/dist/init/mcp-registered.js +0 -14
- package/dist/init/mcp.d.ts +0 -89
- package/dist/init/mcp.js +0 -129
- package/dist/init/next-patch.d.ts +0 -39
- package/dist/init/next-patch.js +0 -196
- package/dist/init/node-io.d.ts +0 -7
- package/dist/init/node-io.js +0 -117
- package/dist/init/non-js-project.d.ts +0 -27
- package/dist/init/non-js-project.js +0 -84
- package/dist/init/patch-kind.d.ts +0 -20
- package/dist/init/patch-kind.js +0 -10
- package/dist/init/plan-framework.d.ts +0 -64
- package/dist/init/plan-framework.js +0 -565
- package/dist/init/plan.d.ts +0 -242
- package/dist/init/plan.js +0 -703
- package/dist/init/preflight.d.ts +0 -28
- package/dist/init/preflight.js +0 -38
- package/dist/init/project-id.d.ts +0 -12
- package/dist/init/project-id.js +0 -25
- package/dist/init/refresh-rules.d.ts +0 -39
- package/dist/init/refresh-rules.js +0 -51
- package/dist/init/run.d.ts +0 -117
- package/dist/init/run.js +0 -806
- package/dist/init/slash-command.d.ts +0 -27
- package/dist/init/slash-command.js +0 -132
- package/dist/init/snippets.d.ts +0 -134
- package/dist/init/snippets.js +0 -572
- package/dist/init/vite-config.d.ts +0 -18
- package/dist/init/vite-config.js +0 -114
- package/dist/init/windows-quote.d.ts +0 -38
- package/dist/init/windows-quote.js +0 -70
- package/dist/init/workspace-apps.d.ts +0 -30
- package/dist/init/workspace-apps.js +0 -178
- package/dist/project/remember-project.d.ts +0 -24
- package/dist/project/remember-project.js +0 -40
- /package/dist/{init → setup}/confirm.d.ts +0 -0
|
@@ -37,6 +37,49 @@ export function routePathOf(pathname, hash) {
|
|
|
37
37
|
const query = withoutHash.indexOf('?');
|
|
38
38
|
return -1 === query ? withoutHash : withoutHash.slice(0, query);
|
|
39
39
|
}
|
|
40
|
+
function asPayload(value) {
|
|
41
|
+
if (null === value || 'object' !== typeof value || Array.isArray(value))
|
|
42
|
+
return undefined;
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function payloadOf(eventOrData) {
|
|
46
|
+
if (eventOrData['type'] === EventType.ROUTE_CHANGE)
|
|
47
|
+
return asPayload(eventOrData['data']);
|
|
48
|
+
// A different event: refuse rather than read whatever happened to sit in `data`.
|
|
49
|
+
if (undefined !== eventOrData['type'] && undefined !== eventOrData['data'])
|
|
50
|
+
return undefined;
|
|
51
|
+
return eventOrData;
|
|
52
|
+
}
|
|
53
|
+
function partsFromPayload(data) {
|
|
54
|
+
const docPath = str(data['pathname']) ?? str(data['to']);
|
|
55
|
+
if (docPath === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
const hash = str(data['hash']) ?? '';
|
|
58
|
+
const search = str(data['search']) ?? '';
|
|
59
|
+
return {
|
|
60
|
+
routePath: routePathOf(docPath, hash),
|
|
61
|
+
docPath,
|
|
62
|
+
hash,
|
|
63
|
+
search,
|
|
64
|
+
full: `${docPath}${search}${hash}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** A `ROUTE_CHANGE` event, or its `data` — one reading, so the sixth copy cannot drift. */
|
|
68
|
+
export function routeOfEvent(eventOrData) {
|
|
69
|
+
const data = payloadOf(eventOrData);
|
|
70
|
+
return data === undefined ? undefined : partsFromPayload(data);
|
|
71
|
+
}
|
|
72
|
+
/** Same shape from a session URL (the fallback when the tab hard-loaded and emitted no route event). */
|
|
73
|
+
export function routeOfUrl(url) {
|
|
74
|
+
if (!URL.canParse(url))
|
|
75
|
+
return undefined;
|
|
76
|
+
const parsed = new URL(url);
|
|
77
|
+
return partsFromPayload({
|
|
78
|
+
pathname: parsed.pathname,
|
|
79
|
+
search: parsed.search,
|
|
80
|
+
hash: parsed.hash,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
40
83
|
/**
|
|
41
84
|
* The session's live URL, split the way a route-change event already carries it.
|
|
42
85
|
*
|
|
@@ -44,14 +87,14 @@ export function routePathOf(pathname, hash) {
|
|
|
44
87
|
* away — an unreadable URL is not worth losing the fallback over.
|
|
45
88
|
*/
|
|
46
89
|
function readCurrentRoute(url) {
|
|
47
|
-
const
|
|
48
|
-
const pathname =
|
|
49
|
-
const search =
|
|
50
|
-
const hash =
|
|
90
|
+
const parts = routeOfUrl(url);
|
|
91
|
+
const pathname = parts?.docPath ?? url;
|
|
92
|
+
const search = parts?.search ?? '';
|
|
93
|
+
const hash = parts?.hash ?? '';
|
|
51
94
|
return {
|
|
52
95
|
pathname,
|
|
53
|
-
routePath: routePathOf(pathname, hash),
|
|
54
|
-
full: `${pathname}${search}${hash}`,
|
|
96
|
+
routePath: parts?.routePath ?? routePathOf(pathname, hash),
|
|
97
|
+
full: parts?.full ?? `${pathname}${search}${hash}`,
|
|
55
98
|
decidedBy: RouteDecidedBy.CURRENT,
|
|
56
99
|
data: { pathname, search, hash, url, decidedBy: RouteDecidedBy.CURRENT },
|
|
57
100
|
};
|
|
@@ -72,25 +115,41 @@ export function evalRoute(events, p,
|
|
|
72
115
|
* `reticle_snapshot({ mode: 'status' }).route` one call later. Optional so a caller with no session
|
|
73
116
|
* (a replayed window) keeps the old answer instead of inventing a route.
|
|
74
117
|
*/
|
|
75
|
-
currentUrl
|
|
118
|
+
currentUrl,
|
|
119
|
+
/** Event-time floor for the window, so a request already in flight before it is not counted. */
|
|
120
|
+
since = 0) {
|
|
76
121
|
const routes = events.filter((e) => e.type === EventType.ROUTE_CHANGE);
|
|
77
122
|
const last = routes.at(-1);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
data: { ...last.data, decidedBy: RouteDecidedBy.CHANGE },
|
|
88
|
-
};
|
|
89
|
-
})()
|
|
123
|
+
const changed = last === undefined ? undefined : routeOfEvent(last);
|
|
124
|
+
const reading = changed !== undefined && last !== undefined
|
|
125
|
+
? {
|
|
126
|
+
pathname: changed.docPath,
|
|
127
|
+
routePath: changed.routePath,
|
|
128
|
+
full: changed.full,
|
|
129
|
+
decidedBy: RouteDecidedBy.CHANGE,
|
|
130
|
+
data: { ...last.data, decidedBy: RouteDecidedBy.CHANGE },
|
|
131
|
+
}
|
|
90
132
|
: currentUrl === undefined || 0 === currentUrl.length
|
|
91
133
|
? undefined
|
|
92
134
|
: readCurrentRoute(currentUrl);
|
|
93
135
|
if (reading === undefined) {
|
|
136
|
+
// A navigation the SERVER never answered reads exactly like one the APP prevented, and only one
|
|
137
|
+
// of those is the user's bug. See unansweredIn.
|
|
138
|
+
const unanswered = unansweredIn(events, since);
|
|
139
|
+
if (unanswered !== undefined) {
|
|
140
|
+
const reason = `no route change, and ${unanswered.count} request(s) started in this window never ` +
|
|
141
|
+
`completed — first: ${unanswered.method} ${unanswered.url}. A navigation the server has ` +
|
|
142
|
+
'not answered is indistinguishable from one the app declined, so this is not evidence ' +
|
|
143
|
+
'against the app. Check the server directly (curl the route) before looking at components';
|
|
144
|
+
return {
|
|
145
|
+
pass: false,
|
|
146
|
+
failureReason: reason,
|
|
147
|
+
inconclusive: reason,
|
|
148
|
+
observed: `${unanswered.count} request(s) still in flight, no route change`,
|
|
149
|
+
expected: `a route change to ${p.pathname ?? p.contains ?? 'any route'}`,
|
|
150
|
+
assertion: 'route.server-pending',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
94
153
|
return {
|
|
95
154
|
pass: false,
|
|
96
155
|
failureReason: 'no route change observed',
|
|
@@ -99,7 +158,7 @@ currentUrl) {
|
|
|
99
158
|
assertion: 'route.changed',
|
|
100
159
|
};
|
|
101
160
|
}
|
|
102
|
-
// The ROUTER's path, not the document's — see
|
|
161
|
+
// The ROUTER's path, not the document's — see routeOfEvent.
|
|
103
162
|
const pathname = reading.routePath;
|
|
104
163
|
if (p.pathname !== undefined && pathname !== p.pathname) {
|
|
105
164
|
return {
|
|
@@ -126,5 +185,74 @@ currentUrl) {
|
|
|
126
185
|
assertion: 'route.contains',
|
|
127
186
|
};
|
|
128
187
|
}
|
|
129
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
pass: true,
|
|
190
|
+
evidence: reading.decidedBy === RouteDecidedBy.CHANGE
|
|
191
|
+
? { ...reading.data, viewSettled: sawActivityAfter(events, last?.t ?? 0) }
|
|
192
|
+
: reading.data,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Did the page do ANYTHING after the URL moved?
|
|
197
|
+
*
|
|
198
|
+
* A route predicate goes green the instant the URL commits, and on an async-loading page that is a
|
|
199
|
+
* verdict taken over a shell. The reporter's sequence is the whole argument: `allOf [route, console
|
|
200
|
+
* absent]` returned `verified: "yes"`, a snapshot of `main` came back with 9 nodes that read as an
|
|
201
|
+
* empty page, and re-snapshotting moments later showed 22 with the real content — one step from
|
|
202
|
+
* filing "this route renders nothing" against code that was fine.
|
|
203
|
+
*
|
|
204
|
+
* The grade is deliberately NOT changed. Requiring a settle would make every route assertion wait
|
|
205
|
+
* and would flip green suites red, and the report's own acceptance allows either that or evidence
|
|
206
|
+
* saying the view had not settled. This is the second, and it costs nothing that already worked.
|
|
207
|
+
*
|
|
208
|
+
* No clock is read: if the route change is the LAST thing in the window, nothing has been observed
|
|
209
|
+
* since the URL moved, which is exactly the trap. Activity BEFORE the change is ignored — a mutation
|
|
210
|
+
* that predates the navigation says nothing about what the new route rendered.
|
|
211
|
+
*
|
|
212
|
+
* Reported only for a CHANGE reading. A pass decided by "the app is already there" has no commit
|
|
213
|
+
* moment to be too close to, and a caveat there would describe a reading it is not about.
|
|
214
|
+
*/
|
|
215
|
+
function sawActivityAfter(events, routeAt) {
|
|
216
|
+
return events.some((e) => e.t > routeAt && ROUTE_SETTLE_ACTIVITY.has(e.type));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* What counts as the view still moving. Same set the `settled` oracle uses, and deliberately the
|
|
220
|
+
* same one: two different answers to "did the page do something" is how a vocabulary rots.
|
|
221
|
+
*/
|
|
222
|
+
const ROUTE_SETTLE_ACTIVITY = new Set([
|
|
223
|
+
EventType.NET_REQUEST,
|
|
224
|
+
EventType.DOM_ADDED,
|
|
225
|
+
EventType.DOM_REMOVED,
|
|
226
|
+
EventType.DOM_ATTR,
|
|
227
|
+
]);
|
|
228
|
+
/**
|
|
229
|
+
* A request that STARTED in this window and never completed.
|
|
230
|
+
*
|
|
231
|
+
* The discriminator the route oracle was missing. `routeChanges: 0, network: 0` is reported the same
|
|
232
|
+
* way whether the app prevented the navigation or the server never answered the request the app
|
|
233
|
+
* made — and only the first is the user's bug. Reticle already saw the pending navigation; it just
|
|
234
|
+
* never said so, and attached a `source:` file:line that pointed at innocent code.
|
|
235
|
+
*
|
|
236
|
+
* Scoped to requests that started INSIDE the window, deliberately. A background poll that predates
|
|
237
|
+
* the action says nothing about this navigation, and counting it would make every route miss on a
|
|
238
|
+
* polling app inconclusive — softening away the finding this oracle exists to make. That is the same
|
|
239
|
+
* trap the duplicate-request and contradiction rules were each narrowed to avoid.
|
|
240
|
+
*
|
|
241
|
+
* Matched on the request id, the way `detectHungRequests` does: a NET_PENDING with no NET_REQUEST
|
|
242
|
+
* carrying the same id.
|
|
243
|
+
*/
|
|
244
|
+
function unansweredIn(events, since) {
|
|
245
|
+
const settled = new Set();
|
|
246
|
+
for (const e of events)
|
|
247
|
+
if (e.type === EventType.NET_REQUEST)
|
|
248
|
+
settled.add(e.data['id']);
|
|
249
|
+
const open = events.filter((e) => e.type === EventType.NET_PENDING && e.t >= since && !settled.has(e.data['id']));
|
|
250
|
+
const first = open[0];
|
|
251
|
+
if (first === undefined)
|
|
252
|
+
return undefined;
|
|
253
|
+
return {
|
|
254
|
+
count: open.length,
|
|
255
|
+
url: str(first.data['url']) ?? '(unknown url)',
|
|
256
|
+
method: str(first.data['method']) ?? 'GET',
|
|
257
|
+
};
|
|
130
258
|
}
|
|
@@ -19,6 +19,8 @@ export type Predicate = {
|
|
|
19
19
|
* `act_and_wait` reports `already_true` for an action that did exactly the right thing.
|
|
20
20
|
*/
|
|
21
21
|
scope?: string;
|
|
22
|
+
/** Match the scope root itself and check its combined subtree text. Requires `scope`. */
|
|
23
|
+
self?: boolean;
|
|
22
24
|
} | {
|
|
23
25
|
kind: typeof PredicateKind.NET;
|
|
24
26
|
method?: string;
|
|
@@ -30,6 +32,10 @@ export type Predicate = {
|
|
|
30
32
|
count?: number;
|
|
31
33
|
/** A substring the RESPONSE body must contain — what the server answered, not what was sent. */
|
|
32
34
|
bodyContains?: string;
|
|
35
|
+
/** A substring the REQUEST body must contain — what the UI sent, not what came back. */
|
|
36
|
+
requestBodyContains?: string;
|
|
37
|
+
/** Shallow JSON match over the REQUEST body, in the style of `signal.dataMatches`. */
|
|
38
|
+
requestBodyMatches?: Record<string, unknown>;
|
|
33
39
|
} | {
|
|
34
40
|
kind: typeof PredicateKind.ROUTE;
|
|
35
41
|
pathname?: string;
|
|
@@ -80,7 +86,7 @@ export type Predicate = {
|
|
|
80
86
|
kind: typeof PredicateKind.NOT;
|
|
81
87
|
predicate: Predicate;
|
|
82
88
|
};
|
|
83
|
-
|
|
89
|
+
interface ResidualQueryChecks {
|
|
84
90
|
/** Dropped fields this side CAN check, as [field, wanted value] pairs. */
|
|
85
91
|
checks: [string, string][];
|
|
86
92
|
/** Dropped fields with no descriptor to check them against — refuse rather than ignore. */
|
|
@@ -159,3 +165,4 @@ export declare function nestedKeysOf(schema: z.ZodTypeAny | undefined): readonly
|
|
|
159
165
|
* list is worse than none, because the agent trusts it and retries into the same wall.
|
|
160
166
|
*/
|
|
161
167
|
export declare function predicateNestedFieldsFor(kind: string): Readonly<Record<string, readonly string[]>>;
|
|
168
|
+
export {};
|
|
@@ -82,6 +82,10 @@ const ELEMENT_QUERY_FIELDS = [
|
|
|
82
82
|
*/
|
|
83
83
|
function usedQueryFields(query) {
|
|
84
84
|
const used = new Set();
|
|
85
|
+
// The browser's `self` branch checks subtree text when supplied, but skips every other locator.
|
|
86
|
+
// Mark only text as consumed so role/name/etc. remain residual checks on the returned descriptor.
|
|
87
|
+
if (true === query.self)
|
|
88
|
+
return query.text === undefined ? used : used.add('text');
|
|
85
89
|
if (query.by !== undefined && query.value !== undefined) {
|
|
86
90
|
used.add('by').add('value');
|
|
87
91
|
if (QueryBy.ROLE === query.by)
|
|
@@ -231,6 +235,7 @@ function predicateUnion() {
|
|
|
231
235
|
visible: z.boolean().optional(),
|
|
232
236
|
absent: z.boolean().optional(),
|
|
233
237
|
scope: z.string().optional(),
|
|
238
|
+
self: z.boolean().optional(),
|
|
234
239
|
})
|
|
235
240
|
.strict(),
|
|
236
241
|
z
|
|
@@ -260,6 +265,35 @@ function predicateUnion() {
|
|
|
260
265
|
* was never recorded rather than reporting an ordinary mismatch.
|
|
261
266
|
*/
|
|
262
267
|
bodyContains: z.string().min(1).optional(),
|
|
268
|
+
/**
|
|
269
|
+
* A substring the REQUEST body must contain — the other half of `bodyContains`.
|
|
270
|
+
*
|
|
271
|
+
* `bodyContains` deliberately searches only the response, and the comment above says why:
|
|
272
|
+
* searching the request too would let it pass on the very defect it exists to catch. That
|
|
273
|
+
* reasoning is sound and it leaves a real gap. For a filter, a search or a form, the thing
|
|
274
|
+
* under test IS the outgoing payload: "applying this filter actually sends it" had no
|
|
275
|
+
* verdict at all, only `reticle_network { bodies: true }` and a human reading `requestBody`
|
|
276
|
+
* by eye (#798).
|
|
277
|
+
*
|
|
278
|
+
* A separate field rather than a mode on `bodyContains`, so neither assertion can ever be
|
|
279
|
+
* satisfied by the wrong half of the exchange.
|
|
280
|
+
*
|
|
281
|
+
* Requires body capture, and says so when the request body was never recorded rather than
|
|
282
|
+
* reporting an ordinary mismatch.
|
|
283
|
+
*/
|
|
284
|
+
requestBodyContains: z.string().min(1).optional(),
|
|
285
|
+
/**
|
|
286
|
+
* Shallow JSON match over the REQUEST body, keyed like `signal.dataMatches` and sharing its
|
|
287
|
+
* `matchValue` operators (`*`, `$gte`, `$contains`, …).
|
|
288
|
+
*
|
|
289
|
+
* `{ requestBodyMatches: { filter: "manual_review" } }` is the one-call verdict for "the UI
|
|
290
|
+
* put the selected value into the payload", and it does not care about key order,
|
|
291
|
+
* whitespace or how the client serialised the object — all of which a substring does.
|
|
292
|
+
*
|
|
293
|
+
* A field whose captured value was REDACTED cannot be matched, and the verdict says so
|
|
294
|
+
* instead of reporting a mismatch: a redacted field is unknown, not different.
|
|
295
|
+
*/
|
|
296
|
+
requestBodyMatches: z.record(z.string(), z.unknown()).optional(),
|
|
263
297
|
})
|
|
264
298
|
.strict(),
|
|
265
299
|
z
|
|
@@ -50,6 +50,8 @@ export interface PredicateSession {
|
|
|
50
50
|
* Optional: a fake that never throttles simply omits it.
|
|
51
51
|
*/
|
|
52
52
|
throttled?(): boolean;
|
|
53
|
+
/** Precondition failure reason (e.g. seedStorage), if any. */
|
|
54
|
+
preconditionFailure?(): string | undefined;
|
|
53
55
|
}
|
|
54
56
|
export declare function evaluatePredicate(session: PredicateSession, predicate: Predicate, since?: number, diagnose?: boolean): Promise<EvalResult>;
|
|
55
57
|
/**
|
package/dist/events/predicate.js
CHANGED
|
@@ -2,13 +2,12 @@ import { ElementState, PredicateKind, ReticleCommand, THROTTLED_STARVED_NOTE, is
|
|
|
2
2
|
import { log } from '../log.js';
|
|
3
3
|
import { bindSpanContext } from '../trace.js';
|
|
4
4
|
import { selectPath, capDepth } from '../session/state-select.js';
|
|
5
|
-
import { describeTestidMiss } from './testid-near-miss.js';
|
|
6
|
-
import { describeSplitTextMiss } from './split-text-miss.js';
|
|
7
5
|
import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
|
|
8
6
|
import { isAmbient, ambientKeyOf } from '../journal/ambient.js';
|
|
9
7
|
import { evalRoute } from './predicate-route.js';
|
|
10
8
|
import { describeSuperseded } from './observed-in-window.js';
|
|
11
|
-
import {
|
|
9
|
+
import { evalElement } from './predicate-element.js';
|
|
10
|
+
import { PredicateSchema, matchValue, evalNet, evalConsole, evalAnimation, evalSignal, evalSettled, } from './predicate-eval.js';
|
|
12
11
|
export { PredicateSchema, PredicateKind };
|
|
13
12
|
/**
|
|
14
13
|
* A composite result that carries a child's "nobody could evaluate this" up to the verdict rule.
|
|
@@ -22,155 +21,6 @@ function unreadableComposite(child, evidence) {
|
|
|
22
21
|
const reason = child.inconclusive ?? 'a sub-predicate could not be evaluated';
|
|
23
22
|
return { pass: false, failureReason: reason, inconclusive: reason, evidence };
|
|
24
23
|
}
|
|
25
|
-
async function matchOnce(session, query, state) {
|
|
26
|
-
const res = await session.command(ReticleCommand.MATCH, { query, state });
|
|
27
|
-
if (!res.ok)
|
|
28
|
-
return { matched: false, count: 0, elements: [] };
|
|
29
|
-
return (res.result ?? { matched: false, count: 0, elements: [] });
|
|
30
|
-
}
|
|
31
|
-
async function evalElement(session, query, state, absent, diagnose) {
|
|
32
|
-
// Fields the browser's locator would have DROPPED, enforced back here — see residualQueryChecks.
|
|
33
|
-
// Checked before the round-trip when nothing can enforce them: a predicate that cannot be evaluated
|
|
34
|
-
// must say so rather than resolve to whatever the surviving half of it happened to match.
|
|
35
|
-
const residual = residualQueryChecks(query);
|
|
36
|
-
if (residual.unusable.length > 0) {
|
|
37
|
-
const reason = `the element locator ignores ${residual.unusable.map((f) => `\`${f}\``).join(', ')} ` +
|
|
38
|
-
`in ${JSON.stringify(query)} — it resolves by the first of by+value, component/source, role, ` +
|
|
39
|
-
'text, label, placeholder, testid, alt that is present, and nothing here can check the rest. ' +
|
|
40
|
-
'Assert them one locator at a time, or move the extra field into the locator';
|
|
41
|
-
return { pass: false, failureReason: reason, inconclusive: reason };
|
|
42
|
-
}
|
|
43
|
-
let match = await matchOnce(session, query, state);
|
|
44
|
-
const subject = JSON.stringify(query);
|
|
45
|
-
// A residual narrows the SET; `count` is every match while `elements` is only the described prefix,
|
|
46
|
-
// so a locator broad enough to be truncated cannot be narrowed honestly. Say so instead of guessing.
|
|
47
|
-
if (residual.checks.length > 0 && match.count > match.elements.length) {
|
|
48
|
-
const reason = `${String(match.count)} elements matched ${subject} and only ${String(match.elements.length)} were described, so ${residual.checks.map(([f]) => `\`${f}\``).join(', ')} could not be checked against all of them — narrow the locator`;
|
|
49
|
-
return { pass: false, failureReason: reason, inconclusive: reason };
|
|
50
|
-
}
|
|
51
|
-
const kept = match.elements.filter((element) => satisfiesResiduals(element, residual.checks));
|
|
52
|
-
// The locator found something and the dropped fields disagree with it. Reported separately from a
|
|
53
|
-
// plain miss because the fixes are opposite: the element IS there, its value is not what was claimed.
|
|
54
|
-
if (residual.checks.length > 0 && match.matched && 0 === kept.length && !absent) {
|
|
55
|
-
const wanted = residual.checks.map(([f, want]) => `${f}=${JSON.stringify(want)}`).join(', ');
|
|
56
|
-
return {
|
|
57
|
-
pass: false,
|
|
58
|
-
failureReason: `element matching ${subject} is present but ${wanted} does not hold`,
|
|
59
|
-
observed: match.elements
|
|
60
|
-
.map((element) => residual.checks.map(([f]) => describeResidual(element, f)).join(', '))
|
|
61
|
-
.join('; '),
|
|
62
|
-
expected: `an element matching ${subject} with ${wanted}`,
|
|
63
|
-
assertion: `element.${residual.checks[0]?.[0] ?? 'residual'}`,
|
|
64
|
-
evidence: match.elements,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
if (residual.checks.length > 0) {
|
|
68
|
-
match = { ...match, matched: kept.length > 0, count: kept.length, elements: kept };
|
|
69
|
-
}
|
|
70
|
-
// A given-but-missing scope is handled ASYMMETRICALLY, because "absent" and "present" ask different
|
|
71
|
-
// questions of a scope that no longer exists:
|
|
72
|
-
// - ABSENT: an element is trivially absent from a container that isn't there. This is also the
|
|
73
|
-
// everyday "wait for the #overlay/#spinner/#modal to disappear" pattern (scope the wait to the
|
|
74
|
-
// node being removed) — treating scopeMissing as a hard fail there burned the whole timeout and
|
|
75
|
-
// flipped a correct green to red. So scopeMissing satisfies an absence check.
|
|
76
|
-
// - PRESENT: you cannot confirm an element is present inside a scope that resolved to nothing, and
|
|
77
|
-
// silently widening to the whole page is the original false green. So scopeMissing FAILS presence
|
|
78
|
-
// (on the wait_for path this just keeps polling until the scope appears).
|
|
79
|
-
if (absent) {
|
|
80
|
-
if (true === match.scopeMissing) {
|
|
81
|
-
return { pass: true, evidence: { absent: true, scopeMissing: true } };
|
|
82
|
-
}
|
|
83
|
-
return match.matched
|
|
84
|
-
? {
|
|
85
|
-
pass: false,
|
|
86
|
-
failureReason: `expected element to be absent but found ${String(match.count)}`,
|
|
87
|
-
observed: `${String(match.count)} element(s) matching ${subject}`,
|
|
88
|
-
expected: `no element matching ${subject}`,
|
|
89
|
-
assertion: 'element.absent',
|
|
90
|
-
evidence: match.elements,
|
|
91
|
-
}
|
|
92
|
-
: { pass: true, evidence: { absent: true } };
|
|
93
|
-
}
|
|
94
|
-
if (true === match.scopeMissing) {
|
|
95
|
-
return {
|
|
96
|
-
pass: false,
|
|
97
|
-
failureReason: `scope resolved to no element — cannot confirm ${subject} is present`,
|
|
98
|
-
observed: 'the requested scope is not on the page (unmounted or selector matched nothing)',
|
|
99
|
-
expected: `an element matching ${subject} within an existing scope`,
|
|
100
|
-
assertion: 'element.present',
|
|
101
|
-
evidence: { scopeMissing: true },
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
if (match.matched)
|
|
105
|
-
return { pass: true, evidence: match.elements };
|
|
106
|
-
// The near-miss diagnostic below costs one or two EXTRA MATCH round-trips. It only enriches a FAILED
|
|
107
|
-
// verdict, and a wait loop's interim rechecks read nothing but `pass` — so on the poll path (diagnose
|
|
108
|
-
// false) skip straight to the plain fail. Under an event flood a role+name element wait was firing
|
|
109
|
-
// two live-DOM scans per recheck for a diagnostic no interim eval ever reads; the final timeout eval
|
|
110
|
-
// still runs with diagnose=true and produces the full near-miss.
|
|
111
|
-
if (!diagnose) {
|
|
112
|
-
return {
|
|
113
|
-
pass: false,
|
|
114
|
-
failureReason: `no element matched ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
|
|
115
|
-
observed: 'no matching element on the page',
|
|
116
|
-
expected: `an element matching ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
|
|
117
|
-
assertion: 'element.present',
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
// Diagnostic near-miss: was it there but in the wrong state, or a similar element present?
|
|
121
|
-
if (state !== undefined) {
|
|
122
|
-
const relaxed = await matchOnce(session, query, undefined);
|
|
123
|
-
if (relaxed.matched) {
|
|
124
|
-
return {
|
|
125
|
-
pass: false,
|
|
126
|
-
failureReason: `element exists but not in state '${state}'`,
|
|
127
|
-
observed: `element matching ${subject} is present, states: ${relaxed.elements[0]?.states.join(', ') ?? 'unknown'}`,
|
|
128
|
-
expected: `element matching ${subject} in state '${state}'`,
|
|
129
|
-
assertion: 'element.state',
|
|
130
|
-
evidence: { nearMiss: relaxed.elements },
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (query.role !== undefined && query.name !== undefined) {
|
|
135
|
-
const roleOnly = await matchOnce(session, { role: query.role }, state);
|
|
136
|
-
if (roleOnly.matched) {
|
|
137
|
-
return {
|
|
138
|
-
pass: false,
|
|
139
|
-
failureReason: `no '${query.role}' named '${query.name}'; saw: ${roleOnly.elements
|
|
140
|
-
.map((e) => e.name)
|
|
141
|
-
.filter((n) => n.length > 0)
|
|
142
|
-
.join(', ')}`,
|
|
143
|
-
observed: `${String(roleOnly.count)} '${query.role}' element(s), named: ${roleOnly.elements
|
|
144
|
-
.map((e) => e.name)
|
|
145
|
-
.filter((n) => n.length > 0)
|
|
146
|
-
.join(', ')}`,
|
|
147
|
-
expected: `a '${query.role}' named '${query.name}'`,
|
|
148
|
-
assertion: 'element.role+name',
|
|
149
|
-
evidence: { nearMiss: roleOnly.elements },
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// The testid near-miss: name what IS here, so a typo is one step from fixed rather than a dead
|
|
154
|
-
// end. reticle_query has always done this; the predicate path had no equivalent. See
|
|
155
|
-
// testid-near-miss.ts.
|
|
156
|
-
const present = match.hint?.presentTestids ?? [];
|
|
157
|
-
const alsoHere = query.testid === undefined ? undefined : describeTestidMiss(query.testid, present);
|
|
158
|
-
// A text miss where the string is on the page but split across children reads exactly like an
|
|
159
|
-
// element that never rendered. Naming the container is the difference between a retry and a bug
|
|
160
|
-
// report against working code. See split-text-miss.ts.
|
|
161
|
-
const splitText = describeSplitTextMiss(match.hint?.splitText);
|
|
162
|
-
const clause = splitText ?? (alsoHere === undefined || '' === alsoHere ? undefined : alsoHere);
|
|
163
|
-
const suffix = clause === undefined ? '' : ` — ${clause}`;
|
|
164
|
-
return {
|
|
165
|
-
pass: false,
|
|
166
|
-
failureReason: `no element matched ${subject}${state === undefined ? '' : ` in state '${state}'`}${suffix}`,
|
|
167
|
-
observed: `no matching element on the page${suffix}`,
|
|
168
|
-
expected: `an element matching ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
|
|
169
|
-
assertion: 'element.present',
|
|
170
|
-
...(present.length > 0 ? { evidence: { presentTestids: present } } : {}),
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
/** The single element of a list, or undefined when there is not exactly one. */
|
|
174
24
|
function oneOf(names) {
|
|
175
25
|
return 1 === names.length ? names[0] : undefined;
|
|
176
26
|
}
|
|
@@ -439,17 +289,48 @@ function predicateSince(predicate) {
|
|
|
439
289
|
* Idempotent, and a more specific `inconclusive` (unreadable locator, superseded window) is never
|
|
440
290
|
* overwritten.
|
|
441
291
|
*/
|
|
442
|
-
function annotateThrottledMiss(session, result) {
|
|
292
|
+
function annotateThrottledMiss(session, predicate, result) {
|
|
443
293
|
if (result.pass)
|
|
444
294
|
return result;
|
|
295
|
+
if (result.inconclusive !== undefined)
|
|
296
|
+
return result;
|
|
297
|
+
const preconditionFailure = session.preconditionFailure?.();
|
|
298
|
+
if (preconditionFailure !== undefined) {
|
|
299
|
+
return { ...result, inconclusive: preconditionFailure };
|
|
300
|
+
}
|
|
445
301
|
if (true !== session.throttled?.())
|
|
446
302
|
return result;
|
|
447
|
-
if (
|
|
303
|
+
if (failureRestsOnSeeing(predicate))
|
|
448
304
|
return result;
|
|
449
305
|
return { ...result, inconclusive: THROTTLED_STARVED_NOTE };
|
|
450
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Does this predicate FAIL by having seen something, rather than by not having seen it?
|
|
309
|
+
*
|
|
310
|
+
* The starved-tab caveat only applies to a negative reading. Throttling can stop the tab rendering,
|
|
311
|
+
* so "I did not find it" may mean "I could not look" — but nothing about a starved tab conjures
|
|
312
|
+
* elements that were not there, so "I found 13 of them" is as true on a throttled tab as anywhere.
|
|
313
|
+
* An `absent: true` predicate inverts exactly that: its failure IS the positive observation.
|
|
314
|
+
*
|
|
315
|
+
* Getting this wrong understated real product failures. An absence assertion that matched a
|
|
316
|
+
* framework debug page's `ProgrammingError` heading came back `unknown`, and `unknown` is what an
|
|
317
|
+
* agent re-drives or walks away from — so the proof it was holding never reached anybody.
|
|
318
|
+
*
|
|
319
|
+
* `not` flips the polarity again, and nests, so this recurses rather than checking one level.
|
|
320
|
+
* Composites (`allOf` / `anyOf`) deliberately fall through to `false`: a composite fails for a
|
|
321
|
+
* reason this cannot name, and keeping the caveat is the conservative half of the trade — an
|
|
322
|
+
* over-cautious `unknown` costs a re-drive, a missing one costs a wrong verdict.
|
|
323
|
+
*/
|
|
324
|
+
function failureRestsOnSeeing(predicate) {
|
|
325
|
+
if (PredicateKind.NOT === predicate.kind)
|
|
326
|
+
return !failureRestsOnSeeing(predicate.predicate);
|
|
327
|
+
if ('absent' in predicate && true === predicate.absent)
|
|
328
|
+
return true;
|
|
329
|
+
// `count: 0` is absence written as arithmetic, and fails the same way: by matching something.
|
|
330
|
+
return 'count' in predicate && 0 === predicate.count;
|
|
331
|
+
}
|
|
451
332
|
export async function evaluatePredicate(session, predicate, since = 0, diagnose = true) {
|
|
452
|
-
return annotateThrottledMiss(session, await evaluatePredicateRaw(session, predicate, since, diagnose));
|
|
333
|
+
return annotateThrottledMiss(session, predicate, await evaluatePredicateRaw(session, predicate, since, diagnose));
|
|
453
334
|
}
|
|
454
335
|
async function evaluatePredicateRaw(session, predicate, since = 0,
|
|
455
336
|
// Compute the (extra-round-trip) near-miss diagnostics on element failures. Default true so a
|
|
@@ -492,11 +373,17 @@ diagnose = true) {
|
|
|
492
373
|
// with only `text` filled in, so scoping it needs the field, not a second code path.
|
|
493
374
|
undefined === predicate.scope
|
|
494
375
|
? { text: predicate.contains }
|
|
495
|
-
: {
|
|
376
|
+
: {
|
|
377
|
+
text: predicate.contains,
|
|
378
|
+
scope: predicate.scope,
|
|
379
|
+
...(true === predicate.self ? { self: true } : {}),
|
|
380
|
+
}, true === predicate.visible ? ElementState.VISIBLE : undefined, predicate.absent ?? false, diagnose);
|
|
496
381
|
case PredicateKind.NET:
|
|
497
382
|
return evalNet(events, predicate);
|
|
498
383
|
case PredicateKind.ROUTE:
|
|
499
|
-
|
|
384
|
+
// `since` so an unanswered request already in flight before this window is not counted
|
|
385
|
+
// against the app — see unansweredIn.
|
|
386
|
+
return evalRoute(events, predicate, session.url, Math.max(since, predicateSince(predicate)));
|
|
500
387
|
case PredicateKind.CONSOLE:
|
|
501
388
|
return evalConsole(events, predicate);
|
|
502
389
|
case PredicateKind.ANIMATION:
|
|
@@ -605,7 +492,13 @@ function assertsExactCount(predicate) {
|
|
|
605
492
|
}
|
|
606
493
|
if (PredicateKind.NOT === predicate.kind)
|
|
607
494
|
return assertsExactCount(predicate.predicate);
|
|
608
|
-
|
|
495
|
+
// `signal` carries `count` for the same reason `net` does, and for the same defect: its schema
|
|
496
|
+
// calls the double-fire "the defect no state-only oracle can see", because a handler wired twice
|
|
497
|
+
// leaves the store in the right shape and a presence check green on both. Reading `net` alone
|
|
498
|
+
// meant a signal count resolved on its first match, so `count: 1` silently meant "at least 1" on
|
|
499
|
+
// the one channel able to see the bug. `evalSignal` counts correctly; the wait stopped early.
|
|
500
|
+
return ((PredicateKind.NET === predicate.kind || PredicateKind.SIGNAL === predicate.kind) &&
|
|
501
|
+
predicate.count !== undefined);
|
|
609
502
|
}
|
|
610
503
|
/**
|
|
611
504
|
* Evaluate now, else wait for it to become true (on each event + a poll) until timeout. `since` is
|
|
@@ -792,7 +685,7 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
792
685
|
// `{ pass, evidence, failureReason }` construction DISCARDED them on every timed-out wait and
|
|
793
686
|
// assert. So the highest-value localization signal was computed and then thrown away exactly
|
|
794
687
|
// on the failure path where it matters, no matter what the schema declared.
|
|
795
|
-
finish(annotateThrottledMiss(session, {
|
|
688
|
+
finish(annotateThrottledMiss(session, predicate, {
|
|
796
689
|
...r,
|
|
797
690
|
pass: false,
|
|
798
691
|
failureReason: r.failureReason ?? 'timed out waiting for predicate',
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* only, so the channel an agent is most likely to copy from produces the one locator this query
|
|
13
13
|
* cannot resolve.
|
|
14
14
|
*
|
|
15
|
-
* Naming the container ends it in one step: `ref` is a locator, and
|
|
16
|
-
*
|
|
15
|
+
* Naming the container ends it in one step: `ref` is a locator, and a scoped text predicate with
|
|
16
|
+
* `self: true` checks the container's combined subtree text. The recovery is a predicate the agent
|
|
17
|
+
* can paste rather than another guess, and it is serialized as JSON so the schema test can parse it.
|
|
17
18
|
*/
|
|
18
19
|
/** The container descriptor the browser attaches to a missed text query. */
|
|
19
|
-
|
|
20
|
+
interface SplitTextOwner {
|
|
20
21
|
ref?: string;
|
|
21
22
|
role?: string;
|
|
22
23
|
name?: string;
|
|
@@ -27,4 +28,5 @@ export interface SplitTextOwner {
|
|
|
27
28
|
* Undefined — not a vaguer sentence — when the browser found no container: an ordinary miss keeps the
|
|
28
29
|
* short message it already had. A clause that fires on every failure stops carrying information.
|
|
29
30
|
*/
|
|
30
|
-
export declare function describeSplitTextMiss(owner: SplitTextOwner | undefined): string | undefined;
|
|
31
|
+
export declare function describeSplitTextMiss(owner: SplitTextOwner | undefined, searchedText?: string): string | undefined;
|
|
32
|
+
export {};
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
* only, so the channel an agent is most likely to copy from produces the one locator this query
|
|
13
13
|
* cannot resolve.
|
|
14
14
|
*
|
|
15
|
-
* Naming the container ends it in one step: `ref` is a locator, and
|
|
16
|
-
*
|
|
15
|
+
* Naming the container ends it in one step: `ref` is a locator, and a scoped text predicate with
|
|
16
|
+
* `self: true` checks the container's combined subtree text. The recovery is a predicate the agent
|
|
17
|
+
* can paste rather than another guess, and it is serialized as JSON so the schema test can parse it.
|
|
17
18
|
*/
|
|
19
|
+
import { PredicateKind } from '@reticlehq/core';
|
|
18
20
|
/** Enough of the label to recognise it; the message is read in a tool result, not a log. */
|
|
19
21
|
const MAX_TEXT = 60;
|
|
20
22
|
/** What the container is, in the fewest words that still identify it on the page. */
|
|
@@ -32,13 +34,18 @@ function truncate(value) {
|
|
|
32
34
|
* Undefined — not a vaguer sentence — when the browser found no container: an ordinary miss keeps the
|
|
33
35
|
* short message it already had. A clause that fires on every failure stops carrying information.
|
|
34
36
|
*/
|
|
35
|
-
export function describeSplitTextMiss(owner) {
|
|
37
|
+
export function describeSplitTextMiss(owner, searchedText) {
|
|
36
38
|
if (owner === undefined)
|
|
37
39
|
return undefined;
|
|
38
40
|
const where = nameOf(owner);
|
|
39
41
|
const scope = owner.ref !== undefined && owner.ref.length > 0 ? owner.ref : undefined;
|
|
40
|
-
const recovery = scope === undefined
|
|
42
|
+
const recovery = scope === undefined || searchedText === undefined
|
|
41
43
|
? 'scope to that container and match it as a whole'
|
|
42
|
-
: `retry as
|
|
44
|
+
: `retry as ${JSON.stringify({
|
|
45
|
+
kind: PredicateKind.TEXT,
|
|
46
|
+
contains: searchedText,
|
|
47
|
+
scope,
|
|
48
|
+
self: true,
|
|
49
|
+
})}`;
|
|
43
50
|
return `that text IS on the page, split across the children of ${where} — no single element's own text carries it, so no text query can match it; ${recovery}`;
|
|
44
51
|
}
|