@reticlehq/server 2.4.0 → 2.5.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 +702 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/{cli-doctor.js → cli/cli-doctor.js} +9 -2
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +174 -92
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +49 -0
- package/dist/daemon/daemon-resilience.js +191 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +80 -48
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +5 -7
- package/dist/flows/flow-replay.js +114 -47
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +16 -3
- package/dist/honesty/blind-spots.js +24 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +37 -0
- package/dist/init/cra.js +90 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +3 -2
- package/dist/init/detect.js +16 -5
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +52 -9
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +14 -2
- package/dist/session/session.js +69 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +84 -115
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +57 -32
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +88 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures.md +28 -0
- package/docs/flows.md +166 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
|
@@ -1,84 +1,147 @@
|
|
|
1
|
-
import { ElementQuerySchema, ElementState, EventType, } from '@reticlehq/core';
|
|
1
|
+
import { ElementQuerySchema, ElementState, EventType, PredicateKind, StreamDirection, isDevToolingUrl, } from '@reticlehq/core';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { describeObserved } from './observed-in-window.js';
|
|
4
|
+
/**
|
|
5
|
+
* Spellings an agent plausibly reaches for, mapped to the real field.
|
|
6
|
+
*
|
|
7
|
+
* `route` spells its field `pathname` while `state`, in the same union, spells its `path`. Every one
|
|
8
|
+
* of these was silently DROPPED before, and because these kinds have all-optional fields, dropping
|
|
9
|
+
* the only key supplied left a predicate that asserts nothing and passes on anything.
|
|
10
|
+
*/
|
|
11
|
+
const PREDICATE_ALIASES = {
|
|
12
|
+
[PredicateKind.ROUTE]: { path: 'pathname' },
|
|
13
|
+
[PredicateKind.NET]: { url: 'urlContains' },
|
|
14
|
+
[PredicateKind.SIGNAL]: { data: 'dataMatches' },
|
|
15
|
+
};
|
|
16
|
+
/** Rename known aliases before parse; an explicit canonical key always wins. */
|
|
17
|
+
function applyPredicateAliases(input) {
|
|
18
|
+
if (typeof input !== 'object' || null === input || Array.isArray(input))
|
|
19
|
+
return input;
|
|
20
|
+
const obj = input;
|
|
21
|
+
const aliases = PREDICATE_ALIASES['string' === typeof obj['kind'] ? obj['kind'] : ''];
|
|
22
|
+
if (aliases === undefined)
|
|
23
|
+
return input;
|
|
24
|
+
const out = { ...obj };
|
|
25
|
+
for (const [from, to] of Object.entries(aliases)) {
|
|
26
|
+
if (out[from] === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
if (out[to] === undefined)
|
|
29
|
+
out[to] = out[from];
|
|
30
|
+
delete out[from];
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Strict on every branch. A key that is nobody's spelling is now a schema error naming it, instead of
|
|
36
|
+
* a stripped field and a green — see predicate-strict.test.ts for the MCP session that found this.
|
|
37
|
+
*/
|
|
38
|
+
export const PredicateSchema = z.lazy(() => z.preprocess(applyPredicateAliases, z.discriminatedUnion('kind', [
|
|
39
|
+
z
|
|
40
|
+
.object({
|
|
41
|
+
kind: z.literal(PredicateKind.ELEMENT),
|
|
6
42
|
query: ElementQuerySchema,
|
|
7
43
|
state: z.nativeEnum(ElementState).optional(),
|
|
8
44
|
absent: z.boolean().optional(),
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
|
|
45
|
+
})
|
|
46
|
+
.strict(),
|
|
47
|
+
z
|
|
48
|
+
.object({
|
|
49
|
+
kind: z.literal(PredicateKind.TEXT),
|
|
12
50
|
contains: z.string(),
|
|
13
51
|
visible: z.boolean().optional(),
|
|
14
52
|
absent: z.boolean().optional(),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
|
|
53
|
+
})
|
|
54
|
+
.strict(),
|
|
55
|
+
z
|
|
56
|
+
.object({
|
|
57
|
+
kind: z.literal(PredicateKind.NET),
|
|
18
58
|
method: z.string().optional(),
|
|
19
59
|
urlContains: z.string().optional(),
|
|
20
60
|
status: z.number().optional(),
|
|
21
61
|
ok: z.boolean().optional(),
|
|
22
62
|
since: z.number().optional(),
|
|
23
63
|
count: z.number().int().nonnegative().optional(),
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
|
|
64
|
+
})
|
|
65
|
+
.strict(),
|
|
66
|
+
z
|
|
67
|
+
.object({
|
|
68
|
+
kind: z.literal(PredicateKind.ROUTE),
|
|
27
69
|
pathname: z.string().optional(),
|
|
28
70
|
contains: z.string().optional(),
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
71
|
+
since: z.number().optional(),
|
|
72
|
+
})
|
|
73
|
+
.strict(),
|
|
74
|
+
z
|
|
75
|
+
.object({
|
|
76
|
+
kind: z.literal(PredicateKind.CONSOLE),
|
|
32
77
|
level: z.string().optional(),
|
|
33
78
|
absent: z.boolean().optional(),
|
|
34
79
|
since: z.number().optional(),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
|
|
80
|
+
})
|
|
81
|
+
.strict(),
|
|
82
|
+
z
|
|
83
|
+
.object({
|
|
84
|
+
kind: z.literal(PredicateKind.ANIMATION),
|
|
38
85
|
name: z.string().optional(),
|
|
39
86
|
target: z.string().optional(),
|
|
40
87
|
completed: z.boolean().optional(),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
88
|
+
since: z.number().optional(),
|
|
89
|
+
})
|
|
90
|
+
.strict(),
|
|
91
|
+
z
|
|
92
|
+
.object({
|
|
93
|
+
kind: z.literal(PredicateKind.SIGNAL),
|
|
44
94
|
name: z.string().optional(),
|
|
45
95
|
dataMatches: z.record(z.unknown()).optional(),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
96
|
+
since: z.number().optional(),
|
|
97
|
+
})
|
|
98
|
+
.strict(),
|
|
99
|
+
z
|
|
100
|
+
.object({
|
|
101
|
+
kind: z.literal(PredicateKind.STATE),
|
|
49
102
|
store: z.string().optional(),
|
|
50
103
|
path: z.string(),
|
|
51
104
|
equals: z.unknown().optional(),
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
z
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
105
|
+
})
|
|
106
|
+
.strict(),
|
|
107
|
+
z
|
|
108
|
+
.object({
|
|
109
|
+
kind: z.literal(PredicateKind.SETTLED),
|
|
110
|
+
quietMs: z.number().positive().optional(),
|
|
111
|
+
})
|
|
112
|
+
.strict(),
|
|
113
|
+
z
|
|
114
|
+
.object({ kind: z.literal(PredicateKind.ALL_OF), predicates: z.array(PredicateSchema) })
|
|
115
|
+
.strict(),
|
|
116
|
+
z
|
|
117
|
+
.object({ kind: z.literal(PredicateKind.ANY_OF), predicates: z.array(PredicateSchema) })
|
|
118
|
+
.strict(),
|
|
119
|
+
z.object({ kind: z.literal(PredicateKind.NOT), predicate: PredicateSchema }).strict(),
|
|
120
|
+
])));
|
|
58
121
|
function str(value) {
|
|
59
|
-
return typeof value
|
|
122
|
+
return 'string' === typeof value ? value : undefined;
|
|
60
123
|
}
|
|
61
124
|
function num(value) {
|
|
62
|
-
return typeof value
|
|
125
|
+
return 'number' === typeof value ? value : undefined;
|
|
63
126
|
}
|
|
64
127
|
/**
|
|
65
128
|
* Match one value against a pattern. Supports `*` (present), strict equality, and operators:
|
|
66
129
|
* `{$gte,$lte,$gt,$lt}` (numbers), `{$contains}` (array membership or substring), `{$length}`.
|
|
67
130
|
*/
|
|
68
131
|
export function matchValue(got, want) {
|
|
69
|
-
if (
|
|
132
|
+
if ('*' === want)
|
|
70
133
|
return got !== undefined;
|
|
71
134
|
// An object is an OPERATOR container only if it actually carries a `$`-prefixed operator. An empty
|
|
72
135
|
// `{}` (or an object with no `$` key) used to enter this branch, iterate zero recognized operators,
|
|
73
136
|
// and `return true` — so `equals: {}` / `dataMatches: {status: {}}` was a green assertion that
|
|
74
137
|
// passed against ANYTHING, undefined included: the exact false green the oracle exists to catch.
|
|
75
138
|
// Without an operator it is a literal to compare, and falls through to strict equality below.
|
|
76
|
-
const ops = typeof want
|
|
139
|
+
const ops = 'object' === typeof want && want !== null && !Array.isArray(want)
|
|
77
140
|
? Object.entries(want)
|
|
78
141
|
: undefined;
|
|
79
142
|
if (ops !== undefined && ops.some(([op]) => op.startsWith('$'))) {
|
|
80
143
|
for (const [op, val] of ops) {
|
|
81
|
-
const n = typeof got
|
|
144
|
+
const n = 'number' === typeof got ? got : NaN;
|
|
82
145
|
switch (op) {
|
|
83
146
|
case '$gte':
|
|
84
147
|
if (!(n >= val))
|
|
@@ -101,7 +164,7 @@ export function matchValue(got, want) {
|
|
|
101
164
|
if (!got.includes(val))
|
|
102
165
|
return false;
|
|
103
166
|
}
|
|
104
|
-
else if (typeof got
|
|
167
|
+
else if ('string' === typeof got) {
|
|
105
168
|
if (!got.includes(String(val)))
|
|
106
169
|
return false;
|
|
107
170
|
}
|
|
@@ -110,7 +173,7 @@ export function matchValue(got, want) {
|
|
|
110
173
|
}
|
|
111
174
|
break;
|
|
112
175
|
case '$length':
|
|
113
|
-
if (!((Array.isArray(got) || typeof got
|
|
176
|
+
if (!((Array.isArray(got) || 'string' === typeof got) && got.length === val)) {
|
|
114
177
|
return false;
|
|
115
178
|
}
|
|
116
179
|
break;
|
|
@@ -140,7 +203,7 @@ function dataMatches(actual, pattern) {
|
|
|
140
203
|
* This exists so an agent can assert on the OUTCOME rather than on a number Reticle invented.
|
|
141
204
|
*/
|
|
142
205
|
function callSucceeded(data) {
|
|
143
|
-
if (typeof data['ok']
|
|
206
|
+
if ('boolean' === typeof data['ok'])
|
|
144
207
|
return data['ok'];
|
|
145
208
|
const status = num(data['status']);
|
|
146
209
|
return status === undefined || status < 400;
|
|
@@ -183,7 +246,11 @@ export function evalNet(events, p) {
|
|
|
183
246
|
: {
|
|
184
247
|
pass: false,
|
|
185
248
|
failureReason: `no network call matched ${JSON.stringify(p)}`,
|
|
186
|
-
|
|
249
|
+
// Same reasoning as the signal miss: "no matching call" cannot be told apart from "the app
|
|
250
|
+
// made no calls at all", and those need different fixes.
|
|
251
|
+
observed: describeObserved('calls', events
|
|
252
|
+
.filter((e) => e.type === EventType.NET_REQUEST)
|
|
253
|
+
.map((e) => `${str(e.data['method']) ?? 'GET'} ${str(e.data['url']) ?? ''}`)),
|
|
187
254
|
expected: `at least one call matching ${JSON.stringify(p)}`,
|
|
188
255
|
assertion: 'net.present',
|
|
189
256
|
};
|
|
@@ -257,17 +324,17 @@ export function evalConsole(events, p) {
|
|
|
257
324
|
e.type === EventType.CONSOLE_ERROR ||
|
|
258
325
|
e.type === EventType.ERROR_UNCAUGHT);
|
|
259
326
|
}
|
|
260
|
-
if (p.level
|
|
327
|
+
if ('error' === p.level)
|
|
261
328
|
return isErr;
|
|
262
329
|
return e.type === CONSOLE_LEVEL_TYPE[p.level];
|
|
263
330
|
});
|
|
264
|
-
if (p.absent
|
|
265
|
-
return matches.length
|
|
331
|
+
if (true === p.absent) {
|
|
332
|
+
return 0 === matches.length
|
|
266
333
|
? { pass: true, evidence: { absent: true } }
|
|
267
334
|
: {
|
|
268
335
|
pass: false,
|
|
269
336
|
failureReason: `expected no ${p.level ?? 'console'} entries but found ${String(matches.length)}`,
|
|
270
|
-
observed: `${String(matches.length)} ${p.level ?? 'console'} entr${matches.length
|
|
337
|
+
observed: `${String(matches.length)} ${p.level ?? 'console'} entr${1 === matches.length ? 'y' : 'ies'}`,
|
|
271
338
|
expected: `no ${p.level ?? 'console'} entries`,
|
|
272
339
|
assertion: 'console.absent',
|
|
273
340
|
evidence: matches.map((e) => e.data),
|
|
@@ -284,7 +351,7 @@ export function evalConsole(events, p) {
|
|
|
284
351
|
};
|
|
285
352
|
}
|
|
286
353
|
export function evalAnimation(events, p) {
|
|
287
|
-
const wantType = p.completed
|
|
354
|
+
const wantType = true === p.completed ? EventType.ANIM_END : EventType.ANIM_START;
|
|
288
355
|
const hit = events.find((e) => {
|
|
289
356
|
if (e.type !== wantType)
|
|
290
357
|
return false;
|
|
@@ -330,7 +397,9 @@ export function evalSignal(events, p) {
|
|
|
330
397
|
: `no signal matched ${JSON.stringify(p)}`,
|
|
331
398
|
observed: sameName.length > 0
|
|
332
399
|
? `signal '${p.name ?? '(any)'}' fired ${String(sameName.length)}x, payload: ${JSON.stringify(sameName[0])}`
|
|
333
|
-
:
|
|
400
|
+
: // Name what DID fire: a typo'd signal name and a genuinely dead action produce the same
|
|
401
|
+
// sentence otherwise, and the agent cannot tell them apart. See observed-in-window.ts.
|
|
402
|
+
`signal '${p.name ?? '(any)'}' never fired — ${describeObserved('signals', events.filter((e) => e.type === EventType.SIGNAL).map((e) => str(e.data['name']) ?? ''))}`,
|
|
334
403
|
expected: p.dataMatches === undefined
|
|
335
404
|
? `signal '${p.name ?? '(any)'}' to fire`
|
|
336
405
|
: `signal '${p.name ?? '(any)'}' with payload matching ${JSON.stringify(p.dataMatches)}`,
|
|
@@ -364,6 +433,12 @@ const SETTLE_ACTIVITY = new Set([
|
|
|
364
433
|
EventType.DOM_REMOVED,
|
|
365
434
|
EventType.DOM_ATTR,
|
|
366
435
|
]);
|
|
436
|
+
/** The three net-shaped event types a URL can be read off — the only ones dev tooling can produce. */
|
|
437
|
+
const NET_TYPES = new Set([
|
|
438
|
+
EventType.NET_PENDING,
|
|
439
|
+
EventType.NET_REQUEST,
|
|
440
|
+
EventType.NET_STREAM,
|
|
441
|
+
]);
|
|
367
442
|
/** Default quiet window — enough to absorb a render+xhr settle without waiting on slow polls. */
|
|
368
443
|
const DEFAULT_QUIET_MS = 500;
|
|
369
444
|
/**
|
|
@@ -372,8 +447,24 @@ const DEFAULT_QUIET_MS = 500;
|
|
|
372
447
|
* now, not to any buffered event — so `now` is injected (CLAUDE.md rule 7), and the wait loop's
|
|
373
448
|
* poll interval is what eventually flips this to pass once activity stops.
|
|
374
449
|
*/
|
|
375
|
-
export function evalSettled(
|
|
450
|
+
export function evalSettled(allEvents, p, now) {
|
|
376
451
|
const quietMs = p.quietMs ?? DEFAULT_QUIET_MS;
|
|
452
|
+
// Dev-tooling traffic is the framework talking about ITSELF (see DevToolingChannel) and says
|
|
453
|
+
// nothing about whether the app finished its work — but it was holding settle open. Dropped from
|
|
454
|
+
// both halves of the calculation below (in-flight AND the quiet timer) and DISCLOSED on the
|
|
455
|
+
// evidence, never silently swallowed.
|
|
456
|
+
const ignoredDevTooling = [];
|
|
457
|
+
const events = allEvents.filter((e) => {
|
|
458
|
+
if (!NET_TYPES.has(e.type))
|
|
459
|
+
return true;
|
|
460
|
+
const url = str(e.data['url']);
|
|
461
|
+
if (!isDevToolingUrl(url))
|
|
462
|
+
return true;
|
|
463
|
+
if (url !== undefined && !ignoredDevTooling.includes(url))
|
|
464
|
+
ignoredDevTooling.push(url);
|
|
465
|
+
return false;
|
|
466
|
+
});
|
|
467
|
+
const disclosure = 0 === ignoredDevTooling.length ? {} : { ignoredDevTooling };
|
|
377
468
|
// A request that STARTED (NET_PENDING) but never completed (NET_REQUEST with the same id) is
|
|
378
469
|
// still in flight — the page is NOT settled no matter how quiet the DOM has gone. Without this,
|
|
379
470
|
// a slow save reads as "settled" the instant its spinner stops mutating the DOM: the exact
|
|
@@ -417,16 +508,16 @@ export function evalSettled(events, p, now) {
|
|
|
417
508
|
if (id === undefined)
|
|
418
509
|
continue;
|
|
419
510
|
const direction = str(e.data['direction']);
|
|
420
|
-
if (
|
|
511
|
+
if (StreamDirection.OPEN === direction)
|
|
421
512
|
openStreams.add(id);
|
|
422
|
-
else if (
|
|
513
|
+
else if (StreamDirection.CLOSE === direction)
|
|
423
514
|
openStreams.delete(id);
|
|
424
515
|
}
|
|
425
516
|
const streaming = openStreams.size;
|
|
426
517
|
if (inFlight + streaming > 0) {
|
|
427
|
-
const what =
|
|
518
|
+
const what = 0 === streaming
|
|
428
519
|
? `${String(inFlight)} request(s) still in flight`
|
|
429
|
-
:
|
|
520
|
+
: 0 === inFlight
|
|
430
521
|
? `${String(streaming)} response body(ies) still streaming`
|
|
431
522
|
: `${String(inFlight)} request(s) in flight and ${String(streaming)} response body(ies) still streaming`;
|
|
432
523
|
return {
|
|
@@ -435,7 +526,12 @@ export function evalSettled(events, p, now) {
|
|
|
435
526
|
observed: what,
|
|
436
527
|
expected: 'no requests in flight and no response bodies still streaming',
|
|
437
528
|
assertion: 'settled.in-flight',
|
|
438
|
-
evidence: {
|
|
529
|
+
evidence: {
|
|
530
|
+
settled: false,
|
|
531
|
+
inFlight,
|
|
532
|
+
...(streaming > 0 ? { streaming } : {}),
|
|
533
|
+
...disclosure,
|
|
534
|
+
},
|
|
439
535
|
};
|
|
440
536
|
}
|
|
441
537
|
let lastT = -1;
|
|
@@ -449,12 +545,15 @@ export function evalSettled(events, p, now) {
|
|
|
449
545
|
if (lastT < 0) {
|
|
450
546
|
return {
|
|
451
547
|
pass: true,
|
|
452
|
-
evidence: { settled: true, quietForMs: null, note: 'no activity to settle' },
|
|
548
|
+
evidence: { settled: true, quietForMs: null, note: 'no activity to settle', ...disclosure },
|
|
453
549
|
};
|
|
454
550
|
}
|
|
455
551
|
const quietForMs = now - lastT;
|
|
456
552
|
if (quietForMs >= quietMs) {
|
|
457
|
-
return {
|
|
553
|
+
return {
|
|
554
|
+
pass: true,
|
|
555
|
+
evidence: { settled: true, quietForMs, lastActivity: lastType, ...disclosure },
|
|
556
|
+
};
|
|
458
557
|
}
|
|
459
558
|
return {
|
|
460
559
|
pass: false,
|
|
@@ -462,6 +561,11 @@ export function evalSettled(events, p, now) {
|
|
|
462
561
|
observed: `last activity was ${String(lastType)}, ${String(quietForMs)}ms ago`,
|
|
463
562
|
expected: `${String(quietMs)}ms of quiet`,
|
|
464
563
|
assertion: 'settled.quiet',
|
|
465
|
-
evidence: { quietForMs, lastActivity: lastType },
|
|
564
|
+
evidence: { quietForMs, lastActivity: lastType, ...disclosure },
|
|
565
|
+
// The one failure in this file that knows exactly when it could stop being one: if nothing else
|
|
566
|
+
// happens, the window closes in this many ms. A waiter that re-checks THEN instead of on its next
|
|
567
|
+
// blind tick stops paying up to a full poll interval on the hottest call in the product. Only a
|
|
568
|
+
// hint — the predicate is still evaluated at that moment, and can still fail.
|
|
569
|
+
retryAfterMs: quietMs - quietForMs,
|
|
466
570
|
};
|
|
467
571
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type CommandResult, type ReticleEvent } from '@reticlehq/core';
|
|
1
|
+
import { PredicateKind, type CommandResult, type ReticleEvent } from '@reticlehq/core';
|
|
2
2
|
import type { ExpectedLink } from '../capsule/divergence.js';
|
|
3
3
|
import { type AmbientCounts } from '../journal/ambient.js';
|
|
4
4
|
import { PredicateSchema, type Predicate, type EvalResult } from './predicate-eval.js';
|
|
5
|
-
export { PredicateSchema };
|
|
5
|
+
export { PredicateSchema, PredicateKind };
|
|
6
6
|
export type { Predicate, EvalResult };
|
|
7
7
|
/** The subset of Session the predicate engine needs — keeps it testable with a fake. */
|
|
8
8
|
export interface PredicateSession {
|
package/dist/events/predicate.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { ElementState, ReticleCommand, } from '@reticlehq/core';
|
|
1
|
+
import { ElementState, PredicateKind, ReticleCommand, } from '@reticlehq/core';
|
|
2
|
+
import { log } from '../log.js';
|
|
3
|
+
import { bindSpanContext } from '../trace.js';
|
|
2
4
|
import { selectPath, capDepth } from '../session/state-select.js';
|
|
5
|
+
import { describeTestidMiss } from './testid-near-miss.js';
|
|
3
6
|
import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
|
|
4
7
|
import { isAmbient, ambientKeyOf } from '../journal/ambient.js';
|
|
5
8
|
import { PredicateSchema, matchValue, evalNet, evalRoute, evalConsole, evalAnimation, evalSignal, evalSettled, } from './predicate-eval.js';
|
|
6
|
-
export { PredicateSchema };
|
|
9
|
+
export { PredicateSchema, PredicateKind };
|
|
7
10
|
async function matchOnce(session, query, state) {
|
|
8
11
|
const res = await session.command(ReticleCommand.MATCH, { query, state });
|
|
9
12
|
if (!res.ok)
|
|
@@ -23,7 +26,7 @@ async function evalElement(session, query, state, absent, diagnose) {
|
|
|
23
26
|
// silently widening to the whole page is the original false green. So scopeMissing FAILS presence
|
|
24
27
|
// (on the wait_for path this just keeps polling until the scope appears).
|
|
25
28
|
if (absent) {
|
|
26
|
-
if (match.scopeMissing
|
|
29
|
+
if (true === match.scopeMissing) {
|
|
27
30
|
return { pass: true, evidence: { absent: true, scopeMissing: true } };
|
|
28
31
|
}
|
|
29
32
|
return match.matched
|
|
@@ -37,7 +40,7 @@ async function evalElement(session, query, state, absent, diagnose) {
|
|
|
37
40
|
}
|
|
38
41
|
: { pass: true, evidence: { absent: true } };
|
|
39
42
|
}
|
|
40
|
-
if (match.scopeMissing
|
|
43
|
+
if (true === match.scopeMissing) {
|
|
41
44
|
return {
|
|
42
45
|
pass: false,
|
|
43
46
|
failureReason: `scope resolved to no element — cannot confirm ${subject} is present`,
|
|
@@ -96,12 +99,19 @@ async function evalElement(session, query, state, absent, diagnose) {
|
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
}
|
|
102
|
+
// The testid near-miss: name what IS here, so a typo is one step from fixed rather than a dead
|
|
103
|
+
// end. reticle_query has always done this; the predicate path had no equivalent. See
|
|
104
|
+
// testid-near-miss.ts.
|
|
105
|
+
const present = match.hint?.presentTestids ?? [];
|
|
106
|
+
const alsoHere = query.testid === undefined ? undefined : describeTestidMiss(query.testid, present);
|
|
107
|
+
const suffix = alsoHere === undefined || '' === alsoHere ? '' : ` — ${alsoHere}`;
|
|
99
108
|
return {
|
|
100
109
|
pass: false,
|
|
101
|
-
failureReason: `no element matched ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
|
|
102
|
-
observed:
|
|
110
|
+
failureReason: `no element matched ${subject}${state === undefined ? '' : ` in state '${state}'`}${suffix}`,
|
|
111
|
+
observed: `no matching element on the page${suffix}`,
|
|
103
112
|
expected: `an element matching ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
|
|
104
113
|
assertion: 'element.present',
|
|
114
|
+
...(present.length > 0 ? { evidence: { presentTestids: present } } : {}),
|
|
105
115
|
};
|
|
106
116
|
}
|
|
107
117
|
async function evalState(session, p) {
|
|
@@ -117,14 +127,15 @@ async function evalState(session, p) {
|
|
|
117
127
|
}
|
|
118
128
|
const stores = (res.result ?? {}).stores ?? {};
|
|
119
129
|
const names = Object.keys(stores);
|
|
120
|
-
const storeName = p.store ?? (names.length
|
|
130
|
+
const storeName = p.store ?? (1 === names.length ? names[0] : undefined);
|
|
121
131
|
if (storeName === undefined) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
// Neither of these is a finding about the app. With several stores registered the call simply did
|
|
133
|
+
// not say which one to read; with none, there is nothing to read at all. No assertion was
|
|
134
|
+
// evaluated either way, so this is inconclusive rather than failed — see honesty/inconclusive.
|
|
135
|
+
const reason = 0 === names.length
|
|
136
|
+
? 'no registered store to read state from'
|
|
137
|
+
: `multiple stores (${names.join(', ')}); name one with \`store\``;
|
|
138
|
+
return { pass: false, failureReason: reason, inconclusive: reason };
|
|
128
139
|
}
|
|
129
140
|
const selection = selectPath(stores[storeName], p.path);
|
|
130
141
|
if (!selection.found) {
|
|
@@ -153,31 +164,39 @@ async function evalState(session, p) {
|
|
|
153
164
|
evidence: { store: storeName, path: p.path, value: capDepth(selection.value, 1) },
|
|
154
165
|
};
|
|
155
166
|
}
|
|
167
|
+
/** The predicate's own event-time floor, if its kind carries one. */
|
|
168
|
+
function predicateSince(predicate) {
|
|
169
|
+
return 'since' in predicate && 'number' === typeof predicate.since ? predicate.since : 0;
|
|
170
|
+
}
|
|
156
171
|
export async function evaluatePredicate(session, predicate, since = 0,
|
|
157
172
|
// Compute the (extra-round-trip) near-miss diagnostics on element failures. Default true so a
|
|
158
173
|
// one-shot assert is fully diagnostic; the wait loop passes false on its interim polls (which read
|
|
159
174
|
// only `pass`) and true on the final timeout eval, so a flood no longer pays for a diagnostic nobody
|
|
160
175
|
// reads. Only element/text failures have a near-miss; everything else ignores this.
|
|
161
176
|
diagnose = true) {
|
|
162
|
-
|
|
177
|
+
// A predicate's own `since` is a TIGHTER event-time floor than the caller's — an agent that took
|
|
178
|
+
// `since` from the act it just performed is scoping the assertion to that action's aftermath. It is
|
|
179
|
+
// applied here, once, rather than in each eval: the floor means the same thing for every kind that
|
|
180
|
+
// reads the event stream, and net/console re-applying it is a no-op.
|
|
181
|
+
const events = session.eventsSince(Math.max(since, predicateSince(predicate)));
|
|
163
182
|
switch (predicate.kind) {
|
|
164
|
-
case
|
|
183
|
+
case PredicateKind.ELEMENT:
|
|
165
184
|
return evalElement(session, predicate.query, predicate.state, predicate.absent ?? false, diagnose);
|
|
166
|
-
case
|
|
167
|
-
return evalElement(session, { text: predicate.contains }, predicate.visible
|
|
168
|
-
case
|
|
185
|
+
case PredicateKind.TEXT:
|
|
186
|
+
return evalElement(session, { text: predicate.contains }, true === predicate.visible ? ElementState.VISIBLE : undefined, predicate.absent ?? false, diagnose);
|
|
187
|
+
case PredicateKind.NET:
|
|
169
188
|
return evalNet(events, predicate);
|
|
170
|
-
case
|
|
189
|
+
case PredicateKind.ROUTE:
|
|
171
190
|
return evalRoute(events, predicate);
|
|
172
|
-
case
|
|
191
|
+
case PredicateKind.CONSOLE:
|
|
173
192
|
return evalConsole(events, predicate);
|
|
174
|
-
case
|
|
193
|
+
case PredicateKind.ANIMATION:
|
|
175
194
|
return evalAnimation(events, predicate);
|
|
176
|
-
case
|
|
195
|
+
case PredicateKind.SIGNAL:
|
|
177
196
|
return evalSignal(events, predicate);
|
|
178
|
-
case
|
|
197
|
+
case PredicateKind.STATE:
|
|
179
198
|
return evalState(session, predicate);
|
|
180
|
-
case
|
|
199
|
+
case PredicateKind.SETTLED: {
|
|
181
200
|
// Drop events on learned-ambient regions (chat/ticker churn) before the settle check — by ref
|
|
182
201
|
// alone, NOT by attribution: window-attribution ("happened during the action window") is a time
|
|
183
202
|
// heuristic, never causation, so a chat message arriving mid-window must not hold settle open.
|
|
@@ -185,7 +204,7 @@ diagnose = true) {
|
|
|
185
204
|
const settleEvents = counts === undefined ? events : events.filter((e) => !isAmbient(counts, ambientKeyOf(e)));
|
|
186
205
|
return evalSettled(settleEvents, predicate, session.elapsed());
|
|
187
206
|
}
|
|
188
|
-
case
|
|
207
|
+
case PredicateKind.ALL_OF: {
|
|
189
208
|
const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since, diagnose)));
|
|
190
209
|
const failed = results.find((r) => !r.pass);
|
|
191
210
|
return failed === undefined
|
|
@@ -196,14 +215,14 @@ diagnose = true) {
|
|
|
196
215
|
evidence: results,
|
|
197
216
|
};
|
|
198
217
|
}
|
|
199
|
-
case
|
|
218
|
+
case PredicateKind.ANY_OF: {
|
|
200
219
|
const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since, diagnose)));
|
|
201
220
|
const passed = results.find((r) => r.pass);
|
|
202
221
|
return passed !== undefined
|
|
203
222
|
? { pass: true, evidence: passed.evidence }
|
|
204
223
|
: { pass: false, failureReason: 'no sub-predicate of anyOf matched', evidence: results };
|
|
205
224
|
}
|
|
206
|
-
case
|
|
225
|
+
case PredicateKind.NOT: {
|
|
207
226
|
const inner = await evaluatePredicate(session, predicate.predicate, since, diagnose);
|
|
208
227
|
return inner.pass
|
|
209
228
|
? { pass: false, failureReason: 'negated predicate unexpectedly held', evidence: inner }
|
|
@@ -244,12 +263,12 @@ const COUNT_CONFIRM_MS = 300;
|
|
|
244
263
|
* must stay that way, or every ordinary wait pays the confirmation delay for nothing.
|
|
245
264
|
*/
|
|
246
265
|
function assertsExactCount(predicate) {
|
|
247
|
-
if (
|
|
266
|
+
if (PredicateKind.ALL_OF === predicate.kind || PredicateKind.ANY_OF === predicate.kind) {
|
|
248
267
|
return predicate.predicates.some(assertsExactCount);
|
|
249
268
|
}
|
|
250
|
-
if (
|
|
269
|
+
if (PredicateKind.NOT === predicate.kind)
|
|
251
270
|
return assertsExactCount(predicate.predicate);
|
|
252
|
-
return
|
|
271
|
+
return PredicateKind.NET === predicate.kind && predicate.count !== undefined;
|
|
253
272
|
}
|
|
254
273
|
/**
|
|
255
274
|
* Evaluate now, else wait for it to become true (on each event + a poll) until timeout. `since` is
|
|
@@ -263,10 +282,24 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
263
282
|
failureReason: error instanceof Error ? error.message : String(error),
|
|
264
283
|
});
|
|
265
284
|
let cooldownTimer;
|
|
285
|
+
/** One-shot re-check timed to when a time-based predicate could first pass. See retryAfterMs. */
|
|
286
|
+
let hintTimer;
|
|
266
287
|
// An exact-count wait keeps watching after it first reads true — see COUNT_CONFIRM_MS.
|
|
267
288
|
const holdsForCount = assertsExactCount(predicate);
|
|
268
289
|
let confirming = false;
|
|
269
290
|
let confirmTimer;
|
|
291
|
+
/** Report a wait that could not run, and END it — see guardedCheck. */
|
|
292
|
+
const failWait = (error) => {
|
|
293
|
+
log('reticle_wait_failed', {
|
|
294
|
+
predicate: predicate.kind,
|
|
295
|
+
error: error instanceof Error ? error.message : String(error),
|
|
296
|
+
});
|
|
297
|
+
finish({
|
|
298
|
+
...failed(error),
|
|
299
|
+
failureReason: 'the wait could not be evaluated and was ended rather than left pending: ' +
|
|
300
|
+
(error instanceof Error ? error.message : String(error)),
|
|
301
|
+
});
|
|
302
|
+
};
|
|
270
303
|
const finish = (result) => {
|
|
271
304
|
if (done)
|
|
272
305
|
return;
|
|
@@ -276,6 +309,8 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
276
309
|
clearTimeout(timer);
|
|
277
310
|
if (cooldownTimer !== undefined)
|
|
278
311
|
clearTimeout(cooldownTimer);
|
|
312
|
+
if (hintTimer !== undefined)
|
|
313
|
+
clearTimeout(hintTimer);
|
|
279
314
|
if (confirmTimer !== undefined)
|
|
280
315
|
clearTimeout(confirmTimer);
|
|
281
316
|
resolve(result);
|
|
@@ -305,8 +340,19 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
305
340
|
// final timeout eval below runs with full diagnostics.
|
|
306
341
|
void evaluatePredicate(session, predicate, since, false)
|
|
307
342
|
.then((r) => {
|
|
308
|
-
if (!r.pass)
|
|
343
|
+
if (!r.pass) {
|
|
344
|
+
// A time-based failure knows when it could stop being one — re-check THEN rather than on
|
|
345
|
+
// the next blind tick. Without this, every `settled` wait paid up to a full poll interval
|
|
346
|
+
// of dead time after the quiet window had already closed: measured at 566–627ms across
|
|
347
|
+
// the fleet for a 500ms window, on the call an agent makes after almost every action.
|
|
348
|
+
// Additive — the backstop interval below still runs, so a missed hint costs nothing.
|
|
349
|
+
const hint = r.retryAfterMs;
|
|
350
|
+
if ('number' === typeof hint && hint > 0 && hint < POLL_INTERVAL_MS) {
|
|
351
|
+
clearTimeout(hintTimer);
|
|
352
|
+
hintTimer = setTimeout(check, hint);
|
|
353
|
+
}
|
|
309
354
|
return;
|
|
355
|
+
}
|
|
310
356
|
// "Exactly N" cannot be concluded from a passing sample — the count can still rise. Hold,
|
|
311
357
|
// re-evaluate WITH diagnostics, and let that second read be the verdict: if an N+1th
|
|
312
358
|
// arrived in the meantime it now fails, carrying observed/expected rather than a bare no.
|
|
@@ -344,10 +390,41 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
344
390
|
}, MIN_RECHECK_GAP_MS);
|
|
345
391
|
});
|
|
346
392
|
};
|
|
393
|
+
/**
|
|
394
|
+
* Run a re-check so that NOTHING can leave this promise pending.
|
|
395
|
+
*
|
|
396
|
+
* `check` is fired from an event listener and an interval, neither of which is inside the
|
|
397
|
+
* awaited chain. A synchronous throw there reached the process as an uncaughtException; a
|
|
398
|
+
* rejection reached it as an unhandledRejection. Either way the wait never resolved and the tool
|
|
399
|
+
* handler never returned — the agent sees a call that simply never comes back.
|
|
400
|
+
*
|
|
401
|
+
* That is the exact shape reported from a Plane (Next 14 + MobX) session, which tears the page
|
|
402
|
+
* session down and rebuilds it on EVERY navigation, so a `{kind:"route"}` predicate always races
|
|
403
|
+
* a teardown of the very session it is watching: `browser.command ok:true` for the click, and
|
|
404
|
+
* then no `tool.handler` for that callId, ever.
|
|
405
|
+
*
|
|
406
|
+
* A wait that cannot evaluate is a FAILED wait, not an eternal one. It now says so and finishes.
|
|
407
|
+
*/
|
|
408
|
+
const guardedCheck = () => {
|
|
409
|
+
try {
|
|
410
|
+
const pending = check();
|
|
411
|
+
if (pending instanceof Promise)
|
|
412
|
+
pending.catch(failWait);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
failWait(error);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
// Bound to the CALL that started this wait. The listener fires from the WebSocket message
|
|
419
|
+
// handler and the interval from the timer queue — neither is in the awaited chain, so without
|
|
420
|
+
// this every re-check opened a new call at depth 0 and emitted a `browser.command` with no
|
|
421
|
+
// `tool.handler`. Measured on one healthy run: 23 such orphans, which is precisely the
|
|
422
|
+
// documented signature of a HUNG call. A diagnostic that fires on healthy runs is not one.
|
|
423
|
+
const boundCheck = bindSpanContext(guardedCheck);
|
|
347
424
|
const unsub = session.onEvent(() => {
|
|
348
|
-
|
|
425
|
+
boundCheck();
|
|
349
426
|
});
|
|
350
|
-
const interval = setInterval(
|
|
427
|
+
const interval = setInterval(boundCheck, POLL_INTERVAL_MS);
|
|
351
428
|
const timer = setTimeout(() => {
|
|
352
429
|
void evaluatePredicate(session, predicate, since)
|
|
353
430
|
.then((r) => {
|
|
@@ -382,11 +459,11 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
382
459
|
* green ⇏ this-branch-held, re-checks each branch and keeps the winners.
|
|
383
460
|
*/
|
|
384
461
|
export async function provenExpectedLinks(session, predicate, since = 0) {
|
|
385
|
-
if (
|
|
462
|
+
if (PredicateKind.ALL_OF === predicate.kind) {
|
|
386
463
|
const per = await Promise.all(predicate.predicates.map((p) => provenExpectedLinks(session, p, since)));
|
|
387
464
|
return per.flat();
|
|
388
465
|
}
|
|
389
|
-
if (
|
|
466
|
+
if (PredicateKind.ANY_OF === predicate.kind) {
|
|
390
467
|
const per = await Promise.all(predicate.predicates.map(async (p) => (await evaluatePredicate(session, p, since)).pass
|
|
391
468
|
? provenExpectedLinks(session, p, since)
|
|
392
469
|
: []));
|