@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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a runtime value came from: state path ← the signal that preceded it ← the request behind it.
|
|
3
|
+
*
|
|
4
|
+
* The prior art this issue names (`firatorhan/sentinel`'s `get_lineage`) builds the same chain by
|
|
5
|
+
* picking "the most recent action whose payload contains this value", then "an API call whose type
|
|
6
|
+
* matches". That is CORRELATION, and for a devtool a human reads it is the right trade — the reader
|
|
7
|
+
* discounts it on sight.
|
|
8
|
+
*
|
|
9
|
+
* Reticle's output is read by an agent that acts on it, so the same chain presented as fact is the
|
|
10
|
+
* false-green shape this project exists to refuse. The join here is deliberately no cleverer than
|
|
11
|
+
* theirs; what is different is that every link carries how much it is worth:
|
|
12
|
+
*
|
|
13
|
+
* - a state path that changed is OBSERVED — we have the event;
|
|
14
|
+
* - "this signal caused it" is INFERRED, and the wording says so;
|
|
15
|
+
* - several candidates are NAMED rather than narrowed to the most recent, because choosing is the
|
|
16
|
+
* one thing a correlation cannot justify;
|
|
17
|
+
* - no candidate says the value was not seen to pass through one, and reaches for nothing.
|
|
18
|
+
*
|
|
19
|
+
* Read-only. Produces no verdict, is not reachable from an assert path, and changes nothing.
|
|
20
|
+
*/
|
|
21
|
+
import { EventType } from '@reticlehq/core';
|
|
22
|
+
import { asString } from '../tools/tools-helpers.js';
|
|
23
|
+
/**
|
|
24
|
+
* How far back to look for a cause.
|
|
25
|
+
*
|
|
26
|
+
* Wide enough for a fetch → dispatch → render sequence, narrow enough that an unrelated signal a
|
|
27
|
+
* page-load earlier cannot be offered as a candidate. Not a correctness bound: widening it produces
|
|
28
|
+
* MORE named candidates, never a wrong single answer, because ambiguity is reported rather than
|
|
29
|
+
* resolved.
|
|
30
|
+
*/
|
|
31
|
+
const CAUSE_WINDOW_MS = 5_000;
|
|
32
|
+
function stateChangesFor(events, query) {
|
|
33
|
+
return events.filter((e) => {
|
|
34
|
+
if (e.type !== EventType.STATE_CHANGE)
|
|
35
|
+
return false;
|
|
36
|
+
const path = asString(e.data['path']);
|
|
37
|
+
if (path === undefined || !path.includes(query.path))
|
|
38
|
+
return false;
|
|
39
|
+
if (query.value === undefined)
|
|
40
|
+
return true;
|
|
41
|
+
return String(e.data['value']) === query.value;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Events of one kind inside the causal window before `t`, oldest first. */
|
|
45
|
+
function precursors(events, type, t) {
|
|
46
|
+
return events.filter((e) => e.type === type && e.t < t && e.t >= t - CAUSE_WINDOW_MS);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Render an inferred link, hedged according to how many things could explain it.
|
|
50
|
+
*
|
|
51
|
+
* The wording IS the deliverable. `← USER_FETCH_SUCCESS` reads as fact; the two forms below read as
|
|
52
|
+
* what they are, and a reader who skims still sees the hedge because it is in the first few words.
|
|
53
|
+
*/
|
|
54
|
+
function inferredLink(kind, labels) {
|
|
55
|
+
const one = labels[0] ?? '';
|
|
56
|
+
if (1 === labels.length) {
|
|
57
|
+
return { kind, text: `← likely ${one} (inferred from timing, 1 candidate)`, observed: false };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
kind,
|
|
61
|
+
text: `← ambiguous: ${String(labels.length)} candidates (${labels.join(', ')}) — not resolved, because timing alone cannot choose between them`,
|
|
62
|
+
observed: false,
|
|
63
|
+
candidates: labels,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function signalLabel(e) {
|
|
67
|
+
return asString(e.data['name']) ?? '(unnamed signal)';
|
|
68
|
+
}
|
|
69
|
+
function netLabel(e) {
|
|
70
|
+
const method = asString(e.data['method']) ?? 'GET';
|
|
71
|
+
const url = asString(e.data['url']) ?? '(unknown url)';
|
|
72
|
+
const status = e.data['status'];
|
|
73
|
+
return `${method} ${url}${'number' === typeof status ? ` (${String(status)})` : ''}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The chain, most recent change first.
|
|
77
|
+
*
|
|
78
|
+
* Stops at the first link with nothing to point at, and says why. Walking past a gap would mean
|
|
79
|
+
* joining across a hole in the evidence, which is exactly the step that turns a correlation into a
|
|
80
|
+
* claim.
|
|
81
|
+
*/
|
|
82
|
+
export function traceLineage(events, query) {
|
|
83
|
+
const changes = stateChangesFor(events, query);
|
|
84
|
+
const latest = changes[changes.length - 1];
|
|
85
|
+
if (latest === undefined) {
|
|
86
|
+
return {
|
|
87
|
+
found: false,
|
|
88
|
+
chain: [],
|
|
89
|
+
note: `'${query.path}' was never seen to change in this window, so there is nothing to trace back from. It may be set before Reticle connected, or held somewhere no registered store exposes.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const observedValue = String(latest.data['value']);
|
|
93
|
+
const chain = [
|
|
94
|
+
{
|
|
95
|
+
kind: 'state',
|
|
96
|
+
text: `${asString(latest.data['path']) ?? query.path} = ${observedValue} (observed: state change at ${String(latest.t)}ms)`,
|
|
97
|
+
observed: true,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
const signals = precursors(events, EventType.SIGNAL, latest.t);
|
|
101
|
+
if (0 === signals.length) {
|
|
102
|
+
return {
|
|
103
|
+
found: true,
|
|
104
|
+
chain,
|
|
105
|
+
note: 'No signal fired in the window before this change, so the value was not seen to pass through one. Nothing further is claimed — the change is real, its cause is not in evidence.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
chain.push(inferredLink('signal', signals.map(signalLabel)));
|
|
109
|
+
// Anchored on the OLDEST candidate signal: a request that preceded any of them could have produced
|
|
110
|
+
// it, and narrowing to the newest would quietly discard candidates for the link below.
|
|
111
|
+
const anchor = signals[0]?.t ?? latest.t;
|
|
112
|
+
const calls = precursors(events, EventType.NET_REQUEST, anchor);
|
|
113
|
+
if (0 === calls.length) {
|
|
114
|
+
return {
|
|
115
|
+
found: true,
|
|
116
|
+
chain,
|
|
117
|
+
note: 'No request preceded that signal in the window, so the chain stops there rather than joining across a gap in the evidence.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
chain.push(inferredLink('net', calls.map(netLabel)));
|
|
121
|
+
return { found: true, chain };
|
|
122
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a network verdict returns as its PROOF.
|
|
3
|
+
*
|
|
4
|
+
* Split out of predicate-eval.ts, which sits at the size backstop, and split HERE because this is a
|
|
5
|
+
* separate question from "did the predicate hold": that file decides the answer, this one decides
|
|
6
|
+
* what is shown to justify it. The two failure modes differ too. A wrong verdict is a wrong verdict;
|
|
7
|
+
* wrong evidence is a verdict nobody can check, or one that costs forty times what it is worth.
|
|
8
|
+
*/
|
|
9
|
+
import { PredicateKind } from '@reticlehq/core';
|
|
10
|
+
import type { Predicate } from './predicate-schema.js';
|
|
11
|
+
/**
|
|
12
|
+
* The matched call, as the proof of the claim that was actually made.
|
|
13
|
+
*
|
|
14
|
+
* A predicate asking about status and url is proved by status and url; returning the whole payload
|
|
15
|
+
* beside them is cost with no bearing on the verdict. A predicate asking `bodyContains` is proved BY
|
|
16
|
+
* the body, and that one keeps it in full — clipping a body a check consumed would be weakening the
|
|
17
|
+
* check to make it cheaper, which is the trade this codebase does not make.
|
|
18
|
+
*/
|
|
19
|
+
export declare function netEvidence(data: Record<string, unknown>, p: Extract<Predicate, {
|
|
20
|
+
kind: typeof PredicateKind.NET;
|
|
21
|
+
}>): unknown;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a network verdict returns as its PROOF.
|
|
3
|
+
*
|
|
4
|
+
* Split out of predicate-eval.ts, which sits at the size backstop, and split HERE because this is a
|
|
5
|
+
* separate question from "did the predicate hold": that file decides the answer, this one decides
|
|
6
|
+
* what is shown to justify it. The two failure modes differ too. A wrong verdict is a wrong verdict;
|
|
7
|
+
* wrong evidence is a verdict nobody can check, or one that costs forty times what it is worth.
|
|
8
|
+
*/
|
|
9
|
+
import { PredicateKind } from '@reticlehq/core';
|
|
10
|
+
import { withoutUrlRaw } from './event-filters.js';
|
|
11
|
+
/**
|
|
12
|
+
* How much of a body the evidence for a PASS may carry when the check never read it.
|
|
13
|
+
*
|
|
14
|
+
* Enough to see the shape of what the server answered — a JSON document's opening keys — and not the
|
|
15
|
+
* document itself. Measured in the field: six `act_and_wait` calls asserting `{ urlContains, status
|
|
16
|
+
* 200 }` through one interview each returned the server's whole answer, ~9.6KB of the same document
|
|
17
|
+
* growing by one slot per step, so almost all of it was a re-send of what the agent already had. The
|
|
18
|
+
* evidence block was 9,693 of an 11,194-byte result.
|
|
19
|
+
*
|
|
20
|
+
* Larger than `MAX_BODY_IN_FAILURE`, and deliberately: a failure quotes the value that DIFFERED and
|
|
21
|
+
* knows which one that is, while a pass is showing the caller what came back and has no such anchor.
|
|
22
|
+
*/
|
|
23
|
+
const MAX_BODY_IN_EVIDENCE = 512;
|
|
24
|
+
/** Body fields a net predicate can read, and the clause that reads each. */
|
|
25
|
+
const BODY_EVIDENCE_FIELDS = {
|
|
26
|
+
responseBody: 'bodyContains',
|
|
27
|
+
requestBody: 'requestBodyContains',
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* How much of the body around a matched needle is quoted.
|
|
31
|
+
*
|
|
32
|
+
* Enough to see the needle in its structure — the key it sits under, the value beside it — without
|
|
33
|
+
* the document it came from.
|
|
34
|
+
*/
|
|
35
|
+
const BODY_MATCH_CONTEXT = 120;
|
|
36
|
+
/** Where a quoted window starts, and the elision markers that keep it honest at both ends. */
|
|
37
|
+
function windowAround(body, at, needle) {
|
|
38
|
+
const from = Math.max(0, at - BODY_MATCH_CONTEXT);
|
|
39
|
+
const to = Math.min(body.length, at + needle.length + BODY_MATCH_CONTEXT);
|
|
40
|
+
return `${from > 0 ? '…' : ''}${body.slice(from, to)}${to < body.length ? '…' : ''}`;
|
|
41
|
+
}
|
|
42
|
+
/** The head of a body, for a claim that never read it. Elision is VISIBLE — see the note below. */
|
|
43
|
+
function headOf(body) {
|
|
44
|
+
return body.length <= MAX_BODY_IN_EVIDENCE ? body : `${body.slice(0, MAX_BODY_IN_EVIDENCE)}…`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The matched call, as the proof of the claim that was actually made.
|
|
48
|
+
*
|
|
49
|
+
* A predicate asking about status and url is proved by status and url; returning the whole payload
|
|
50
|
+
* beside them is cost with no bearing on the verdict. A predicate asking `bodyContains` is proved BY
|
|
51
|
+
* the body, and that one keeps it in full — clipping a body a check consumed would be weakening the
|
|
52
|
+
* check to make it cheaper, which is the trade this codebase does not make.
|
|
53
|
+
*/
|
|
54
|
+
export function netEvidence(data, p) {
|
|
55
|
+
const stripped = withoutUrlRaw({ data }).data;
|
|
56
|
+
if (null === stripped || 'object' !== typeof stripped)
|
|
57
|
+
return stripped;
|
|
58
|
+
const clause = p;
|
|
59
|
+
const out = { ...stripped };
|
|
60
|
+
for (const [field, askedBy] of Object.entries(BODY_EVIDENCE_FIELDS)) {
|
|
61
|
+
const body = out[field];
|
|
62
|
+
if ('string' !== typeof body || body.length <= MAX_BODY_IN_EVIDENCE)
|
|
63
|
+
continue;
|
|
64
|
+
const needle = clause[askedBy];
|
|
65
|
+
const at = 'string' === typeof needle ? body.indexOf(needle) : -1;
|
|
66
|
+
if (at < 0) {
|
|
67
|
+
// No clause read this body, or the pass came from elsewhere: the head shows the shape of the
|
|
68
|
+
// answer. The ellipsis is load-bearing — a silently shortened body reads as "the server
|
|
69
|
+
// answered this much", a different and wrong fact — and `responseSize` rides alongside
|
|
70
|
+
// untouched, so the real length stays available and `reticle_network` fetches the rest.
|
|
71
|
+
out[field] = headOf(body);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// The clause matched. What proves it is the MATCH, not the document it was found in: the needle
|
|
75
|
+
// in its context, plus where it was. Six field verdicts each returned the same 8KB interview
|
|
76
|
+
// document to prove one substring of it, and the document proved nothing the window does not.
|
|
77
|
+
out[field] = windowAround(body, at, needle);
|
|
78
|
+
out['bodyMatchAt'] = at;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console predicates: the assertion agents lean on hardest, and the one with a structural blind spot.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `predicate-eval.ts` when it crossed the file cap. Cohesive on its own terms — the
|
|
5
|
+
* level map, the channel-liveness gate, and the absent/present branches are one subject, and nothing
|
|
6
|
+
* else in the evaluator reads a CONSOLE_* event.
|
|
7
|
+
*/
|
|
8
|
+
import { PredicateKind, type ReticleEvent } from '@reticlehq/core';
|
|
9
|
+
import type { EvalResult } from './predicate-eval.js';
|
|
10
|
+
import type { Predicate } from './predicate-schema.js';
|
|
11
|
+
export declare function evalConsole(events: ReticleEvent[], p: Extract<Predicate, {
|
|
12
|
+
kind: typeof PredicateKind.CONSOLE;
|
|
13
|
+
}>): EvalResult;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console predicates: the assertion agents lean on hardest, and the one with a structural blind spot.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `predicate-eval.ts` when it crossed the file cap. Cohesive on its own terms — the
|
|
5
|
+
* level map, the channel-liveness gate, and the absent/present branches are one subject, and nothing
|
|
6
|
+
* else in the evaluator reads a CONSOLE_* event.
|
|
7
|
+
*/
|
|
8
|
+
import { CONSOLE_ATTACH_NOTE, EventType, PredicateKind } from '@reticlehq/core';
|
|
9
|
+
/** The only console levels Reticle instruments (console.info/debug/trace are NOT patched). */
|
|
10
|
+
const CONSOLE_LEVEL_TYPE = {
|
|
11
|
+
log: EventType.CONSOLE_LOG,
|
|
12
|
+
warn: EventType.CONSOLE_WARN,
|
|
13
|
+
error: EventType.CONSOLE_ERROR,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Has the console channel reported ANYTHING in this window, at any level?
|
|
17
|
+
*
|
|
18
|
+
* Liveness, on the same argument `observerSawSubresources` makes for resource timing: one entry
|
|
19
|
+
* anywhere proves the channel reaches this document, so the caller is not looking at a dead read.
|
|
20
|
+
* Any level counts — a `console.log` proves the patch is in place just as well as an error would,
|
|
21
|
+
* and asking only about the ASSERTED level would re-fire the caveat on every quiet window of a
|
|
22
|
+
* chatty page, which is a toll paid where the warning is not true.
|
|
23
|
+
*
|
|
24
|
+
* It does not CLOSE the blind stretch — that is about time before attach and cannot be closed from
|
|
25
|
+
* this side. It only rules out the reading the caveat's wording would get wrong.
|
|
26
|
+
*/
|
|
27
|
+
function channelReported(events, since) {
|
|
28
|
+
return events.some((e) => e.t >= since &&
|
|
29
|
+
(e.type === EventType.CONSOLE_LOG ||
|
|
30
|
+
e.type === EventType.CONSOLE_WARN ||
|
|
31
|
+
e.type === EventType.CONSOLE_ERROR ||
|
|
32
|
+
e.type === EventType.CONSOLE_INFO ||
|
|
33
|
+
e.type === EventType.CONSOLE_DEBUG ||
|
|
34
|
+
e.type === EventType.ERROR_UNCAUGHT));
|
|
35
|
+
}
|
|
36
|
+
export function evalConsole(events, p) {
|
|
37
|
+
const since = p.since ?? 0;
|
|
38
|
+
// Reticle only instruments console.log/warn/error. A level outside that set is never captured,
|
|
39
|
+
// so its events can't exist — and an `absent` assertion on it would verify NOTHING while
|
|
40
|
+
// reporting green. Fail loudly instead of false-passing.
|
|
41
|
+
if (p.level !== undefined && p.level !== 'error' && CONSOLE_LEVEL_TYPE[p.level] === undefined) {
|
|
42
|
+
return {
|
|
43
|
+
pass: false,
|
|
44
|
+
failureReason: `console level '${p.level}' is not captured — Reticle instruments console.log, console.warn, console.error only`,
|
|
45
|
+
observed: `level '${p.level}' is not instrumented, so no event of it can ever exist`,
|
|
46
|
+
expected: 'a level Reticle captures: log, warn, or error',
|
|
47
|
+
assertion: 'console.uninstrumented-level',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const matches = events.filter((e) => {
|
|
51
|
+
if (e.t < since)
|
|
52
|
+
return false;
|
|
53
|
+
const isErr = e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT;
|
|
54
|
+
if (p.level === undefined) {
|
|
55
|
+
return (e.type === EventType.CONSOLE_LOG ||
|
|
56
|
+
e.type === EventType.CONSOLE_WARN ||
|
|
57
|
+
e.type === EventType.CONSOLE_ERROR ||
|
|
58
|
+
e.type === EventType.ERROR_UNCAUGHT);
|
|
59
|
+
}
|
|
60
|
+
if ('error' === p.level)
|
|
61
|
+
return isErr;
|
|
62
|
+
return e.type === CONSOLE_LEVEL_TYPE[p.level];
|
|
63
|
+
});
|
|
64
|
+
// A text match narrows the population to entries whose captured message contains the substring.
|
|
65
|
+
// With `absent: true` this is the whole point: "THIS message did not appear", not "no messages
|
|
66
|
+
// appeared" — the difference between a regression check and a fragile one that any unrelated
|
|
67
|
+
// warning anywhere in the app breaks.
|
|
68
|
+
const wanted = 'contains' in p ? p.contains : undefined;
|
|
69
|
+
// Captured messages are strings (stringifyArgs in the browser observer), but a malformed or
|
|
70
|
+
// foreign event must not crash the evaluator: non-strings stringify defensively, and objects
|
|
71
|
+
// go through JSON.stringify rather than a default toString that would print '[object Object]'.
|
|
72
|
+
const asText = (v) => {
|
|
73
|
+
if ('string' === typeof v)
|
|
74
|
+
return v;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.stringify(v) ?? '';
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const matching = wanted !== undefined
|
|
83
|
+
? matches.filter((e) => asText(e.data['message']).includes(wanted))
|
|
84
|
+
: matches;
|
|
85
|
+
if (true === p.absent) {
|
|
86
|
+
if (wanted !== undefined && 0 === matching.length && matches.length > 0) {
|
|
87
|
+
// Other entries exist but none carries the substring: exactly the pass an absence-with-match
|
|
88
|
+
// asserts. Name both counts so the caller can tell this from a silent window.
|
|
89
|
+
return {
|
|
90
|
+
pass: true,
|
|
91
|
+
evidence: { absent: true, contains: wanted },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return 0 === matching.length
|
|
95
|
+
? {
|
|
96
|
+
pass: true,
|
|
97
|
+
evidence: {
|
|
98
|
+
absent: true,
|
|
99
|
+
// Only when the window starts at attach AND nothing came through at all. An entry
|
|
100
|
+
// anywhere in the window proves the channel reaches this document, which is the case
|
|
101
|
+
// the caveat's wording would misdescribe; a window an action opened has `since > 0` and
|
|
102
|
+
// was fully observed. Costs nothing on every other pass, which is most of them.
|
|
103
|
+
...(0 === since && !channelReported(events, since)
|
|
104
|
+
? { unobservedBefore: CONSOLE_ATTACH_NOTE }
|
|
105
|
+
: {}),
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
: {
|
|
109
|
+
pass: false,
|
|
110
|
+
failureReason: wanted !== undefined
|
|
111
|
+
? `expected no ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)} but found ${String(matching.length)}`
|
|
112
|
+
: `expected no ${p.level ?? 'console'} entries but found ${String(matches.length)}`,
|
|
113
|
+
observed: wanted !== undefined
|
|
114
|
+
? `${String(matching.length)} ${p.level ?? 'console'} entr${1 === matching.length ? 'y' : 'ies'} containing ${JSON.stringify(wanted)}`
|
|
115
|
+
: `${String(matches.length)} ${p.level ?? 'console'} entr${1 === matches.length ? 'y' : 'ies'}`,
|
|
116
|
+
expected: wanted !== undefined
|
|
117
|
+
? `no ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)}`
|
|
118
|
+
: `no ${p.level ?? 'console'} entries`,
|
|
119
|
+
assertion: wanted !== undefined ? 'console.absent-contains' : 'console.absent',
|
|
120
|
+
evidence: matching.map((e) => e.data),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return matching.length > 0
|
|
124
|
+
? { pass: true, evidence: matching.map((e) => e.data) }
|
|
125
|
+
: {
|
|
126
|
+
pass: false,
|
|
127
|
+
failureReason: wanted !== undefined
|
|
128
|
+
? `no ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)} found`
|
|
129
|
+
: `no ${p.level ?? 'console'} entries found`,
|
|
130
|
+
observed: wanted !== undefined
|
|
131
|
+
? `no ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)} in the window`
|
|
132
|
+
: `no ${p.level ?? 'console'} entries in the window`,
|
|
133
|
+
expected: wanted !== undefined
|
|
134
|
+
? `at least one ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)}`
|
|
135
|
+
: `at least one ${p.level ?? 'console'} entry`,
|
|
136
|
+
assertion: 'console.present',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element and text predicates: everything that resolves a locator against the live DOM.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `predicate.ts` when it crossed the file cap. It is a cohesive unit on its own terms —
|
|
5
|
+
* one round-trip protocol (`matchOnce`), the fields the browser locator drops and this side enforces
|
|
6
|
+
* back (`residualQueryChecks`), the absent/present asymmetry, and the near-miss diagnostics that only
|
|
7
|
+
* ever enrich a failure. Nothing else in the predicate layer talks to `ReticleCommand.MATCH`.
|
|
8
|
+
*/
|
|
9
|
+
import { type ElementQuery, type ElementState, type MatchResult } from '@reticlehq/core';
|
|
10
|
+
import { type EvalResult } from './predicate-eval.js';
|
|
11
|
+
import type { PredicateSession } from './predicate.js';
|
|
12
|
+
export declare function matchOnce(session: PredicateSession, query: ElementQuery, state: ElementState | undefined): Promise<MatchResult>;
|
|
13
|
+
export declare function evalElement(session: PredicateSession, query: ElementQuery, state: ElementState | undefined, absent: boolean, diagnose: boolean): Promise<EvalResult>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element and text predicates: everything that resolves a locator against the live DOM.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `predicate.ts` when it crossed the file cap. It is a cohesive unit on its own terms —
|
|
5
|
+
* one round-trip protocol (`matchOnce`), the fields the browser locator drops and this side enforces
|
|
6
|
+
* back (`residualQueryChecks`), the absent/present asymmetry, and the near-miss diagnostics that only
|
|
7
|
+
* ever enrich a failure. Nothing else in the predicate layer talks to `ReticleCommand.MATCH`.
|
|
8
|
+
*/
|
|
9
|
+
import { ReticleCommand, } from '@reticlehq/core';
|
|
10
|
+
import { residualQueryChecks, satisfiesResiduals, describeResidual, } from './predicate-eval.js';
|
|
11
|
+
import { describeTestidMiss } from './testid-near-miss.js';
|
|
12
|
+
import { describeSplitTextMiss } from './split-text-miss.js';
|
|
13
|
+
export async function matchOnce(session, query, state) {
|
|
14
|
+
const res = await session.command(ReticleCommand.MATCH, { query, state });
|
|
15
|
+
if (!res.ok)
|
|
16
|
+
return { matched: false, count: 0, elements: [] };
|
|
17
|
+
return (res.result ?? { matched: false, count: 0, elements: [] });
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Roles that exist to ANNOUNCE something. An empty one is the container, not the message.
|
|
21
|
+
*
|
|
22
|
+
* Every toast and notification library mounts its live region at boot and never removes it, so a
|
|
23
|
+
* bare `{ role: "alert" }` presence check is satisfied on those apps with nothing on screen — it
|
|
24
|
+
* cannot fail, and a predicate that cannot fail is not a check. Restricted to these roles because
|
|
25
|
+
* they are the ones whose whole purpose is content: an unnamed `img`, `separator` or `progressbar`
|
|
26
|
+
* is an ordinary thing to assert the presence of, and is left alone.
|
|
27
|
+
*/
|
|
28
|
+
const ANNOUNCEMENT_ROLES = new Set([
|
|
29
|
+
'alert',
|
|
30
|
+
'alertdialog',
|
|
31
|
+
'status',
|
|
32
|
+
'log',
|
|
33
|
+
'marquee',
|
|
34
|
+
'timer',
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* The matches that actually carry content, for a query that named none of its own.
|
|
38
|
+
*
|
|
39
|
+
* A `name`, `text` or `testid` in the query is already a specific claim and is never second-guessed
|
|
40
|
+
* here — this only fills the gap left by a query that asked for a role and nothing else.
|
|
41
|
+
*/
|
|
42
|
+
function substantiveMatches(query, elements) {
|
|
43
|
+
const bareRole = query.role !== undefined &&
|
|
44
|
+
query.name === undefined &&
|
|
45
|
+
query.text === undefined &&
|
|
46
|
+
query.testid === undefined;
|
|
47
|
+
if (!bareRole || !ANNOUNCEMENT_ROLES.has(query.role ?? ''))
|
|
48
|
+
return elements;
|
|
49
|
+
// Nothing described, nothing to judge. `elements` is only the described PREFIX of a match — the
|
|
50
|
+
// browser truncates it, and it can be empty while `count` is not. Calling that hollow would turn a
|
|
51
|
+
// truncation into a verdict, so an unjudgeable set keeps the old answer.
|
|
52
|
+
if (0 === elements.length)
|
|
53
|
+
return elements;
|
|
54
|
+
return elements.filter((e) => e.name.length > 0 || (e.text ?? '').trim().length > 0);
|
|
55
|
+
}
|
|
56
|
+
export async function evalElement(session, query, state, absent, diagnose) {
|
|
57
|
+
// Fields the browser's locator would have DROPPED, enforced back here — see residualQueryChecks.
|
|
58
|
+
// Checked before the round-trip when nothing can enforce them: a predicate that cannot be evaluated
|
|
59
|
+
// must say so rather than resolve to whatever the surviving half of it happened to match.
|
|
60
|
+
const residual = residualQueryChecks(query);
|
|
61
|
+
if (residual.unusable.length > 0) {
|
|
62
|
+
const reason = `the element locator ignores ${residual.unusable.map((f) => `\`${f}\``).join(', ')} ` +
|
|
63
|
+
`in ${JSON.stringify(query)} — it resolves by the first of by+value, component/source, role, ` +
|
|
64
|
+
'text, label, placeholder, testid, alt that is present, and nothing here can check the rest. ' +
|
|
65
|
+
'Assert them one locator at a time, or move the extra field into the locator';
|
|
66
|
+
return { pass: false, failureReason: reason, inconclusive: reason };
|
|
67
|
+
}
|
|
68
|
+
let match = await matchOnce(session, query, state);
|
|
69
|
+
const subject = JSON.stringify(query);
|
|
70
|
+
// A residual narrows the SET; `count` is every match while `elements` is only the described prefix,
|
|
71
|
+
// so a locator broad enough to be truncated cannot be narrowed honestly. Say so instead of guessing.
|
|
72
|
+
if (residual.checks.length > 0 && match.count > match.elements.length) {
|
|
73
|
+
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`;
|
|
74
|
+
return { pass: false, failureReason: reason, inconclusive: reason };
|
|
75
|
+
}
|
|
76
|
+
const kept = match.elements.filter((element) => satisfiesResiduals(element, residual.checks));
|
|
77
|
+
// The locator found something and the dropped fields disagree with it. Reported separately from a
|
|
78
|
+
// plain miss because the fixes are opposite: the element IS there, its value is not what was claimed.
|
|
79
|
+
if (residual.checks.length > 0 && match.matched && 0 === kept.length && !absent) {
|
|
80
|
+
const wanted = residual.checks.map(([f, want]) => `${f}=${JSON.stringify(want)}`).join(', ');
|
|
81
|
+
return {
|
|
82
|
+
pass: false,
|
|
83
|
+
failureReason: `element matching ${subject} is present but ${wanted} does not hold`,
|
|
84
|
+
observed: match.elements
|
|
85
|
+
.map((element) => residual.checks.map(([f]) => describeResidual(element, f)).join(', '))
|
|
86
|
+
.join('; '),
|
|
87
|
+
expected: `an element matching ${subject} with ${wanted}`,
|
|
88
|
+
assertion: `element.${residual.checks[0]?.[0] ?? 'residual'}`,
|
|
89
|
+
evidence: match.elements,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (residual.checks.length > 0) {
|
|
93
|
+
match = { ...match, matched: kept.length > 0, count: kept.length, elements: kept };
|
|
94
|
+
}
|
|
95
|
+
// A given-but-missing scope is handled ASYMMETRICALLY, because "absent" and "present" ask different
|
|
96
|
+
// questions of a scope that no longer exists:
|
|
97
|
+
// - ABSENT: an element is trivially absent from a container that isn't there. This is also the
|
|
98
|
+
// everyday "wait for the #overlay/#spinner/#modal to disappear" pattern (scope the wait to the
|
|
99
|
+
// node being removed) — treating scopeMissing as a hard fail there burned the whole timeout and
|
|
100
|
+
// flipped a correct green to red. So scopeMissing satisfies an absence check.
|
|
101
|
+
// - PRESENT: you cannot confirm an element is present inside a scope that resolved to nothing, and
|
|
102
|
+
// silently widening to the whole page is the original false green. So scopeMissing FAILS presence
|
|
103
|
+
// (on the wait_for path this just keeps polling until the scope appears).
|
|
104
|
+
if (absent) {
|
|
105
|
+
if (true === match.scopeMissing) {
|
|
106
|
+
return { pass: true, evidence: { absent: true, scopeMissing: true } };
|
|
107
|
+
}
|
|
108
|
+
return match.matched
|
|
109
|
+
? {
|
|
110
|
+
pass: false,
|
|
111
|
+
failureReason: `expected element to be absent but found ${String(match.count)}`,
|
|
112
|
+
observed: `${String(match.count)} element(s) matching ${subject}`,
|
|
113
|
+
expected: `no element matching ${subject}`,
|
|
114
|
+
assertion: 'element.absent',
|
|
115
|
+
evidence: match.elements,
|
|
116
|
+
}
|
|
117
|
+
: { pass: true, evidence: { absent: true } };
|
|
118
|
+
}
|
|
119
|
+
if (true === match.scopeMissing) {
|
|
120
|
+
return {
|
|
121
|
+
pass: false,
|
|
122
|
+
failureReason: `scope resolved to no element — cannot confirm ${subject} is present`,
|
|
123
|
+
observed: 'the requested scope is not on the page (unmounted or selector matched nothing)',
|
|
124
|
+
expected: `an element matching ${subject} within an existing scope`,
|
|
125
|
+
assertion: 'element.present',
|
|
126
|
+
evidence: { scopeMissing: true },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const announced = substantiveMatches(query, match.elements);
|
|
130
|
+
if (match.matched && (announced.length > 0 || 0 === match.elements.length)) {
|
|
131
|
+
return { pass: true, evidence: announced };
|
|
132
|
+
}
|
|
133
|
+
// Matched, but every match was a mounted-and-empty live region. Reported as its own failure rather
|
|
134
|
+
// than the plain miss below, because the fixes are opposite: the container IS on the page, so
|
|
135
|
+
// "no element matched" would send the caller looking for a render that already happened.
|
|
136
|
+
if (match.matched) {
|
|
137
|
+
return {
|
|
138
|
+
pass: false,
|
|
139
|
+
failureReason: `${String(match.count)} element(s) matched ${subject}, and every one is empty — ` +
|
|
140
|
+
'a live region mounted with no name and no text has announced nothing. Assert the ' +
|
|
141
|
+
'message itself (a `name` or `text` in the query) rather than the container',
|
|
142
|
+
observed: `${String(match.count)} empty '${query.role ?? ''}' region(s) with no name or text`,
|
|
143
|
+
expected: `a '${query.role ?? ''}' carrying a name or rendered text`,
|
|
144
|
+
assertion: 'element.announced',
|
|
145
|
+
evidence: match.elements,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// The near-miss diagnostic below costs one or two EXTRA MATCH round-trips. It only enriches a FAILED
|
|
149
|
+
// verdict, and a wait loop's interim rechecks read nothing but `pass` — so on the poll path (diagnose
|
|
150
|
+
// false) skip straight to the plain fail. Under an event flood a role+name element wait was firing
|
|
151
|
+
// two live-DOM scans per recheck for a diagnostic no interim eval ever reads; the final timeout eval
|
|
152
|
+
// still runs with diagnose=true and produces the full near-miss.
|
|
153
|
+
if (!diagnose) {
|
|
154
|
+
return {
|
|
155
|
+
pass: false,
|
|
156
|
+
failureReason: `no element matched ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
|
|
157
|
+
observed: 'no matching element on the page',
|
|
158
|
+
expected: `an element matching ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
|
|
159
|
+
assertion: 'element.present',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Diagnostic near-miss: was it there but in the wrong state, or a similar element present?
|
|
163
|
+
if (state !== undefined) {
|
|
164
|
+
const relaxed = await matchOnce(session, query, undefined);
|
|
165
|
+
if (relaxed.matched) {
|
|
166
|
+
return {
|
|
167
|
+
pass: false,
|
|
168
|
+
failureReason: `element exists but not in state '${state}'`,
|
|
169
|
+
observed: `element matching ${subject} is present, states: ${relaxed.elements[0]?.states.join(', ') ?? 'unknown'}`,
|
|
170
|
+
expected: `element matching ${subject} in state '${state}'`,
|
|
171
|
+
assertion: 'element.state',
|
|
172
|
+
evidence: { nearMiss: relaxed.elements },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (query.role !== undefined && query.name !== undefined) {
|
|
177
|
+
const roleOnly = await matchOnce(session, { role: query.role }, state);
|
|
178
|
+
if (roleOnly.matched) {
|
|
179
|
+
return {
|
|
180
|
+
pass: false,
|
|
181
|
+
failureReason: `no '${query.role}' named '${query.name}'; saw: ${roleOnly.elements
|
|
182
|
+
.map((e) => e.name)
|
|
183
|
+
.filter((n) => n.length > 0)
|
|
184
|
+
.join(', ')}`,
|
|
185
|
+
observed: `${String(roleOnly.count)} '${query.role}' element(s), named: ${roleOnly.elements
|
|
186
|
+
.map((e) => e.name)
|
|
187
|
+
.filter((n) => n.length > 0)
|
|
188
|
+
.join(', ')}`,
|
|
189
|
+
expected: `a '${query.role}' named '${query.name}'`,
|
|
190
|
+
assertion: 'element.role+name',
|
|
191
|
+
evidence: { nearMiss: roleOnly.elements },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// The testid near-miss: name what IS here, so a typo is one step from fixed rather than a dead
|
|
196
|
+
// end. reticle_query has always done this; the predicate path had no equivalent. See
|
|
197
|
+
// testid-near-miss.ts.
|
|
198
|
+
const present = match.hint?.presentTestids ?? [];
|
|
199
|
+
const alsoHere = query.testid === undefined ? undefined : describeTestidMiss(query.testid, present);
|
|
200
|
+
// A text miss where the string is on the page but split across children reads exactly like an
|
|
201
|
+
// element that never rendered. Naming the container is the difference between a retry and a bug
|
|
202
|
+
// report against working code. See split-text-miss.ts.
|
|
203
|
+
const splitText = describeSplitTextMiss(match.hint?.splitText, query.text);
|
|
204
|
+
const clause = splitText ?? (alsoHere === undefined || '' === alsoHere ? undefined : alsoHere);
|
|
205
|
+
const suffix = clause === undefined ? '' : ` — ${clause}`;
|
|
206
|
+
return {
|
|
207
|
+
pass: false,
|
|
208
|
+
failureReason: `no element matched ${subject}${state === undefined ? '' : ` in state '${state}'`}${suffix}`,
|
|
209
|
+
observed: `no matching element on the page${suffix}`,
|
|
210
|
+
expected: `an element matching ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
|
|
211
|
+
assertion: 'element.present',
|
|
212
|
+
...(present.length > 0 ? { evidence: { presentTestids: present } } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PredicateKind, type ReticleEvent } from '@reticlehq/core';
|
|
2
|
+
export { evalConsole } from './predicate-console.js';
|
|
2
3
|
import type { Predicate } from './predicate-schema.js';
|
|
3
4
|
export * from './predicate-schema.js';
|
|
4
5
|
export interface EvalResult {
|
|
@@ -66,12 +67,23 @@ export declare function str(value: unknown): string | undefined;
|
|
|
66
67
|
* `{$gte,$lte,$gt,$lt}` (numbers), `{$contains}` (array membership or substring), `{$length}`.
|
|
67
68
|
*/
|
|
68
69
|
export declare function matchValue(got: unknown, want: unknown): boolean;
|
|
70
|
+
/** Shallow JSON pattern match: each key in `pattern` must match (see matchValue). */
|
|
71
|
+
export declare function dataMatches(actual: Record<string, unknown>, pattern: Record<string, unknown>): boolean;
|
|
72
|
+
/** Enough of a body to see what differed, without paying for a whole payload in the verdict. */
|
|
73
|
+
export declare function clipBody(body: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* The filter as APPLIED, every field of it.
|
|
76
|
+
*
|
|
77
|
+
* The count failure used to print `{method, urlContains, status}` while `ok` and `bodyContains` were
|
|
78
|
+
* applied too, so the caller was shown a predicate it had not written and told nothing matched it.
|
|
79
|
+
* A printed filter that is narrower than the real one is worse than none: it is believed.
|
|
80
|
+
*/
|
|
81
|
+
export declare function describeNetFilter(p: Extract<Predicate, {
|
|
82
|
+
kind: typeof PredicateKind.NET;
|
|
83
|
+
}>): string;
|
|
69
84
|
export declare function evalNet(events: ReticleEvent[], p: Extract<Predicate, {
|
|
70
85
|
kind: typeof PredicateKind.NET;
|
|
71
86
|
}>): EvalResult;
|
|
72
|
-
export declare function evalConsole(events: ReticleEvent[], p: Extract<Predicate, {
|
|
73
|
-
kind: typeof PredicateKind.CONSOLE;
|
|
74
|
-
}>): EvalResult;
|
|
75
87
|
export declare function evalAnimation(events: ReticleEvent[], p: Extract<Predicate, {
|
|
76
88
|
kind: typeof PredicateKind.ANIMATION;
|
|
77
89
|
}>): EvalResult;
|