@reticlehq/server 2.13.0 → 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/dev-server-wait.d.ts +8 -0
- package/dist/setup/dev-server-wait.js +3 -1
- 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 +155 -48
- 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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { EventType, PredicateKind, StreamDirection, isDevToolingUrl, urlForMatch, REDACTED_VALUE, } from '@reticlehq/core';
|
|
2
2
|
import { describeObserved } from './observed-in-window.js';
|
|
3
|
-
import {
|
|
3
|
+
import { netEvidence } from './net-evidence.js';
|
|
4
|
+
import { checkRequestBody, newRequestBodyState, requestBodyVerdict, } from './predicate-request-body.js';
|
|
5
|
+
export { evalConsole } from './predicate-console.js';
|
|
4
6
|
// The predicate SHAPE — the discriminated union, its aliases and its zod schema — lives in
|
|
5
7
|
// predicate-schema.ts. Re-exported here so every existing importer of this module is unaffected:
|
|
6
8
|
// the two halves are one public surface, split only because the file outgrew the size backstop.
|
|
@@ -103,7 +105,7 @@ function structurallyEqual(got, want) {
|
|
|
103
105
|
return keys.every((k) => k in b && structurallyEqual(a[k], b[k]));
|
|
104
106
|
}
|
|
105
107
|
/** Shallow JSON pattern match: each key in `pattern` must match (see matchValue). */
|
|
106
|
-
function dataMatches(actual, pattern) {
|
|
108
|
+
export function dataMatches(actual, pattern) {
|
|
107
109
|
for (const [key, want] of Object.entries(pattern)) {
|
|
108
110
|
if (!matchValue(actual[key], want))
|
|
109
111
|
return false;
|
|
@@ -158,16 +160,13 @@ function observedNetCalls(events, urlContains) {
|
|
|
158
160
|
});
|
|
159
161
|
return redacted ? `${base}; ${REDACTED_PATH_HINT}` : base;
|
|
160
162
|
}
|
|
161
|
-
function netEvidence(data) {
|
|
162
|
-
return withoutUrlRaw({ data }).data;
|
|
163
|
-
}
|
|
164
163
|
/**
|
|
165
164
|
* How much of a response body a failure may quote. Enough to see the value that differed on the
|
|
166
165
|
* bodies this field is used against (a JSON answer), and not a whole payload in every verdict.
|
|
167
166
|
*/
|
|
168
167
|
const MAX_BODY_IN_FAILURE = 200;
|
|
169
168
|
/** Enough of a body to see what differed, without paying for a whole payload in the verdict. */
|
|
170
|
-
function clipBody(body) {
|
|
169
|
+
export function clipBody(body) {
|
|
171
170
|
return body.length <= MAX_BODY_IN_FAILURE ? body : `${body.slice(0, MAX_BODY_IN_FAILURE)}…`;
|
|
172
171
|
}
|
|
173
172
|
/**
|
|
@@ -177,7 +176,7 @@ function clipBody(body) {
|
|
|
177
176
|
* applied too, so the caller was shown a predicate it had not written and told nothing matched it.
|
|
178
177
|
* A printed filter that is narrower than the real one is worse than none: it is believed.
|
|
179
178
|
*/
|
|
180
|
-
function describeNetFilter(p) {
|
|
179
|
+
export function describeNetFilter(p) {
|
|
181
180
|
const { kind: _kind, count: _count, since: _since, ...filter } = p;
|
|
182
181
|
return JSON.stringify(filter);
|
|
183
182
|
}
|
|
@@ -251,6 +250,45 @@ const DOCUMENT_ONLY_SUFFIXES = [
|
|
|
251
250
|
'.eot',
|
|
252
251
|
'.webmanifest',
|
|
253
252
|
];
|
|
253
|
+
/**
|
|
254
|
+
* File suffixes a click typically downloads or navigates to as a document, never as fetch/XHR.
|
|
255
|
+
*
|
|
256
|
+
* Sibling of DOCUMENT_ONLY_SUFFIXES, not a member of it. Those are subresources: once resource
|
|
257
|
+
* timing is live, a miss is evidence. A click on `<a href="/export.pdf">` is a navigation (or a
|
|
258
|
+
* Content-Disposition attachment). Resource timing of stylesheets does not make it visible, so
|
|
259
|
+
* grading the miss as "0 network calls" / a 404 is the lie #805 exists to stop — even on a page
|
|
260
|
+
* whose observer is running. `.js` / `.json` stay off this list for the same reason they stay off
|
|
261
|
+
* the other: they are routinely fetched.
|
|
262
|
+
*/
|
|
263
|
+
const NATIVE_DOWNLOAD_SUFFIXES = [
|
|
264
|
+
'.pdf',
|
|
265
|
+
'.csv',
|
|
266
|
+
'.tsv',
|
|
267
|
+
'.zip',
|
|
268
|
+
'.gz',
|
|
269
|
+
'.tar',
|
|
270
|
+
'.xlsx',
|
|
271
|
+
'.xls',
|
|
272
|
+
'.docx',
|
|
273
|
+
'.doc',
|
|
274
|
+
'.pptx',
|
|
275
|
+
'.ppt',
|
|
276
|
+
'.ods',
|
|
277
|
+
'.odt',
|
|
278
|
+
'.rtf',
|
|
279
|
+
'.7z',
|
|
280
|
+
'.rar',
|
|
281
|
+
];
|
|
282
|
+
/**
|
|
283
|
+
* Does this filter target a class of request the observer cannot see?
|
|
284
|
+
*
|
|
285
|
+
* Read off `urlContains` only, and only when the pattern ENDS in one of the suffixes — a filter of
|
|
286
|
+
* `/api/` that happens to contain `.css` somewhere in a query string is still an ordinary XHR
|
|
287
|
+
* target. Query strings and fragments are stripped first, since `favicon.ico?v=2` is the same asset.
|
|
288
|
+
*/
|
|
289
|
+
function filterPath(urlContains) {
|
|
290
|
+
return (urlContains.split('#')[0] ?? '').split('?')[0]?.toLowerCase() ?? '';
|
|
291
|
+
}
|
|
254
292
|
/**
|
|
255
293
|
* Does this filter target a class of request the observer cannot see?
|
|
256
294
|
*
|
|
@@ -259,10 +297,36 @@ const DOCUMENT_ONLY_SUFFIXES = [
|
|
|
259
297
|
* target. Query strings and fragments are stripped first, since `favicon.ico?v=2` is the same asset.
|
|
260
298
|
*/
|
|
261
299
|
function targetsUnobservedChannel(p) {
|
|
262
|
-
|
|
300
|
+
const url = p.urlContains;
|
|
301
|
+
if (undefined === url)
|
|
263
302
|
return false;
|
|
264
|
-
|
|
265
|
-
|
|
303
|
+
return DOCUMENT_ONLY_SUFFIXES.some((suffix) => filterPath(url).endsWith(suffix));
|
|
304
|
+
}
|
|
305
|
+
function targetsNativeDownload(p) {
|
|
306
|
+
const url = p.urlContains;
|
|
307
|
+
if (undefined === url)
|
|
308
|
+
return false;
|
|
309
|
+
return NATIVE_DOWNLOAD_SUFFIXES.some((suffix) => filterPath(url).endsWith(suffix));
|
|
310
|
+
}
|
|
311
|
+
function nativeDownloadReason(p) {
|
|
312
|
+
return (`no fetch or XHR matched ${describeNetFilter(p)}. A native download or document navigation ` +
|
|
313
|
+
`(an <a href> to a file, a Content-Disposition attachment, <a download>) never goes through ` +
|
|
314
|
+
`fetch or XMLHttpRequest, so an empty net window is not a 404 and is not evidence the export ` +
|
|
315
|
+
`failed. This is unobservable on the net channel. Assert the link href, or check the file ` +
|
|
316
|
+
`outside the browser — Reticle cannot see the bytes land`);
|
|
317
|
+
}
|
|
318
|
+
function nativeDownloadMiss(events, p) {
|
|
319
|
+
if (!targetsNativeDownload(p))
|
|
320
|
+
return undefined;
|
|
321
|
+
const reason = nativeDownloadReason(p);
|
|
322
|
+
return {
|
|
323
|
+
pass: false,
|
|
324
|
+
failureReason: reason,
|
|
325
|
+
inconclusive: reason,
|
|
326
|
+
observed: observedNetCalls(events, p.urlContains),
|
|
327
|
+
expected: `a fetch or XHR matching ${describeNetFilter(p)}`,
|
|
328
|
+
assertion: 'net.native-download',
|
|
329
|
+
};
|
|
266
330
|
}
|
|
267
331
|
/**
|
|
268
332
|
* Initiator types the browser SDK's resource-timing observer stamps onto document-initiated records
|
|
@@ -308,6 +372,34 @@ function unobservedChannelReason(p) {
|
|
|
308
372
|
`Nothing here says the app is wrong. Check it outside the browser, or assert something the ` +
|
|
309
373
|
`document does not fetch on its own`);
|
|
310
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Appended to a zero-match `net` negative whose window starts at SDK attach.
|
|
377
|
+
*
|
|
378
|
+
* Every event's `t` is stamped `performance.now() - #start`, where `#start` is taken when the SDK
|
|
379
|
+
* is constructed, so `t` is never negative and a window with `since === 0` begins AT attach — never
|
|
380
|
+
* before it. Whatever the page did between navigation start and attach left no event at all, so
|
|
381
|
+
* "no matching call" over such a window cannot be told apart from "the call was made while nothing
|
|
382
|
+
* was watching yet".
|
|
383
|
+
*
|
|
384
|
+
* The gap is routine rather than exotic: a `fetch` from an effect in a root provider, or a classic
|
|
385
|
+
* `<script>` at the end of `<body>`, fires before a deferred module script has run. Reported from
|
|
386
|
+
* the field, the verdict then read as proof the request was never made, and reporters went looking
|
|
387
|
+
* for the defect in code that was working — restarting dev servers and re-reading providers to
|
|
388
|
+
* establish the request was invisible rather than absent.
|
|
389
|
+
*
|
|
390
|
+
* Same argument as DOCUMENT_ONLY_SUFFIXES one axis over: that one is a channel Reticle does not
|
|
391
|
+
* watch, this is a stretch of TIME it was not yet watching.
|
|
392
|
+
*
|
|
393
|
+
* The grade is deliberately NOT downgraded to `inconclusive`. A missing API call is the finding this
|
|
394
|
+
* oracle exists to make, and it is the strongest grade available for the startup class — session
|
|
395
|
+
* restore, feature flags, bootstrap config — which is exactly the class that silently breaks on
|
|
396
|
+
* reload. Every window an action opens carries `since > 0` and is untouched. What changes is only
|
|
397
|
+
* what the negative CLAIMS: it stops asserting the request never happened, and names the assertion
|
|
398
|
+
* that can settle it, because the state such a request produces IS observable after attach.
|
|
399
|
+
*/
|
|
400
|
+
const PRE_ATTACH_CAVEAT = ' — note that this window starts where the SDK attached, and requests made before that are never ' +
|
|
401
|
+
'captured, so a miss here cannot tell a call that was never made apart from one made before the ' +
|
|
402
|
+
'page connected; if the call is expected during startup, assert on the state it produces instead';
|
|
311
403
|
export function evalNet(events, p) {
|
|
312
404
|
const since = p.since ?? 0;
|
|
313
405
|
/**
|
|
@@ -341,6 +433,7 @@ export function evalNet(events, p) {
|
|
|
341
433
|
* a truncated one cannot (#614).
|
|
342
434
|
*/
|
|
343
435
|
let truncatedBody;
|
|
436
|
+
const requestState = newRequestBodyState();
|
|
344
437
|
const matches = events.filter((e) => {
|
|
345
438
|
if (e.type !== EventType.NET_REQUEST || e.t < since)
|
|
346
439
|
return false;
|
|
@@ -390,6 +483,8 @@ export function evalNet(events, p) {
|
|
|
390
483
|
return false;
|
|
391
484
|
}
|
|
392
485
|
}
|
|
486
|
+
if (!checkRequestBody(d, p, requestState))
|
|
487
|
+
return false;
|
|
393
488
|
return true;
|
|
394
489
|
});
|
|
395
490
|
if (unobservableStatus && 0 === matches.length) {
|
|
@@ -413,6 +508,9 @@ export function evalNet(events, p) {
|
|
|
413
508
|
assertion: 'net.bodyContains',
|
|
414
509
|
};
|
|
415
510
|
}
|
|
511
|
+
const requestVerdict = requestBodyVerdict(requestState, p, matches.length);
|
|
512
|
+
if (requestVerdict !== undefined)
|
|
513
|
+
return requestVerdict;
|
|
416
514
|
// Ranked ABOVE the mismatch branch: when both a truncated and a full body missed the needle,
|
|
417
515
|
// the honest verdict is the undecidable one. Deciding on the full body would report a failure
|
|
418
516
|
// the truncated call may well contradict.
|
|
@@ -440,6 +538,11 @@ export function evalNet(events, p) {
|
|
|
440
538
|
// useEffect-double-fire / retry-storm regression class, where the request DID fire (presence passes)
|
|
441
539
|
// but fired the WRONG number of times. Without `count`, the matcher is presence-only (≥1).
|
|
442
540
|
if (p.count !== undefined) {
|
|
541
|
+
if (0 === matches.length) {
|
|
542
|
+
const download = nativeDownloadMiss(events, p);
|
|
543
|
+
if (download !== undefined)
|
|
544
|
+
return download;
|
|
545
|
+
}
|
|
443
546
|
if (matches.length !== p.count &&
|
|
444
547
|
0 === matches.length &&
|
|
445
548
|
targetsUnobservedChannel(p) &&
|
|
@@ -451,15 +554,27 @@ export function evalNet(events, p) {
|
|
|
451
554
|
assertion: 'net.count',
|
|
452
555
|
};
|
|
453
556
|
}
|
|
454
|
-
|
|
557
|
+
const counted = evalExactCount({
|
|
455
558
|
matched: matches.length,
|
|
456
559
|
want: p.count,
|
|
457
560
|
noun: 'network call(s)',
|
|
458
561
|
filter: describeNetFilter(p),
|
|
459
562
|
assertion: 'net.count',
|
|
460
563
|
});
|
|
564
|
+
// Same blind head, second door: "saw 0" over a whole-session window is the same claim the
|
|
565
|
+
// presence branch makes, and just as unable to see a startup call. A `count: 0` assertion is
|
|
566
|
+
// left alone on purpose — it PASSES here, and turning that green into a non-pass is a grade
|
|
567
|
+
// change, not a wording one.
|
|
568
|
+
return 0 === matches.length && 0 === since && counted.failureReason !== undefined
|
|
569
|
+
? { ...counted, failureReason: `${counted.failureReason}${PRE_ATTACH_CAVEAT}` }
|
|
570
|
+
: counted;
|
|
461
571
|
}
|
|
462
572
|
const hit = matches[0];
|
|
573
|
+
if (hit === undefined) {
|
|
574
|
+
const download = nativeDownloadMiss(events, p);
|
|
575
|
+
if (download !== undefined)
|
|
576
|
+
return download;
|
|
577
|
+
}
|
|
463
578
|
if (hit === undefined && targetsUnobservedChannel(p) && !sawSubresources) {
|
|
464
579
|
const reason = unobservedChannelReason(p);
|
|
465
580
|
return {
|
|
@@ -472,10 +587,10 @@ export function evalNet(events, p) {
|
|
|
472
587
|
};
|
|
473
588
|
}
|
|
474
589
|
return hit !== undefined
|
|
475
|
-
? { pass: true, evidence: netEvidence(hit.data) }
|
|
590
|
+
? { pass: true, evidence: netEvidence(hit.data, p) }
|
|
476
591
|
: {
|
|
477
592
|
pass: false,
|
|
478
|
-
failureReason: `no network call matched ${JSON.stringify(p)}`,
|
|
593
|
+
failureReason: `no network call matched ${JSON.stringify(p)}${0 === since ? PRE_ATTACH_CAVEAT : ''}`,
|
|
479
594
|
// Same reasoning as the signal miss: "no matching call" cannot be told apart from "the app
|
|
480
595
|
// made no calls at all", and those need different fixes.
|
|
481
596
|
observed: observedNetCalls(events, p.urlContains),
|
|
@@ -483,103 +598,6 @@ export function evalNet(events, p) {
|
|
|
483
598
|
assertion: 'net.present',
|
|
484
599
|
};
|
|
485
600
|
}
|
|
486
|
-
/** The only console levels Reticle instruments (console.info/debug/trace are NOT patched). */
|
|
487
|
-
const CONSOLE_LEVEL_TYPE = {
|
|
488
|
-
log: EventType.CONSOLE_LOG,
|
|
489
|
-
warn: EventType.CONSOLE_WARN,
|
|
490
|
-
error: EventType.CONSOLE_ERROR,
|
|
491
|
-
};
|
|
492
|
-
export function evalConsole(events, p) {
|
|
493
|
-
const since = p.since ?? 0;
|
|
494
|
-
// Reticle only instruments console.log/warn/error. A level outside that set is never captured,
|
|
495
|
-
// so its events can't exist — and an `absent` assertion on it would verify NOTHING while
|
|
496
|
-
// reporting green. Fail loudly instead of false-passing.
|
|
497
|
-
if (p.level !== undefined && p.level !== 'error' && CONSOLE_LEVEL_TYPE[p.level] === undefined) {
|
|
498
|
-
return {
|
|
499
|
-
pass: false,
|
|
500
|
-
failureReason: `console level '${p.level}' is not captured — Reticle instruments console.log, console.warn, console.error only`,
|
|
501
|
-
observed: `level '${p.level}' is not instrumented, so no event of it can ever exist`,
|
|
502
|
-
expected: 'a level Reticle captures: log, warn, or error',
|
|
503
|
-
assertion: 'console.uninstrumented-level',
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
const matches = events.filter((e) => {
|
|
507
|
-
if (e.t < since)
|
|
508
|
-
return false;
|
|
509
|
-
const isErr = e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT;
|
|
510
|
-
if (p.level === undefined) {
|
|
511
|
-
return (e.type === EventType.CONSOLE_LOG ||
|
|
512
|
-
e.type === EventType.CONSOLE_WARN ||
|
|
513
|
-
e.type === EventType.CONSOLE_ERROR ||
|
|
514
|
-
e.type === EventType.ERROR_UNCAUGHT);
|
|
515
|
-
}
|
|
516
|
-
if ('error' === p.level)
|
|
517
|
-
return isErr;
|
|
518
|
-
return e.type === CONSOLE_LEVEL_TYPE[p.level];
|
|
519
|
-
});
|
|
520
|
-
// A text match narrows the population to entries whose captured message contains the substring.
|
|
521
|
-
// With `absent: true` this is the whole point: "THIS message did not appear", not "no messages
|
|
522
|
-
// appeared" — the difference between a regression check and a fragile one that any unrelated
|
|
523
|
-
// warning anywhere in the app breaks.
|
|
524
|
-
const wanted = 'contains' in p ? p.contains : undefined;
|
|
525
|
-
// Captured messages are strings (stringifyArgs in the browser observer), but a malformed or
|
|
526
|
-
// foreign event must not crash the evaluator: non-strings stringify defensively, and objects
|
|
527
|
-
// go through JSON.stringify rather than a default toString that would print '[object Object]'.
|
|
528
|
-
const asText = (v) => {
|
|
529
|
-
if ('string' === typeof v)
|
|
530
|
-
return v;
|
|
531
|
-
try {
|
|
532
|
-
return JSON.stringify(v) ?? '';
|
|
533
|
-
}
|
|
534
|
-
catch {
|
|
535
|
-
return '';
|
|
536
|
-
}
|
|
537
|
-
};
|
|
538
|
-
const matching = wanted !== undefined
|
|
539
|
-
? matches.filter((e) => asText(e.data['message']).includes(wanted))
|
|
540
|
-
: matches;
|
|
541
|
-
if (true === p.absent) {
|
|
542
|
-
if (wanted !== undefined && 0 === matching.length && matches.length > 0) {
|
|
543
|
-
// Other entries exist but none carries the substring: exactly the pass an absence-with-match
|
|
544
|
-
// asserts. Name both counts so the caller can tell this from a silent window.
|
|
545
|
-
return {
|
|
546
|
-
pass: true,
|
|
547
|
-
evidence: { absent: true, contains: wanted },
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
return 0 === matching.length
|
|
551
|
-
? { pass: true, evidence: { absent: true } }
|
|
552
|
-
: {
|
|
553
|
-
pass: false,
|
|
554
|
-
failureReason: wanted !== undefined
|
|
555
|
-
? `expected no ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)} but found ${String(matching.length)}`
|
|
556
|
-
: `expected no ${p.level ?? 'console'} entries but found ${String(matches.length)}`,
|
|
557
|
-
observed: wanted !== undefined
|
|
558
|
-
? `${String(matching.length)} ${p.level ?? 'console'} entr${1 === matching.length ? 'y' : 'ies'} containing ${JSON.stringify(wanted)}`
|
|
559
|
-
: `${String(matches.length)} ${p.level ?? 'console'} entr${1 === matches.length ? 'y' : 'ies'}`,
|
|
560
|
-
expected: wanted !== undefined
|
|
561
|
-
? `no ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)}`
|
|
562
|
-
: `no ${p.level ?? 'console'} entries`,
|
|
563
|
-
assertion: wanted !== undefined ? 'console.absent-contains' : 'console.absent',
|
|
564
|
-
evidence: matching.map((e) => e.data),
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
return matching.length > 0
|
|
568
|
-
? { pass: true, evidence: matching.map((e) => e.data) }
|
|
569
|
-
: {
|
|
570
|
-
pass: false,
|
|
571
|
-
failureReason: wanted !== undefined
|
|
572
|
-
? `no ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)} found`
|
|
573
|
-
: `no ${p.level ?? 'console'} entries found`,
|
|
574
|
-
observed: wanted !== undefined
|
|
575
|
-
? `no ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)} in the window`
|
|
576
|
-
: `no ${p.level ?? 'console'} entries in the window`,
|
|
577
|
-
expected: wanted !== undefined
|
|
578
|
-
? `at least one ${p.level ?? 'console'} entry containing ${JSON.stringify(wanted)}`
|
|
579
|
-
: `at least one ${p.level ?? 'console'} entry`,
|
|
580
|
-
assertion: 'console.present',
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
601
|
export function evalAnimation(events, p) {
|
|
584
602
|
const wantType = true === p.completed ? EventType.ANIM_END : EventType.ANIM_START;
|
|
585
603
|
const hit = events.find((e) => {
|
|
@@ -106,9 +106,42 @@ export function parsePredicate(input) {
|
|
|
106
106
|
// back is how the zod array became unreadable in the first place.
|
|
107
107
|
const issues = parsed.error.issues.slice(0, 3).map(describeIssue).join('; ');
|
|
108
108
|
throw new Error(`that predicate did not parse (kind "${kind}"): ${issues}. Nothing ran — the predicate was ` +
|
|
109
|
-
`not evaluated, so no verdict was produced. ${accepted(kind, parsed.error.issues)}
|
|
109
|
+
`not evaluated, so no verdict was produced. ${accepted(kind, parsed.error.issues)}` +
|
|
110
|
+
`${misplacedCallFields(input)} ` +
|
|
110
111
|
`A valid ${kind} predicate looks like: ${exampleFor(kind)}`);
|
|
111
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Arguments of the CALL that agents write inside the predicate instead.
|
|
115
|
+
*
|
|
116
|
+
* Reported from the field on `timeout_ms`, nested in `until` on an `act_and_wait` that had already
|
|
117
|
+
* timed out once at the default budget. The rejection said "unknown field", which reads as "there is
|
|
118
|
+
* no such thing" — so the obvious retry is to delete it and silently give up the longer budget that
|
|
119
|
+
* was the point of writing it. It is not an unknown field; it is a real argument one level too deep.
|
|
120
|
+
*/
|
|
121
|
+
const CALL_LEVEL_FIELDS = [
|
|
122
|
+
'timeout_ms',
|
|
123
|
+
'sessionId',
|
|
124
|
+
'ref',
|
|
125
|
+
'action',
|
|
126
|
+
'args',
|
|
127
|
+
'target',
|
|
128
|
+
'intent',
|
|
129
|
+
];
|
|
130
|
+
/** The clause naming where a misplaced call argument goes, or '' when nothing was misplaced. */
|
|
131
|
+
function misplacedCallFields(input) {
|
|
132
|
+
if ('object' !== typeof input || null === input)
|
|
133
|
+
return '';
|
|
134
|
+
const written = Object.keys(input);
|
|
135
|
+
const misplaced = CALL_LEVEL_FIELDS.filter((field) => written.includes(field));
|
|
136
|
+
if (0 === misplaced.length)
|
|
137
|
+
return '';
|
|
138
|
+
const list = misplaced.join(', ');
|
|
139
|
+
// Named as a MOVE, not as a removal. Deleting the field is the reading of "unknown field" that
|
|
140
|
+
// costs the caller the thing it asked for.
|
|
141
|
+
return (` ${list} ${1 === misplaced.length ? 'is an argument' : 'are arguments'} of the CALL, not of ` +
|
|
142
|
+
`the predicate: ${1 === misplaced.length ? 'move it' : 'move them'} up beside \`until\` rather ` +
|
|
143
|
+
'than dropping it — nesting is why the predicate did not parse.');
|
|
144
|
+
}
|
|
112
145
|
/**
|
|
113
146
|
* What WOULD have worked, in the same breath as what did not.
|
|
114
147
|
*
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type EvalResult } from './predicate-eval.js';
|
|
2
|
+
import type { PredicateKind } from '@reticlehq/core';
|
|
3
|
+
import type { Predicate } from './predicate-schema.js';
|
|
4
|
+
/** The net predicate as `describeNetFilter` types it. */
|
|
5
|
+
type NetPredicate = Extract<Predicate, {
|
|
6
|
+
kind: typeof PredicateKind.NET;
|
|
7
|
+
}>;
|
|
8
|
+
/** A net predicate, narrowed to the fields this module reads. */
|
|
9
|
+
interface RequestBodyPredicate {
|
|
10
|
+
requestBodyContains?: string;
|
|
11
|
+
requestBodyMatches?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What the filtering pass learned about calls that matched everything else.
|
|
15
|
+
*
|
|
16
|
+
* Accumulated during the single pass over events rather than recomputed after it, the way the
|
|
17
|
+
* response-side trackers already are.
|
|
18
|
+
*/
|
|
19
|
+
export interface RequestBodyState {
|
|
20
|
+
/** A call matched everything else but carried no recorded request body. */
|
|
21
|
+
unrecorded: boolean;
|
|
22
|
+
/** The request body a clause was checked against and did not match. */
|
|
23
|
+
mismatch?: string;
|
|
24
|
+
/** The prefix of a TRUNCATED request body the clause did not hold over — undecidable, not absent. */
|
|
25
|
+
truncated?: string;
|
|
26
|
+
/**
|
|
27
|
+
* A key whose CAPTURED value is `[REDACTED]`, so the clause cannot be judged at all.
|
|
28
|
+
*
|
|
29
|
+
* Request bodies go through the same redaction path as responses before they are ever recorded,
|
|
30
|
+
* so a clause on a sensitive key is unsatisfiable whatever the app sent. Grading that a mismatch
|
|
31
|
+
* would blame the application for the SDK's own redaction and send someone to fix a correct
|
|
32
|
+
* payload.
|
|
33
|
+
*/
|
|
34
|
+
redactedField?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function newRequestBodyState(): RequestBodyState;
|
|
37
|
+
/**
|
|
38
|
+
* Does this event satisfy the predicate's request-body clauses? Records why not in `state`.
|
|
39
|
+
*
|
|
40
|
+
* Returns true when there is nothing to check, so callers can apply it unconditionally.
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkRequestBody(data: Record<string, unknown>, predicate: RequestBodyPredicate, state: RequestBodyState): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The verdict for a request-body clause nothing satisfied, or `undefined` to fall through.
|
|
45
|
+
*
|
|
46
|
+
* Ranked the way the response side ranks its own: the two nobody could have answered (redacted,
|
|
47
|
+
* unrecorded, truncated) come before the one that WAS answered and came out false.
|
|
48
|
+
*/
|
|
49
|
+
export declare function requestBodyVerdict(state: RequestBodyState, predicate: NetPredicate, matchCount: number): EvalResult | undefined;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asserting on what the UI SENT, not on what came back (#798).
|
|
3
|
+
*
|
|
4
|
+
* `net.bodyContains` searches the response only, and the comment on it says why: searching the
|
|
5
|
+
* request too would let it pass on the very defect it exists to catch (the app sent `1187.01`, so
|
|
6
|
+
* that string is in the request whatever the server then did with it). That reasoning is right, and
|
|
7
|
+
* it leaves a real gap. For a filter, a search box or a form, the thing under test IS the outgoing
|
|
8
|
+
* payload — "applying this filter actually sends it" had no verdict at all, only
|
|
9
|
+
* `reticle_network { bodies: true }` and a person reading `requestBody` by eye.
|
|
10
|
+
*
|
|
11
|
+
* So these are separate fields rather than a mode on `bodyContains`: neither assertion can then be
|
|
12
|
+
* satisfied by the wrong half of the exchange.
|
|
13
|
+
*
|
|
14
|
+
* Extracted from `predicate-eval.ts`, which was at the 1000-line cap.
|
|
15
|
+
*/
|
|
16
|
+
import { REDACTED_VALUE } from '@reticlehq/core';
|
|
17
|
+
import { clipBody, dataMatches, describeNetFilter, str, } from './predicate-eval.js';
|
|
18
|
+
export function newRequestBodyState() {
|
|
19
|
+
return { unrecorded: false };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Does this event satisfy the predicate's request-body clauses? Records why not in `state`.
|
|
23
|
+
*
|
|
24
|
+
* Returns true when there is nothing to check, so callers can apply it unconditionally.
|
|
25
|
+
*/
|
|
26
|
+
export function checkRequestBody(data, predicate, state) {
|
|
27
|
+
const { requestBodyContains, requestBodyMatches } = predicate;
|
|
28
|
+
if (undefined === requestBodyContains && undefined === requestBodyMatches)
|
|
29
|
+
return true;
|
|
30
|
+
const sent = str(data['requestBody']);
|
|
31
|
+
if (sent === undefined) {
|
|
32
|
+
state.unrecorded = true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const wasTruncated = true === data['requestBodyTruncated'];
|
|
36
|
+
const note = () => {
|
|
37
|
+
// The same rule the response side keeps: a needle missing from a body we hold only the first N
|
|
38
|
+
// bytes of is undecidable, not absent.
|
|
39
|
+
if (wasTruncated)
|
|
40
|
+
state.truncated ??= sent;
|
|
41
|
+
else
|
|
42
|
+
state.mismatch ??= sent;
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
if (requestBodyContains !== undefined && !sent.includes(requestBodyContains))
|
|
46
|
+
return note();
|
|
47
|
+
if (requestBodyMatches === undefined)
|
|
48
|
+
return true;
|
|
49
|
+
let payload;
|
|
50
|
+
try {
|
|
51
|
+
payload = JSON.parse(sent);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Not JSON (a form post, plain text). A shallow key match cannot be applied, and guessing at
|
|
55
|
+
// form encoding here would answer a different question than the one asked. Use
|
|
56
|
+
// `requestBodyContains` for those.
|
|
57
|
+
return note();
|
|
58
|
+
}
|
|
59
|
+
if (null === payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
60
|
+
return note();
|
|
61
|
+
const actual = payload;
|
|
62
|
+
const redacted = Object.keys(requestBodyMatches).find((key) => REDACTED_VALUE === actual[key]);
|
|
63
|
+
if (redacted !== undefined) {
|
|
64
|
+
state.redactedField ??= redacted;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return dataMatches(actual, requestBodyMatches) ? true : note();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The verdict for a request-body clause nothing satisfied, or `undefined` to fall through.
|
|
71
|
+
*
|
|
72
|
+
* Ranked the way the response side ranks its own: the two nobody could have answered (redacted,
|
|
73
|
+
* unrecorded, truncated) come before the one that WAS answered and came out false.
|
|
74
|
+
*/
|
|
75
|
+
export function requestBodyVerdict(state, predicate, matchCount) {
|
|
76
|
+
if (matchCount > 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
const wanted = JSON.stringify(predicate.requestBodyContains ?? predicate.requestBodyMatches);
|
|
79
|
+
if (state.redactedField !== undefined) {
|
|
80
|
+
const field = JSON.stringify(state.redactedField);
|
|
81
|
+
return {
|
|
82
|
+
pass: false,
|
|
83
|
+
inconclusive: `a call matching ${describeNetFilter(predicate)} was made, but its ${field} was REDACTED before the body was recorded, so this clause cannot be judged — a redacted field is unknown, not different. Assert on a non-sensitive key, or on the effect the value had`,
|
|
84
|
+
observed: `a matching request whose ${field} is ${REDACTED_VALUE}`,
|
|
85
|
+
expected: `a request body matching ${wanted}`,
|
|
86
|
+
assertion: 'net.requestBodyMatches',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (state.unrecorded) {
|
|
90
|
+
return {
|
|
91
|
+
pass: false,
|
|
92
|
+
failureReason: `a call matched but its REQUEST body was not recorded, so the request-body clause could not be checked — enable it where the app calls connect(): reticle({ captureNetworkBodies: true })`,
|
|
93
|
+
observed: 'a matching call with no recorded request body',
|
|
94
|
+
expected: `a request body matching ${wanted}`,
|
|
95
|
+
assertion: 'net.requestBody',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (state.truncated !== undefined) {
|
|
99
|
+
return {
|
|
100
|
+
pass: false,
|
|
101
|
+
inconclusive: `a call matching ${describeNetFilter(predicate)} was sent with a request body that was TRUNCATED before it was recorded, and the clause did not hold over the part that was kept — so this is undecidable, not a failure`,
|
|
102
|
+
observed: `the first ${String(state.truncated.length)} characters of a truncated request body ${JSON.stringify(clipBody(state.truncated))}`,
|
|
103
|
+
expected: `a request body matching ${wanted}`,
|
|
104
|
+
assertion: 'net.requestBody',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (state.mismatch !== undefined) {
|
|
108
|
+
// Named separately from "no call matched", for the reason the response side already learned: the
|
|
109
|
+
// request DID fire, and reporting zero matches sends the caller to check the url and the method,
|
|
110
|
+
// which are both fine, instead of to the value the UI actually sent.
|
|
111
|
+
return {
|
|
112
|
+
pass: false,
|
|
113
|
+
failureReason: `a call matching ${describeNetFilter(predicate)} was sent with ${JSON.stringify(clipBody(state.mismatch))}, which does not match the request-body clause — the request fired, the payload is what differed`,
|
|
114
|
+
observed: `request body ${JSON.stringify(clipBody(state.mismatch))}`,
|
|
115
|
+
expected: `a request body matching ${wanted}`,
|
|
116
|
+
assertion: 'net.requestBody',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
@@ -31,6 +31,24 @@ export declare const RouteDecidedBy: {
|
|
|
31
31
|
* about where the app is, which is the failure this predicate exists to prevent.
|
|
32
32
|
*/
|
|
33
33
|
export declare function routePathOf(pathname: string, hash: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* The resolved parts of a route, from a `ROUTE_CHANGE` event or a session URL.
|
|
36
|
+
*
|
|
37
|
+
* `routePath` is where the ROUTER is (the fragment path under a hash router). `docPath` + `hash`
|
|
38
|
+
* is the NAVIGABLE value a `reticle_navigate` can be pointed at. Call sites pick; collapsing
|
|
39
|
+
* them into one string is how a hash-router miss lands as "every page is `/`".
|
|
40
|
+
*/
|
|
41
|
+
export interface RouteParts {
|
|
42
|
+
routePath: string;
|
|
43
|
+
docPath: string;
|
|
44
|
+
hash: string;
|
|
45
|
+
search: string;
|
|
46
|
+
full: string;
|
|
47
|
+
}
|
|
48
|
+
/** A `ROUTE_CHANGE` event, or its `data` — one reading, so the sixth copy cannot drift. */
|
|
49
|
+
export declare function routeOfEvent(eventOrData: ReticleEvent | Record<string, unknown>): RouteParts | undefined;
|
|
50
|
+
/** Same shape from a session URL (the fallback when the tab hard-loaded and emitted no route event). */
|
|
51
|
+
export declare function routeOfUrl(url: string): RouteParts | undefined;
|
|
34
52
|
export declare function evalRoute(events: ReticleEvent[], p: Extract<Predicate, {
|
|
35
53
|
kind: typeof PredicateKind.ROUTE;
|
|
36
54
|
}>,
|
|
@@ -43,4 +61,6 @@ export declare function evalRoute(events: ReticleEvent[], p: Extract<Predicate,
|
|
|
43
61
|
* `reticle_snapshot({ mode: 'status' }).route` one call later. Optional so a caller with no session
|
|
44
62
|
* (a replayed window) keeps the old answer instead of inventing a route.
|
|
45
63
|
*/
|
|
46
|
-
currentUrl?: string
|
|
64
|
+
currentUrl?: string,
|
|
65
|
+
/** Event-time floor for the window, so a request already in flight before it is not counted. */
|
|
66
|
+
since?: number): EvalResult;
|