@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/heal-run.js
CHANGED
|
@@ -12,7 +12,7 @@ import { replayFlow } from './flow-replay.js';
|
|
|
12
12
|
import { applyHealChanges, collectProposals } from './heal.js';
|
|
13
13
|
import { assertSuccess, dynamicTestids, successLabel } from './flow-success.js';
|
|
14
14
|
import { flowErrorMessage, sessionProjectId } from './flow-replay-run.js';
|
|
15
|
-
|
|
15
|
+
const HEAL_MESSAGES = {
|
|
16
16
|
NOTHING: 'nothing to heal — every anchor resolved on replay',
|
|
17
17
|
HEALED: "rewrote drifted testid anchors to their nearest surviving match and re-verified the flow's success consequence still fires",
|
|
18
18
|
DRIFT_DRY: 'confident rebind(s) proposed — re-run with apply:true to write them to disk',
|
|
@@ -25,7 +25,7 @@ function toChange(proposal) {
|
|
|
25
25
|
}
|
|
26
26
|
export async function healFlow(deps, args) {
|
|
27
27
|
const name = asString(args['flowName']) ?? '';
|
|
28
|
-
const apply = args['apply']
|
|
28
|
+
const apply = true === args['apply'];
|
|
29
29
|
const projectId = sessionProjectId(deps, asString(args['sessionId']));
|
|
30
30
|
const loaded = await deps.flows.load(name, projectId);
|
|
31
31
|
if (!loaded.ok) {
|
|
@@ -40,7 +40,7 @@ export async function healFlow(deps, args) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
43
|
-
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args['confirmDangerous']
|
|
43
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, true === args['confirmDangerous']);
|
|
44
44
|
const drifted = steps.some((s) => s.drift !== undefined);
|
|
45
45
|
const failed = steps.find((s) => !s.ok && s.drift === undefined);
|
|
46
46
|
if (failed !== undefined) {
|
|
@@ -66,7 +66,7 @@ export async function healFlow(deps, args) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
const proposals = collectProposals(steps);
|
|
69
|
-
if (proposals.length
|
|
69
|
+
if (0 === proposals.length) {
|
|
70
70
|
return {
|
|
71
71
|
name,
|
|
72
72
|
status: HealStatus.UNHEALABLE,
|
|
@@ -97,7 +97,7 @@ export async function healFlow(deps, args) {
|
|
|
97
97
|
// Floor the success oracle at the start of the VERIFY replay so the earlier drift replay's signal
|
|
98
98
|
// cannot fake the verification.
|
|
99
99
|
const verifyFloor = session.elapsed();
|
|
100
|
-
const verifySteps = await replayFlow(session, toVerify, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args['confirmDangerous']
|
|
100
|
+
const verifySteps = await replayFlow(session, toVerify, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, true === args['confirmDangerous']);
|
|
101
101
|
const verifyClean = verifySteps.length > 0 && verifySteps.every((s) => s.ok && s.drift === undefined);
|
|
102
102
|
const verdict = verifyClean
|
|
103
103
|
? await assertSuccess(session, healed.success, dynamicTestids(healed), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, verifyFloor)
|
package/dist/flows/heal.js
CHANGED
|
@@ -23,7 +23,7 @@ export function confidenceFor(from, to) {
|
|
|
23
23
|
if (from === to)
|
|
24
24
|
return 1;
|
|
25
25
|
const span = Math.max(from.length, to.length);
|
|
26
|
-
if (
|
|
26
|
+
if (0 === span)
|
|
27
27
|
return 1;
|
|
28
28
|
const raw = 1 - editDistance(from, to) / span;
|
|
29
29
|
if (raw >= 1)
|
|
@@ -62,10 +62,10 @@ function proposeRebindWith(drift, step, minConfidence) {
|
|
|
62
62
|
// Never auto-heal an ambiguous drift: when two present testids tie at the minimum distance, the
|
|
63
63
|
// `nearest` pick is arbitrary, so a rebind would be a coin-flip that can land on the wrong element
|
|
64
64
|
// and ship a bug green. Surface it (the drift still carries `nearest`) and defer to a human.
|
|
65
|
-
if (drift.ambiguous
|
|
65
|
+
if (true === drift.ambiguous)
|
|
66
66
|
return undefined;
|
|
67
67
|
const to = drift.nearest;
|
|
68
|
-
if (
|
|
68
|
+
if (null === to)
|
|
69
69
|
return undefined;
|
|
70
70
|
const confidence = confidenceFor(drift.anchor, to);
|
|
71
71
|
if (confidence < minConfidence)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assertion the agent MADE, in the shape a saved flow can keep.
|
|
3
|
+
*
|
|
4
|
+
* `reticle_act_and_wait { until }` is how agents assert — 12 of 14 calls in a day of telemetry
|
|
5
|
+
* carried an `until`. `compileActStep` recorded only the ACTION, so a flow saved after an asserted
|
|
6
|
+
* drive came back graded `assertion-free`: "performs actions but asserts no observable consequence —
|
|
7
|
+
* it will pass even if the feature is broken." The agent had already said what success meant;
|
|
8
|
+
* Reticle discarded it and then warned the agent that the flow asserts nothing.
|
|
9
|
+
*
|
|
10
|
+
* That is the regression-suite story failing at its last step. "Record once, verify forever" is only
|
|
11
|
+
* worth anything if the recorded flow can go RED, and locally 32 of 39 saved flows cannot.
|
|
12
|
+
*
|
|
13
|
+
* The inverse of `successToPredicate`. Only the kinds FlowExpect can express are carried: `settled`
|
|
14
|
+
* is a wait rather than a claim, and `route`/`animation`/`anyOf`/`not` have no representation at all.
|
|
15
|
+
* Inventing one would write an assertion into the file that the agent never made, which is worse
|
|
16
|
+
* than recording none — a flow that asserts something nobody chose is a false green with extra steps.
|
|
17
|
+
*/
|
|
18
|
+
import type { FlowExpect } from '@reticlehq/core';
|
|
19
|
+
import type { Predicate } from '../events/predicate.js';
|
|
20
|
+
export declare function predicateToExpect(predicate: Predicate): FlowExpect | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The subset a REPLAY actually enforces today.
|
|
23
|
+
*
|
|
24
|
+
* `flow-replay` checks exactly two things per step: `expect.element.testid` is present after the
|
|
25
|
+
* action, and `expect.state` holds. A recorded `net` or `signal` expect is graded as a consequence
|
|
26
|
+
* assertion by `classifyFlowAssertions` and then never evaluated — so writing one into a flow file
|
|
27
|
+
* would make it report `grade: "asserted"` while nothing checks it.
|
|
28
|
+
*
|
|
29
|
+
* That is a false green, in the feature whose entire purpose is preventing them, produced by the
|
|
30
|
+
* change that was meant to strengthen it. So only the enforced kinds are recorded: a flow that says
|
|
31
|
+
* it asserts something must actually go red when that thing breaks.
|
|
32
|
+
*
|
|
33
|
+
* Lifting this is worth doing — net and signal are the assertions agents most often make, and the
|
|
34
|
+
* conversion above already handles them — but it requires replay to evaluate them per step (the
|
|
35
|
+
* machinery exists: successToPredicate + the predicate engine). Until then, recording them would
|
|
36
|
+
* trade a weak flow for a lying one.
|
|
37
|
+
*/
|
|
38
|
+
export declare function enforcedOnReplay(expect: FlowExpect | undefined): FlowExpect | undefined;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { PredicateKind } from '@reticlehq/core';
|
|
2
|
+
/** Merge two partial expectations; later keys win only where the earlier one said nothing. */
|
|
3
|
+
function merge(into, from) {
|
|
4
|
+
return { ...from, ...into };
|
|
5
|
+
}
|
|
6
|
+
export function predicateToExpect(predicate) {
|
|
7
|
+
switch (predicate.kind) {
|
|
8
|
+
case PredicateKind.SIGNAL: {
|
|
9
|
+
if (predicate.name === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
return predicate.dataMatches === undefined
|
|
12
|
+
? { signal: predicate.name }
|
|
13
|
+
: { signal: predicate.name, signalData: predicate.dataMatches };
|
|
14
|
+
}
|
|
15
|
+
case PredicateKind.NET: {
|
|
16
|
+
const net = {};
|
|
17
|
+
if (predicate.method !== undefined)
|
|
18
|
+
net.method = predicate.method;
|
|
19
|
+
if (predicate.urlContains !== undefined)
|
|
20
|
+
net.urlContains = predicate.urlContains;
|
|
21
|
+
if (predicate.status !== undefined)
|
|
22
|
+
net.status = predicate.status;
|
|
23
|
+
if (predicate.count !== undefined)
|
|
24
|
+
net.count = predicate.count;
|
|
25
|
+
return 0 === Object.keys(net).length ? undefined : { net };
|
|
26
|
+
}
|
|
27
|
+
case PredicateKind.CONSOLE: {
|
|
28
|
+
const console_ = {};
|
|
29
|
+
if (predicate.level !== undefined)
|
|
30
|
+
console_.level = predicate.level;
|
|
31
|
+
if (predicate.absent !== undefined)
|
|
32
|
+
console_.absent = predicate.absent;
|
|
33
|
+
return 0 === Object.keys(console_).length ? undefined : { console: console_ };
|
|
34
|
+
}
|
|
35
|
+
case PredicateKind.ELEMENT: {
|
|
36
|
+
const element = {};
|
|
37
|
+
if (predicate.query.testid !== undefined)
|
|
38
|
+
element.testid = predicate.query.testid;
|
|
39
|
+
if (predicate.query.role !== undefined)
|
|
40
|
+
element.role = predicate.query.role;
|
|
41
|
+
if (predicate.query.name !== undefined)
|
|
42
|
+
element.name = predicate.query.name;
|
|
43
|
+
return 0 === Object.keys(element).length ? undefined : { element };
|
|
44
|
+
}
|
|
45
|
+
case PredicateKind.STATE: {
|
|
46
|
+
const state = { path: predicate.path };
|
|
47
|
+
if (predicate.store !== undefined)
|
|
48
|
+
state.store = predicate.store;
|
|
49
|
+
if (predicate.equals !== undefined)
|
|
50
|
+
state.equals = predicate.equals;
|
|
51
|
+
return { state };
|
|
52
|
+
}
|
|
53
|
+
case PredicateKind.ALL_OF: {
|
|
54
|
+
// `settled` members drop out on their own by returning undefined.
|
|
55
|
+
let combined;
|
|
56
|
+
for (const part of predicate.predicates) {
|
|
57
|
+
const expect = predicateToExpect(part);
|
|
58
|
+
if (expect === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
combined = combined === undefined ? expect : merge(combined, expect);
|
|
61
|
+
}
|
|
62
|
+
return combined;
|
|
63
|
+
}
|
|
64
|
+
default:
|
|
65
|
+
// settled | route | animation | anyOf | not — nothing FlowExpect can say honestly.
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The subset a REPLAY actually enforces today.
|
|
71
|
+
*
|
|
72
|
+
* `flow-replay` checks exactly two things per step: `expect.element.testid` is present after the
|
|
73
|
+
* action, and `expect.state` holds. A recorded `net` or `signal` expect is graded as a consequence
|
|
74
|
+
* assertion by `classifyFlowAssertions` and then never evaluated — so writing one into a flow file
|
|
75
|
+
* would make it report `grade: "asserted"` while nothing checks it.
|
|
76
|
+
*
|
|
77
|
+
* That is a false green, in the feature whose entire purpose is preventing them, produced by the
|
|
78
|
+
* change that was meant to strengthen it. So only the enforced kinds are recorded: a flow that says
|
|
79
|
+
* it asserts something must actually go red when that thing breaks.
|
|
80
|
+
*
|
|
81
|
+
* Lifting this is worth doing — net and signal are the assertions agents most often make, and the
|
|
82
|
+
* conversion above already handles them — but it requires replay to evaluate them per step (the
|
|
83
|
+
* machinery exists: successToPredicate + the predicate engine). Until then, recording them would
|
|
84
|
+
* trade a weak flow for a lying one.
|
|
85
|
+
*/
|
|
86
|
+
export function enforcedOnReplay(expect) {
|
|
87
|
+
if (expect === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
const kept = {};
|
|
90
|
+
if (expect.element?.testid !== undefined)
|
|
91
|
+
kept.element = expect.element;
|
|
92
|
+
if (expect.state !== undefined)
|
|
93
|
+
kept.state = expect.state;
|
|
94
|
+
return 0 === Object.keys(kept).length ? undefined : kept;
|
|
95
|
+
}
|
package/dist/flows/replay.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export declare function ambiguousTestidNote(value: string): string;
|
|
|
15
15
|
export declare function queryRefs(result: CommandResult): string[];
|
|
16
16
|
/** A destructive-action confirmation is one-shot and must never persist into a recording. */
|
|
17
17
|
export declare function replayActionArgs(value: unknown, confirmDangerous?: boolean): Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Capture an act into every in-flight recording, or do nothing when none is running.
|
|
20
|
+
*
|
|
21
|
+
* Every tool that drives the page must call this. reticle_act_and_wait did not, which is the tool
|
|
22
|
+
* the documented agent recipe drives with — so "record start -> drive -> record stop -> flow_save"
|
|
23
|
+
* saved a flow with zero steps.
|
|
24
|
+
*/
|
|
25
|
+
export declare function captureAct(recordings: {
|
|
26
|
+
active: () => string[];
|
|
27
|
+
capture: (step: RecordedStep) => void;
|
|
28
|
+
}, args: Record<string, unknown>, res: unknown): void;
|
|
18
29
|
/**
|
|
19
30
|
* Compile a single reticle_act invocation into a normalized RecordedStep using the action result. Anchor
|
|
20
31
|
* priority mirrors synthesizeAnchor: a data-testid is the gold standard; failing that, the element's
|
|
@@ -22,7 +33,11 @@ export declare function replayActionArgs(value: unknown, confirmDangerous?: bool
|
|
|
22
33
|
* a volatile ref. Only when neither is available is the step ref-bound (in-session only).
|
|
23
34
|
*/
|
|
24
35
|
export declare function compileActStep(args: Record<string, unknown>, res: unknown): RecordedStep;
|
|
25
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Compile an reticle_act_sequence invocation, normalizing each sub-step through the SAME anchor
|
|
38
|
+
* priority a single act uses (see compileAnchorArgs) — not testid-or-nothing, which is what made
|
|
39
|
+
* every sequence unreplayable on an app without testids.
|
|
40
|
+
*/
|
|
26
41
|
export declare function compileSequenceStep(args: Record<string, unknown>, res: unknown): RecordedStep;
|
|
27
42
|
interface ReplayStepResult {
|
|
28
43
|
tool: string;
|
package/dist/flows/replay.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { DANGEROUS_ACTION_CONFIRM_ARG, ReticleCommand, QueryBy, } from '@reticlehq/core';
|
|
2
2
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
3
|
+
import { predicateToExpect, enforcedOnReplay } from './predicate-to-expect.js';
|
|
4
|
+
import { PredicateSchema } from '../events/predicate.js';
|
|
3
5
|
import { asString, asRecord } from '../tools/tools-helpers.js';
|
|
4
6
|
/**
|
|
5
7
|
* The note attached when a testid resolves to multiple live elements (the first match is used).
|
|
@@ -35,10 +37,38 @@ function sourceFromResult(res) {
|
|
|
35
37
|
if (typeof source['file'] !== 'string' || typeof source['line'] !== 'number')
|
|
36
38
|
return undefined;
|
|
37
39
|
const out = { file: source['file'], line: source['line'] };
|
|
38
|
-
if (typeof source['column']
|
|
40
|
+
if ('number' === typeof source['column'])
|
|
39
41
|
out['column'] = source['column'];
|
|
40
42
|
return out;
|
|
41
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Capture an act into every in-flight recording, or do nothing when none is running.
|
|
46
|
+
*
|
|
47
|
+
* Every tool that drives the page must call this. reticle_act_and_wait did not, which is the tool
|
|
48
|
+
* the documented agent recipe drives with — so "record start -> drive -> record stop -> flow_save"
|
|
49
|
+
* saved a flow with zero steps.
|
|
50
|
+
*/
|
|
51
|
+
export function captureAct(recordings, args, res) {
|
|
52
|
+
if (0 === recordings.active().length)
|
|
53
|
+
return;
|
|
54
|
+
const step = compileActStep(args, res);
|
|
55
|
+
// Keep the assertion the agent actually made. `act_and_wait { until }` IS the agent saying what
|
|
56
|
+
// success means — 12 of 14 calls in a day carried one — and dropping it produced a flow graded
|
|
57
|
+
// "assertion-free: it will pass even if the feature is broken", which is the regression-suite
|
|
58
|
+
// story failing at its last step. Only kinds FlowExpect can express survive; see
|
|
59
|
+
// predicate-to-expect.ts.
|
|
60
|
+
const until = args['until'] ?? args['predicate'];
|
|
61
|
+
if (until !== undefined) {
|
|
62
|
+
const parsed = PredicateSchema.safeParse(until);
|
|
63
|
+
// Only what a replay actually CHECKS — recording an unenforced assertion would grade the flow
|
|
64
|
+
// "asserted" while nothing verifies it, which is a false green in the feature built to prevent
|
|
65
|
+
// them. See enforcedOnReplay.
|
|
66
|
+
const expect = parsed.success ? enforcedOnReplay(predicateToExpect(parsed.data)) : undefined;
|
|
67
|
+
if (expect !== undefined)
|
|
68
|
+
step.expect = expect;
|
|
69
|
+
}
|
|
70
|
+
recordings.capture(step);
|
|
71
|
+
}
|
|
42
72
|
/**
|
|
43
73
|
* Compile a single reticle_act invocation into a normalized RecordedStep using the action result. Anchor
|
|
44
74
|
* priority mirrors synthesizeAnchor: a data-testid is the gold standard; failing that, the element's
|
|
@@ -46,10 +76,20 @@ function sourceFromResult(res) {
|
|
|
46
76
|
* a volatile ref. Only when neither is available is the step ref-bound (in-session only).
|
|
47
77
|
*/
|
|
48
78
|
export function compileActStep(args, res) {
|
|
49
|
-
const
|
|
79
|
+
const { args: compiled, stable } = compileAnchorArgs(asRecord(res), asString(args['ref']) ?? '', asString(args['action']) ?? '', replayActionArgs(args['args']));
|
|
80
|
+
return { tool: ReticleTool.ACT, stable, args: compiled };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The ONE anchor chooser: testid (gold) > role+name (identifies an instance) > component/source
|
|
84
|
+
* (the JSX site) > a volatile ref (in-session only).
|
|
85
|
+
*
|
|
86
|
+
* It is shared because it was not: `compileSequenceStep` understood only a testid, so every
|
|
87
|
+
* act_sequence sub-step on a testid-less app compiled to a ref, saved as the degraded `unresolved`
|
|
88
|
+
* sentinel, and drifted on every replay — while a single act on the very same element compiled a
|
|
89
|
+
* perfectly stable anchor.
|
|
90
|
+
*/
|
|
91
|
+
function compileAnchorArgs(r, ref, action, actArgs) {
|
|
50
92
|
const testid = asString(r['testid']);
|
|
51
|
-
const action = asString(args['action']) ?? '';
|
|
52
|
-
const actArgs = replayActionArgs(args['args']);
|
|
53
93
|
const source = sourceFromResult(r);
|
|
54
94
|
if (testid !== undefined) {
|
|
55
95
|
// The testid anchors the step; source rides along purely so a failure can name a file. Carrying
|
|
@@ -62,7 +102,7 @@ export function compileActStep(args, res) {
|
|
|
62
102
|
};
|
|
63
103
|
if (source !== undefined)
|
|
64
104
|
testidArgs['source'] = source;
|
|
65
|
-
return {
|
|
105
|
+
return { args: testidArgs, stable: true };
|
|
66
106
|
}
|
|
67
107
|
// Role + NAME before component: a component/source anchor names the JSX site, so every row's copy
|
|
68
108
|
// of the same control collapses onto it. The accessible name separates them, and it is also the
|
|
@@ -80,7 +120,7 @@ export function compileActStep(args, res) {
|
|
|
80
120
|
};
|
|
81
121
|
if (source !== undefined)
|
|
82
122
|
roleArgs['source'] = source;
|
|
83
|
-
return {
|
|
123
|
+
return { args: roleArgs, stable: true };
|
|
84
124
|
}
|
|
85
125
|
const component = asString(r['component']);
|
|
86
126
|
if (component !== undefined || source !== undefined) {
|
|
@@ -89,15 +129,15 @@ export function compileActStep(args, res) {
|
|
|
89
129
|
componentArgs['component'] = component;
|
|
90
130
|
if (source !== undefined)
|
|
91
131
|
componentArgs['source'] = source;
|
|
92
|
-
return {
|
|
132
|
+
return { args: componentArgs, stable: true };
|
|
93
133
|
}
|
|
94
|
-
return {
|
|
95
|
-
tool: ReticleTool.ACT,
|
|
96
|
-
stable: false,
|
|
97
|
-
args: { ref: asString(args['ref']) ?? '', action, args: actArgs },
|
|
98
|
-
};
|
|
134
|
+
return { args: { ref, action, args: actArgs }, stable: false };
|
|
99
135
|
}
|
|
100
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Compile an reticle_act_sequence invocation, normalizing each sub-step through the SAME anchor
|
|
138
|
+
* priority a single act uses (see compileAnchorArgs) — not testid-or-nothing, which is what made
|
|
139
|
+
* every sequence unreplayable on an app without testids.
|
|
140
|
+
*/
|
|
101
141
|
export function compileSequenceStep(args, res) {
|
|
102
142
|
const inputSteps = Array.isArray(args['steps']) ? args['steps'] : [];
|
|
103
143
|
const resolved = Array.isArray(asRecord(res)['steps'])
|
|
@@ -106,14 +146,10 @@ export function compileSequenceStep(args, res) {
|
|
|
106
146
|
let stable = inputSteps.length > 0;
|
|
107
147
|
const subSteps = inputSteps.map((raw, i) => {
|
|
108
148
|
const step = asRecord(raw);
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
114
|
-
}
|
|
115
|
-
stable = false;
|
|
116
|
-
return { ref: asString(step['ref']) ?? '', action, args: stepArgs };
|
|
149
|
+
const compiled = compileAnchorArgs(asRecord(resolved[i]), asString(step['ref']) ?? '', asString(step['action']) ?? '', replayActionArgs(step['args']));
|
|
150
|
+
if (!compiled.stable)
|
|
151
|
+
stable = false;
|
|
152
|
+
return compiled.args;
|
|
117
153
|
});
|
|
118
154
|
return { tool: ReticleTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
119
155
|
}
|
|
@@ -181,7 +217,7 @@ async function resolveRef(session, step) {
|
|
|
181
217
|
: { ref };
|
|
182
218
|
}
|
|
183
219
|
const ref = asString(step.ref);
|
|
184
|
-
if (ref === undefined || ref.length
|
|
220
|
+
if (ref === undefined || 0 === ref.length) {
|
|
185
221
|
throw new Error('step has no resolvable anchor (testid, role+name, component) and no ref');
|
|
186
222
|
}
|
|
187
223
|
return { ref, note: 'replayed by stale ref (not portable across sessions)' };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closest surviving control to a role+name anchor that no longer resolves.
|
|
3
|
+
*
|
|
4
|
+
* `runRoleStep` returned `nearest: null` as a literal, so `flow_heal` answered "no nearest match
|
|
5
|
+
* cleared the confidence floor" for every role-anchored drift — which reads as a judgement about
|
|
6
|
+
* candidates when in fact nothing had looked. Reported from a real sweep: a three-step flow failed
|
|
7
|
+
* on `button named 'Menu' did not resolve` and heal called it unhealable, however small the change.
|
|
8
|
+
*
|
|
9
|
+
* What this does NOT do is make role anchors auto-healable, and that restraint is the point. A
|
|
10
|
+
* testid is an identifier a developer put there deliberately; a role name is user-visible text where
|
|
11
|
+
* "Save" and "Save as" are one edit apart. Silently rebinding a step to a similar-looking control is
|
|
12
|
+
* precisely the wrong-element green this product exists to catch. So the bar here is deliberately
|
|
13
|
+
* higher than the testid path's: same role, unambiguous, and genuinely close — otherwise null, and
|
|
14
|
+
* the agent is told to add a testid.
|
|
15
|
+
*/
|
|
16
|
+
import { editDistance } from './flow-replay.js';
|
|
17
|
+
/**
|
|
18
|
+
* A name must be at least this similar to be worth showing: at most a third of it changed. A looser
|
|
19
|
+
* bar turns "no candidate" into "a confident wrong one", which is worse than saying nothing.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_CHANGED_FRACTION = 1 / 3;
|
|
22
|
+
export function nearestRoleName(role, missing, present) {
|
|
23
|
+
const sameRole = present.filter((candidate) => candidate.role === role);
|
|
24
|
+
if (0 === sameRole.length)
|
|
25
|
+
return null;
|
|
26
|
+
const budget = Math.floor(missing.length * MAX_CHANGED_FRACTION);
|
|
27
|
+
let best = null;
|
|
28
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
29
|
+
let tied = false;
|
|
30
|
+
for (const candidate of sameRole) {
|
|
31
|
+
const distance = editDistance(missing, candidate.name);
|
|
32
|
+
if (distance < bestDistance) {
|
|
33
|
+
bestDistance = distance;
|
|
34
|
+
best = candidate.name;
|
|
35
|
+
tied = false;
|
|
36
|
+
}
|
|
37
|
+
else if (distance === bestDistance) {
|
|
38
|
+
tied = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// A tie is a coin flip between two live controls; naming one would be inventing confidence.
|
|
42
|
+
if (null === best || tied || bestDistance > budget)
|
|
43
|
+
return null;
|
|
44
|
+
return best;
|
|
45
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { SuiteVerdict } from '@reticlehq/core';
|
|
9
9
|
import type { ToolDeps } from '../tools/tools.js';
|
|
10
|
-
import { type ProjectCloud } from '
|
|
10
|
+
import { type ProjectCloud } from '../cloud/cloud-config.js';
|
|
11
11
|
import { type ServerVerification } from '../cloud/cloud-sync.js';
|
|
12
12
|
/** Map the hosted runner's report onto the local suite-verdict shape (status/total/passed/failed/failures). */
|
|
13
13
|
export declare function toSuiteVerdict(report: ServerVerification): SuiteVerdict;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VerifyMode } from '
|
|
1
|
+
import { VerifyMode } from '../cloud/cloud-config.js';
|
|
2
2
|
import { submitServerVerification } from '../cloud/cloud-sync.js';
|
|
3
3
|
const PASS = 'pass';
|
|
4
4
|
const UNVERIFIED = 'unverified';
|
|
@@ -14,12 +14,12 @@ export function toSuiteVerdict(report) {
|
|
|
14
14
|
}));
|
|
15
15
|
const total = report.flows.length;
|
|
16
16
|
const passed = total - failures.length;
|
|
17
|
-
const status = report.verdict === PASS && failures.length
|
|
17
|
+
const status = report.verdict === PASS && 0 === failures.length ? 'pass' : 'fail';
|
|
18
18
|
return { status, total, passed, failed: failures.length, summary: report.summary, failures };
|
|
19
19
|
}
|
|
20
20
|
/** Run the suite on the hosted runner, or null to signal the caller should replay locally. */
|
|
21
21
|
export async function runServerVerify(deps, cloud, sessionId, flows) {
|
|
22
|
-
if (cloud.verify !== VerifyMode.SERVER || cloud.config
|
|
22
|
+
if (cloud.verify !== VerifyMode.SERVER || null === cloud.config)
|
|
23
23
|
return null;
|
|
24
24
|
let previewUrl;
|
|
25
25
|
try {
|
|
@@ -29,10 +29,10 @@ export async function runServerVerify(deps, cloud, sessionId, flows) {
|
|
|
29
29
|
previewUrl = undefined;
|
|
30
30
|
}
|
|
31
31
|
// The server hits the URL itself; with no URL (or a localhost one it can't reach) fall back to local.
|
|
32
|
-
if (previewUrl === undefined || previewUrl.length
|
|
32
|
+
if (previewUrl === undefined || 0 === previewUrl.length)
|
|
33
33
|
return null;
|
|
34
34
|
const report = await submitServerVerification({ previewUrl, flows, source: SOURCE }, cloud.config, (url, init) => fetch(url, init));
|
|
35
|
-
if (
|
|
35
|
+
if (null === report)
|
|
36
36
|
return null;
|
|
37
37
|
// The hosted runner said it couldn't actually verify (e.g. it's not enabled yet). Never surface that as
|
|
38
38
|
// a pass or a fail — fall back to the real local replay so the verdict reflects a browser that ran.
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { verdictForSuite } from './verify-change-verdict.js';
|
|
3
|
+
import { attributedFailures } from './attributed-failure.js';
|
|
2
4
|
import { Verified } from '@reticlehq/core';
|
|
3
5
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
6
|
import { asNumber, asRecord, asString } from '../tools/tools-helpers.js';
|
|
5
|
-
import { loadNamedFlows, resolveChangedFiles } from '../cli-flow-commands.js';
|
|
7
|
+
import { loadNamedFlows, resolveChangedFiles } from '../cli/cli-flow-commands.js';
|
|
6
8
|
import { affectedSavedFlows } from './flow-sources.js';
|
|
7
9
|
import { FLOW_TOOLS } from './flow-tools.js';
|
|
8
10
|
import { findContradictions } from '../events/contradictions.js';
|
|
9
11
|
import { parseControls } from '../tools/coverage-tools.js';
|
|
10
12
|
import { ReticleCommand, SnapshotMode } from '@reticlehq/core';
|
|
11
|
-
const PASS = 'pass';
|
|
12
13
|
export const VERIFY_CHANGE_TOOLS = [
|
|
13
14
|
{
|
|
14
15
|
name: ReticleTool.VERIFY_CHANGE,
|
|
@@ -57,13 +58,13 @@ export const VERIFY_CHANGE_TOOLS = [
|
|
|
57
58
|
},
|
|
58
59
|
handler: async (deps, args) => {
|
|
59
60
|
const files = Array.isArray(args['files'])
|
|
60
|
-
? args['files'].filter((f) => typeof f
|
|
61
|
+
? args['files'].filter((f) => 'string' === typeof f)
|
|
61
62
|
: [];
|
|
62
63
|
const since = asString(args['since']);
|
|
63
64
|
const changedFiles = await resolveChangedFiles(files, since);
|
|
64
65
|
const flows = await loadNamedFlows(deps.fs, deps.reticleRoot);
|
|
65
66
|
const { affected, unknownProvenance } = affectedSavedFlows(flows, changedFiles);
|
|
66
|
-
if (changedFiles.length
|
|
67
|
+
if (0 === changedFiles.length) {
|
|
67
68
|
return {
|
|
68
69
|
verified: Verified.UNKNOWN,
|
|
69
70
|
because: 'no changed files were given, so there was nothing to decide about',
|
|
@@ -77,10 +78,10 @@ export const VERIFY_CHANGE_TOOLS = [
|
|
|
77
78
|
}
|
|
78
79
|
// The guard: an uncovered change is UNKNOWN. Reporting `yes` here would mean "your change is
|
|
79
80
|
// fine" on the strength of having run nothing at all.
|
|
80
|
-
if (affected.length
|
|
81
|
+
if (0 === affected.length) {
|
|
81
82
|
return {
|
|
82
83
|
verified: Verified.UNKNOWN,
|
|
83
|
-
because: `no saved flow covers ${changedFiles.length
|
|
84
|
+
because: `no saved flow covers ${1 === changedFiles.length ? 'this file' : 'these files'} — record one with reticle_record { action: "start" } then reticle_flow_save, or verify by driving the app directly`,
|
|
84
85
|
changedFiles,
|
|
85
86
|
flowsRun: [],
|
|
86
87
|
unknownProvenance,
|
|
@@ -104,11 +105,32 @@ export const VERIFY_CHANGE_TOOLS = [
|
|
|
104
105
|
}));
|
|
105
106
|
const failed = suite.failed ?? 0;
|
|
106
107
|
const passed = suite.passed ?? 0;
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// `unverifiable` is NOT a failure — the suite ran and proved nothing, which is the same fact
|
|
109
|
+
// this tool answers UNKNOWN for an uncovered change. Treating it as a red emitted a bug_found
|
|
110
|
+
// that nothing earned. See verify-change-verdict.
|
|
111
|
+
const verdict = verdictForSuite(String(suite.status), failed);
|
|
112
|
+
// A NO must be EARNED by a failing flow genuinely tied to the changed files. `affectedSavedFlows`
|
|
113
|
+
// re-runs any flow whose sources it cannot determine — over-running beats skipping — and those
|
|
114
|
+
// land in `unknownProvenance`. Judging the change on them produced a negative verdict whose own
|
|
115
|
+
// explanation admitted the evidence was not tied to the file. See attributed-failure.
|
|
116
|
+
const failingNames = Array.isArray(suite.failures)
|
|
117
|
+
? suite.failures
|
|
118
|
+
.map((f) => 'object' === typeof f && f !== null ? asString(asRecord(f)['flow']) : undefined)
|
|
119
|
+
.filter((n) => n !== undefined)
|
|
120
|
+
: // No per-failure detail: the failure is somewhere in the set that ran, so ask the question
|
|
121
|
+
// of all of them rather than guessing which.
|
|
122
|
+
affected;
|
|
123
|
+
const attributed = attributedFailures(failingNames, unknownProvenance);
|
|
124
|
+
const earnedNo = verdict === Verified.NO && attributed.length > 0;
|
|
125
|
+
const reported = verdict === Verified.NO && !earnedNo ? Verified.UNKNOWN : verdict;
|
|
126
|
+
if (reported !== Verified.YES) {
|
|
109
127
|
return {
|
|
110
|
-
verified:
|
|
111
|
-
because:
|
|
128
|
+
verified: reported,
|
|
129
|
+
because: earnedNo
|
|
130
|
+
? `${String(failed)} of ${String(suite.total ?? affected.length)} covering flows failed, including ${attributed.join(', ')}, which cover the changed files${provenanceNote}`
|
|
131
|
+
: verdict === Verified.NO
|
|
132
|
+
? `flows failed, but Reticle cannot tell which sources any of them cover (${failingNames.join(', ')}) — so this says nothing about the files you changed. Re-record them so their sources are stamped, or read the replay yourself`
|
|
133
|
+
: `the covering flows ran but proved nothing (${String(suite.status)}) — they assert no observable consequence, so a green from them would not have meant your change is safe${provenanceNote}`,
|
|
112
134
|
changedFiles,
|
|
113
135
|
flowsRun: affected,
|
|
114
136
|
suite,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a replayed suite's status entitles `verify_change` to claim.
|
|
3
|
+
*
|
|
4
|
+
* This was `suite.status !== 'pass'` meaning failure. That was true while the only statuses were
|
|
5
|
+
* pass and fail — but `flow_verify` now reports `unverifiable` for a suite that ran and proved
|
|
6
|
+
* nothing (an empty suite used to report `pass`, which was a false green). Under the old test,
|
|
7
|
+
* `unverifiable` fell into the failure branch: a suite that proved nothing came back as proof the
|
|
8
|
+
* change was broken, and emitted a `bug_found` for it.
|
|
9
|
+
*
|
|
10
|
+
* `unverifiable` is the same fact this tool already handles elsewhere as "no saved flow covers this
|
|
11
|
+
* change" — answered UNKNOWN, with the comment calling it "the honest answer, never a green: nothing
|
|
12
|
+
* ran, so nothing was proved". A red nothing earned is the mirror image of that green.
|
|
13
|
+
*/
|
|
14
|
+
import { Verified } from '@reticlehq/core';
|
|
15
|
+
export declare function verdictForSuite(status: string, failedCount: number): Verified;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a replayed suite's status entitles `verify_change` to claim.
|
|
3
|
+
*
|
|
4
|
+
* This was `suite.status !== 'pass'` meaning failure. That was true while the only statuses were
|
|
5
|
+
* pass and fail — but `flow_verify` now reports `unverifiable` for a suite that ran and proved
|
|
6
|
+
* nothing (an empty suite used to report `pass`, which was a false green). Under the old test,
|
|
7
|
+
* `unverifiable` fell into the failure branch: a suite that proved nothing came back as proof the
|
|
8
|
+
* change was broken, and emitted a `bug_found` for it.
|
|
9
|
+
*
|
|
10
|
+
* `unverifiable` is the same fact this tool already handles elsewhere as "no saved flow covers this
|
|
11
|
+
* change" — answered UNKNOWN, with the comment calling it "the honest answer, never a green: nothing
|
|
12
|
+
* ran, so nothing was proved". A red nothing earned is the mirror image of that green.
|
|
13
|
+
*/
|
|
14
|
+
import { Verified } from '@reticlehq/core';
|
|
15
|
+
const PASS = 'pass';
|
|
16
|
+
const FAIL = 'fail';
|
|
17
|
+
export function verdictForSuite(status, failedCount) {
|
|
18
|
+
// A real failure outranks everything: flows ran and some went red.
|
|
19
|
+
if (failedCount > 0 || status === FAIL)
|
|
20
|
+
return Verified.NO;
|
|
21
|
+
if (status === PASS)
|
|
22
|
+
return Verified.YES;
|
|
23
|
+
// `unverifiable`, or any status added later. Nothing was proved, so nothing is claimed.
|
|
24
|
+
return Verified.UNKNOWN;
|
|
25
|
+
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* a quiet window, so we compute affected flows once per edit, not per keystroke. The scheduler (setTimeout)
|
|
5
5
|
* is injected, keeping the batching logic pure and unit-testable.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
interface WatchBatcher {
|
|
8
8
|
onChange(file: string): void;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
interface WatchBatcherOptions {
|
|
11
11
|
debounceMs: number;
|
|
12
12
|
/** Injected scheduler (setTimeout) — deterministic in tests. */
|
|
13
13
|
schedule: (fn: () => void, ms: number) => void;
|
|
@@ -15,3 +15,4 @@ export interface WatchBatcherOptions {
|
|
|
15
15
|
onFlush: (files: string[]) => void;
|
|
16
16
|
}
|
|
17
17
|
export declare function createWatchBatcher(options: WatchBatcherOptions): WatchBatcher;
|
|
18
|
+
export {};
|