@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DevServerEntry } from '@reticlehq/core';
|
|
1
|
+
import { type DevServerEntry } from '@reticlehq/core/artifacts';
|
|
2
2
|
/** Injected so the reading is unit-testable without a real home or real processes. */
|
|
3
3
|
export interface DevServerIo {
|
|
4
4
|
readdir: (dir: string) => string[];
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
-
import { DevServerEntrySchema, devServerRegistryPort, liveDevServers, } from '@reticlehq/core';
|
|
21
|
+
import { DevServerEntrySchema, devServerRegistryPort, liveDevServers, } from '@reticlehq/core/artifacts';
|
|
22
22
|
/** process.kill(pid, 0) throws iff the process is gone — the same probe the daemon registry uses. */
|
|
23
23
|
function isAliveNow(pid) {
|
|
24
24
|
try {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
/** IPv6 loopback. The other half of what `localhost` can mean. */
|
|
20
20
|
export declare const IPV6_LOOPBACK = "::1";
|
|
21
|
-
|
|
21
|
+
interface LoopbackAlias {
|
|
22
22
|
/** False when the alias could not be bound — the daemon still serves IPv4 as before. */
|
|
23
23
|
opened: boolean;
|
|
24
24
|
close: () => Promise<void>;
|
|
@@ -30,3 +30,4 @@ export interface LoopbackAlias {
|
|
|
30
30
|
* trust holds — the alias does not widen who may connect, only which address they may name.
|
|
31
31
|
*/
|
|
32
32
|
export declare function openLoopbackAlias(port: number): Promise<LoopbackAlias>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The structured log event emitted when the daemon child cannot start. */
|
|
2
|
+
export declare const DAEMON_START_FAILED_EVENT = "reticle_daemon_start_failed";
|
|
3
|
+
/**
|
|
4
|
+
* Return the most recent startup cause written by this launch attempt.
|
|
5
|
+
*
|
|
6
|
+
* Daemon logs are append-only across launches, so an event name alone is not enough: surfacing an
|
|
7
|
+
* older failure would confidently diagnose the wrong attempt. The parent supplies the timestamp it
|
|
8
|
+
* captured immediately before spawning and this reader ignores every earlier record. Malformed or
|
|
9
|
+
* unavailable logs are best-effort misses; the caller still has its generic fallback diagnosis.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readDaemonStartupCause(path: string, sinceMs: number): string | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/** The structured log event emitted when the daemon child cannot start. */
|
|
3
|
+
export const DAEMON_START_FAILED_EVENT = 'reticle_daemon_start_failed';
|
|
4
|
+
/**
|
|
5
|
+
* Return the most recent startup cause written by this launch attempt.
|
|
6
|
+
*
|
|
7
|
+
* Daemon logs are append-only across launches, so an event name alone is not enough: surfacing an
|
|
8
|
+
* older failure would confidently diagnose the wrong attempt. The parent supplies the timestamp it
|
|
9
|
+
* captured immediately before spawning and this reader ignores every earlier record. Malformed or
|
|
10
|
+
* unavailable logs are best-effort misses; the caller still has its generic fallback diagnosis.
|
|
11
|
+
*/
|
|
12
|
+
export function readDaemonStartupCause(path, sinceMs) {
|
|
13
|
+
let contents;
|
|
14
|
+
try {
|
|
15
|
+
contents = readFileSync(path, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const lines = contents.split('\n');
|
|
21
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
22
|
+
const line = lines[index]?.trim();
|
|
23
|
+
if (undefined === line || 0 === line.length)
|
|
24
|
+
continue;
|
|
25
|
+
let record;
|
|
26
|
+
try {
|
|
27
|
+
record = JSON.parse(line);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (typeof record !== 'object' || null === record)
|
|
33
|
+
continue;
|
|
34
|
+
const candidate = record;
|
|
35
|
+
if (candidate.event !== DAEMON_START_FAILED_EVENT)
|
|
36
|
+
continue;
|
|
37
|
+
if (typeof candidate.t !== 'string' || typeof candidate.error !== 'string')
|
|
38
|
+
continue;
|
|
39
|
+
const recordedAt = Date.parse(candidate.t);
|
|
40
|
+
const error = candidate.error.trim();
|
|
41
|
+
if (!Number.isFinite(recordedAt) || recordedAt < sinceMs || 0 === error.length)
|
|
42
|
+
continue;
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
@@ -156,21 +156,15 @@ export interface ContradictionOptions {
|
|
|
156
156
|
* one: a caller that cannot say which page is under test gets the behaviour it had before this.
|
|
157
157
|
*/
|
|
158
158
|
appOrigin?: string | undefined;
|
|
159
|
+
/**
|
|
160
|
+
* The traffic the assertion actually named — `urlContains` from each net clause, `''` for a clause
|
|
161
|
+
* that named the whole channel.
|
|
162
|
+
*
|
|
163
|
+
* Used only by `duplicate-request`, to tell "the write you asked about fired twice" from "some
|
|
164
|
+
* other endpoint is busy". Undefined disables the split and every duplicate is reported as it was
|
|
165
|
+
* before, which is what a caller with no predicate (a bare `observe`) should get: with nothing
|
|
166
|
+
* declared, nothing is unrelated.
|
|
167
|
+
*/
|
|
168
|
+
namedNetUrls?: readonly string[] | undefined;
|
|
159
169
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Cross-channel contradictions in this window, with the edit-epoch caveat attached when it applies.
|
|
162
|
-
*
|
|
163
|
-
* Cross-epoch evidence is LABELLED rather than excluded, which is the opposite of what the document
|
|
164
|
-
* scoping does, and the difference is the point. A navigation is total — it throws away the page,
|
|
165
|
-
* the refs, the in-flight requests and the state — so nothing recorded before it is still about the
|
|
166
|
-
* world, and dropping it is the only honest option. A hot update is not: most modules, most of the
|
|
167
|
-
* DOM, the whole network log and every console line survive one, so most of what was observed a
|
|
168
|
-
* second before an edit is still true a second after. Excluding it would empty windows that hold
|
|
169
|
-
* real findings, and an emptied window reads as "nothing happened" — which is the more expensive of
|
|
170
|
-
* the two wrong answers and the one this whole family of checks exists to prevent.
|
|
171
|
-
*
|
|
172
|
-
* So the findings stand and the caveat is said out loud, and only when it is unambiguous: EVERY
|
|
173
|
-
* observation in the window predates the edit. One post-edit observation and the agent is already
|
|
174
|
-
* looking at the code it wrote, so the label would be noise.
|
|
175
|
-
*/
|
|
176
170
|
export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
|
|
@@ -21,6 +21,32 @@ function netCall(e) {
|
|
|
21
21
|
: true === e.data['ok'] || (e.data['ok'] === undefined && (status ?? 0) < 400),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The failed calls that a LATER successful call to the same endpoint replaced.
|
|
26
|
+
*
|
|
27
|
+
* Identity, not index: each `netCall` is a fresh object held by `settled`, and `settled` is built
|
|
28
|
+
* from events in sequence order, so "later" is simply "further along the array". Compared on
|
|
29
|
+
* `matchUrl` so a cache-buster or a changing query token cannot make a retry look like a different
|
|
30
|
+
* call — the same normalisation every other url comparison here uses.
|
|
31
|
+
*/
|
|
32
|
+
function recoveredByRetry(settled) {
|
|
33
|
+
const out = new Set();
|
|
34
|
+
for (let i = 0; i < settled.length; i += 1) {
|
|
35
|
+
const call = settled[i];
|
|
36
|
+
if (call === undefined || false !== call.ok)
|
|
37
|
+
continue;
|
|
38
|
+
for (let j = i + 1; j < settled.length; j += 1) {
|
|
39
|
+
const later = settled[j];
|
|
40
|
+
if (later === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
if (true === later.ok && later.method === call.method && later.matchUrl === call.matchUrl) {
|
|
43
|
+
out.add(call);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
24
50
|
/**
|
|
25
51
|
* Did the user-visible application state move forward? DOM, store and route only — deliberately NOT
|
|
26
52
|
* network, animation or signal. The question every rule below asks is "did the app act as if it
|
|
@@ -221,6 +247,42 @@ function splitForeignTraffic(events, appOrigin) {
|
|
|
221
247
|
* observation in the window predates the edit. One post-edit observation and the agent is already
|
|
222
248
|
* looking at the code it wrote, so the label would be noise.
|
|
223
249
|
*/
|
|
250
|
+
/**
|
|
251
|
+
* Below this, repeated writes are a BURST, whatever their spacing.
|
|
252
|
+
*
|
|
253
|
+
* A double submit is two clicks, or one click and a re-render: milliseconds apart. Nothing a human
|
|
254
|
+
* or a StrictMode remount does lands on a quarter-second grid, so this is the floor under which
|
|
255
|
+
* regularity means nothing.
|
|
256
|
+
*/
|
|
257
|
+
const POLL_MIN_INTERVAL_MS = 250;
|
|
258
|
+
/**
|
|
259
|
+
* How far a gap may sit from the median and still count as the same cadence.
|
|
260
|
+
*
|
|
261
|
+
* Loose on purpose: a real poll drifts under load, and a `setInterval` competing with a busy main
|
|
262
|
+
* thread is not metronomic. Tight enough that a burst followed by a late retry — the shape a double
|
|
263
|
+
* submit plus a user's second attempt makes — is not read as a rhythm.
|
|
264
|
+
*/
|
|
265
|
+
const POLL_JITTER_RATIO = 0.4;
|
|
266
|
+
/**
|
|
267
|
+
* Is this the same write on a steady interval, rather than the same write twice?
|
|
268
|
+
*
|
|
269
|
+
* THREE samples minimum, and that is the load-bearing part: two writes give one gap, and a single
|
|
270
|
+
* gap cannot distinguish a cadence from a coincidence. Two writes stay a duplicate however far
|
|
271
|
+
* apart they are, which is the classic double submit and every case this rule was written for.
|
|
272
|
+
*/
|
|
273
|
+
function isSteadyCadence(times) {
|
|
274
|
+
if (times.length < 3)
|
|
275
|
+
return false;
|
|
276
|
+
const ordered = [...times].sort((a, b) => a - b);
|
|
277
|
+
const gaps = [];
|
|
278
|
+
for (let i = 1; i < ordered.length; i += 1)
|
|
279
|
+
gaps.push((ordered[i] ?? 0) - (ordered[i - 1] ?? 0));
|
|
280
|
+
const sorted = [...gaps].sort((a, b) => a - b);
|
|
281
|
+
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
282
|
+
if (median < POLL_MIN_INTERVAL_MS)
|
|
283
|
+
return false;
|
|
284
|
+
return gaps.every((gap) => Math.abs(gap - median) <= median * POLL_JITTER_RATIO);
|
|
285
|
+
}
|
|
224
286
|
export function findContradictions(allEvents, options = {}) {
|
|
225
287
|
const found = findWindowContradictions(allEvents, options);
|
|
226
288
|
const predates = allEvents.length > 0 &&
|
|
@@ -303,7 +365,20 @@ function findWindowContradictions(allEvents, options) {
|
|
|
303
365
|
},
|
|
304
366
|
];
|
|
305
367
|
}
|
|
306
|
-
|
|
368
|
+
// A failure the app RECOVERED from is not evidence against anything.
|
|
369
|
+
//
|
|
370
|
+
// Reported from the field on an app whose api-client documents the pattern every token-refreshing
|
|
371
|
+
// app has: a 401 re-hydrates the session and retries once. One extraction produced a 401 and then
|
|
372
|
+
// a 200 to the same endpoint, with exactly one extraction in state, and Reticle called it two
|
|
373
|
+
// separate contradictions. Both were false, both cost calls to disprove, and a rule that
|
|
374
|
+
// manufactures reds costs more trust than a missed bug does.
|
|
375
|
+
//
|
|
376
|
+
// Structural, not a timing heuristic: what makes it a retry is that a LATER call to the same
|
|
377
|
+
// method and url SUCCEEDED. The write landed, so the UI was entitled to move and a success signal
|
|
378
|
+
// was entitled to fire. Filtered at the definition rather than at the two reading sites, because
|
|
379
|
+
// both rules make the same claim about the same evidence and a fix in one is a fix in half.
|
|
380
|
+
const recovered = recoveredByRetry(settled);
|
|
381
|
+
const failed = settled.filter((c) => false === c.ok && !recovered.has(c));
|
|
307
382
|
// The failures NOBODY declared — see ContradictionOptions.expectedFailures. Only the heuristic
|
|
308
383
|
// "the UI moved while a request failed" rule reads this; the sharp rules still read `failed`.
|
|
309
384
|
const unexpected = failed.filter((c) => !matchesDeclaredFailure(c, options.expectedFailures ?? []));
|
|
@@ -352,12 +427,33 @@ function findWindowContradictions(allEvents, options) {
|
|
|
352
427
|
e.type === EventType.DOM_TEXT);
|
|
353
428
|
const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
|
|
354
429
|
if (routed && !hashAnchorOnly && !rendered && !fetched && true !== options.renderProved) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
430
|
+
// A console error in the SAME window turns "nothing rendered" from an absence into a positive
|
|
431
|
+
// claim: the destination did not merely fail to produce content, it crashed while trying to.
|
|
432
|
+
// Reported once as `unknown` when this held — a React hooks error and an empty destination were
|
|
433
|
+
// both in hand, and the honest, definitive answer was available and not given (#897).
|
|
434
|
+
const consoleErrors = events
|
|
435
|
+
.filter((e) => e.type === EventType.CONSOLE_ERROR)
|
|
436
|
+
.map((e) => asString(e.data['message']))
|
|
437
|
+
.filter((m) => undefined !== m && m.length > 0);
|
|
438
|
+
if (consoleErrors.length > 0) {
|
|
439
|
+
found.push({
|
|
440
|
+
kind: ContradictionKind.ROUTE_RENDERED_NOTHING_CRASHED,
|
|
441
|
+
claim: 'the app navigated to a new route',
|
|
442
|
+
counter: `nothing was rendered for it, and the console shows why: ${consoleErrors[0] ?? ''}`,
|
|
443
|
+
detail: `the URL moved but the destination produced no content, and the same window logged ` +
|
|
444
|
+
`${String(consoleErrors.length)} console error(s) — the first: "${consoleErrors[0] ?? ''}". ` +
|
|
445
|
+
'The destination crashed rather than merely rendering nothing; read the error for the ' +
|
|
446
|
+
'component and line at fault.',
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
found.push({
|
|
451
|
+
kind: ContradictionKind.ROUTE_RENDERED_NOTHING,
|
|
452
|
+
claim: 'the app navigated to a new route',
|
|
453
|
+
counter: 'nothing was rendered for it — no content added or removed, and no request made',
|
|
454
|
+
detail: 'the URL moved but the destination produced no content: a route with no view, a view that returned null, or data the page never asked for. A control that navigates always looks alive, so this is invisible to a dead-control check. Confirm by reading the page — a view revealed from DOM that already existed emits this same window',
|
|
455
|
+
});
|
|
456
|
+
}
|
|
361
457
|
}
|
|
362
458
|
// ── The app claimed success while its own request failed ────────────────────────────────────
|
|
363
459
|
// A signal is the sharper claim: the app did not merely LOOK right, it explicitly asserted
|
|
@@ -524,7 +620,7 @@ function findWindowContradictions(allEvents, options) {
|
|
|
524
620
|
* distinguishes them.
|
|
525
621
|
*/
|
|
526
622
|
const navigatedAt = events.find((e) => EventType.ROUTE_CHANGE === e.type && e.t >= actionSince)?.t;
|
|
527
|
-
const
|
|
623
|
+
const writeTimes = new Map();
|
|
528
624
|
for (const event of events) {
|
|
529
625
|
if (event.type !== EventType.NET_REQUEST || event.t < actionSince)
|
|
530
626
|
continue;
|
|
@@ -536,18 +632,54 @@ function findWindowContradictions(allEvents, options) {
|
|
|
536
632
|
const label = `${call.method} ${call.url}`;
|
|
537
633
|
const body = asString(event.data['requestBody']);
|
|
538
634
|
const key = body === undefined || 0 === body.length ? label : `${label} ${body}`;
|
|
539
|
-
const entry =
|
|
540
|
-
entry.
|
|
541
|
-
|
|
635
|
+
const entry = writeTimes.get(key) ?? { label, times: [], landed: 0 };
|
|
636
|
+
entry.times.push(event.t);
|
|
637
|
+
// Counted separately from the occurrences, because the claim is about what APPLIED.
|
|
638
|
+
if (false !== call.ok)
|
|
639
|
+
entry.landed += 1;
|
|
640
|
+
writeTimes.set(key, entry);
|
|
542
641
|
}
|
|
543
|
-
|
|
544
|
-
|
|
642
|
+
/**
|
|
643
|
+
* Did the assertion name this endpoint?
|
|
644
|
+
*
|
|
645
|
+
* `undefined` means the caller declared nothing (a bare `observe`), and with nothing declared
|
|
646
|
+
* nothing is unrelated — every duplicate keeps the behaviour it had. An empty-string entry is a
|
|
647
|
+
* net clause with no `urlContains`, which named the whole channel and therefore matches
|
|
648
|
+
* everything.
|
|
649
|
+
*/
|
|
650
|
+
const named = options.namedNetUrls;
|
|
651
|
+
const wasNamed = (label) => named === undefined || named.some((u) => label.includes(u));
|
|
652
|
+
for (const [, { label, times, landed }] of writeTimes) {
|
|
653
|
+
if (times.length < 2)
|
|
654
|
+
continue;
|
|
655
|
+
// A DOUBLE SUBMIT is a write that landed twice. Two attempts of which one failed is a RETRY,
|
|
656
|
+
// and the field case is the commonest retry there is: a 401 that refreshed a token and went
|
|
657
|
+
// again, one row created, reported here as `duplicate-request ×2`. Two attempts that both
|
|
658
|
+
// failed are not a double submit either — nothing applied even once, so the claim "one user
|
|
659
|
+
// action was performed" is not contradicted by them. A call with no verdict at all still
|
|
660
|
+
// counts, because absence of a status is not evidence the write was rejected.
|
|
661
|
+
if (landed < 2)
|
|
662
|
+
continue;
|
|
663
|
+
// A steady cadence is a POLL, and a poll is not a double submit. An app that polls could not
|
|
664
|
+
// produce a verdict at all: a camera scan loop POSTing until it acquires a lock had every
|
|
665
|
+
// assertion that had already seen its consequence come back `unknown` behind writes that
|
|
666
|
+
// were the app working correctly (#673).
|
|
667
|
+
if (isSteadyCadence(times))
|
|
545
668
|
continue;
|
|
669
|
+
// A burst the assertion never mentioned is still worth telling the caller about -- a retry
|
|
670
|
+
// loop or a bursty beacon is a real finding -- but it is not evidence about the consequence
|
|
671
|
+
// they declared, so it is reported and decides nothing. The named case keeps its downgrade:
|
|
672
|
+
// "the write you asked about fired twice" is exactly what this rule is for.
|
|
673
|
+
const related = wasNamed(label);
|
|
546
674
|
found.push({
|
|
547
|
-
kind:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
675
|
+
kind: related
|
|
676
|
+
? ContradictionKind.DUPLICATE_REQUEST
|
|
677
|
+
: ContradictionKind.DUPLICATE_REQUEST_UNRELATED,
|
|
678
|
+
claim: related
|
|
679
|
+
? 'one user action was performed'
|
|
680
|
+
: 'the assertion did not name this endpoint',
|
|
681
|
+
counter: `the same write fired ${String(times.length)} times`,
|
|
682
|
+
detail: `${label} ×${String(times.length)}`,
|
|
551
683
|
});
|
|
552
684
|
}
|
|
553
685
|
}
|
|
@@ -24,11 +24,22 @@ export interface DeclaredNetFailure {
|
|
|
24
24
|
urlContains?: string;
|
|
25
25
|
status?: number;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
interface DeclaredExpectations {
|
|
28
28
|
/** Requests the caller declared would FAIL, so failing is the expected outcome, not a disagreement. */
|
|
29
29
|
netFailures: readonly DeclaredNetFailure[];
|
|
30
30
|
/** The caller required something to be ON SCREEN — an element or text, present rather than absent. */
|
|
31
31
|
rendersContent: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The `urlContains` of every net clause the predicate carries, failure or not.
|
|
34
|
+
*
|
|
35
|
+
* Distinct from `netFailures`, which is about EXPECTED failures. This answers a different
|
|
36
|
+
* question: which traffic did the caller actually mention? A rule that reports on writes the
|
|
37
|
+
* assertion never named is reporting on something else, and must not decide this verdict (#673).
|
|
38
|
+
*
|
|
39
|
+
* A net clause with no `urlContains` names ALL traffic — it asked about the whole channel — which
|
|
40
|
+
* is why an empty-string entry is meaningful and is preserved rather than skipped.
|
|
41
|
+
*/
|
|
42
|
+
netUrls: readonly string[];
|
|
32
43
|
}
|
|
33
44
|
export declare function declaredExpectations(predicate: Predicate | undefined): DeclaredExpectations;
|
|
34
45
|
/**
|
|
@@ -60,3 +71,4 @@ export declare function matchesDeclaredFailure(call: {
|
|
|
60
71
|
matchUrl?: string;
|
|
61
72
|
status: number | undefined;
|
|
62
73
|
}, declared: readonly DeclaredNetFailure[]): boolean;
|
|
74
|
+
export {};
|
package/dist/events/declared.js
CHANGED
|
@@ -17,11 +17,57 @@
|
|
|
17
17
|
* opposite of a consequence — honouring either would suppress a real finding on the strength of
|
|
18
18
|
* something that never happened. Pure: a predicate in, a description out.
|
|
19
19
|
*/
|
|
20
|
-
import { PredicateKind } from '@reticlehq/core';
|
|
20
|
+
import { PredicateKind, QueryBy } from '@reticlehq/core';
|
|
21
21
|
/** Below this, a status is a success or a redirect: not a declared failure. */
|
|
22
22
|
const FAILURE_STATUS_MIN = 400;
|
|
23
|
+
/** Statuses that mean the caller was refused — the proof of an auth/authz denial, not a fault. */
|
|
24
|
+
const AUTH_DENIAL_STATUSES = [401, 403];
|
|
25
|
+
/** ARIA roles that ARE the error UI — waiting for an alert is how the first report asserted a 401. */
|
|
26
|
+
const DENIAL_ROLES = new Set(['alert', 'alertdialog']);
|
|
27
|
+
/**
|
|
28
|
+
* Phrases that name a denial rather than a success. Conservative and multi-word where a short
|
|
29
|
+
* token would match a greeting ("invalid date" is not an auth failure).
|
|
30
|
+
*/
|
|
31
|
+
const DENIAL_TEXT_MARKERS = [
|
|
32
|
+
'access denied',
|
|
33
|
+
'unauthorized',
|
|
34
|
+
'forbidden',
|
|
35
|
+
'invalid key',
|
|
36
|
+
'invalid credentials',
|
|
37
|
+
'incorrect email',
|
|
38
|
+
'incorrect password',
|
|
39
|
+
'not authorized',
|
|
40
|
+
'permission denied',
|
|
41
|
+
'unauthenticated',
|
|
42
|
+
'not allowed',
|
|
43
|
+
];
|
|
44
|
+
function isDenialPhrase(text) {
|
|
45
|
+
const lower = text.toLowerCase();
|
|
46
|
+
return DENIAL_TEXT_MARKERS.some((marker) => lower.includes(marker));
|
|
47
|
+
}
|
|
48
|
+
function isDenialRole(role) {
|
|
49
|
+
return DENIAL_ROLES.has(role.toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
function isDenialQuery(query) {
|
|
52
|
+
if (undefined !== query.role && isDenialRole(query.role))
|
|
53
|
+
return true;
|
|
54
|
+
if (QueryBy.ROLE === query.by && undefined !== query.value && isDenialRole(query.value)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
for (const field of [query.name, query.text, query.value, query.label]) {
|
|
58
|
+
if ('string' === typeof field && isDenialPhrase(field))
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function pushAuthDenialStatuses(into) {
|
|
64
|
+
for (const status of AUTH_DENIAL_STATUSES) {
|
|
65
|
+
into.push({ status });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
23
68
|
export function declaredExpectations(predicate) {
|
|
24
69
|
const netFailures = [];
|
|
70
|
+
const netUrls = [];
|
|
25
71
|
let rendersContent = false;
|
|
26
72
|
const walk = (p) => {
|
|
27
73
|
switch (p.kind) {
|
|
@@ -30,6 +76,7 @@ export function declaredExpectations(predicate) {
|
|
|
30
76
|
walk(child);
|
|
31
77
|
return;
|
|
32
78
|
case PredicateKind.NET: {
|
|
79
|
+
netUrls.push(p.urlContains ?? '');
|
|
33
80
|
const declaredFailure = false === p.ok || (p.status !== undefined && p.status >= FAILURE_STATUS_MIN);
|
|
34
81
|
if (!declaredFailure)
|
|
35
82
|
return;
|
|
@@ -41,9 +88,18 @@ export function declaredExpectations(predicate) {
|
|
|
41
88
|
return;
|
|
42
89
|
}
|
|
43
90
|
case PredicateKind.ELEMENT:
|
|
91
|
+
if (true !== p.absent) {
|
|
92
|
+
rendersContent = true;
|
|
93
|
+
if (isDenialQuery(p.query))
|
|
94
|
+
pushAuthDenialStatuses(netFailures);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
44
97
|
case PredicateKind.TEXT:
|
|
45
|
-
if (true !== p.absent)
|
|
98
|
+
if (true !== p.absent) {
|
|
46
99
|
rendersContent = true;
|
|
100
|
+
if (isDenialPhrase(p.contains))
|
|
101
|
+
pushAuthDenialStatuses(netFailures);
|
|
102
|
+
}
|
|
47
103
|
return;
|
|
48
104
|
default:
|
|
49
105
|
return;
|
|
@@ -51,7 +107,7 @@ export function declaredExpectations(predicate) {
|
|
|
51
107
|
};
|
|
52
108
|
if (predicate !== undefined)
|
|
53
109
|
walk(predicate);
|
|
54
|
-
return { netFailures, rendersContent };
|
|
110
|
+
return { netFailures, rendersContent, netUrls };
|
|
55
111
|
}
|
|
56
112
|
/**
|
|
57
113
|
* Did the caller name a consequence that does not depend on the response body?
|
|
@@ -23,6 +23,9 @@ import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core'
|
|
|
23
23
|
* as a half-applied write. The gate was on the RESPONSE being a success and never on the REQUEST
|
|
24
24
|
* being a write, so every successful call carrying a body read as a save — and the same key name
|
|
25
25
|
* legitimately means different things on the two sides of a request/response pair;
|
|
26
|
+
* - identity keys (`id`, `*_id`) and create sentinels (`0`, `""`, `null`) are not compared. A
|
|
27
|
+
* create that sends `sub_category_id: 0` and gets back `19314` assigned the row; two id spaces
|
|
28
|
+
* that share a field name were never expected to match. Both used to fire on every healthy POST;
|
|
26
29
|
* - only keys the request actually SENT are considered;
|
|
27
30
|
* - only scalars, since deep structural diffing is where the false positives live;
|
|
28
31
|
* - values are compared NORMALISED (trimmed, case-folded, numbers as numbers), so `FR` vs `fr` and
|
|
@@ -71,6 +74,23 @@ function normalize(value) {
|
|
|
71
74
|
return String(value);
|
|
72
75
|
return undefined;
|
|
73
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Values that mean "server, you decide", not a field the caller asked to persist. A create that
|
|
79
|
+
* sends `0` for the new row's id, or an empty string the backend fills in, is the usual REST shape
|
|
80
|
+
* — not a dropped write.
|
|
81
|
+
*/
|
|
82
|
+
function isSentinelValue(value) {
|
|
83
|
+
return 0 === value || '' === value || null === value;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Keys that name an identity, not a persisted attribute. The server assigns these (create returns a
|
|
87
|
+
* new id; a public id and an internal row id share a field name). Comparing them as echoes is how
|
|
88
|
+
* this kind fired on every healthy POST-create and poisoned later asserts in the same window.
|
|
89
|
+
*/
|
|
90
|
+
function isIdentityKey(key) {
|
|
91
|
+
const lower = key.toLowerCase();
|
|
92
|
+
return 'id' === lower || lower.endsWith('_id');
|
|
93
|
+
}
|
|
74
94
|
/**
|
|
75
95
|
* Every scalar value the response carries for each key, at any depth.
|
|
76
96
|
*
|
|
@@ -78,20 +98,22 @@ function normalize(value) {
|
|
|
78
98
|
* `{result:{...}}` — and demanding the same path as the request would miss nearly every real API.
|
|
79
99
|
* Collecting ALL values for a key is what makes the "echoes both old and new" case safe.
|
|
80
100
|
*/
|
|
81
|
-
function scalarsByKey(body, into = new Map(), depth = 0) {
|
|
101
|
+
function scalarsByKey(body, into = new Map(), depth = 0, omitSentinels = false) {
|
|
82
102
|
if (depth > 6 || into.size > MAX_ECHO_KEYS)
|
|
83
103
|
return into;
|
|
84
104
|
if (Array.isArray(body)) {
|
|
85
105
|
for (const item of body)
|
|
86
|
-
scalarsByKey(item, into, depth + 1);
|
|
106
|
+
scalarsByKey(item, into, depth + 1, omitSentinels);
|
|
87
107
|
return into;
|
|
88
108
|
}
|
|
89
109
|
if (!isRecord(body))
|
|
90
110
|
return into;
|
|
91
111
|
for (const [key, value] of Object.entries(body)) {
|
|
112
|
+
if (omitSentinels && isSentinelValue(value))
|
|
113
|
+
continue;
|
|
92
114
|
const scalar = normalize(value);
|
|
93
115
|
if (scalar === undefined) {
|
|
94
|
-
scalarsByKey(value, into, depth + 1);
|
|
116
|
+
scalarsByKey(value, into, depth + 1, omitSentinels);
|
|
95
117
|
continue;
|
|
96
118
|
}
|
|
97
119
|
const seen = into.get(key) ?? new Set();
|
|
@@ -117,6 +139,46 @@ const EchoDiscriminator = {
|
|
|
117
139
|
* A response carrying this many times more scalar keys than the request, overlapping on fewer than
|
|
118
140
|
* `MIN_SHARED_KEYS`, is a snapshot that happened to reuse a name — not an echo of the write.
|
|
119
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* The two halves of an ADDRESSED write: a key that NAMES the field to set, and a generic container
|
|
144
|
+
* holding the value to set it to — `{"slot":"_dim","value":"3D"}`, `{"field":"locale","value":"fr"}`.
|
|
145
|
+
*
|
|
146
|
+
* A container key cannot be compared across documents by name. `value` in the request means "the
|
|
147
|
+
* value of the field named by `slot`"; `value` in the response means "the value of whichever field
|
|
148
|
+
* this node belongs to", and a document holding many addressed fields holds many `value` nodes. The
|
|
149
|
+
* flat by-key comparison below therefore measures the request's value against an unrelated sibling
|
|
150
|
+
* field's value, which is not evidence about the target field at all.
|
|
151
|
+
*
|
|
152
|
+
* Measured in the field, and it produced a FALSE RED on a passing assertion: the request asked
|
|
153
|
+
* `{"slot":"_dim","value":"3D"}`; the server applied it (`brief.dim: "3D"`, and the interview
|
|
154
|
+
* advanced from `{kind:"question"}` to `{kind:"geometry", payload:{dim:"3D"}}`); and this kind
|
|
155
|
+
* reported "asked 3d, got cfd", where `cfd` is `brief.slots._family.value` — a different slot,
|
|
156
|
+
* set earlier in the same interview. The next call on the same endpoint DID mirror into
|
|
157
|
+
* `slots.<slot>.value` and passed clean, so the kind misfires exactly when a server persists an
|
|
158
|
+
* addressed field somewhere other than a mirror named after it. That is a normal API shape.
|
|
159
|
+
*
|
|
160
|
+
* Cost of getting this wrong: a manufactured red is worse than a missed one here. Reticle's whole
|
|
161
|
+
* proposition is that an agent must not talk itself past a red; a heuristic that invents them
|
|
162
|
+
* teaches, inside a single session, that a `contradicted` verdict may be noise worth arguing with.
|
|
163
|
+
*
|
|
164
|
+
* The addressing key itself is still compared. If the server echoes a DIFFERENT `slot` than the one
|
|
165
|
+
* requested, that is a genuine dropped write and this file should say so.
|
|
166
|
+
*/
|
|
167
|
+
const ADDRESSING_KEYS = new Set(['slot', 'field', 'key', 'path', 'property']);
|
|
168
|
+
const CONTAINER_KEYS = new Set(['value', 'val', 'newvalue']);
|
|
169
|
+
/** True when the request says "set the field named X" rather than carrying the fields themselves. */
|
|
170
|
+
function isAddressedWrite(asked) {
|
|
171
|
+
let addressed = false;
|
|
172
|
+
let container = false;
|
|
173
|
+
for (const key of asked.keys()) {
|
|
174
|
+
const lower = key.toLowerCase();
|
|
175
|
+
if (ADDRESSING_KEYS.has(lower))
|
|
176
|
+
addressed = true;
|
|
177
|
+
if (CONTAINER_KEYS.has(lower))
|
|
178
|
+
container = true;
|
|
179
|
+
}
|
|
180
|
+
return addressed && container;
|
|
181
|
+
}
|
|
120
182
|
const SNAPSHOT_KEY_RATIO = 3;
|
|
121
183
|
const MIN_SHARED_KEYS = 2;
|
|
122
184
|
/**
|
|
@@ -194,16 +256,23 @@ export function findEchoMismatches(events, actionSince) {
|
|
|
194
256
|
if (request === undefined || response === undefined)
|
|
195
257
|
continue;
|
|
196
258
|
const echoed = scalarsByKey(response);
|
|
197
|
-
const asked = scalarsByKey(request);
|
|
259
|
+
const asked = scalarsByKey(request, new Map(), 0, true);
|
|
198
260
|
if (!responseRestatesRequest(asked, echoed))
|
|
199
261
|
continue;
|
|
262
|
+
// See ADDRESSING_KEYS: on an addressed write the container key names nothing on its own, so
|
|
263
|
+
// comparing it by name across the two documents compares two different fields.
|
|
264
|
+
const addressed = isAddressedWrite(asked);
|
|
200
265
|
const dropped = [];
|
|
201
266
|
for (const [key, wanted] of asked) {
|
|
267
|
+
if (addressed && CONTAINER_KEYS.has(key.toLowerCase()))
|
|
268
|
+
continue;
|
|
202
269
|
// More than one requested value for a key (a before/after pair, a list of items) makes "what
|
|
203
270
|
// was asked for" ambiguous, and a guess here is exactly how this kind would earn a reputation
|
|
204
271
|
// for crying wolf.
|
|
205
272
|
if (wanted.size !== 1)
|
|
206
273
|
continue;
|
|
274
|
+
if (isIdentityKey(key))
|
|
275
|
+
continue;
|
|
207
276
|
const values = echoed.get(key);
|
|
208
277
|
// Not echoed at all = no evidence either way. Only a key the server chose to report back can
|
|
209
278
|
// contradict the request, and silence is not a contradiction.
|
|
@@ -222,8 +291,8 @@ export function findEchoMismatches(events, actionSince) {
|
|
|
222
291
|
found.push({
|
|
223
292
|
kind: ContradictionKind.WRITE_FIELD_IGNORED,
|
|
224
293
|
claim: 'the write returned success and the page treated it as saved',
|
|
225
|
-
counter: `its own echo shows ${String(dropped.length)} field(s)
|
|
226
|
-
detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set
|
|
294
|
+
counter: `its own echo shows ${String(dropped.length)} field(s) that differ from the request — ${dropped.join('; ')}`,
|
|
295
|
+
detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set${attribution}`,
|
|
227
296
|
});
|
|
228
297
|
}
|
|
229
298
|
return found;
|
|
@@ -0,0 +1,52 @@
|
|
|
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 { type ReticleEvent } from '@reticlehq/core';
|
|
22
|
+
/** What the caller is asking about. `value` narrows a path that changed more than once. */
|
|
23
|
+
export interface LineageQuery {
|
|
24
|
+
path: string;
|
|
25
|
+
value?: string;
|
|
26
|
+
}
|
|
27
|
+
/** One line of the chain, with the confidence that produced it. */
|
|
28
|
+
export interface LineageLink {
|
|
29
|
+
/** Which stream this came from. */
|
|
30
|
+
kind: 'state' | 'signal' | 'net';
|
|
31
|
+
/** The rendered line, already carrying its own hedge. */
|
|
32
|
+
text: string;
|
|
33
|
+
/** True only when this is an event we hold. A join is never observed. */
|
|
34
|
+
observed: boolean;
|
|
35
|
+
/** Every candidate, when more than one could explain the link. Present only when ambiguous. */
|
|
36
|
+
candidates?: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface Lineage {
|
|
39
|
+
/** False when the path was never seen to change — a different answer from an empty chain. */
|
|
40
|
+
found: boolean;
|
|
41
|
+
chain: LineageLink[];
|
|
42
|
+
/** Why the chain stops where it does, when it stops early. */
|
|
43
|
+
note?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The chain, most recent change first.
|
|
47
|
+
*
|
|
48
|
+
* Stops at the first link with nothing to point at, and says why. Walking past a gap would mean
|
|
49
|
+
* joining across a hole in the evidence, which is exactly the step that turns a correlation into a
|
|
50
|
+
* claim.
|
|
51
|
+
*/
|
|
52
|
+
export declare function traceLineage(events: readonly ReticleEvent[], query: LineageQuery): Lineage;
|