@reticlehq/server 2.2.1 → 2.4.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/README.md +1 -1
- package/dist/bridge-security.d.ts +0 -1
- package/dist/bridge-security.js +0 -1
- package/dist/bridge.d.ts +0 -1
- package/dist/bridge.js +102 -12
- package/dist/bridge.test-harness.d.ts +5 -1
- package/dist/bridge.test-harness.js +12 -1
- package/dist/capsule/capsule-store.d.ts +2 -3
- package/dist/capsule/capsule-store.js +0 -1
- package/dist/capsule/capsule.d.ts +0 -1
- package/dist/capsule/capsule.js +0 -1
- package/dist/capsule/causal-summary.d.ts +21 -1
- package/dist/capsule/causal-summary.js +20 -2
- package/dist/capsule/divergence.d.ts +0 -1
- package/dist/capsule/divergence.js +0 -1
- package/dist/capsule/minimize.d.ts +0 -1
- package/dist/capsule/minimize.js +0 -1
- package/dist/capsule/predicate-to-links.d.ts +0 -1
- package/dist/capsule/predicate-to-links.js +0 -1
- package/dist/cli-doctor.d.ts +6 -0
- package/dist/cli-doctor.js +60 -0
- package/dist/cli-flow-commands.d.ts +0 -1
- package/dist/cli-flow-commands.js +0 -1
- package/dist/cli-launch.d.ts +0 -1
- package/dist/cli-launch.js +7 -3
- package/dist/cli-parse.d.ts +47 -3
- package/dist/cli-parse.js +147 -13
- package/dist/cli-port.d.ts +3 -1
- package/dist/cli-port.js +21 -1
- package/dist/cli-verify.d.ts +0 -1
- package/dist/cli-verify.js +1 -2
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +88 -69
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +0 -1
- package/dist/cloud/cloud-sync.d.ts +4 -5
- package/dist/cloud/cloud-sync.js +0 -1
- package/dist/cloud-cli.d.ts +0 -1
- package/dist/cloud-cli.js +9 -2
- package/dist/crawl/crawl-tools.d.ts +0 -1
- package/dist/crawl/crawl-tools.js +1 -2
- package/dist/crawl/crawl.d.ts +17 -3
- package/dist/crawl/crawl.js +43 -4
- package/dist/daemon-resilience.d.ts +6 -1
- package/dist/daemon-resilience.js +63 -1
- package/dist/daemon.d.ts +23 -3
- package/dist/daemon.js +36 -24
- package/dist/domain/domain-model.d.ts +0 -1
- package/dist/domain/domain-model.js +1 -2
- package/dist/domain/domain-tools.d.ts +0 -7
- package/dist/domain/domain-tools.js +21 -4
- package/dist/domain/flow-risk.d.ts +0 -1
- package/dist/domain/flow-risk.js +0 -1
- package/dist/ee/audit-log.d.ts +0 -1
- package/dist/ee/audit-log.js +0 -1
- package/dist/events/body-failures.d.ts +4 -0
- package/dist/events/body-failures.js +113 -0
- package/dist/events/contradictions.d.ts +53 -0
- package/dist/events/contradictions.js +282 -0
- package/dist/events/echo-mismatch.d.ts +4 -0
- package/dist/events/echo-mismatch.js +138 -0
- package/dist/events/event-filters.d.ts +17 -3
- package/dist/events/event-filters.js +13 -3
- package/dist/events/json-salvage.d.ts +32 -0
- package/dist/events/json-salvage.js +96 -0
- package/dist/events/predicate-eval.d.ts +2 -1
- package/dist/events/predicate-eval.js +56 -9
- package/dist/events/predicate.d.ts +0 -1
- package/dist/events/predicate.js +56 -2
- package/dist/events/reaction.d.ts +0 -1
- package/dist/events/reaction.js +0 -1
- package/dist/events/reconcile.d.ts +39 -0
- package/dist/events/reconcile.js +211 -0
- package/dist/events/ring-buffer.d.ts +0 -1
- package/dist/events/ring-buffer.js +0 -1
- package/dist/events/stale-response.d.ts +4 -0
- package/dist/events/stale-response.js +139 -0
- package/dist/events/unit-mismatch.d.ts +16 -0
- package/dist/events/unit-mismatch.js +167 -0
- package/dist/flows/affected-tools.d.ts +18 -0
- package/dist/flows/affected-tools.js +52 -0
- package/dist/flows/affected.d.ts +0 -1
- package/dist/flows/affected.js +0 -1
- package/dist/flows/annotate-tools.d.ts +0 -1
- package/dist/flows/annotate-tools.js +21 -3
- package/dist/flows/annotate.d.ts +0 -1
- package/dist/flows/annotate.js +0 -1
- package/dist/flows/annotation-store.d.ts +0 -1
- package/dist/flows/annotation-store.js +0 -1
- package/dist/flows/assertion-integrity.d.ts +0 -1
- package/dist/flows/assertion-integrity.js +0 -1
- package/dist/flows/assertion-tiers-store.d.ts +0 -1
- package/dist/flows/assertion-tiers-store.js +0 -1
- package/dist/flows/buddy-status.d.ts +0 -1
- package/dist/flows/buddy-status.js +0 -1
- package/dist/flows/coverage.d.ts +0 -1
- package/dist/flows/coverage.js +0 -1
- package/dist/flows/decision.d.ts +0 -7
- package/dist/flows/decision.js +47 -7
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +0 -1
- package/dist/flows/flake.d.ts +0 -1
- package/dist/flows/flake.js +0 -1
- package/dist/flows/flow-classify.d.ts +0 -1
- package/dist/flows/flow-classify.js +0 -1
- package/dist/flows/flow-replay-run.d.ts +0 -1
- package/dist/flows/flow-replay-run.js +2 -3
- package/dist/flows/flow-replay.d.ts +20 -4
- package/dist/flows/flow-replay.js +9 -43
- package/dist/flows/flow-report.d.ts +0 -1
- package/dist/flows/flow-report.js +0 -1
- package/dist/flows/flow-scope.d.ts +0 -1
- package/dist/flows/flow-scope.js +0 -1
- package/dist/flows/flow-sources.d.ts +0 -1
- package/dist/flows/flow-sources.js +0 -1
- package/dist/flows/flow-step-runners.d.ts +23 -0
- package/dist/flows/flow-step-runners.js +102 -0
- package/dist/flows/flow-success.d.ts +0 -1
- package/dist/flows/flow-success.js +0 -1
- package/dist/flows/flow-tools.d.ts +0 -1
- package/dist/flows/flow-tools.js +44 -14
- package/dist/flows/flows.d.ts +6 -2
- package/dist/flows/flows.js +15 -2
- package/dist/flows/gate.d.ts +0 -1
- package/dist/flows/gate.js +0 -1
- package/dist/flows/git-changed.d.ts +0 -1
- package/dist/flows/git-changed.js +0 -1
- package/dist/flows/heal-run.d.ts +0 -1
- package/dist/flows/heal-run.js +0 -1
- package/dist/flows/heal.d.ts +0 -1
- package/dist/flows/heal.js +0 -1
- package/dist/flows/parallel-suite.d.ts +0 -1
- package/dist/flows/parallel-suite.js +0 -1
- package/dist/flows/recordings.d.ts +0 -1
- package/dist/flows/recordings.js +0 -1
- package/dist/flows/replay.d.ts +0 -1
- package/dist/flows/replay.js +71 -4
- package/dist/flows/server-verify.d.ts +0 -1
- package/dist/flows/server-verify.js +0 -1
- package/dist/flows/verify-change-tools.d.ts +2 -0
- package/dist/flows/verify-change-tools.js +187 -0
- package/dist/flows/watch-batcher.d.ts +0 -1
- package/dist/flows/watch-batcher.js +0 -1
- package/dist/honesty/accepted-write.d.ts +14 -0
- package/dist/honesty/accepted-write.js +17 -0
- package/dist/honesty/blind-spots.d.ts +18 -1
- package/dist/honesty/blind-spots.js +47 -5
- package/dist/honesty/honesty.d.ts +0 -1
- package/dist/honesty/honesty.js +0 -1
- package/dist/honesty/uncaptured-bodies.d.ts +19 -0
- package/dist/honesty/uncaptured-bodies.js +30 -0
- package/dist/honesty/unread-outcome.d.ts +2 -0
- package/dist/honesty/unread-outcome.js +50 -0
- package/dist/honesty/verified.d.ts +40 -0
- package/dist/honesty/verified.js +86 -0
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +5 -1
- package/dist/hunt/hunt-report.d.ts +50 -0
- package/dist/hunt/hunt-report.js +84 -0
- package/dist/idle-shutdown.d.ts +0 -1
- package/dist/idle-shutdown.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/init/agent-rules.d.ts +0 -1
- package/dist/init/agent-rules.js +4 -2
- package/dist/init/capabilities.d.ts +17 -0
- package/dist/init/capabilities.js +51 -0
- package/dist/init/cursor.d.ts +0 -1
- package/dist/init/cursor.js +0 -1
- package/dist/init/desktop-doctor.d.ts +40 -0
- package/dist/init/desktop-doctor.js +198 -0
- package/dist/init/detect.d.ts +25 -1
- package/dist/init/detect.js +65 -4
- package/dist/init/mcp.d.ts +0 -1
- package/dist/init/mcp.js +0 -1
- package/dist/init/next-patch.d.ts +41 -0
- package/dist/init/next-patch.js +106 -0
- package/dist/init/node-io.d.ts +0 -1
- package/dist/init/node-io.js +25 -1
- package/dist/init/patch-kind.d.ts +20 -0
- package/dist/init/patch-kind.js +10 -0
- package/dist/init/plan-framework.d.ts +35 -0
- package/dist/init/plan-framework.js +217 -0
- package/dist/init/plan.d.ts +55 -4
- package/dist/init/plan.js +130 -112
- package/dist/init/project-id.d.ts +0 -1
- package/dist/init/project-id.js +0 -1
- package/dist/init/run.d.ts +17 -1
- package/dist/init/run.js +237 -18
- package/dist/init/slash-command.d.ts +18 -0
- package/dist/init/slash-command.js +52 -0
- package/dist/init/snippets.d.ts +48 -3
- package/dist/init/snippets.js +156 -6
- package/dist/init/vite-config.d.ts +4 -11
- package/dist/init/vite-config.js +15 -8
- package/dist/input/driven-redaction.d.ts +15 -0
- package/dist/input/driven-redaction.js +76 -0
- package/dist/input/network-detail.d.ts +9 -4
- package/dist/input/network-detail.js +35 -16
- package/dist/input/network-mock-tools.d.ts +0 -1
- package/dist/input/network-mock-tools.js +3 -4
- package/dist/input/network-mock.d.ts +0 -1
- package/dist/input/network-mock.js +0 -1
- package/dist/input/real-input.d.ts +14 -1
- package/dist/input/real-input.js +45 -3
- package/dist/input/scroll-find.d.ts +0 -1
- package/dist/input/scroll-find.js +0 -1
- package/dist/input/scroll-tools.d.ts +0 -1
- package/dist/input/scroll-tools.js +0 -1
- package/dist/input/viewport-tools.d.ts +0 -1
- package/dist/input/viewport-tools.js +3 -4
- package/dist/journal/ambient-store.d.ts +0 -1
- package/dist/journal/ambient-store.js +0 -1
- package/dist/journal/ambient.d.ts +0 -1
- package/dist/journal/ambient.js +0 -1
- package/dist/journal/attach-journal.d.ts +0 -1
- package/dist/journal/attach-journal.js +0 -1
- package/dist/journal/deviation-report.d.ts +0 -1
- package/dist/journal/deviation-report.js +0 -1
- package/dist/journal/deviation-service.d.ts +0 -1
- package/dist/journal/deviation-service.js +0 -1
- package/dist/journal/envelope-store.d.ts +0 -1
- package/dist/journal/envelope-store.js +0 -1
- package/dist/journal/envelope.d.ts +8 -9
- package/dist/journal/envelope.js +0 -1
- package/dist/journal/journal-query.d.ts +0 -1
- package/dist/journal/journal-query.js +0 -1
- package/dist/journal/journal-recorder.d.ts +0 -1
- package/dist/journal/journal-recorder.js +0 -1
- package/dist/journal/retention.d.ts +0 -1
- package/dist/journal/retention.js +0 -1
- package/dist/journal/rollups.d.ts +0 -1
- package/dist/journal/rollups.js +0 -1
- package/dist/journal/session-end.d.ts +0 -1
- package/dist/journal/session-end.js +0 -1
- package/dist/journal/session-journal.d.ts +0 -1
- package/dist/journal/session-journal.js +0 -1
- package/dist/license/license.d.ts +0 -1
- package/dist/license/license.js +0 -1
- package/dist/log.d.ts +0 -1
- package/dist/log.js +0 -1
- package/dist/mcp-proxy.d.ts +35 -4
- package/dist/mcp-proxy.js +174 -28
- package/dist/mcp.d.ts +15 -1
- package/dist/mcp.js +145 -17
- package/dist/oracles/flow-instrument-gaps.d.ts +0 -1
- package/dist/oracles/flow-instrument-gaps.js +0 -1
- package/dist/oracles/propose-consequences.d.ts +0 -1
- package/dist/oracles/propose-consequences.js +0 -1
- package/dist/oracles/self-instrument.d.ts +0 -1
- package/dist/oracles/self-instrument.js +0 -1
- package/dist/pairing-token.d.ts +0 -1
- package/dist/pairing-token.js +0 -1
- package/dist/phenomena/phenomena.d.ts +0 -1
- package/dist/phenomena/phenomena.js +0 -1
- package/dist/pool/browser-pool.d.ts +0 -1
- package/dist/pool/browser-pool.js +0 -1
- package/dist/pool/lease-reaper.d.ts +0 -1
- package/dist/pool/lease-reaper.js +0 -1
- package/dist/pool/playwright-launcher.d.ts +0 -1
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/project/baselines.d.ts +0 -1
- package/dist/project/baselines.js +0 -1
- package/dist/project/file-lock.d.ts +2 -1
- package/dist/project/file-lock.js +12 -2
- package/dist/project/fs-port.d.ts +0 -1
- package/dist/project/fs-port.js +0 -1
- package/dist/project/project-store.d.ts +0 -1
- package/dist/project/project-store.js +0 -1
- package/dist/project/project-tools.d.ts +0 -1
- package/dist/project/project-tools.js +29 -4
- package/dist/project/reticle-dir.d.ts +0 -1
- package/dist/project/reticle-dir.js +0 -1
- package/dist/runs/build-verification-run.d.ts +0 -1
- package/dist/runs/build-verification-run.js +0 -1
- package/dist/runs/profile-redact.d.ts +0 -1
- package/dist/runs/profile-redact.js +0 -1
- package/dist/runs/render-report.d.ts +0 -1
- package/dist/runs/render-report.js +0 -1
- package/dist/runs/repair-prompt.d.ts +0 -1
- package/dist/runs/repair-prompt.js +0 -1
- package/dist/runs/replay-mapping.d.ts +0 -1
- package/dist/runs/replay-mapping.js +0 -1
- package/dist/runs/reticle-runner.d.ts +0 -1
- package/dist/runs/reticle-runner.js +4 -2
- package/dist/runs/risk-classify.d.ts +0 -1
- package/dist/runs/risk-classify.js +0 -1
- package/dist/runs/run-diff.d.ts +0 -1
- package/dist/runs/run-diff.js +0 -1
- package/dist/runs/run-store.d.ts +0 -1
- package/dist/runs/run-store.js +0 -1
- package/dist/runs/run-tools.d.ts +0 -1
- package/dist/runs/run-tools.js +0 -1
- package/dist/runs/runner-port.d.ts +0 -1
- package/dist/runs/runner-port.js +0 -1
- package/dist/runs/verification-sync.d.ts +0 -1
- package/dist/runs/verification-sync.js +0 -1
- package/dist/runs/verify-http.d.ts +0 -1
- package/dist/runs/verify-http.js +0 -1
- package/dist/runs/verify-server.d.ts +0 -1
- package/dist/runs/verify-server.js +0 -1
- package/dist/server-version.d.ts +0 -1
- package/dist/server-version.js +0 -1
- package/dist/session/command-timeout.d.ts +35 -0
- package/dist/session/command-timeout.js +41 -0
- package/dist/session/control-envelope.d.ts +2 -3
- package/dist/session/control-envelope.js +2 -3
- package/dist/session/last-act.d.ts +31 -0
- package/dist/session/last-act.js +24 -0
- package/dist/session/live-control-tools.d.ts +0 -1
- package/dist/session/live-control-tools.js +3 -4
- package/dist/session/live-control.d.ts +0 -1
- package/dist/session/live-control.js +0 -1
- package/dist/session/observed-state.d.ts +12 -2
- package/dist/session/observed-state.js +29 -2
- package/dist/session/output-budget.d.ts +0 -1
- package/dist/session/output-budget.js +0 -1
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +36 -0
- package/dist/session/presenter-args.d.ts +0 -1
- package/dist/session/presenter-args.js +0 -1
- package/dist/session/review-store.d.ts +0 -1
- package/dist/session/review-store.js +0 -1
- package/dist/session/session-health.d.ts +26 -2
- package/dist/session/session-health.js +13 -1
- package/dist/session/session-info.d.ts +29 -0
- package/dist/session/session-info.js +1 -0
- package/dist/session/session-lease.d.ts +25 -0
- package/dist/session/session-lease.js +14 -0
- package/dist/session/session-manager.d.ts +7 -19
- package/dist/session/session-manager.js +35 -2
- package/dist/session/session-readiness.d.ts +0 -1
- package/dist/session/session-readiness.js +3 -4
- package/dist/session/session-reaper.d.ts +0 -1
- package/dist/session/session-reaper.js +0 -1
- package/dist/session/session-recommendation.d.ts +0 -1
- package/dist/session/session-recommendation.js +0 -1
- package/dist/session/session-tools.d.ts +0 -1
- package/dist/session/session-tools.js +2 -3
- package/dist/session/session.d.ts +33 -53
- package/dist/session/session.js +102 -66
- package/dist/session/state-select.d.ts +0 -1
- package/dist/session/state-select.js +0 -1
- package/dist/telemetry/argument-shape.d.ts +41 -0
- package/dist/telemetry/argument-shape.js +87 -0
- package/dist/telemetry/bug-found.d.ts +33 -0
- package/dist/telemetry/bug-found.js +95 -0
- package/dist/telemetry/cli-telemetry.d.ts +13 -0
- package/dist/telemetry/cli-telemetry.js +43 -0
- package/dist/telemetry/connect-failure.d.ts +16 -0
- package/dist/telemetry/connect-failure.js +44 -0
- package/dist/telemetry/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +76 -0
- package/dist/telemetry/dev-repo.d.ts +10 -0
- package/dist/telemetry/dev-repo.js +80 -0
- package/dist/telemetry/error-fingerprint.d.ts +24 -0
- package/dist/telemetry/error-fingerprint.js +88 -0
- package/dist/telemetry/feedback-cli.d.ts +31 -0
- package/dist/telemetry/feedback-cli.js +102 -0
- package/dist/telemetry/feedback-context.d.ts +32 -0
- package/dist/telemetry/feedback-context.js +143 -0
- package/dist/telemetry/feedback.d.ts +66 -0
- package/dist/telemetry/feedback.js +175 -0
- package/dist/telemetry/git-facts.d.ts +21 -0
- package/dist/telemetry/git-facts.js +109 -0
- package/dist/telemetry/identify.d.ts +22 -0
- package/dist/telemetry/identify.js +73 -0
- package/dist/telemetry/init-telemetry.d.ts +26 -0
- package/dist/telemetry/init-telemetry.js +33 -0
- package/dist/telemetry/machine-snapshot.d.ts +6 -0
- package/dist/telemetry/machine-snapshot.js +39 -0
- package/dist/telemetry/mcp-connection.d.ts +5 -0
- package/dist/telemetry/mcp-connection.js +44 -0
- package/dist/telemetry/project-profile.d.ts +44 -0
- package/dist/telemetry/project-profile.js +205 -0
- package/dist/telemetry/run-telemetry.d.ts +21 -0
- package/dist/telemetry/run-telemetry.js +62 -0
- package/dist/telemetry/session-metrics.d.ts +103 -0
- package/dist/telemetry/session-metrics.js +329 -0
- package/dist/telemetry/telemetry.d.ts +35 -8
- package/dist/telemetry/telemetry.js +105 -12
- package/dist/token-auth.d.ts +0 -1
- package/dist/token-auth.js +0 -1
- package/dist/tools/act-capsule.d.ts +23 -0
- package/dist/tools/act-capsule.js +47 -0
- package/dist/tools/act-danger.d.ts +7 -0
- package/dist/tools/act-danger.js +44 -0
- package/dist/tools/act-helpers.d.ts +17 -0
- package/dist/tools/act-helpers.js +40 -0
- package/dist/tools/act-tools.d.ts +8 -1
- package/dist/tools/act-tools.js +96 -98
- package/dist/tools/act-view.d.ts +7 -1
- package/dist/tools/act-view.js +13 -1
- package/dist/tools/assert-grade.d.ts +22 -1
- package/dist/tools/assert-grade.js +46 -1
- package/dist/tools/assert-verdict.d.ts +21 -0
- package/dist/tools/assert-verdict.js +62 -0
- package/dist/tools/browser-tools.d.ts +0 -1
- package/dist/tools/browser-tools.js +2 -2
- package/dist/tools/contract-tools.d.ts +0 -1
- package/dist/tools/contract-tools.js +0 -1
- package/dist/tools/coverage-tools.d.ts +9 -0
- package/dist/tools/coverage-tools.js +92 -0
- package/dist/tools/dynamic-tools.d.ts +0 -1
- package/dist/tools/dynamic-tools.js +21 -2
- package/dist/tools/error-recovery.d.ts +18 -2
- package/dist/tools/error-recovery.js +42 -7
- package/dist/tools/feedback-tools.d.ts +24 -0
- package/dist/tools/feedback-tools.js +180 -0
- package/dist/tools/invoke-tool.d.ts +0 -1
- package/dist/tools/invoke-tool.js +115 -5
- package/dist/tools/lease-tools.d.ts +0 -1
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +0 -1
- package/dist/tools/merge-tools.js +0 -1
- package/dist/tools/observe-tools.d.ts +0 -1
- package/dist/tools/observe-tools.js +91 -39
- package/dist/tools/profiles.d.ts +1 -2
- package/dist/tools/profiles.js +43 -16
- package/dist/tools/query-paginate.d.ts +0 -1
- package/dist/tools/query-paginate.js +0 -1
- package/dist/tools/read-tools.d.ts +0 -1
- package/dist/tools/read-tools.js +14 -8
- package/dist/tools/reconcile-tools.d.ts +2 -0
- package/dist/tools/reconcile-tools.js +111 -0
- package/dist/tools/snapshot-delta.d.ts +0 -1
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-invoker.d.ts +0 -1
- package/dist/tools/tool-invoker.js +0 -1
- package/dist/tools/tool-kit.d.ts +16 -1
- package/dist/tools/tool-kit.js +8 -2
- package/dist/tools/tool-names.d.ts +6 -1
- package/dist/tools/tool-names.js +6 -1
- package/dist/tools/tools-helpers.d.ts +0 -1
- package/dist/tools/tools-helpers.js +0 -1
- package/dist/tools/tools.d.ts +0 -1
- package/dist/tools/tools.js +24 -4
- package/dist/update/update-checker.d.ts +0 -1
- package/dist/update/update-checker.js +0 -1
- package/dist/update/update-nudge.d.ts +20 -0
- package/dist/update/update-nudge.js +72 -0
- package/dist/update/updater.d.ts +14 -1
- package/dist/update/updater.js +33 -2
- package/dist/visual/visual-diff.d.ts +0 -1
- package/dist/visual/visual-diff.js +0 -1
- package/dist/visual/visual-store.d.ts +0 -1
- package/dist/visual/visual-store.js +0 -1
- package/dist/visual/visual-tools.d.ts +0 -1
- package/dist/visual/visual-tools.js +103 -16
- package/package.json +2 -2
- package/dist/bridge-security.d.ts.map +0 -1
- package/dist/bridge-security.js.map +0 -1
- package/dist/bridge.d.ts.map +0 -1
- package/dist/bridge.js.map +0 -1
- package/dist/bridge.test-harness.d.ts.map +0 -1
- package/dist/bridge.test-harness.js.map +0 -1
- package/dist/capsule/capsule-store.d.ts.map +0 -1
- package/dist/capsule/capsule-store.js.map +0 -1
- package/dist/capsule/capsule.d.ts.map +0 -1
- package/dist/capsule/capsule.js.map +0 -1
- package/dist/capsule/causal-summary.d.ts.map +0 -1
- package/dist/capsule/causal-summary.js.map +0 -1
- package/dist/capsule/divergence.d.ts.map +0 -1
- package/dist/capsule/divergence.js.map +0 -1
- package/dist/capsule/minimize.d.ts.map +0 -1
- package/dist/capsule/minimize.js.map +0 -1
- package/dist/capsule/predicate-to-links.d.ts.map +0 -1
- package/dist/capsule/predicate-to-links.js.map +0 -1
- package/dist/cli-flow-commands.d.ts.map +0 -1
- package/dist/cli-flow-commands.js.map +0 -1
- package/dist/cli-launch.d.ts.map +0 -1
- package/dist/cli-launch.js.map +0 -1
- package/dist/cli-parse.d.ts.map +0 -1
- package/dist/cli-parse.js.map +0 -1
- package/dist/cli-port.d.ts.map +0 -1
- package/dist/cli-port.js.map +0 -1
- package/dist/cli-verify.d.ts.map +0 -1
- package/dist/cli-verify.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/cloud/cloud-config.d.ts.map +0 -1
- package/dist/cloud/cloud-config.js.map +0 -1
- package/dist/cloud/cloud-sync.d.ts.map +0 -1
- package/dist/cloud/cloud-sync.js.map +0 -1
- package/dist/cloud-cli.d.ts.map +0 -1
- package/dist/cloud-cli.js.map +0 -1
- package/dist/crawl/crawl-tools.d.ts.map +0 -1
- package/dist/crawl/crawl-tools.js.map +0 -1
- package/dist/crawl/crawl.d.ts.map +0 -1
- package/dist/crawl/crawl.js.map +0 -1
- package/dist/daemon-resilience.d.ts.map +0 -1
- package/dist/daemon-resilience.js.map +0 -1
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js.map +0 -1
- package/dist/domain/domain-model.d.ts.map +0 -1
- package/dist/domain/domain-model.js.map +0 -1
- package/dist/domain/domain-tools.d.ts.map +0 -1
- package/dist/domain/domain-tools.js.map +0 -1
- package/dist/domain/flow-risk.d.ts.map +0 -1
- package/dist/domain/flow-risk.js.map +0 -1
- package/dist/ee/audit-log.d.ts.map +0 -1
- package/dist/ee/audit-log.js.map +0 -1
- package/dist/events/event-filters.d.ts.map +0 -1
- package/dist/events/event-filters.js.map +0 -1
- package/dist/events/predicate-eval.d.ts.map +0 -1
- package/dist/events/predicate-eval.js.map +0 -1
- package/dist/events/predicate.d.ts.map +0 -1
- package/dist/events/predicate.js.map +0 -1
- package/dist/events/reaction.d.ts.map +0 -1
- package/dist/events/reaction.js.map +0 -1
- package/dist/events/ring-buffer.d.ts.map +0 -1
- package/dist/events/ring-buffer.js.map +0 -1
- package/dist/flows/affected.d.ts.map +0 -1
- package/dist/flows/affected.js.map +0 -1
- package/dist/flows/annotate-tools.d.ts.map +0 -1
- package/dist/flows/annotate-tools.js.map +0 -1
- package/dist/flows/annotate.d.ts.map +0 -1
- package/dist/flows/annotate.js.map +0 -1
- package/dist/flows/annotation-store.d.ts.map +0 -1
- package/dist/flows/annotation-store.js.map +0 -1
- package/dist/flows/assertion-integrity.d.ts.map +0 -1
- package/dist/flows/assertion-integrity.js.map +0 -1
- package/dist/flows/assertion-tiers-store.d.ts.map +0 -1
- package/dist/flows/assertion-tiers-store.js.map +0 -1
- package/dist/flows/buddy-status.d.ts.map +0 -1
- package/dist/flows/buddy-status.js.map +0 -1
- package/dist/flows/coverage.d.ts.map +0 -1
- package/dist/flows/coverage.js.map +0 -1
- package/dist/flows/decision.d.ts.map +0 -1
- package/dist/flows/decision.js.map +0 -1
- package/dist/flows/flake-store.d.ts.map +0 -1
- package/dist/flows/flake-store.js.map +0 -1
- package/dist/flows/flake.d.ts.map +0 -1
- package/dist/flows/flake.js.map +0 -1
- package/dist/flows/flow-classify.d.ts.map +0 -1
- package/dist/flows/flow-classify.js.map +0 -1
- package/dist/flows/flow-replay-run.d.ts.map +0 -1
- package/dist/flows/flow-replay-run.js.map +0 -1
- package/dist/flows/flow-replay.d.ts.map +0 -1
- package/dist/flows/flow-replay.js.map +0 -1
- package/dist/flows/flow-report.d.ts.map +0 -1
- package/dist/flows/flow-report.js.map +0 -1
- package/dist/flows/flow-scope.d.ts.map +0 -1
- package/dist/flows/flow-scope.js.map +0 -1
- package/dist/flows/flow-sources.d.ts.map +0 -1
- package/dist/flows/flow-sources.js.map +0 -1
- package/dist/flows/flow-success.d.ts.map +0 -1
- package/dist/flows/flow-success.js.map +0 -1
- package/dist/flows/flow-tools.d.ts.map +0 -1
- package/dist/flows/flow-tools.js.map +0 -1
- package/dist/flows/flows.d.ts.map +0 -1
- package/dist/flows/flows.js.map +0 -1
- package/dist/flows/gate.d.ts.map +0 -1
- package/dist/flows/gate.js.map +0 -1
- package/dist/flows/git-changed.d.ts.map +0 -1
- package/dist/flows/git-changed.js.map +0 -1
- package/dist/flows/heal-run.d.ts.map +0 -1
- package/dist/flows/heal-run.js.map +0 -1
- package/dist/flows/heal.d.ts.map +0 -1
- package/dist/flows/heal.js.map +0 -1
- package/dist/flows/parallel-suite.d.ts.map +0 -1
- package/dist/flows/parallel-suite.js.map +0 -1
- package/dist/flows/recordings.d.ts.map +0 -1
- package/dist/flows/recordings.js.map +0 -1
- package/dist/flows/replay.d.ts.map +0 -1
- package/dist/flows/replay.js.map +0 -1
- package/dist/flows/server-verify.d.ts.map +0 -1
- package/dist/flows/server-verify.js.map +0 -1
- package/dist/flows/watch-batcher.d.ts.map +0 -1
- package/dist/flows/watch-batcher.js.map +0 -1
- package/dist/honesty/blind-spots.d.ts.map +0 -1
- package/dist/honesty/blind-spots.js.map +0 -1
- package/dist/honesty/honesty.d.ts.map +0 -1
- package/dist/honesty/honesty.js.map +0 -1
- package/dist/http-server.d.ts.map +0 -1
- package/dist/http-server.js.map +0 -1
- package/dist/idle-shutdown.d.ts.map +0 -1
- package/dist/idle-shutdown.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/init/agent-rules.d.ts.map +0 -1
- package/dist/init/agent-rules.js.map +0 -1
- package/dist/init/cursor.d.ts.map +0 -1
- package/dist/init/cursor.js.map +0 -1
- package/dist/init/detect.d.ts.map +0 -1
- package/dist/init/detect.js.map +0 -1
- package/dist/init/mcp.d.ts.map +0 -1
- package/dist/init/mcp.js.map +0 -1
- package/dist/init/node-io.d.ts.map +0 -1
- package/dist/init/node-io.js.map +0 -1
- package/dist/init/plan.d.ts.map +0 -1
- package/dist/init/plan.js.map +0 -1
- package/dist/init/project-id.d.ts.map +0 -1
- package/dist/init/project-id.js.map +0 -1
- package/dist/init/run.d.ts.map +0 -1
- package/dist/init/run.js.map +0 -1
- package/dist/init/snippets.d.ts.map +0 -1
- package/dist/init/snippets.js.map +0 -1
- package/dist/init/vite-config.d.ts.map +0 -1
- package/dist/init/vite-config.js.map +0 -1
- package/dist/input/network-detail.d.ts.map +0 -1
- package/dist/input/network-detail.js.map +0 -1
- package/dist/input/network-mock-tools.d.ts.map +0 -1
- package/dist/input/network-mock-tools.js.map +0 -1
- package/dist/input/network-mock.d.ts.map +0 -1
- package/dist/input/network-mock.js.map +0 -1
- package/dist/input/real-input.d.ts.map +0 -1
- package/dist/input/real-input.js.map +0 -1
- package/dist/input/scroll-find.d.ts.map +0 -1
- package/dist/input/scroll-find.js.map +0 -1
- package/dist/input/scroll-tools.d.ts.map +0 -1
- package/dist/input/scroll-tools.js.map +0 -1
- package/dist/input/viewport-tools.d.ts.map +0 -1
- package/dist/input/viewport-tools.js.map +0 -1
- package/dist/journal/ambient-store.d.ts.map +0 -1
- package/dist/journal/ambient-store.js.map +0 -1
- package/dist/journal/ambient.d.ts.map +0 -1
- package/dist/journal/ambient.js.map +0 -1
- package/dist/journal/attach-journal.d.ts.map +0 -1
- package/dist/journal/attach-journal.js.map +0 -1
- package/dist/journal/deviation-report.d.ts.map +0 -1
- package/dist/journal/deviation-report.js.map +0 -1
- package/dist/journal/deviation-service.d.ts.map +0 -1
- package/dist/journal/deviation-service.js.map +0 -1
- package/dist/journal/envelope-store.d.ts.map +0 -1
- package/dist/journal/envelope-store.js.map +0 -1
- package/dist/journal/envelope.d.ts.map +0 -1
- package/dist/journal/envelope.js.map +0 -1
- package/dist/journal/journal-query.d.ts.map +0 -1
- package/dist/journal/journal-query.js.map +0 -1
- package/dist/journal/journal-recorder.d.ts.map +0 -1
- package/dist/journal/journal-recorder.js.map +0 -1
- package/dist/journal/retention.d.ts.map +0 -1
- package/dist/journal/retention.js.map +0 -1
- package/dist/journal/rollups.d.ts.map +0 -1
- package/dist/journal/rollups.js.map +0 -1
- package/dist/journal/session-end.d.ts.map +0 -1
- package/dist/journal/session-end.js.map +0 -1
- package/dist/journal/session-journal.d.ts.map +0 -1
- package/dist/journal/session-journal.js.map +0 -1
- package/dist/license/license.d.ts.map +0 -1
- package/dist/license/license.js.map +0 -1
- package/dist/log.d.ts.map +0 -1
- package/dist/log.js.map +0 -1
- package/dist/mcp-proxy.d.ts.map +0 -1
- package/dist/mcp-proxy.js.map +0 -1
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js.map +0 -1
- package/dist/oracles/flow-instrument-gaps.d.ts.map +0 -1
- package/dist/oracles/flow-instrument-gaps.js.map +0 -1
- package/dist/oracles/propose-consequences.d.ts.map +0 -1
- package/dist/oracles/propose-consequences.js.map +0 -1
- package/dist/oracles/self-instrument.d.ts.map +0 -1
- package/dist/oracles/self-instrument.js.map +0 -1
- package/dist/pairing-token.d.ts.map +0 -1
- package/dist/pairing-token.js.map +0 -1
- package/dist/phenomena/phenomena.d.ts.map +0 -1
- package/dist/phenomena/phenomena.js.map +0 -1
- package/dist/pool/browser-pool.d.ts.map +0 -1
- package/dist/pool/browser-pool.js.map +0 -1
- package/dist/pool/lease-reaper.d.ts.map +0 -1
- package/dist/pool/lease-reaper.js.map +0 -1
- package/dist/pool/playwright-launcher.d.ts.map +0 -1
- package/dist/pool/playwright-launcher.js.map +0 -1
- package/dist/project/baselines.d.ts.map +0 -1
- package/dist/project/baselines.js.map +0 -1
- package/dist/project/file-lock.d.ts.map +0 -1
- package/dist/project/file-lock.js.map +0 -1
- package/dist/project/fs-port.d.ts.map +0 -1
- package/dist/project/fs-port.js.map +0 -1
- package/dist/project/project-store.d.ts.map +0 -1
- package/dist/project/project-store.js.map +0 -1
- package/dist/project/project-tools.d.ts.map +0 -1
- package/dist/project/project-tools.js.map +0 -1
- package/dist/project/reticle-dir.d.ts.map +0 -1
- package/dist/project/reticle-dir.js.map +0 -1
- package/dist/runs/build-verification-run.d.ts.map +0 -1
- package/dist/runs/build-verification-run.js.map +0 -1
- package/dist/runs/profile-redact.d.ts.map +0 -1
- package/dist/runs/profile-redact.js.map +0 -1
- package/dist/runs/render-report.d.ts.map +0 -1
- package/dist/runs/render-report.js.map +0 -1
- package/dist/runs/repair-prompt.d.ts.map +0 -1
- package/dist/runs/repair-prompt.js.map +0 -1
- package/dist/runs/replay-mapping.d.ts.map +0 -1
- package/dist/runs/replay-mapping.js.map +0 -1
- package/dist/runs/reticle-runner.d.ts.map +0 -1
- package/dist/runs/reticle-runner.js.map +0 -1
- package/dist/runs/risk-classify.d.ts.map +0 -1
- package/dist/runs/risk-classify.js.map +0 -1
- package/dist/runs/run-diff.d.ts.map +0 -1
- package/dist/runs/run-diff.js.map +0 -1
- package/dist/runs/run-store.d.ts.map +0 -1
- package/dist/runs/run-store.js.map +0 -1
- package/dist/runs/run-tools.d.ts.map +0 -1
- package/dist/runs/run-tools.js.map +0 -1
- package/dist/runs/runner-port.d.ts.map +0 -1
- package/dist/runs/runner-port.js.map +0 -1
- package/dist/runs/verification-sync.d.ts.map +0 -1
- package/dist/runs/verification-sync.js.map +0 -1
- package/dist/runs/verify-http.d.ts.map +0 -1
- package/dist/runs/verify-http.js.map +0 -1
- package/dist/runs/verify-server.d.ts.map +0 -1
- package/dist/runs/verify-server.js.map +0 -1
- package/dist/server-version.d.ts.map +0 -1
- package/dist/server-version.js.map +0 -1
- package/dist/session/control-envelope.d.ts.map +0 -1
- package/dist/session/control-envelope.js.map +0 -1
- package/dist/session/live-control-tools.d.ts.map +0 -1
- package/dist/session/live-control-tools.js.map +0 -1
- package/dist/session/live-control.d.ts.map +0 -1
- package/dist/session/live-control.js.map +0 -1
- package/dist/session/observed-state.d.ts.map +0 -1
- package/dist/session/observed-state.js.map +0 -1
- package/dist/session/output-budget.d.ts.map +0 -1
- package/dist/session/output-budget.js.map +0 -1
- package/dist/session/presenter-args.d.ts.map +0 -1
- package/dist/session/presenter-args.js.map +0 -1
- package/dist/session/review-store.d.ts.map +0 -1
- package/dist/session/review-store.js.map +0 -1
- package/dist/session/session-health.d.ts.map +0 -1
- package/dist/session/session-health.js.map +0 -1
- package/dist/session/session-manager.d.ts.map +0 -1
- package/dist/session/session-manager.js.map +0 -1
- package/dist/session/session-readiness.d.ts.map +0 -1
- package/dist/session/session-readiness.js.map +0 -1
- package/dist/session/session-reaper.d.ts.map +0 -1
- package/dist/session/session-reaper.js.map +0 -1
- package/dist/session/session-recommendation.d.ts.map +0 -1
- package/dist/session/session-recommendation.js.map +0 -1
- package/dist/session/session-tools.d.ts.map +0 -1
- package/dist/session/session-tools.js.map +0 -1
- package/dist/session/session.d.ts.map +0 -1
- package/dist/session/session.js.map +0 -1
- package/dist/session/state-select.d.ts.map +0 -1
- package/dist/session/state-select.js.map +0 -1
- package/dist/telemetry/telemetry.d.ts.map +0 -1
- package/dist/telemetry/telemetry.js.map +0 -1
- package/dist/token-auth.d.ts.map +0 -1
- package/dist/token-auth.js.map +0 -1
- package/dist/tools/act-tools.d.ts.map +0 -1
- package/dist/tools/act-tools.js.map +0 -1
- package/dist/tools/act-view.d.ts.map +0 -1
- package/dist/tools/act-view.js.map +0 -1
- package/dist/tools/assert-grade.d.ts.map +0 -1
- package/dist/tools/assert-grade.js.map +0 -1
- package/dist/tools/browser-tools.d.ts.map +0 -1
- package/dist/tools/browser-tools.js.map +0 -1
- package/dist/tools/contract-tools.d.ts.map +0 -1
- package/dist/tools/contract-tools.js.map +0 -1
- package/dist/tools/dynamic-tools.d.ts.map +0 -1
- package/dist/tools/dynamic-tools.js.map +0 -1
- package/dist/tools/error-recovery.d.ts.map +0 -1
- package/dist/tools/error-recovery.js.map +0 -1
- package/dist/tools/invoke-tool.d.ts.map +0 -1
- package/dist/tools/invoke-tool.js.map +0 -1
- package/dist/tools/lease-tools.d.ts.map +0 -1
- package/dist/tools/lease-tools.js.map +0 -1
- package/dist/tools/merge-tools.d.ts.map +0 -1
- package/dist/tools/merge-tools.js.map +0 -1
- package/dist/tools/observe-tools.d.ts.map +0 -1
- package/dist/tools/observe-tools.js.map +0 -1
- package/dist/tools/profiles.d.ts.map +0 -1
- package/dist/tools/profiles.js.map +0 -1
- package/dist/tools/query-paginate.d.ts.map +0 -1
- package/dist/tools/query-paginate.js.map +0 -1
- package/dist/tools/read-tools.d.ts.map +0 -1
- package/dist/tools/read-tools.js.map +0 -1
- package/dist/tools/snapshot-delta.d.ts.map +0 -1
- package/dist/tools/snapshot-delta.js.map +0 -1
- package/dist/tools/tool-invoker.d.ts.map +0 -1
- package/dist/tools/tool-invoker.js.map +0 -1
- package/dist/tools/tool-kit.d.ts.map +0 -1
- package/dist/tools/tool-kit.js.map +0 -1
- package/dist/tools/tool-names.d.ts.map +0 -1
- package/dist/tools/tool-names.js.map +0 -1
- package/dist/tools/tools-helpers.d.ts.map +0 -1
- package/dist/tools/tools-helpers.js.map +0 -1
- package/dist/tools/tools.d.ts.map +0 -1
- package/dist/tools/tools.js.map +0 -1
- package/dist/update/update-checker.d.ts.map +0 -1
- package/dist/update/update-checker.js.map +0 -1
- package/dist/update/updater.d.ts.map +0 -1
- package/dist/update/updater.js.map +0 -1
- package/dist/visual/visual-diff.d.ts.map +0 -1
- package/dist/visual/visual-diff.js.map +0 -1
- package/dist/visual/visual-store.d.ts.map +0 -1
- package/dist/visual/visual-store.js.map +0 -1
- package/dist/visual/visual-tools.d.ts.map +0 -1
- package/dist/visual/visual-tools.js.map +0 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ContradictionKind, EventType } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* A request that returned 2xx and reported failure INSIDE its body.
|
|
4
|
+
*
|
|
5
|
+
* The status line is not the outcome. Three shapes make this routine rather than exotic:
|
|
6
|
+
*
|
|
7
|
+
* - **GraphQL** answers HTTP 200 for every error it has ever produced. `errors: [...]` in a 200 body
|
|
8
|
+
* is the normal way a GraphQL request fails, so any verdict that reads only the status is blind to
|
|
9
|
+
* the entire error surface of an entire ecosystem.
|
|
10
|
+
* - **Bulk endpoints** answer 200 for the batch and put per-item results in the body. Measured on a
|
|
11
|
+
* shipments console: `POST /api/bulk-hold` → 200, banner reads "9 shipments held", and three of
|
|
12
|
+
* the nine carry `{"ok": false, "error": "carrier_locked"}`. Reticle had that body in hand and
|
|
13
|
+
* reported `verified: "yes"`.
|
|
14
|
+
* - **Envelope APIs** answer 200 with `{"success": false}` — common wherever a gateway normalises
|
|
15
|
+
* status codes.
|
|
16
|
+
*
|
|
17
|
+
* Every channel above the body agrees with the lie: the request succeeded, the UI advanced, the page
|
|
18
|
+
* settled, nothing 5xx'd. Only the payload disagrees, which is why this needs body capture — and why
|
|
19
|
+
* an agent reading a status column can never catch it.
|
|
20
|
+
*/
|
|
21
|
+
/** Keys whose truthiness means "this failed". Checked at the top level and per item. */
|
|
22
|
+
const FAILURE_FLAGS = ['ok', 'success', 'succeeded'];
|
|
23
|
+
/** Keys that, when carrying content, name a failure. */
|
|
24
|
+
const ERROR_KEYS = ['error', 'errors', 'errorMessage', 'failureReason'];
|
|
25
|
+
/** Arrays worth walking for per-item outcomes. Bulk APIs converge on a handful of names. */
|
|
26
|
+
const ITEM_KEYS = ['results', 'items', 'data', 'records', 'responses'];
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
/** Whether one object reports a failure of its own. */
|
|
31
|
+
function reportsFailure(item) {
|
|
32
|
+
if (!isRecord(item))
|
|
33
|
+
return false;
|
|
34
|
+
for (const flag of FAILURE_FLAGS) {
|
|
35
|
+
if (item[flag] === false)
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
for (const key of ERROR_KEYS) {
|
|
39
|
+
const value = item[key];
|
|
40
|
+
if (value === undefined || value === null)
|
|
41
|
+
continue;
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
if (value.length > 0)
|
|
44
|
+
return true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// An empty string or an explicit null is an API saying "no error", not an error.
|
|
48
|
+
if (typeof value === 'string' && value.length === 0)
|
|
49
|
+
continue;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/** Failed / total across the first item array found. Zero total when the body is not a batch. */
|
|
55
|
+
function itemOutcomes(body) {
|
|
56
|
+
for (const key of ITEM_KEYS) {
|
|
57
|
+
const list = body[key];
|
|
58
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
59
|
+
continue;
|
|
60
|
+
const failed = list.filter(reportsFailure).length;
|
|
61
|
+
return { failed, total: list.length };
|
|
62
|
+
}
|
|
63
|
+
return { failed: 0, total: 0 };
|
|
64
|
+
}
|
|
65
|
+
function parse(raw) {
|
|
66
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined; // not JSON — nothing to read, and guessing at prose would invent findings
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const OK_MIN = 200;
|
|
77
|
+
const OK_MAX = 300;
|
|
78
|
+
/** Contradictions for 2xx responses whose bodies report failure. One per offending call. */
|
|
79
|
+
export function findBodyFailures(events) {
|
|
80
|
+
const found = [];
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
if (event.type !== EventType.NET_REQUEST)
|
|
83
|
+
continue;
|
|
84
|
+
const status = event.data['status'];
|
|
85
|
+
if (typeof status !== 'number' || status < OK_MIN || status >= OK_MAX)
|
|
86
|
+
continue;
|
|
87
|
+
const body = parse(event.data['responseBody']);
|
|
88
|
+
if (body === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
const url = typeof event.data['url'] === 'string' ? event.data['url'] : '';
|
|
91
|
+
const method = typeof event.data['method'] === 'string' ? event.data['method'] : '';
|
|
92
|
+
const where = `${method} ${url} → ${String(status)}`;
|
|
93
|
+
const { failed, total } = itemOutcomes(body);
|
|
94
|
+
if (failed > 0) {
|
|
95
|
+
found.push({
|
|
96
|
+
kind: ContradictionKind.PARTIAL_FAILURE_IN_OK_RESPONSE,
|
|
97
|
+
claim: `the request returned ${String(status)} and the page treated it as done`,
|
|
98
|
+
counter: `its body reports ${String(failed)} of ${String(total)} items FAILED`,
|
|
99
|
+
detail: `${where} — a batch endpoint's status describes the BATCH, not the items; whatever the UI says it did, ${String(failed)} of them did not happen`,
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (reportsFailure(body)) {
|
|
104
|
+
found.push({
|
|
105
|
+
kind: ContradictionKind.PARTIAL_FAILURE_IN_OK_RESPONSE,
|
|
106
|
+
claim: `the request returned ${String(status)} and the page treated it as done`,
|
|
107
|
+
counter: 'its body reports the operation FAILED',
|
|
108
|
+
detail: `${where} — the status line and the payload disagree; the payload is the outcome (a GraphQL error is always a 200)`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ContradictionKind, type ReticleEvent } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* The contradiction hunter.
|
|
4
|
+
*
|
|
5
|
+
* Every other check in Reticle reads ONE channel and asks "did something bad happen there?" — a
|
|
6
|
+
* console error, a 500, a control that did nothing. A human can do that too, just slower.
|
|
7
|
+
*
|
|
8
|
+
* This asks a question a human structurally cannot: do the channels DISAGREE with each other? A
|
|
9
|
+
* person watching an app has exactly one channel open — the screen. The agent holds the DOM, the
|
|
10
|
+
* store, the app's own signals, the console and the network in one causally ordered window, so it
|
|
11
|
+
* can catch the case where the screen says one thing and the network says the opposite.
|
|
12
|
+
*
|
|
13
|
+
* That gap is where false greens live. The archetype ships in both desktop demos: click Archive, the
|
|
14
|
+
* row disappears, the status line reads "archived", and the IPC call rejected into a swallowed
|
|
15
|
+
* `.catch()`. A screenshot agrees. A DOM assertion agrees. A human agrees. Only the disagreement
|
|
16
|
+
* between channels reveals it.
|
|
17
|
+
*
|
|
18
|
+
* Pure: a window of events in, findings out. No session, no IO, no clock.
|
|
19
|
+
*/
|
|
20
|
+
export interface Contradiction {
|
|
21
|
+
kind: ContradictionKind;
|
|
22
|
+
/** What one channel asserted — the optimistic half. */
|
|
23
|
+
claim: string;
|
|
24
|
+
/** What the other channel asserted — the half that contradicts it. */
|
|
25
|
+
counter: string;
|
|
26
|
+
/** Concrete evidence, so the agent can go straight to the call or the control. */
|
|
27
|
+
detail: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ContradictionOptions {
|
|
30
|
+
/** The action that opened this window, when one did. Enables the no-effect check. */
|
|
31
|
+
action?: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Events from BEFORE the window, used only to LEARN — never reported on. Some disagreements are
|
|
34
|
+
* with something the API stated earlier in the session, which an action-scoped window cannot
|
|
35
|
+
* contain. See `findUnitMismatches` for the measured case this exists for.
|
|
36
|
+
*/
|
|
37
|
+
prior?: readonly ReticleEvent[] | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* DOM mutations observed INSIDE the target's own subtree, as the act tool measured them.
|
|
40
|
+
*
|
|
41
|
+
* The no-effect check used to require a completely empty window, which is a statement about the
|
|
42
|
+
* PAGE rather than about the action — and no real app has a quiet page. Measured on a shipments
|
|
43
|
+
* console with a background event stream: clicking an inert heading produced `domMutatedWithin: 0`
|
|
44
|
+
* and four ambient events (a scroll position, an unrelated store update, a perf sample), so the
|
|
45
|
+
* window was not empty and the dead control went unreported. The user's framing of this is exact:
|
|
46
|
+
* the DOM moving after an action is not evidence that the action moved it.
|
|
47
|
+
*
|
|
48
|
+
* Undefined means the caller did not measure it — the check then falls back to the empty-window
|
|
49
|
+
* test, which is weaker but never wrong in the direction of a false accusation.
|
|
50
|
+
*/
|
|
51
|
+
mutatedWithin?: number | undefined;
|
|
52
|
+
}
|
|
53
|
+
export declare function findContradictions(events: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core';
|
|
2
|
+
import { findStaleResponses } from './stale-response.js';
|
|
3
|
+
import { findBodyFailures } from './body-failures.js';
|
|
4
|
+
import { findEchoMismatches } from './echo-mismatch.js';
|
|
5
|
+
import { findUnitMismatches } from './unit-mismatch.js';
|
|
6
|
+
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
7
|
+
function netCall(e) {
|
|
8
|
+
const status = asNumber(e.data['status']);
|
|
9
|
+
return {
|
|
10
|
+
method: (asString(e.data['method']) ?? '').toUpperCase(),
|
|
11
|
+
url: asString(e.data['url']) ?? '',
|
|
12
|
+
status,
|
|
13
|
+
// `ok` is authoritative when present (IPC sets it explicitly); status is the HTTP fallback.
|
|
14
|
+
// Neither present = no verdict was ever reported, which stays undefined all the way through.
|
|
15
|
+
ok: e.data['ok'] === undefined && status === undefined
|
|
16
|
+
? undefined
|
|
17
|
+
: e.data['ok'] === true || (e.data['ok'] === undefined && (status ?? 0) < 400),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Did the user-visible application state move forward? DOM, store and route only — deliberately NOT
|
|
22
|
+
* network, animation or signal. The question every rule below asks is "did the app act as if it
|
|
23
|
+
* succeeded", and a request firing is not the app acting as if anything.
|
|
24
|
+
*/
|
|
25
|
+
function uiAdvanced(events) {
|
|
26
|
+
return events.some((e) => e.type === EventType.DOM_ADDED ||
|
|
27
|
+
e.type === EventType.DOM_REMOVED ||
|
|
28
|
+
e.type === EventType.DOM_ATTR ||
|
|
29
|
+
e.type === EventType.DOM_TEXT ||
|
|
30
|
+
e.type === EventType.STATE_CHANGE ||
|
|
31
|
+
e.type === EventType.ROUTE_CHANGE);
|
|
32
|
+
}
|
|
33
|
+
function isMutating(call) {
|
|
34
|
+
return MUTATING_METHODS.includes(call.method);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* State paths/values that read as the app recording a failure rather than hiding one.
|
|
38
|
+
*
|
|
39
|
+
* English-only, and knowingly so — this is the softest edge in the file. It is a fallback for when
|
|
40
|
+
* the structural check below cannot decide, not the primary signal.
|
|
41
|
+
*/
|
|
42
|
+
const ACKNOWLEDGED = /error|fail|invalid|reject|denied|unable|could not|couldn't/i;
|
|
43
|
+
/**
|
|
44
|
+
* Below this length an error string is too generic to be evidence — "no", "err", a bare code — and
|
|
45
|
+
* could coincide with unrelated state text.
|
|
46
|
+
*/
|
|
47
|
+
const MIN_ECHOED_ERROR_LENGTH = 8;
|
|
48
|
+
/**
|
|
49
|
+
* Wording that blames the USER — bad credentials, no permission, wrong input. Distinct from
|
|
50
|
+
* ACKNOWLEDGED, which merely means "a failure was recorded": an app can honestly report a failure
|
|
51
|
+
* ("server error, try again") without misattributing it.
|
|
52
|
+
*/
|
|
53
|
+
const BLAMES_USER = /denied|invalid|unauthor|forbidden|incorrect|wrong |not allowed|not permitted/i;
|
|
54
|
+
/** A server-fault status: the user cannot fix it and should never be asked to. */
|
|
55
|
+
const SERVER_FAULT_MIN = 500;
|
|
56
|
+
/**
|
|
57
|
+
* Did the app record the failure in its OWN state — the layer the UI renders from?
|
|
58
|
+
*
|
|
59
|
+
* Without this, "the UI moved while a request failed" fires on correct code: a handler that catches
|
|
60
|
+
* the rejection and renders "could not add" also moves the UI. Both look identical at the level of
|
|
61
|
+
* "DOM changed + request failed"; what separates them is whether the app acknowledged the failure
|
|
62
|
+
* anywhere, or silently proceeded as if it had succeeded.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately NOT satisfied by a console error. `console.error` is invisible to the user, so an app
|
|
65
|
+
* that logs and then shows success is still lying to whoever is looking at it — precisely the case
|
|
66
|
+
* worth reporting.
|
|
67
|
+
*
|
|
68
|
+
* A heuristic, and the one soft edge in this file: an app that surfaces failure through a value this
|
|
69
|
+
* pattern does not recognize will produce a finding a human must dismiss. That direction is the safe
|
|
70
|
+
* one — a false alarm costs a glance, a missed false green ships.
|
|
71
|
+
*/
|
|
72
|
+
function failureAcknowledged(events) {
|
|
73
|
+
// STRUCTURAL first, and language-independent: if the app put the failed call's OWN error text into
|
|
74
|
+
// its state, it plainly knows the call failed — whatever language it says so in. The lexical
|
|
75
|
+
// patterns below are English-only, so without this a German or Japanese app that surfaces its
|
|
76
|
+
// failure perfectly well would be reported as hiding it.
|
|
77
|
+
const errors = events
|
|
78
|
+
.filter((e) => e.type === EventType.NET_REQUEST && e.data['ok'] !== true)
|
|
79
|
+
.map((e) => asString(e.data['error']))
|
|
80
|
+
.filter((text) => text !== undefined && text.length >= MIN_ECHOED_ERROR_LENGTH);
|
|
81
|
+
const echoesAnError = events.some((e) => {
|
|
82
|
+
if (e.type !== EventType.STATE_CHANGE)
|
|
83
|
+
return false;
|
|
84
|
+
const value = asString(e.data['value']);
|
|
85
|
+
return value !== undefined && errors.some((text) => value.includes(text));
|
|
86
|
+
});
|
|
87
|
+
if (echoesAnError)
|
|
88
|
+
return true;
|
|
89
|
+
return events.some((e) => {
|
|
90
|
+
// A failure-shaped SIGNAL is an acknowledgement too. An app that fires `auth:denied` has plainly
|
|
91
|
+
// not proceeded as if it succeeded, whatever its state paths happen to be named.
|
|
92
|
+
if (e.type === EventType.SIGNAL)
|
|
93
|
+
return ACKNOWLEDGED.test(asString(e.data['name']) ?? '');
|
|
94
|
+
if (e.type !== EventType.STATE_CHANGE)
|
|
95
|
+
return false;
|
|
96
|
+
const path = asString(e.data['path']) ?? '';
|
|
97
|
+
const value = e.data['value'];
|
|
98
|
+
return ACKNOWLEDGED.test(path) || (typeof value === 'string' && ACKNOWLEDGED.test(value));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function describe(call) {
|
|
102
|
+
return `${call.method} ${call.url}${call.status === undefined ? '' : ` → ${String(call.status)}`}`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Actions that are SUPPOSED to make something happen, so producing nothing is a finding.
|
|
106
|
+
*
|
|
107
|
+
* Deliberately narrow. `hover`, `focus` and `scrollIntoView` can legitimately move nothing, and
|
|
108
|
+
* `fill`/`type` change an input's value without necessarily mutating the DOM tree — flagging those
|
|
109
|
+
* would manufacture noise, which is the failure mode opposite to a false green and just as bad.
|
|
110
|
+
*/
|
|
111
|
+
const MUST_DO_SOMETHING = new Set(['click', 'dblclick', 'submit']);
|
|
112
|
+
/**
|
|
113
|
+
* Whether NOTHING in this window is attributable to the action.
|
|
114
|
+
*
|
|
115
|
+
* Ambient churn is deliberately not counted as evidence the action worked — a background event
|
|
116
|
+
* stream, a polling store, a perf sample and a scroll position all move on their own. What counts:
|
|
117
|
+
*
|
|
118
|
+
* - a mutation inside the target's own subtree (what the target itself did),
|
|
119
|
+
* - a request (the action asked the server for something),
|
|
120
|
+
* - a navigation, or a dialog/live region appearing — the two ways a real reaction legitimately
|
|
121
|
+
* lands OUTSIDE the target, e.g. a modal portalled to the body.
|
|
122
|
+
*
|
|
123
|
+
* The last clause is what keeps this from manufacturing noise: a button that opens a modal mutates
|
|
124
|
+
* nothing within itself, and must not be called dead.
|
|
125
|
+
*/
|
|
126
|
+
function didNothing(events, requests, mutatedWithin) {
|
|
127
|
+
if (mutatedWithin === undefined)
|
|
128
|
+
return events.length === 0;
|
|
129
|
+
if (mutatedWithin > 0 || requests.length > 0)
|
|
130
|
+
return false;
|
|
131
|
+
return !events.some((e) => e.type === EventType.ROUTE_CHANGE || e.type === EventType.VISIBLE_SHOWN);
|
|
132
|
+
}
|
|
133
|
+
export function findContradictions(events, options = {}) {
|
|
134
|
+
const found = [];
|
|
135
|
+
const settled = events.filter((e) => e.type === EventType.NET_REQUEST).map(netCall);
|
|
136
|
+
// ── Overlapping reads that settled out of order ─────────────────────────────────────────────
|
|
137
|
+
// Independent of any action, so it runs on every window: the race is a property of the timeline.
|
|
138
|
+
found.push(...findStaleResponses(events));
|
|
139
|
+
// ── A 2xx whose BODY says it failed ─────────────────────────────────────────────────────────
|
|
140
|
+
// Needs body capture; silent without it, which is why the assert path also declares when bodies
|
|
141
|
+
// were never recorded rather than letting an unread payload read as an empty one.
|
|
142
|
+
found.push(...findBodyFailures(events));
|
|
143
|
+
found.push(...findEchoMismatches(events));
|
|
144
|
+
// ── A money value written back at the wrong SCALE ───────────────────────────────────────────
|
|
145
|
+
found.push(...findUnitMismatches(events, options.prior ?? []));
|
|
146
|
+
// ── The action landed on something that does not react ──────────────────────────────────────
|
|
147
|
+
// Checked first and returned alone: nothing is attributable to the action, so every rule below is
|
|
148
|
+
// reasoning about someone else's events.
|
|
149
|
+
const action = (options.action ?? '').toLowerCase();
|
|
150
|
+
if (MUST_DO_SOMETHING.has(action) && didNothing(events, settled, options.mutatedWithin)) {
|
|
151
|
+
const measured = options.mutatedWithin !== undefined;
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
kind: ContradictionKind.ACTION_HAD_NO_EFFECT,
|
|
155
|
+
claim: `the ${action} was dispatched and the page settled`,
|
|
156
|
+
counter: measured
|
|
157
|
+
? 'nothing changed inside the target, and no request, navigation or dialog followed — whatever else moved on the page was not this'
|
|
158
|
+
: 'no channel observed anything at all — DOM, store, route, network, signal, console',
|
|
159
|
+
detail: 'the target does not react to this action (a non-interactive wrapper resolved instead of the control, a disabled handler, or a no-op) — settling proves only that the page was quiet, which a page that did nothing always is',
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
const failed = settled.filter((c) => c.ok === false);
|
|
164
|
+
const advanced = uiAdvanced(events);
|
|
165
|
+
const signals = events
|
|
166
|
+
.filter((e) => e.type === EventType.SIGNAL)
|
|
167
|
+
.map((e) => asString(e.data['name']) ?? 'signal');
|
|
168
|
+
// ── The route moved and nothing was rendered for it ─────────────────────────────────────────
|
|
169
|
+
// A navigation that neither fetches nor renders arrived nowhere. Distinct from a dead control:
|
|
170
|
+
// the control worked, the DESTINATION is empty — which is why every "did the click do something"
|
|
171
|
+
// heuristic passes it, a route change being unambiguously something.
|
|
172
|
+
const routed = events.some((e) => e.type === EventType.ROUTE_CHANGE);
|
|
173
|
+
const rendered = events.some((e) => e.type === EventType.DOM_ADDED || e.type === EventType.DOM_REMOVED);
|
|
174
|
+
const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
|
|
175
|
+
if (routed && !rendered && !fetched) {
|
|
176
|
+
found.push({
|
|
177
|
+
kind: ContradictionKind.ROUTE_RENDERED_NOTHING,
|
|
178
|
+
claim: 'the app navigated to a new route',
|
|
179
|
+
counter: 'nothing was rendered for it — no content added or removed, and no request made',
|
|
180
|
+
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',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// ── The app claimed success while its own request failed ────────────────────────────────────
|
|
184
|
+
// A signal is the sharper claim: the app did not merely LOOK right, it explicitly asserted
|
|
185
|
+
// success. When both hold it is one fact, so only the sharper one is reported.
|
|
186
|
+
// ── The server faulted and the app blamed the user ──────────────────────────────────────────
|
|
187
|
+
// Checked BEFORE the success-claim rules, because it is the sharper reading of the same events:
|
|
188
|
+
// the app did not claim success, it claimed the wrong failure. Telling someone their password is
|
|
189
|
+
// wrong while the backend is down sends them to fix something they cannot fix.
|
|
190
|
+
const serverFaults = settled.filter((c) => c.ok === false && c.status !== undefined && c.status >= SERVER_FAULT_MIN);
|
|
191
|
+
const userBlame = [
|
|
192
|
+
...signals.filter((name) => BLAMES_USER.test(name)),
|
|
193
|
+
...events
|
|
194
|
+
.filter((e) => e.type === EventType.STATE_CHANGE)
|
|
195
|
+
.map((e) => asString(e.data['value']) ?? '')
|
|
196
|
+
.filter((value) => BLAMES_USER.test(value)),
|
|
197
|
+
];
|
|
198
|
+
const misattributed = serverFaults.length > 0 && userBlame.length > 0;
|
|
199
|
+
if (misattributed) {
|
|
200
|
+
found.push({
|
|
201
|
+
kind: ContradictionKind.FAILURE_MISATTRIBUTED,
|
|
202
|
+
claim: `the app told the user they were at fault (${userBlame.map((b) => `"${b}"`).join(', ')})`,
|
|
203
|
+
counter: 'the server returned a 5xx — the user cannot fix this and the real fault is unreported',
|
|
204
|
+
detail: serverFaults.map(describe).join('; '),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// A failure-shaped signal is not a success claim, so it must not be read as one: saying "the app
|
|
208
|
+
// claimed success" about an app that plainly reported a failure is true in outline and wrong in
|
|
209
|
+
// its reasoning, which is how a checker stops being believed.
|
|
210
|
+
const successSignals = signals.filter((name) => !ACKNOWLEDGED.test(name));
|
|
211
|
+
if (failed.length > 0 && successSignals.length > 0) {
|
|
212
|
+
found.push({
|
|
213
|
+
kind: ContradictionKind.SIGNAL_CONTRADICTED,
|
|
214
|
+
claim: `the app fired ${successSignals.map((s) => `"${s}"`).join(', ')}`,
|
|
215
|
+
counter: `${String(failed.length)} request(s) in the same window failed`,
|
|
216
|
+
detail: failed.map(describe).join('; '),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (failed.length > 0 && advanced && !misattributed && !failureAcknowledged(events)) {
|
|
220
|
+
found.push({
|
|
221
|
+
kind: ContradictionKind.UI_ADVANCED_REQUEST_FAILED,
|
|
222
|
+
claim: 'the UI moved forward (DOM/store/route changed)',
|
|
223
|
+
counter: `${String(failed.length)} request(s) in the same window failed`,
|
|
224
|
+
detail: failed.map(describe).join('; '),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// ── A write succeeded and nothing on the client moved ───────────────────────────────────────
|
|
228
|
+
// Writes only: a GET that changes nothing is a prefetch; a POST that changes nothing is a lost
|
|
229
|
+
// write, a response parsed into the void, or a render that never happened.
|
|
230
|
+
if (!advanced) {
|
|
231
|
+
const ignoredWrites = settled.filter((c) => c.ok === true && isMutating(c));
|
|
232
|
+
if (ignoredWrites.length > 0) {
|
|
233
|
+
found.push({
|
|
234
|
+
kind: ContradictionKind.RESPONSE_IGNORED,
|
|
235
|
+
claim: `${String(ignoredWrites.length)} write(s) succeeded on the server`,
|
|
236
|
+
counter: 'nothing on the client changed — no DOM, store or route movement',
|
|
237
|
+
detail: ignoredWrites.map(describe).join('; '),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ── The same write fired more than once ─────────────────────────────────────────────────────
|
|
242
|
+
const writeCounts = new Map();
|
|
243
|
+
for (const call of settled) {
|
|
244
|
+
if (!isMutating(call))
|
|
245
|
+
continue;
|
|
246
|
+
const key = `${call.method} ${call.url}`;
|
|
247
|
+
writeCounts.set(key, (writeCounts.get(key) ?? 0) + 1);
|
|
248
|
+
}
|
|
249
|
+
for (const [key, count] of writeCounts) {
|
|
250
|
+
if (count < 2)
|
|
251
|
+
continue;
|
|
252
|
+
found.push({
|
|
253
|
+
kind: ContradictionKind.DUPLICATE_REQUEST,
|
|
254
|
+
claim: 'one user action was performed',
|
|
255
|
+
counter: `the same write fired ${String(count)} times`,
|
|
256
|
+
detail: `${key} ×${String(count)}`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
// ── The UI advanced over a request that never came back ─────────────────────────────────────
|
|
260
|
+
// Gated on the UI having moved: an in-flight request while the app is still visibly waiting is
|
|
261
|
+
// just a slow request, not a contradiction. It becomes one when the app proceeded regardless —
|
|
262
|
+
// which is also what makes a later `{ kind: "settled" }` assertion a false green.
|
|
263
|
+
if (advanced) {
|
|
264
|
+
const settledIds = new Set(events
|
|
265
|
+
.filter((e) => e.type === EventType.NET_REQUEST)
|
|
266
|
+
.map((e) => asString(e.data['id']))
|
|
267
|
+
.filter((id) => id !== undefined));
|
|
268
|
+
const inFlight = events
|
|
269
|
+
.filter((e) => e.type === EventType.NET_PENDING)
|
|
270
|
+
.map((e) => ({ id: asString(e.data['id']), call: netCall(e) }))
|
|
271
|
+
.filter((p) => p.id === undefined || !settledIds.has(p.id));
|
|
272
|
+
if (inFlight.length > 0) {
|
|
273
|
+
found.push({
|
|
274
|
+
kind: ContradictionKind.REQUEST_NEVER_SETTLED,
|
|
275
|
+
claim: 'the UI moved forward and the action reported done',
|
|
276
|
+
counter: `${String(inFlight.length)} request(s) were still in flight`,
|
|
277
|
+
detail: inFlight.map((p) => describe(p.call)).join('; '),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return found;
|
|
282
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ReticleEvent } from '@reticlehq/core';
|
|
2
|
+
import type { Contradiction } from './contradictions.js';
|
|
3
|
+
/** Contradictions for writes whose response echoes a different value than the request asked for. */
|
|
4
|
+
export declare function findEchoMismatches(events: readonly ReticleEvent[]): Contradiction[];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ContradictionKind, EventType } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* A write that returned OK and quietly did not apply part of what it was asked to.
|
|
4
|
+
*
|
|
5
|
+
* Measured on a desktop preferences write: the request asked for `{density:'compact', locale:'fr'}`,
|
|
6
|
+
* the response echoed `{"ok":true,"saved":{"density":"compact","locale":"en"}}`, the UI said
|
|
7
|
+
* "Preferences saved", and every channel agreed. `locale` was silently discarded. Nothing above the
|
|
8
|
+
* payload can see this — the status is 200, the body reports no failure, the UI advanced, the page
|
|
9
|
+
* settled. It is a false green with the evidence already in hand.
|
|
10
|
+
*
|
|
11
|
+
* The shape is ordinary in real backends: a field not in the UPDATE statement, a schema that strips
|
|
12
|
+
* unknown keys, a PATCH that honours a subset, an enum that falls back to a default.
|
|
13
|
+
*
|
|
14
|
+
* ## Why this is narrow on purpose
|
|
15
|
+
*
|
|
16
|
+
* Servers legitimately transform what they echo — trimming, lower-casing, rounding, canonicalising,
|
|
17
|
+
* filling defaults, assigning ids and timestamps. A rule that flagged every echoed field that differs
|
|
18
|
+
* would fire on healthy APIs constantly, and a contradiction kind that cries wolf gets filtered out
|
|
19
|
+
* by the agents reading it, taking the true positives with it. So:
|
|
20
|
+
*
|
|
21
|
+
* - only keys the request actually SENT are considered;
|
|
22
|
+
* - only scalars, since deep structural diffing is where the false positives live;
|
|
23
|
+
* - values are compared NORMALISED (trimmed, case-folded, numbers as numbers), so `FR` vs `fr` and
|
|
24
|
+
* `1` vs `1.0` stay silent while `fr` vs `en` speaks;
|
|
25
|
+
* - if the key appears anywhere in the response carrying the requested value, it is treated as
|
|
26
|
+
* applied — an envelope that echoes both the old and the new value is not a dropped write.
|
|
27
|
+
*
|
|
28
|
+
* The residue after those filters is a field the caller set, the server echoed back, and the echoed
|
|
29
|
+
* value is a genuinely different value. That is worth one line of an agent's attention.
|
|
30
|
+
*/
|
|
31
|
+
/** Bodies larger than this are skipped — a diff over a huge payload is neither cheap nor legible. */
|
|
32
|
+
const MAX_ECHO_KEYS = 200;
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Any JSON value, not just an object. An HTTP write sends an object; an IPC call sends its ARGUMENT
|
|
38
|
+
* LIST, so `savePrefs({locale})` arrives as `[{"locale":"fr"}]`. Requiring a top-level object here
|
|
39
|
+
* silently excluded every desktop write from this check — the exact platform the finding came from.
|
|
40
|
+
*/
|
|
41
|
+
function parse(raw) {
|
|
42
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Comparable form of a scalar. Non-scalars return undefined and are never compared. */
|
|
52
|
+
function normalize(value) {
|
|
53
|
+
if (typeof value === 'string')
|
|
54
|
+
return value.trim().toLowerCase();
|
|
55
|
+
if (typeof value === 'number')
|
|
56
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
57
|
+
if (typeof value === 'boolean')
|
|
58
|
+
return String(value);
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every scalar value the response carries for each key, at any depth.
|
|
63
|
+
*
|
|
64
|
+
* Depth matters because the echo is usually nested — `{ok:true, saved:{...}}`, `{data:{...}}`,
|
|
65
|
+
* `{result:{...}}` — and demanding the same path as the request would miss nearly every real API.
|
|
66
|
+
* Collecting ALL values for a key is what makes the "echoes both old and new" case safe.
|
|
67
|
+
*/
|
|
68
|
+
function scalarsByKey(body, into = new Map(), depth = 0) {
|
|
69
|
+
if (depth > 6 || into.size > MAX_ECHO_KEYS)
|
|
70
|
+
return into;
|
|
71
|
+
if (Array.isArray(body)) {
|
|
72
|
+
for (const item of body)
|
|
73
|
+
scalarsByKey(item, into, depth + 1);
|
|
74
|
+
return into;
|
|
75
|
+
}
|
|
76
|
+
if (!isRecord(body))
|
|
77
|
+
return into;
|
|
78
|
+
for (const [key, value] of Object.entries(body)) {
|
|
79
|
+
const scalar = normalize(value);
|
|
80
|
+
if (scalar === undefined) {
|
|
81
|
+
scalarsByKey(value, into, depth + 1);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const seen = into.get(key) ?? new Set();
|
|
85
|
+
seen.add(scalar);
|
|
86
|
+
into.set(key, seen);
|
|
87
|
+
}
|
|
88
|
+
return into;
|
|
89
|
+
}
|
|
90
|
+
const OK_MIN = 200;
|
|
91
|
+
const OK_MAX = 300;
|
|
92
|
+
/** Contradictions for writes whose response echoes a different value than the request asked for. */
|
|
93
|
+
export function findEchoMismatches(events) {
|
|
94
|
+
const found = [];
|
|
95
|
+
for (const event of events) {
|
|
96
|
+
if (event.type !== EventType.NET_REQUEST)
|
|
97
|
+
continue;
|
|
98
|
+
// IPC reports `ok` rather than a status; HTTP reports a status. Either must say success — a write
|
|
99
|
+
// that already failed is a different (and louder) finding than one that half-applied.
|
|
100
|
+
const status = event.data['status'];
|
|
101
|
+
const httpOk = typeof status === 'number' && status >= OK_MIN && status < OK_MAX;
|
|
102
|
+
if (!httpOk && event.data['ok'] !== true)
|
|
103
|
+
continue;
|
|
104
|
+
const request = parse(event.data['requestBody']);
|
|
105
|
+
const response = parse(event.data['responseBody']);
|
|
106
|
+
if (request === undefined || response === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
const echoed = scalarsByKey(response);
|
|
109
|
+
const asked = scalarsByKey(request);
|
|
110
|
+
const dropped = [];
|
|
111
|
+
for (const [key, wanted] of asked) {
|
|
112
|
+
// More than one requested value for a key (a before/after pair, a list of items) makes "what
|
|
113
|
+
// was asked for" ambiguous, and a guess here is exactly how this kind would earn a reputation
|
|
114
|
+
// for crying wolf.
|
|
115
|
+
if (wanted.size !== 1)
|
|
116
|
+
continue;
|
|
117
|
+
const values = echoed.get(key);
|
|
118
|
+
// Not echoed at all = no evidence either way. Only a key the server chose to report back can
|
|
119
|
+
// contradict the request, and silence is not a contradiction.
|
|
120
|
+
if (values === undefined || values.size === 0)
|
|
121
|
+
continue;
|
|
122
|
+
const [want] = [...wanted];
|
|
123
|
+
if (want !== undefined && !values.has(want))
|
|
124
|
+
dropped.push(`${key}: asked ${want}, got ${[...values].join('/')}`);
|
|
125
|
+
}
|
|
126
|
+
if (dropped.length === 0)
|
|
127
|
+
continue;
|
|
128
|
+
const url = typeof event.data['url'] === 'string' ? event.data['url'] : '';
|
|
129
|
+
const method = typeof event.data['method'] === 'string' ? event.data['method'] : '';
|
|
130
|
+
found.push({
|
|
131
|
+
kind: ContradictionKind.WRITE_FIELD_IGNORED,
|
|
132
|
+
claim: 'the write returned success and the page treated it as saved',
|
|
133
|
+
counter: `its own echo shows ${String(dropped.length)} field(s) NOT applied — ${dropped.join('; ')}`,
|
|
134
|
+
detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set, so the write half-applied; the UI has no way to know and will show the value the user typed`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return found;
|
|
138
|
+
}
|
|
@@ -16,8 +16,15 @@ export declare const OBSERVE_FILTER_BUCKETS: Record<string, readonly string[]>;
|
|
|
16
16
|
* nothing rather than throwing — a typo narrows the result, it never crashes the observe.
|
|
17
17
|
*/
|
|
18
18
|
export declare function eventMatchesFilters(e: ReticleEvent, filters: readonly string[]): boolean;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Match a net.request event against optional method/url/status/ok filters (reticle_network).
|
|
21
|
+
*
|
|
22
|
+
* `ok` is the filter the tool's own description points at for desktop IPC, whose 200/500 is derived
|
|
23
|
+
* rather than sent by any server. A still-PENDING call never matches an `ok` filter in either
|
|
24
|
+
* direction: unresolved is not the same as failed, and answering "yes, it failed" about a request
|
|
25
|
+
* still in flight is the false green this whole surface exists to prevent.
|
|
26
|
+
*/
|
|
27
|
+
export declare function matchNet(e: ReticleEvent, method: string | undefined, urlContains: string | undefined, status: number | undefined, ok?: boolean): boolean;
|
|
21
28
|
/**
|
|
22
29
|
* Reconcile network events into completed calls + still-in-flight calls. A NET_PENDING whose
|
|
23
30
|
* `id` never gets a matching NET_REQUEST is an unresolved/hung request — the case
|
|
@@ -41,6 +48,14 @@ interface NetCallView {
|
|
|
41
48
|
ms?: number;
|
|
42
49
|
/** Authoritative response headers merged from the driven (CDP) path — present only when driven. */
|
|
43
50
|
headers?: Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* A fire-and-forget desktop IPC send (`ipcRenderer.send`): dispatched, outcome unknowable.
|
|
53
|
+
*
|
|
54
|
+
* Carried into the projection on purpose. Such a record has no `status` and no `ok`, and without
|
|
55
|
+
* this flag a reader has no way to tell "the outcome is unknown" from "the fields were dropped" —
|
|
56
|
+
* which is the reading that turns an unverifiable call into an assumed success.
|
|
57
|
+
*/
|
|
58
|
+
oneWay?: boolean;
|
|
44
59
|
}
|
|
45
60
|
export declare function projectNetCall(e: ReticleEvent): NetCallView;
|
|
46
61
|
/**
|
|
@@ -96,4 +111,3 @@ interface ConsoleEmptyHint {
|
|
|
96
111
|
}
|
|
97
112
|
export declare function consoleEmptyHint(allConsole: ReticleEvent[]): ConsoleEmptyHint;
|
|
98
113
|
export {};
|
|
99
|
-
//# sourceMappingURL=event-filters.d.ts.map
|