@reticlehq/server 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +702 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/{cli-doctor.js → cli/cli-doctor.js} +9 -2
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +174 -92
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +49 -0
- package/dist/daemon/daemon-resilience.js +191 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +80 -48
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +5 -7
- package/dist/flows/flow-replay.js +114 -47
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +16 -3
- package/dist/honesty/blind-spots.js +24 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +37 -0
- package/dist/init/cra.js +90 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +3 -2
- package/dist/init/detect.js +16 -5
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +52 -9
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +14 -2
- package/dist/session/session.js +69 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +84 -115
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +57 -32
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +88 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures.md +28 -0
- package/docs/flows.md +166 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
package/dist/flows/decision.js
CHANGED
|
@@ -35,7 +35,7 @@ export function buildDecision(result, flow) {
|
|
|
35
35
|
// Green — but is it green-for-the-right-reason? A flow that asserts no consequence can pass while
|
|
36
36
|
// broken, so the honest next action is to add one.
|
|
37
37
|
const grade = flow !== undefined ? classifyFlowAssertions(flow) : undefined;
|
|
38
|
-
const verifiesOutcome = grade?.hasConsequenceAssertion
|
|
38
|
+
const verifiesOutcome = true === grade?.hasConsequenceAssertion;
|
|
39
39
|
const intent = flow?.intent;
|
|
40
40
|
return {
|
|
41
41
|
verdict: 'pass',
|
|
@@ -103,7 +103,7 @@ function suiteVerdictOf(status) {
|
|
|
103
103
|
function unverifiableReason(flow) {
|
|
104
104
|
if (flow === undefined)
|
|
105
105
|
return undefined;
|
|
106
|
-
if (flow.steps.length
|
|
106
|
+
if (0 === flow.steps.length) {
|
|
107
107
|
return 'the flow has no steps — it replays green whatever the app does. Record it again, or add steps with reticle_annotate.';
|
|
108
108
|
}
|
|
109
109
|
const c = classifyFlowAssertions(flow);
|
|
@@ -129,6 +129,14 @@ export function buildSuiteVerdict(runs) {
|
|
|
129
129
|
}
|
|
130
130
|
const decision = replay.decision ?? buildDecision(replay, flow);
|
|
131
131
|
const row = { flow: replay.name, verdict: suiteVerdictOf(replay.status) };
|
|
132
|
+
// Zero step results PLUS an error is the signature of a replay that never STARTED — the flow file
|
|
133
|
+
// failed to load (before the session is even resolved), or its leased context never came up. A
|
|
134
|
+
// flow that ran and failed always carries its failing step; a flow with no steps replays OK and
|
|
135
|
+
// never reaches here. Both arrive as ReplayStatus.ERROR, so without this the suite cannot tell
|
|
136
|
+
// "the app broke" from "Reticle could not run this" — and the bug metric reported the second as
|
|
137
|
+
// the first, 8 times in one sweep.
|
|
138
|
+
if (0 === replay.steps.length && replay.error !== undefined)
|
|
139
|
+
row.couldNotRun = true;
|
|
132
140
|
if (decision.whatChanged !== undefined)
|
|
133
141
|
row.whatChanged = decision.whatChanged;
|
|
134
142
|
if (decision.whereInSource !== undefined)
|
|
@@ -139,15 +147,22 @@ export function buildSuiteVerdict(runs) {
|
|
|
139
147
|
const total = runs.length;
|
|
140
148
|
const failed = failures.length;
|
|
141
149
|
// A real failure outranks an unverifiable flow: a broken flow is worse news than an empty one.
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
// An EMPTY suite is unverifiable for the same reason a flow that asserts nothing is — it is the
|
|
151
|
+
// purest green that cannot go red. It used to report `pass` with the summary "all 0 flows pass",
|
|
152
|
+
// which meant `reticle verify` went green on every project that had not written a flow yet, and on
|
|
153
|
+
// any project where the flows directory failed to resolve. Found by the adversarial MCP sweep; it
|
|
154
|
+
// was the only invented answer in 994 calls.
|
|
155
|
+
const status = failed > 0 ? 'fail' : unverifiable.length > 0 || 0 === total ? 'unverifiable' : 'pass';
|
|
156
|
+
const cannotFail = 0 === unverifiable.length
|
|
144
157
|
? ''
|
|
145
158
|
: ` — ${String(unverifiable.length)} verified nothing (${unverifiable.map((u) => u.flow).join(', ')})`;
|
|
146
|
-
const summary =
|
|
147
|
-
?
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
const summary = 0 === total
|
|
160
|
+
? 'no flows to verify — nothing was checked. Record one with reticle_record { action: "start" }, then reticle_flow_save.'
|
|
161
|
+
: 0 === failed
|
|
162
|
+
? 0 === unverifiable.length
|
|
163
|
+
? `all ${total} flow${1 === total ? '' : 's'} pass`
|
|
164
|
+
: `${String(passed)}/${String(total)} flows verified${cannotFail}`
|
|
165
|
+
: `${passed}/${total} flows pass — ${failed} need attention: ${failures.map((f) => f.flow).join(', ')}${cannotFail}`;
|
|
151
166
|
return {
|
|
152
167
|
status,
|
|
153
168
|
total,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse to save a flow with no steps.
|
|
3
|
+
*
|
|
4
|
+
* It used to succeed: `flow_save` returned `{ stepCount: 0, grade: "assertion-free" }` with no error,
|
|
5
|
+
* `flow_list` then showed one more flow, and `flow_verify` called it `unverifiable` — forever. The
|
|
6
|
+
* agent believes it saved a regression test; what it wrote is a permanent suite entry that can never
|
|
7
|
+
* go green or red.
|
|
8
|
+
*
|
|
9
|
+
* Zero steps is not a degraded flow, it is the absence of one. And the refusal matters most on the
|
|
10
|
+
* stacks where the recorder is known to capture nothing, because there the empty save is the only
|
|
11
|
+
* signal the agent would ever get that something went wrong.
|
|
12
|
+
*/
|
|
13
|
+
import { ReticleTool } from '../tools/tool-names.js';
|
|
14
|
+
export function emptyFlowRefusal(stepCount, name) {
|
|
15
|
+
if (stepCount > 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
return {
|
|
18
|
+
error: `'${name}' has no steps, so there is nothing to save — a flow with no steps can never pass or fail, and would sit in the suite reporting "unverifiable" forever`,
|
|
19
|
+
recovery: `Nothing was captured between ${ReticleTool.RECORD} { action: "start" } and { action: "stop" }. ` +
|
|
20
|
+
`Drive at least one action with ${ReticleTool.ACT} or ${ReticleTool.ACT_AND_WAIT} while the ` +
|
|
21
|
+
`recording is open, then save. If you DID act and this still says zero, the recorder captured ` +
|
|
22
|
+
`nothing for this stack — report it with ${ReticleTool.FEEDBACK}.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { FileSystemPort } from '../project/fs-port.js';
|
|
2
2
|
import { type FlakeLedger } from './flake.js';
|
|
3
|
-
export declare const FLAKE_FILE_VERSION = 1;
|
|
4
3
|
/**
|
|
5
4
|
* Persists the per-flow flake ledger at `.reticle/flake.json`. Local single-project memory (stays OSS —
|
|
6
5
|
* OSS-VS-SERVER). Loads never throw: missing/malformed/wrong-version degrades to an empty ledger, so
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname } from 'node:path';
|
|
2
2
|
import { reticleDirPaths } from '../project/reticle-dir.js';
|
|
3
3
|
import { FlakeFileSchema, emptyRecord, isFlaky, recordOutcome } from './flake.js';
|
|
4
|
-
|
|
4
|
+
const FLAKE_FILE_VERSION = 1;
|
|
5
5
|
/**
|
|
6
6
|
* Persists the per-flow flake ledger at `.reticle/flake.json`. Local single-project memory (stays OSS —
|
|
7
7
|
* OSS-VS-SERVER). Loads never throw: missing/malformed/wrong-version degrades to an empty ledger, so
|
package/dist/flows/flake.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
* (excluded from gate-blocking) and surfaced for repair, never silently blocking. The caller accrues an
|
|
6
6
|
* outcome ONLY for unchanged-code replays (a fail after a code change is a legitimate red, not flake).
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
declare const FlakeRecordSchema: z.ZodObject<{
|
|
9
9
|
runs: z.ZodNumber;
|
|
10
10
|
fails: z.ZodNumber;
|
|
11
11
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -55,3 +55,4 @@ export declare function recordOutcome(record: FlakeRecord, passed: boolean): Fla
|
|
|
55
55
|
export declare function isFlaky(record: FlakeRecord, minRuns?: number): boolean;
|
|
56
56
|
/** Observed intermittent-failure rate (0 when too few runs to judge). */
|
|
57
57
|
export declare function flakeRate(record: FlakeRecord): number;
|
|
58
|
+
export {};
|
package/dist/flows/flake.js
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
* (excluded from gate-blocking) and surfaced for repair, never silently blocking. The caller accrues an
|
|
6
6
|
* outcome ONLY for unchanged-code replays (a fail after a code change is a legitimate red, not flake).
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
const FlakeRecordSchema = z.object({
|
|
9
9
|
runs: z.number().int().min(0),
|
|
10
10
|
fails: z.number().int().min(0),
|
|
11
11
|
});
|
|
@@ -31,5 +31,5 @@ export function isFlaky(record, minRuns = DEFAULT_MIN_FLAKE_RUNS) {
|
|
|
31
31
|
}
|
|
32
32
|
/** Observed intermittent-failure rate (0 when too few runs to judge). */
|
|
33
33
|
export function flakeRate(record) {
|
|
34
|
-
return record.runs
|
|
34
|
+
return 0 === record.runs ? 0 : record.fails / record.runs;
|
|
35
35
|
}
|
|
@@ -33,6 +33,18 @@ const INTENT_WITHOUT_OUTCOME_WARNING = 'This flow declares a business intent but
|
|
|
33
33
|
// Consequence vs presence-only classification lives in @reticlehq/core (one source for all graders).
|
|
34
34
|
const expectIsConsequence = flowExpectHasConsequence;
|
|
35
35
|
const expectIsWeak = flowExpectIsPresenceOnly;
|
|
36
|
+
/**
|
|
37
|
+
* A step expect counts the same as a flow-level one, because replay now evaluates the same set.
|
|
38
|
+
*
|
|
39
|
+
* This was briefly narrowed to `state` only — correctly, at the time: replay checked element presence
|
|
40
|
+
* and state and nothing else, so grading a step signal/net as a consequence produced a flow that
|
|
41
|
+
* reported `grade: "asserted"` and could not go red. `assertStepExpect` closed that by compiling
|
|
42
|
+
* every step expect through the same `successToPredicate` the flow-level path uses, so the narrow
|
|
43
|
+
* rule is no longer true and would now UNDER-grade real assertions.
|
|
44
|
+
*
|
|
45
|
+
* The invariant to keep: this function and what replay enforces must move together. If one ever
|
|
46
|
+
* describes more than the other, the difference is a false green or a lost verification.
|
|
47
|
+
*/
|
|
36
48
|
/** Walk steps + act_sequence sub-steps so an expect on either level is counted. */
|
|
37
49
|
function flattenSteps(steps) {
|
|
38
50
|
const out = [];
|
|
@@ -64,7 +64,7 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs, proje
|
|
|
64
64
|
});
|
|
65
65
|
// Per-project cloud: push memory outcomes only when cloud is attached AND memory sync is enabled.
|
|
66
66
|
const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
|
|
67
|
-
if (cloud.config
|
|
67
|
+
if (null === cloud.config || !cloud.policy.memory)
|
|
68
68
|
return; // not attached / memory disabled → local only
|
|
69
69
|
const result = await syncRunRecordToCloud({ kind: RunKind.FLOW_REPLAY, name, status: runStatus, at: deps.now(), durationMs }, projectId, cloud.config, (url, init) => fetch(url, init));
|
|
70
70
|
if (result.outcome !== SyncOutcome.SYNCED) {
|
|
@@ -82,7 +82,7 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs, proje
|
|
|
82
82
|
*/
|
|
83
83
|
export function startPathMismatchHint(flow, session) {
|
|
84
84
|
const startPath = flow.startPath;
|
|
85
|
-
if (startPath === undefined || startPath.length
|
|
85
|
+
if (startPath === undefined || 0 === startPath.length)
|
|
86
86
|
return undefined;
|
|
87
87
|
const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE);
|
|
88
88
|
const last = routes.at(-1);
|
|
@@ -127,7 +127,7 @@ export async function replayNamedFlow(deps, args) {
|
|
|
127
127
|
// Floor the success oracle at the start of THIS replay so a stale signal from a prior run
|
|
128
128
|
// in the same session can't fake a pass.
|
|
129
129
|
const replayFloor = session.elapsed();
|
|
130
|
-
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args['confirmDangerous']
|
|
130
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, true === args['confirmDangerous']);
|
|
131
131
|
// "green means intent satisfied": when every step ran clean, assert the flow's success
|
|
132
132
|
// end-condition as a real consequence. A signal/net success that never fires FAILS the replay
|
|
133
133
|
// even though all locators resolved — the regression a healed-but-wrong locator ships green.
|
|
@@ -188,7 +188,7 @@ export async function replayNamedFlow(deps, args) {
|
|
|
188
188
|
async function computeReplayDeviation(deps, session, floor) {
|
|
189
189
|
try {
|
|
190
190
|
const segments = computeSegments(session.eventsSince(floor));
|
|
191
|
-
if (segments.length
|
|
191
|
+
if (0 === segments.length)
|
|
192
192
|
return undefined;
|
|
193
193
|
return await reportAndAccumulate(new EnvelopeStore(deps.fs, deps.reticleRoot), segments);
|
|
194
194
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnchorKind, type CommandResult, type FlowAnchor, type FlowFile, type FlowStepResult, type ReticleEvent, type QueryEmptyHint } from '@reticlehq/core';
|
|
1
|
+
import { AnchorKind, type CommandResult, type Drift, type FlowAnchor, type FlowFile, type FlowStepResult, type ReticleEvent, type QueryEmptyHint } from '@reticlehq/core';
|
|
2
2
|
import type { EvalResult, Predicate } from '../events/predicate.js';
|
|
3
3
|
/**
|
|
4
4
|
* The session surface flow-replay needs: QUERY to re-resolve a testid anchor against the live
|
|
@@ -47,12 +47,8 @@ export type Sleep = (ms: number) => Promise<void>;
|
|
|
47
47
|
* Such a drift is surfaced (with a nearest) but never auto-healed.
|
|
48
48
|
*/
|
|
49
49
|
export declare function nearestIsAmbiguous(missing: string, present: string[]): boolean;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
* returns zero refs, sleep and retry up to ANCHOR_SETTLE_ATTEMPTS. Returns as soon as refs appear,
|
|
53
|
-
* so a present anchor costs one query; a genuinely missing one costs the full (bounded) settle and
|
|
54
|
-
* then drifts. The last result's near-miss hint is returned for the drift record.
|
|
55
|
-
*/
|
|
50
|
+
/** Build the legible-drift record for a testid anchor that resolved to zero live elements. */
|
|
51
|
+
export declare function testidDrift(value: string, hint: QueryEmptyHint | undefined): Drift;
|
|
56
52
|
export declare function resolveQuery(session: FlowReplaySession, queryArgs: Record<string, unknown>, sleep: Sleep): Promise<{
|
|
57
53
|
refs: string[];
|
|
58
54
|
hint?: QueryEmptyHint;
|
|
@@ -61,6 +57,8 @@ export declare function resolveQuery(session: FlowReplaySession, queryArgs: Reco
|
|
|
61
57
|
export declare function componentLabel(anchor: Extract<FlowAnchor, {
|
|
62
58
|
kind: typeof AnchorKind.COMPONENT;
|
|
63
59
|
}>): string;
|
|
60
|
+
/** The value of a step's primary anchor, for labelling the result row. */
|
|
61
|
+
export declare function anchorLabel(anchor: FlowAnchor): string;
|
|
64
62
|
/** QUERY args for a component auto-anchor — source (precise) + component name (coarse) as given. */
|
|
65
63
|
export declare function componentQueryArgs(anchor: Extract<FlowAnchor, {
|
|
66
64
|
kind: typeof AnchorKind.COMPONENT;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { span } from '../trace.js';
|
|
2
|
+
import { AnchorKind, DriftReason, EventType, ReticleCommand, QueryBy, PredicateKind, } from '@reticlehq/core';
|
|
2
3
|
import { asRecord, asString } from '../tools/tools-helpers.js';
|
|
3
4
|
import { replayActionArgs, ambiguousTestidNote, queryRefs } from './replay.js';
|
|
4
|
-
import {
|
|
5
|
+
import { degradedStepResult, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
|
|
6
|
+
import { successToPredicate } from './flow-success.js';
|
|
5
7
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
6
8
|
/**
|
|
7
9
|
* A single ASCII-ish edit distance (case-insensitive). Small inputs (testids), so O(n*m) is fine.
|
|
@@ -67,10 +69,10 @@ function readQuery(result) {
|
|
|
67
69
|
return { refs };
|
|
68
70
|
const payload = asRecord(result.result);
|
|
69
71
|
const rawHint = payload['hint'];
|
|
70
|
-
if (typeof rawHint
|
|
72
|
+
if ('object' === typeof rawHint && rawHint !== null) {
|
|
71
73
|
const hint = asRecord(rawHint);
|
|
72
74
|
const present = Array.isArray(hint['presentTestids'])
|
|
73
|
-
? hint['presentTestids'].filter((t) => typeof t
|
|
75
|
+
? hint['presentTestids'].filter((t) => 'string' === typeof t)
|
|
74
76
|
: [];
|
|
75
77
|
return {
|
|
76
78
|
refs,
|
|
@@ -78,7 +80,7 @@ function readQuery(result) {
|
|
|
78
80
|
route: asString(hint['route']) ?? '',
|
|
79
81
|
presentTestids: present,
|
|
80
82
|
presentRegions: [],
|
|
81
|
-
knownEmptyState: hint['knownEmptyState']
|
|
83
|
+
knownEmptyState: true === hint['knownEmptyState'],
|
|
82
84
|
},
|
|
83
85
|
};
|
|
84
86
|
}
|
|
@@ -107,7 +109,7 @@ export function nearestIsAmbiguous(missing, present) {
|
|
|
107
109
|
return count >= 2;
|
|
108
110
|
}
|
|
109
111
|
/** Build the legible-drift record for a testid anchor that resolved to zero live elements. */
|
|
110
|
-
function testidDrift(value, hint) {
|
|
112
|
+
export function testidDrift(value, hint) {
|
|
111
113
|
const present = hint?.presentTestids ?? [];
|
|
112
114
|
const drift = {
|
|
113
115
|
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
@@ -125,10 +127,41 @@ function testidDrift(value, hint) {
|
|
|
125
127
|
* so a present anchor costs one query; a genuinely missing one costs the full (bounded) settle and
|
|
126
128
|
* then drifts. The last result's near-miss hint is returned for the drift record.
|
|
127
129
|
*/
|
|
130
|
+
/**
|
|
131
|
+
* One wait between anchor attempts: the fixed tick, or the next DOM event — whichever comes first.
|
|
132
|
+
*
|
|
133
|
+
* An element mounting IS a mutation, and the session already streams those, so sleeping out the rest
|
|
134
|
+
* of a 150ms tick after the thing has already appeared is pure latency. Measured on next-app-router:
|
|
135
|
+
* a single `flow.step` span was 1079ms around nine QUERY round-trips of 1–2ms each — the cost was
|
|
136
|
+
* entirely the sleeping, and four such steps made up 4.3s of that app's 7.6s.
|
|
137
|
+
*
|
|
138
|
+
* It can only resolve the wait EARLIER, never end the loop earlier: the attempt budget above is
|
|
139
|
+
* untouched, so a genuinely missing anchor still spends the full settle before it drifts. An early
|
|
140
|
+
* "not found" would be a false drift, which is the failure this loop exists to prevent.
|
|
141
|
+
*/
|
|
142
|
+
async function settleTick(session, sleep) {
|
|
143
|
+
let unsubscribe;
|
|
144
|
+
try {
|
|
145
|
+
await new Promise((resolve) => {
|
|
146
|
+
let done = false;
|
|
147
|
+
const finish = () => {
|
|
148
|
+
if (done)
|
|
149
|
+
return;
|
|
150
|
+
done = true;
|
|
151
|
+
resolve();
|
|
152
|
+
};
|
|
153
|
+
unsubscribe = session.onEvent(finish);
|
|
154
|
+
void sleep(ANCHOR_SETTLE_DELAY_MS).then(finish);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
unsubscribe?.();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
128
161
|
export async function resolveQuery(session, queryArgs, sleep) {
|
|
129
162
|
let last = readQuery(await session.command(ReticleCommand.QUERY, queryArgs));
|
|
130
|
-
for (let attempt = 1; last.refs.length
|
|
131
|
-
await sleep
|
|
163
|
+
for (let attempt = 1; 0 === last.refs.length && attempt < ANCHOR_SETTLE_ATTEMPTS; attempt += 1) {
|
|
164
|
+
await settleTick(session, sleep);
|
|
132
165
|
last = readQuery(await session.command(ReticleCommand.QUERY, queryArgs));
|
|
133
166
|
}
|
|
134
167
|
return last;
|
|
@@ -188,7 +221,7 @@ function summarizeConsequence(events) {
|
|
|
188
221
|
const data = n.data ?? {};
|
|
189
222
|
const method = asString(data['method']) ?? 'GET';
|
|
190
223
|
const path = trimUrl(asString(data['url']) ?? '');
|
|
191
|
-
const status = typeof data['status']
|
|
224
|
+
const status = 'number' === typeof data['status'] ? ` ${data['status']}` : '';
|
|
192
225
|
parts.push(`${method} ${path}${status}`.trim());
|
|
193
226
|
}
|
|
194
227
|
const errors = events.filter((e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT).length;
|
|
@@ -206,7 +239,7 @@ export function componentLabel(anchor) {
|
|
|
206
239
|
return anchor.component ?? anchor.name ?? anchor.role ?? 'component';
|
|
207
240
|
}
|
|
208
241
|
/** The value of a step's primary anchor, for labelling the result row. */
|
|
209
|
-
function anchorLabel(anchor) {
|
|
242
|
+
export function anchorLabel(anchor) {
|
|
210
243
|
if (anchor.kind === AnchorKind.TESTID)
|
|
211
244
|
return anchor.value;
|
|
212
245
|
if (anchor.kind === AnchorKind.SIGNAL)
|
|
@@ -227,7 +260,7 @@ export function componentQueryArgs(anchor) {
|
|
|
227
260
|
/** Run one testid-anchored step: re-resolve via QUERY, then ACT on the live ref, else drift. */
|
|
228
261
|
async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
|
|
229
262
|
const { refs, hint } = await resolveTestid(session, value, sleep);
|
|
230
|
-
if (refs.length
|
|
263
|
+
if (0 === refs.length) {
|
|
231
264
|
return {
|
|
232
265
|
step: index,
|
|
233
266
|
tool: step.tool,
|
|
@@ -263,7 +296,7 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
|
|
|
263
296
|
const expectTestid = step.expect?.element?.testid;
|
|
264
297
|
if (expectTestid !== undefined && !dynamic.has(expectTestid)) {
|
|
265
298
|
const expectRefs = await resolveTestid(session, expectTestid, sleep);
|
|
266
|
-
if (expectRefs.refs.length
|
|
299
|
+
if (0 === expectRefs.refs.length) {
|
|
267
300
|
return {
|
|
268
301
|
step: index,
|
|
269
302
|
tool: step.tool,
|
|
@@ -278,27 +311,50 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
|
|
|
278
311
|
return result;
|
|
279
312
|
}
|
|
280
313
|
/**
|
|
281
|
-
* After a step's anchor resolves
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
314
|
+
* After a step's anchor resolves and its action runs, its `expect` is EVALUATED — every kind of it.
|
|
315
|
+
*
|
|
316
|
+
* For a long time only `expect.state` was, so a recorded `expect.signal` or `expect.net` was written
|
|
317
|
+
* to disk and read by nothing while `flow_save` graded the flow "asserted". Driven end to end over
|
|
318
|
+
* MCP: annotate a step with a signal that never fires, save (grade "asserted"), replay -> status
|
|
319
|
+
* "ok". A green that cannot go red, inside the feature whose job is catching exactly that.
|
|
320
|
+
*
|
|
321
|
+
* It compiles through the SAME `successToPredicate` the flow-level `success` has always used, so
|
|
322
|
+
* there is one definition of what an expect means and the step form cannot drift from it again.
|
|
323
|
+
* Turning this on makes previously-green flows go red. That is the point — they were green because
|
|
324
|
+
* nothing was looking.
|
|
285
325
|
*/
|
|
286
|
-
async function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
326
|
+
async function assertStepExpect(session, expect, dynamic, waitForSignal, timeoutMs, since) {
|
|
327
|
+
// `element` is deliberately dropped: the step runner already asserts expect.element.testid against
|
|
328
|
+
// the live DOM before we get here, and re-asserting it through the predicate engine would be a
|
|
329
|
+
// second round trip for the same fact.
|
|
330
|
+
const { element: _element, ...consequences } = expect;
|
|
331
|
+
const predicate = successToPredicate(consequences, dynamic);
|
|
332
|
+
if (predicate === undefined)
|
|
333
|
+
return undefined;
|
|
292
334
|
const verdict = await waitForSignal(session, predicate, timeoutMs, since);
|
|
293
335
|
if (verdict.pass)
|
|
294
336
|
return undefined;
|
|
295
337
|
return {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
338
|
+
// The store case keeps its own kind because heal and the run report branch on it; everything
|
|
339
|
+
// else is a consequence that did not hold, and the reason carries observed-vs-expected.
|
|
340
|
+
reasonKind: expect.state !== undefined ? DriftReason.STATE_MISMATCH : DriftReason.SIGNAL_NOT_OBSERVED,
|
|
341
|
+
reason: verdict.failureReason ?? "the step's declared consequence did not hold",
|
|
342
|
+
anchor: expectLabel(expect),
|
|
299
343
|
nearest: null,
|
|
300
344
|
};
|
|
301
345
|
}
|
|
346
|
+
/** Name the thing that was asserted, for the drift's `anchor` column. */
|
|
347
|
+
function expectLabel(expect) {
|
|
348
|
+
if (expect.signal !== undefined)
|
|
349
|
+
return `signal:${expect.signal}`;
|
|
350
|
+
if (expect.net !== undefined)
|
|
351
|
+
return `net:${expect.net.urlContains ?? expect.net.method ?? '*'}`;
|
|
352
|
+
if (expect.state !== undefined)
|
|
353
|
+
return `state:${expect.state.path}`;
|
|
354
|
+
if (expect.console !== undefined)
|
|
355
|
+
return `console:${expect.console.level ?? '*'}`;
|
|
356
|
+
return 'expect';
|
|
357
|
+
}
|
|
302
358
|
/** Run one signal-anchored step: wait for the signal predicate, else drift (no nearest for signals). */
|
|
303
359
|
async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs, since) {
|
|
304
360
|
// Scope to THIS replay's floor, not the whole buffer.
|
|
@@ -310,7 +366,7 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
|
|
|
310
366
|
// satisfied a LATER flow's signal step even when that flow's own action never fired it — a
|
|
311
367
|
// cross-flow false green on the exact suite-verify path the regression-cost claim rests on. The
|
|
312
368
|
// replay-start floor excludes prior flows/runs while still seeing this run's adjacent-step signal.
|
|
313
|
-
const verdict = await waitForSignal(session, { kind:
|
|
369
|
+
const verdict = await waitForSignal(session, { kind: PredicateKind.SIGNAL, name }, signalTimeoutMs, since);
|
|
314
370
|
if (verdict.pass)
|
|
315
371
|
return { step: index, tool: step.tool, anchor: name, ok: true };
|
|
316
372
|
return {
|
|
@@ -348,28 +404,39 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
348
404
|
const page = currentRoute(session);
|
|
349
405
|
// Event-time floor so the consequence reflects only THIS step's aftermath, not prior steps'.
|
|
350
406
|
const cursorBefore = session.elapsed();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
407
|
+
const subSteps = step.steps;
|
|
408
|
+
// Traced per step, so a slow replay says WHICH step and which anchor kind spent the time. The
|
|
409
|
+
// per-step `durationMs` below is what the agent gets back; this is what a developer profiling
|
|
410
|
+
// the replay engine gets, nested under the tool call with the browser round-trips beneath it.
|
|
411
|
+
const result = await span('flow.step', { index, anchor: step.anchor.kind, label }, async () => {
|
|
412
|
+
if (isDegradedAnchor(step.anchor)) {
|
|
413
|
+
// Never QUERY the sentinel — it marks "no anchor was determined", not an element to find.
|
|
414
|
+
return degradedStepResult(step, index, label);
|
|
415
|
+
}
|
|
416
|
+
if (subSteps !== undefined && subSteps.length > 0) {
|
|
417
|
+
return runSequenceStep(session, step, index, subSteps, confirmDangerous, sleep);
|
|
418
|
+
}
|
|
419
|
+
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
420
|
+
return runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs, replayFloor);
|
|
421
|
+
}
|
|
422
|
+
if (step.anchor.kind === AnchorKind.COMPONENT) {
|
|
423
|
+
return runComponentStep(session, step, index, step.anchor, confirmDangerous, sleep);
|
|
424
|
+
}
|
|
425
|
+
if (step.anchor.kind === AnchorKind.ROLE && step.anchor.name !== undefined) {
|
|
426
|
+
// A NAMED role anchor addresses one element. The nameless one is the degraded placeholder
|
|
427
|
+
// and keeps its old path, where it fails legibly rather than querying a role as a testid.
|
|
428
|
+
return runRoleStep(session, step, index, step.anchor, confirmDangerous, sleep);
|
|
429
|
+
}
|
|
430
|
+
return runTestidStep(session, step, index, label, dynamic, confirmDangerous, sleep);
|
|
431
|
+
});
|
|
432
|
+
// Once the anchor resolved and the action ran, the step's own expect is evaluated — signal, net,
|
|
433
|
+
// console and store truth alike — deterministically, in the same cheap replay loop, with no LLM.
|
|
434
|
+
const stepExpect = step.expect;
|
|
435
|
+
if (result.ok && result.drift === undefined && stepExpect !== undefined) {
|
|
436
|
+
const expectDrift = await assertStepExpect(session, stepExpect, dynamic, waitForSignal, signalTimeoutMs, cursorBefore);
|
|
437
|
+
if (expectDrift !== undefined) {
|
|
371
438
|
result.ok = false;
|
|
372
|
-
result.drift =
|
|
439
|
+
result.drift = expectDrift;
|
|
373
440
|
}
|
|
374
441
|
}
|
|
375
442
|
if (page !== undefined)
|
|
@@ -4,12 +4,7 @@
|
|
|
4
4
|
* app's flows. These pure helpers pick the flows that belong on a given session's panel and shape them
|
|
5
5
|
* into replay chips — testable without a bridge or a filesystem.
|
|
6
6
|
*/
|
|
7
|
-
import { type FlowFile } from '@reticlehq/core';
|
|
8
|
-
/** A replayable flow as the HUD renders it: a name + an optional page it can start from. */
|
|
9
|
-
export interface FlowChip {
|
|
10
|
-
name: string;
|
|
11
|
-
start?: string;
|
|
12
|
-
}
|
|
7
|
+
import { type FlowChip, type FlowFile } from '@reticlehq/core';
|
|
13
8
|
/**
|
|
14
9
|
* A flow belongs on a session's HUD when it carries no projectId (legacy/global — visible everywhere
|
|
15
10
|
* for back-compat) OR its projectId matches the connecting session's. A flow stamped for project A
|
|
@@ -8,16 +8,36 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { AnchorKind, type FlowAnchor, type FlowStep, type FlowStepResult } from '@reticlehq/core';
|
|
10
10
|
import type { FlowReplaySession, Sleep } from './flow-replay.js';
|
|
11
|
-
/** Run one role+name-anchored step: re-resolve via QUERY by:'role', ACT on the live ref, else drift. */
|
|
12
11
|
export declare function runRoleStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
|
|
13
12
|
kind: typeof AnchorKind.ROLE;
|
|
14
13
|
}>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
|
|
15
|
-
/**
|
|
16
|
-
* Dispatch one already-resolved step. Shared so every anchor kind runs the action the same way —
|
|
17
|
-
* including the action window, whose open/close must not depend on which anchor found the element.
|
|
18
|
-
*/
|
|
19
|
-
export declare function actOnResolvedRef(session: FlowReplaySession, step: FlowStep, index: number, label: string, ref: string, confirmDangerous: boolean): Promise<FlowStepResult>;
|
|
20
14
|
/** Run one component-anchored step: re-resolve via QUERY by:'component', ACT on the live ref, else drift. */
|
|
21
15
|
export declare function runComponentStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
|
|
22
16
|
kind: typeof AnchorKind.COMPONENT;
|
|
23
17
|
}>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
|
|
18
|
+
/**
|
|
19
|
+
* DEGRADED_ANCHOR_ROLE is a MARKER ("no anchor could be determined"), never a locator.
|
|
20
|
+
*
|
|
21
|
+
* Recognising it is most of the fix: a nameless-ROLE anchor used to fall through to the testid
|
|
22
|
+
* runner, which asked the DOM eight times for a testid literally named "unresolved", found none (it
|
|
23
|
+
* never exists), and reported a MISSING ELEMENT. It then ran edit-distance against the word
|
|
24
|
+
* "unresolved" and offered the nearest testid as a rebind target — which flow_verify printed while
|
|
25
|
+
* flow_heal refused it on confidence. Two tools contradicting each other over a candidate that never
|
|
26
|
+
* meant anything.
|
|
27
|
+
*
|
|
28
|
+
* Both recorders emit this sentinel, so the check belongs here on the replay side, where all of them
|
|
29
|
+
* land.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isDegradedAnchor(anchor: FlowAnchor): boolean;
|
|
32
|
+
export declare function degradedStepResult(step: FlowStep, index: number, label: string): FlowStepResult;
|
|
33
|
+
/**
|
|
34
|
+
* Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
|
|
35
|
+
* ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
|
|
36
|
+
* execute identically.
|
|
37
|
+
*
|
|
38
|
+
* `replayFlow` had no sequence branch at all. It dispatches on `anchor.kind`, so a saved sequence fell
|
|
39
|
+
* to the testid runner and ran ONE act with `action: ''` (a saved sequence carries no top-level
|
|
40
|
+
* action) — sub-steps 2..n never executed. Fixing the anchor without this would have turned a visible
|
|
41
|
+
* drift into a silent partial replay reporting ok.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runSequenceStep(session: FlowReplaySession, step: FlowStep, index: number, subs: readonly FlowStep[], confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
|