@reticlehq/server 3.8.2 → 3.10.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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/SKILL.md +13 -12
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -160
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +46 -5
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -24
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +47 -4
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +22 -17
- package/dist/command/cli/cli-parse.js +54 -32
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +61 -31
- package/dist/command/cli/cli-verify.d.ts +17 -3
- package/dist/command/cli/cli-verify.js +59 -21
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +7 -6
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +11 -0
- package/dist/command/cli/ports/port-holder.js +36 -4
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +2 -0
- package/dist/command/cli/status/status-lines.js +72 -17
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +24 -6
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +112 -70
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +22 -4
- package/dist/command/daemon/daemon.js +52 -10
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +149 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +11 -15
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +63 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +36 -15
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +14 -3
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +67 -99
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +19 -7
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +51 -5
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +21 -16
- package/dist/language/flows/recording/tape/recordings.js +112 -49
- package/dist/language/flows/replay.d.ts +36 -12
- package/dist/language/flows/replay.js +193 -50
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +30 -10
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +35 -18
- package/dist/portal/input/real-input.js +99 -33
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +81 -21
- package/dist/portal/pool/browser-pool.d.ts +16 -30
- package/dist/portal/pool/browser-pool.js +64 -52
- package/dist/portal/pool/context-permissions.d.ts +34 -17
- package/dist/portal/pool/context-permissions.js +94 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +56 -0
- package/dist/portal/pool/lease-page-error.js +160 -0
- package/dist/portal/pool/page-error-journal.d.ts +63 -0
- package/dist/portal/pool/page-error-journal.js +208 -0
- package/dist/portal/pool/playwright-launcher.js +71 -15
- package/dist/portal/pool/pool-contract.d.ts +49 -1
- package/dist/portal/pool/pool-lease.d.ts +70 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +91 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +11 -2
- package/dist/portal/session/dev-server/dev-command.js +110 -16
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +76 -27
- package/dist/portal/session/no-session-next-action.d.ts +14 -0
- package/dist/portal/session/no-session-next-action.js +56 -7
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +41 -4
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +211 -81
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +30 -2
- package/dist/start-wiring.js +74 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +8 -5
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +38 -18
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +10 -0
- package/dist/surface/tools/act/after-match.js +40 -0
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +17 -1
- package/dist/surface/tools/act/resolve-target.js +59 -9
- package/dist/surface/tools/act/sequence-grade.d.ts +15 -1
- package/dist/surface/tools/act/sequence-grade.js +45 -27
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +81 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +83 -95
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +18 -12
- package/dist/surface/tools/browser-tools.js +87 -13
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +11 -3
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +131 -40
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +29 -21
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +42 -1
- package/dist/surface/tools/lease-hint.js +52 -3
- package/dist/surface/tools/lease-readiness.d.ts +47 -0
- package/dist/surface/tools/lease-readiness.js +126 -0
- package/dist/surface/tools/lease-seed.d.ts +16 -1
- package/dist/surface/tools/lease-seed.js +37 -4
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +104 -124
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +18 -0
- package/dist/surface/tools/tool-kit.js +9 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +54 -18
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +17 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +15 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/architecture.md +3 -6
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +13 -12
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +34 -24
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +17 -22
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +6 -4
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +1 -1
- package/docs/usage.md +17 -14
- package/package.json +18 -14
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive one build through an action plan and read what it shows after each planned sequence.
|
|
3
|
+
*
|
|
4
|
+
* The page is read only after it settles: read, wait, read again, until two reads in a row match,
|
|
5
|
+
* up to `SETTLE_READS` reads. A page that never settles (a slideshow) gives its last read.
|
|
6
|
+
*
|
|
7
|
+
* A sequence that starts another planned sequence is not driven on its own: the longer one is
|
|
8
|
+
* driven, and the page is read after each of its steps. Each planned sequence is read once, in the
|
|
9
|
+
* first run that reaches it, and the order is the same on every build.
|
|
10
|
+
*
|
|
11
|
+
* Each reading also holds what the app did since the sequence's first action: its network calls and
|
|
12
|
+
* console errors and warnings (`effect-lines.ts`), read from the cursor that first action returned.
|
|
13
|
+
*
|
|
14
|
+
* A step that could not be done (the control is not there, or the action was refused) is recorded
|
|
15
|
+
* for that sequence and each longer one in the run. A driver that lost its browser session throws
|
|
16
|
+
* `DriverLostError`: that says nothing about the build, so the caller makes the whole run invalid.
|
|
17
|
+
*/
|
|
18
|
+
import { ActionType } from '@reticlehq/core';
|
|
19
|
+
import { isPendingLine } from './outside/effect-lines.js';
|
|
20
|
+
import { ControlKind, FaultKind, firstRef, longestRuns, refFor, sequenceKey, stepLabel, } from './action-plan.js';
|
|
21
|
+
import { structureLines } from './visible-content.js';
|
|
22
|
+
const FAULT_ANSWER = {
|
|
23
|
+
[FaultKind.SERVER]: { status: 500, body: '{"error":"fault"}' },
|
|
24
|
+
[FaultKind.ABORT]: { abort: true },
|
|
25
|
+
[FaultKind.EMPTY]: { status: 200, body: '[]' },
|
|
26
|
+
};
|
|
27
|
+
/** The start page again: an empty query keeps the url as it is. */
|
|
28
|
+
const RELOAD = '';
|
|
29
|
+
/** The driver lost its browser session. The run says nothing about the build. */
|
|
30
|
+
export class DriverLostError extends Error {
|
|
31
|
+
name = 'DriverLostError';
|
|
32
|
+
}
|
|
33
|
+
export const SETTLE_READS = 4;
|
|
34
|
+
export const SETTLE_GAP_MS = 400;
|
|
35
|
+
/** The first action of every sequence. */
|
|
36
|
+
export const START_STEP = 'open the start page';
|
|
37
|
+
/** Elapsed times in a page's own log ("0.04s") change between two runs. Keep whole seconds. */
|
|
38
|
+
const ELAPSED = /(?<![\w.])(\d+\.\d+)(?:s\b|$)/g;
|
|
39
|
+
/** An elapsed time in milliseconds ("3ms", "1400 ms"). */
|
|
40
|
+
const ELAPSED_MS = /(?<![\w.])(\d+(?:\.\d+)?) ?ms\b/g;
|
|
41
|
+
const MS_PER_S = 1000;
|
|
42
|
+
/** A line with its sub-second elapsed times rounded to whole seconds. */
|
|
43
|
+
const roundTimes = (line) => line
|
|
44
|
+
.replace(ELAPSED_MS, (_, n) => `${String(Math.round(Number(n) / MS_PER_S))}s`)
|
|
45
|
+
.replace(ELAPSED, (_, n) => `${String(Math.round(Number(n)))}s`);
|
|
46
|
+
/**
|
|
47
|
+
* What a person sees on the page, one line for each element, with the role of each control and
|
|
48
|
+
* structure element kept (`structureLines`) and sub-second times rounded.
|
|
49
|
+
*/
|
|
50
|
+
export function pageLines(tree) {
|
|
51
|
+
return structureLines(tree).map(roundTimes);
|
|
52
|
+
}
|
|
53
|
+
const same = (a, b) => a.length === b.length && a.every((line, i) => line === b[i]);
|
|
54
|
+
/** Read the page once it stops changing. */
|
|
55
|
+
export async function settledRead(driver, sleep) {
|
|
56
|
+
let tree = await driver.read();
|
|
57
|
+
let lines = pageLines(tree);
|
|
58
|
+
for (let i = 1; i < SETTLE_READS; i += 1) {
|
|
59
|
+
await sleep(SETTLE_GAP_MS);
|
|
60
|
+
const nextTree = await driver.read();
|
|
61
|
+
const next = pageLines(nextTree);
|
|
62
|
+
tree = nextTree;
|
|
63
|
+
const settled = same(next, lines);
|
|
64
|
+
lines = next;
|
|
65
|
+
if (settled) {
|
|
66
|
+
return { tree, lines, settled: true };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { tree, lines, settled: false };
|
|
70
|
+
}
|
|
71
|
+
/** What the app did since `since`, with times rounded like page text. None before the first act. */
|
|
72
|
+
async function effectsSince(driver, since) {
|
|
73
|
+
return since === undefined ? [] : (await driver.effects(since)).map(roundTimes);
|
|
74
|
+
}
|
|
75
|
+
/** The ref of `target`, reading again while the page is still settling. */
|
|
76
|
+
async function locate(driver, tree, target, sleep) {
|
|
77
|
+
let now = tree;
|
|
78
|
+
for (let i = 1; i < SETTLE_READS; i += 1) {
|
|
79
|
+
const ref = refFor(now, target);
|
|
80
|
+
if (ref !== undefined) {
|
|
81
|
+
return { ref, tree: now };
|
|
82
|
+
}
|
|
83
|
+
await sleep(SETTLE_GAP_MS);
|
|
84
|
+
now = await driver.read();
|
|
85
|
+
}
|
|
86
|
+
const ref = refFor(now, target);
|
|
87
|
+
return ref === undefined ? { tree: now } : { ref, tree: now };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Do one step on the page `tree` shows. `ok` is false when it could not be done. `since` is the
|
|
91
|
+
* cursor from before its first action.
|
|
92
|
+
*/
|
|
93
|
+
async function perform(driver, step, tree, sleep) {
|
|
94
|
+
if ('wait' === step.action) {
|
|
95
|
+
await sleep(step.ms);
|
|
96
|
+
return { ok: true };
|
|
97
|
+
}
|
|
98
|
+
if ('goto' === step.action) {
|
|
99
|
+
return { ok: true === (await driver.goto?.(step.query)) };
|
|
100
|
+
}
|
|
101
|
+
if ('fault' === step.action) {
|
|
102
|
+
const rule = { urlContains: step.path, method: step.method, ...FAULT_ANSWER[step.fault] };
|
|
103
|
+
// Loaded again, so the requests the start page makes on load also see the fault.
|
|
104
|
+
const set = true === (await driver.mock?.([rule]));
|
|
105
|
+
return { ok: set && true === (await driver.goto?.(RELOAD)) };
|
|
106
|
+
}
|
|
107
|
+
if ('press' === step.action) {
|
|
108
|
+
const ref = firstRef(tree);
|
|
109
|
+
return ref === undefined ? { ok: false } : driver.act(ref, ActionType.PRESS, step.key);
|
|
110
|
+
}
|
|
111
|
+
const acts = 'fillall' === step.action
|
|
112
|
+
? step.fields.map((f) => ({ action: 'fill', target: f.target, value: f.value }))
|
|
113
|
+
: [step];
|
|
114
|
+
let now = tree;
|
|
115
|
+
let since;
|
|
116
|
+
for (const act of acts) {
|
|
117
|
+
// A text probe is one click, never part of "fill every field".
|
|
118
|
+
if (act.target.text !== undefined) {
|
|
119
|
+
return driver.act({ text: act.target.text }, ControlKind.CLICK);
|
|
120
|
+
}
|
|
121
|
+
const found = await locate(driver, now, act.target, sleep);
|
|
122
|
+
now = found.tree;
|
|
123
|
+
if (found.ref === undefined) {
|
|
124
|
+
return { ok: false };
|
|
125
|
+
}
|
|
126
|
+
const value = 'click' === act.action ? undefined : act.value;
|
|
127
|
+
const done = await driver.act(found.ref, act.action, value);
|
|
128
|
+
since ??= done.since;
|
|
129
|
+
if (!done.ok) {
|
|
130
|
+
return { ok: false };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return since === undefined ? { ok: true } : { ok: true, since };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Drive one sequence from a fresh load, for the plan, or undefined when a step could not be done.
|
|
137
|
+
*
|
|
138
|
+
* For a sequence that ends in a wait, the page is also read, settled, before the wait. The wait is
|
|
139
|
+
* `active` when the page did not settle before it, a network call was still pending before it, or
|
|
140
|
+
* the page changed during it.
|
|
141
|
+
*/
|
|
142
|
+
export async function probeSequence(driver, seq, sleep) {
|
|
143
|
+
if (!(await driver.open())) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
let tree = await driver.read();
|
|
147
|
+
let since;
|
|
148
|
+
let busy = false;
|
|
149
|
+
let before;
|
|
150
|
+
for (const [i, step] of seq.entries()) {
|
|
151
|
+
const last = i === seq.length - 1;
|
|
152
|
+
if (last && 'wait' === step.action) {
|
|
153
|
+
const read = await settledRead(driver, sleep);
|
|
154
|
+
const pending = (await effectsSince(driver, since)).some(isPendingLine);
|
|
155
|
+
busy = !read.settled || pending;
|
|
156
|
+
before = read.lines;
|
|
157
|
+
}
|
|
158
|
+
const done = await perform(driver, step, tree, sleep);
|
|
159
|
+
if (!done.ok) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
since ??= done.since;
|
|
163
|
+
// The last step is followed by the settled read.
|
|
164
|
+
if (!last) {
|
|
165
|
+
tree = await driver.read();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const after = await settledRead(driver, sleep);
|
|
169
|
+
const changed = before !== undefined && !same(before, after.lines);
|
|
170
|
+
const effects = await effectsSince(driver, since);
|
|
171
|
+
return { tree: after.tree, active: busy || changed, effects };
|
|
172
|
+
}
|
|
173
|
+
const labelsOf = (seq) => [START_STEP, ...seq.map(stepLabel)];
|
|
174
|
+
/**
|
|
175
|
+
* Drive the plan on one build. With `readState`, the app's stored data is read after each sequence
|
|
176
|
+
* is read, and its lines (`outside/state-sources.ts`) follow the page and effect lines.
|
|
177
|
+
*/
|
|
178
|
+
export async function readBuild(driver, plan, sleep, readState) {
|
|
179
|
+
const planned = new Set(plan.map(sequenceKey));
|
|
180
|
+
const out = new Map();
|
|
181
|
+
const isNew = (seq) => {
|
|
182
|
+
const key = sequenceKey(seq);
|
|
183
|
+
return planned.has(key) && !out.has(key);
|
|
184
|
+
};
|
|
185
|
+
/** The cursor of the current run's first action. */
|
|
186
|
+
let since;
|
|
187
|
+
/** Read after `seq`: settled and recorded the first time, else one read for the refs. */
|
|
188
|
+
/** Set while a run is tried again: the step that failed on the first pass. */
|
|
189
|
+
let retrying;
|
|
190
|
+
const take = async (seq) => {
|
|
191
|
+
if (!isNew(seq)) {
|
|
192
|
+
return driver.read();
|
|
193
|
+
}
|
|
194
|
+
const read = await settledRead(driver, sleep);
|
|
195
|
+
const lines = [...read.lines, ...(await effectsSince(driver, since))];
|
|
196
|
+
if (readState !== undefined) {
|
|
197
|
+
lines.push(...(await readState()));
|
|
198
|
+
}
|
|
199
|
+
out.set(sequenceKey(seq), {
|
|
200
|
+
actions: labelsOf(seq),
|
|
201
|
+
lines,
|
|
202
|
+
...(retrying === undefined ? {} : { retriedAt: retrying }),
|
|
203
|
+
});
|
|
204
|
+
return read.tree;
|
|
205
|
+
};
|
|
206
|
+
const failFrom = (run, from, failedAt) => {
|
|
207
|
+
for (let n = from; n <= run.length; n += 1) {
|
|
208
|
+
const seq = run.slice(0, n);
|
|
209
|
+
if (isNew(seq)) {
|
|
210
|
+
out.set(sequenceKey(seq), { actions: labelsOf(seq), failedAt });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
/** One pass over `run` from a fresh page. Returns the failed step's index, or undefined when every step was done. */
|
|
215
|
+
const pass = async (run) => {
|
|
216
|
+
since = undefined;
|
|
217
|
+
let tree = await take([]);
|
|
218
|
+
for (const [i, step] of run.entries()) {
|
|
219
|
+
const done = await perform(driver, step, tree, sleep);
|
|
220
|
+
if (!done.ok) {
|
|
221
|
+
return i;
|
|
222
|
+
}
|
|
223
|
+
since ??= done.since;
|
|
224
|
+
tree = await take(run.slice(0, i + 1));
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
};
|
|
228
|
+
for (const run of longestRuns(plan)) {
|
|
229
|
+
if (!(await driver.open())) {
|
|
230
|
+
failFrom(run, 0, START_STEP);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
let failed = await pass(run);
|
|
234
|
+
// A step that fails once can be timing (a select whose options load late, on a loaded machine): try the run
|
|
235
|
+
// ONE more time from a fresh page before it counts as a step the build could not do. A real breakage fails
|
|
236
|
+
// again. Never silently: the readings the retry records carry `retriedAt`, and the comparison lists them,
|
|
237
|
+
// because a fault that comes and goes is a fault. Readings already taken are kept.
|
|
238
|
+
const first = failed === undefined ? undefined : run[failed];
|
|
239
|
+
if (first !== undefined && (await driver.open())) {
|
|
240
|
+
retrying = stepLabel(first);
|
|
241
|
+
failed = await pass(run);
|
|
242
|
+
retrying = undefined;
|
|
243
|
+
}
|
|
244
|
+
if (failed !== undefined) {
|
|
245
|
+
const step = run[failed];
|
|
246
|
+
failFrom(run, failed + 1, step === undefined ? START_STEP : stepLabel(step));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** One step of one journey, as one build showed it. */
|
|
2
|
+
export interface StepReading {
|
|
3
|
+
/** The journey's position in the run. The same on every build, because every build gets one list. */
|
|
4
|
+
journey: number;
|
|
5
|
+
/** The step's position in its journey. */
|
|
6
|
+
step: number;
|
|
7
|
+
/** What was done to reach this step, in order, ending with this step's own action. */
|
|
8
|
+
actions: readonly string[];
|
|
9
|
+
/** The visible lines after the step. Absent when the step could not be done. */
|
|
10
|
+
lines?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export declare const DifferenceKind: {
|
|
13
|
+
/** The step was done on both builds, and they show different lines. */
|
|
14
|
+
readonly CHANGED: "changed";
|
|
15
|
+
/** The reference did the step, and the candidate could not. */
|
|
16
|
+
readonly UNREACHED: "unreached";
|
|
17
|
+
};
|
|
18
|
+
export type DifferenceKind = (typeof DifferenceKind)[keyof typeof DifferenceKind];
|
|
19
|
+
export interface Difference {
|
|
20
|
+
/** A hash of the difference's lines: the same lines give the same id on every run. */
|
|
21
|
+
id: string;
|
|
22
|
+
kind: DifferenceKind;
|
|
23
|
+
/** The actions that first reached it. */
|
|
24
|
+
actions: readonly string[];
|
|
25
|
+
/** Lines only the reference shows. */
|
|
26
|
+
removed: readonly string[];
|
|
27
|
+
/** Lines only the candidate shows. */
|
|
28
|
+
added: readonly string[];
|
|
29
|
+
/** At how many steps this same difference was seen. */
|
|
30
|
+
seen: number;
|
|
31
|
+
}
|
|
32
|
+
export interface Comparison {
|
|
33
|
+
differences: Difference[];
|
|
34
|
+
/** Steps both builds did, and were compared. */
|
|
35
|
+
compared: number;
|
|
36
|
+
/** Steps the reference did on only one of its two runs, so they were not compared. */
|
|
37
|
+
unstable: number;
|
|
38
|
+
/** Lines that differed but were ignored, because they also differed between the reference runs. */
|
|
39
|
+
ignored: number;
|
|
40
|
+
}
|
|
41
|
+
/** A short stable id for a difference: the hash of what it holds. */
|
|
42
|
+
export declare const differenceId: (content: unknown) => string;
|
|
43
|
+
/** Lines only in `a` (removed) and only in `b` (added). */
|
|
44
|
+
export declare function lineDiff(a: readonly string[], b: readonly string[]): {
|
|
45
|
+
removed: string[];
|
|
46
|
+
added: string[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The lines of `got` that differ from `ours`, with the noise removed. A line that differs between
|
|
50
|
+
* `ours` and `repeat` (two runs of the reference) is noise, and so is a line `alsoNoise` names.
|
|
51
|
+
* `ignored` counts the lines removed as noise.
|
|
52
|
+
*/
|
|
53
|
+
export declare function maskedDiff(ours: readonly string[], repeat: readonly string[], got: readonly string[], alsoNoise?: (line: string) => boolean): {
|
|
54
|
+
removed: string[];
|
|
55
|
+
added: string[];
|
|
56
|
+
ignored: number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Every difference between the candidate and the reference, in the order the reference reached it.
|
|
60
|
+
* `again` is the second run of the reference, which sets what is noise.
|
|
61
|
+
*/
|
|
62
|
+
export declare function compareReadings(reference: readonly StepReading[], again: readonly StepReading[], candidate: readonly StepReading[]): Comparison;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The differences between what two builds show, after the same actions.
|
|
3
|
+
*
|
|
4
|
+
* Each build gives one reading for each step of each journey: the visible lines after that step, or
|
|
5
|
+
* no lines when the step could not be done. The reference is read two times. A line that differs
|
|
6
|
+
* between those two runs changes with no change to the build (a clock, a random id), so it is
|
|
7
|
+
* ignored at that step. A step the reference did on only one of its runs is not compared at all.
|
|
8
|
+
*
|
|
9
|
+
* Differences with the same lines are one difference, with one id: a renamed header shows at every
|
|
10
|
+
* step after it, and a reader needs to see it once.
|
|
11
|
+
*
|
|
12
|
+
* A step the candidate could not do is a difference, unless the step before it in the same journey
|
|
13
|
+
* already showed a difference. That difference is the cause (the button was renamed, so the click
|
|
14
|
+
* on the old name found nothing), and a reader needs it once.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
export const DifferenceKind = {
|
|
18
|
+
/** The step was done on both builds, and they show different lines. */
|
|
19
|
+
CHANGED: 'changed',
|
|
20
|
+
/** The reference did the step, and the candidate could not. */
|
|
21
|
+
UNREACHED: 'unreached',
|
|
22
|
+
};
|
|
23
|
+
const ID_LENGTH = 12;
|
|
24
|
+
const HASH = 'sha256';
|
|
25
|
+
const HEX = 'hex';
|
|
26
|
+
/** A short stable id for a difference: the hash of what it holds. */
|
|
27
|
+
export const differenceId = (content) => createHash(HASH).update(JSON.stringify(content)).digest(HEX).slice(0, ID_LENGTH);
|
|
28
|
+
const keyOf = (r) => `${String(r.journey)}:${String(r.step)}`;
|
|
29
|
+
/** The runs of lines only in `a` (removed) and only in `b` (added), from a longest common subsequence. */
|
|
30
|
+
function hunks(a, b) {
|
|
31
|
+
// ponytail: O(n*m) table; a page is tens to a few hundred lines. Use Myers if pages reach thousands.
|
|
32
|
+
const table = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
33
|
+
const at = (i, j) => table[i]?.[j] ?? 0;
|
|
34
|
+
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
35
|
+
const row = table[i];
|
|
36
|
+
for (let j = b.length - 1; j >= 0 && row !== undefined; j -= 1) {
|
|
37
|
+
row[j] = a[i] === b[j] ? at(i + 1, j + 1) + 1 : Math.max(at(i + 1, j), at(i, j + 1));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const out = [];
|
|
41
|
+
let open = { removed: [], added: [] };
|
|
42
|
+
const close = () => {
|
|
43
|
+
if (open.removed.length > 0 || open.added.length > 0) {
|
|
44
|
+
out.push(open);
|
|
45
|
+
open = { removed: [], added: [] };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
let i = 0;
|
|
49
|
+
let j = 0;
|
|
50
|
+
while (i < a.length && j < b.length) {
|
|
51
|
+
if (a[i] === b[j]) {
|
|
52
|
+
close();
|
|
53
|
+
i += 1;
|
|
54
|
+
j += 1;
|
|
55
|
+
}
|
|
56
|
+
else if (at(i + 1, j) >= at(i, j + 1)) {
|
|
57
|
+
open.removed.push(a[i] ?? '');
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
open.added.push(b[j] ?? '');
|
|
62
|
+
j += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
open.removed.push(...a.slice(i));
|
|
66
|
+
open.added.push(...b.slice(j));
|
|
67
|
+
close();
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** Lines only in `a` (removed) and only in `b` (added). */
|
|
71
|
+
export function lineDiff(a, b) {
|
|
72
|
+
const all = hunks(a, b);
|
|
73
|
+
return { removed: all.flatMap((h) => h.removed), added: all.flatMap((h) => h.added) };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The lines of one hunk that are not noise. A reference line that also changed between the two
|
|
77
|
+
* reference runs is noise, and so is the candidate line in its place: a clock reads 10:01, 10:02 and
|
|
78
|
+
* 10:03 on three runs, and none of the three is a change to the build.
|
|
79
|
+
*
|
|
80
|
+
* A noise line the candidate shows in its place counts first, so a real change next to a counter
|
|
81
|
+
* in the same hunk is kept.
|
|
82
|
+
*
|
|
83
|
+
* ponytail: "in its place" is by count inside the hunk, so a real change next to a clock whose new
|
|
84
|
+
* value is not noise can still be ignored with it. Align by position if that is seen in practice.
|
|
85
|
+
*/
|
|
86
|
+
function unmasked(hunk, isNoise) {
|
|
87
|
+
const removed = hunk.removed.filter((line) => !isNoise(line));
|
|
88
|
+
const addedNoise = hunk.added.filter(isNoise).length;
|
|
89
|
+
let replaced = Math.max(0, hunk.removed.length - removed.length - addedNoise);
|
|
90
|
+
const added = hunk.added.filter((line) => {
|
|
91
|
+
if (isNoise(line)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (replaced > 0) {
|
|
95
|
+
replaced -= 1;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
return { removed, added };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The lines of `got` that differ from `ours`, with the noise removed. A line that differs between
|
|
104
|
+
* `ours` and `repeat` (two runs of the reference) is noise, and so is a line `alsoNoise` names.
|
|
105
|
+
* `ignored` counts the lines removed as noise.
|
|
106
|
+
*/
|
|
107
|
+
export function maskedDiff(ours, repeat, got, alsoNoise = () => false) {
|
|
108
|
+
const noise = lineDiff(ours, repeat);
|
|
109
|
+
const set = new Set([...noise.removed, ...noise.added]);
|
|
110
|
+
const masked = (line) => set.has(line) || alsoNoise(line);
|
|
111
|
+
const removed = [];
|
|
112
|
+
const added = [];
|
|
113
|
+
let ignored = 0;
|
|
114
|
+
for (const hunk of hunks(ours, got)) {
|
|
115
|
+
const kept = unmasked(hunk, masked);
|
|
116
|
+
removed.push(...kept.removed);
|
|
117
|
+
added.push(...kept.added);
|
|
118
|
+
ignored += hunk.removed.length + hunk.added.length - kept.removed.length - kept.added.length;
|
|
119
|
+
}
|
|
120
|
+
return { removed, added, ignored };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Every difference between the candidate and the reference, in the order the reference reached it.
|
|
124
|
+
* `again` is the second run of the reference, which sets what is noise.
|
|
125
|
+
*/
|
|
126
|
+
export function compareReadings(reference, again, candidate) {
|
|
127
|
+
const second = new Map(again.map((r) => [keyOf(r), r]));
|
|
128
|
+
const theirs = new Map(candidate.map((r) => [keyOf(r), r]));
|
|
129
|
+
const found = new Map();
|
|
130
|
+
const blocked = new Set();
|
|
131
|
+
/** Journeys whose last compared step showed a difference. */
|
|
132
|
+
const differed = new Set();
|
|
133
|
+
let compared = 0;
|
|
134
|
+
let unstable = 0;
|
|
135
|
+
let ignored = 0;
|
|
136
|
+
const note = (difference, content) => {
|
|
137
|
+
const id = differenceId(content);
|
|
138
|
+
const known = found.get(id);
|
|
139
|
+
if (known === undefined) {
|
|
140
|
+
found.set(id, { id, seen: 1, ...difference });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
known.seen += 1;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
for (const ours of reference) {
|
|
147
|
+
const repeat = second.get(keyOf(ours))?.lines;
|
|
148
|
+
if (ours.lines === undefined || blocked.has(ours.journey)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (repeat === undefined) {
|
|
152
|
+
unstable += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const got = theirs.get(keyOf(ours))?.lines;
|
|
156
|
+
if (got === undefined) {
|
|
157
|
+
// Only the first step: the steps after it were not tried, and that is the same one cause.
|
|
158
|
+
blocked.add(ours.journey);
|
|
159
|
+
if (differed.has(ours.journey)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const action = ours.actions.at(-1) ?? '';
|
|
163
|
+
note({ kind: DifferenceKind.UNREACHED, actions: ours.actions, removed: [], added: [] }, { unreached: action });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
compared += 1;
|
|
167
|
+
const { removed, added, ignored: skipped } = maskedDiff(ours.lines, repeat, got);
|
|
168
|
+
ignored += skipped;
|
|
169
|
+
if (0 === removed.length && 0 === added.length) {
|
|
170
|
+
differed.delete(ours.journey);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
differed.add(ours.journey);
|
|
174
|
+
note({ kind: DifferenceKind.CHANGED, actions: ours.actions, removed, added }, { removed, added });
|
|
175
|
+
}
|
|
176
|
+
return { differences: [...found.values()], compared, unstable, ignored };
|
|
177
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { compareReportJson } from './behaviour-report.js';
|
|
2
|
+
type CompareReport = ReturnType<typeof compareReportJson>;
|
|
3
|
+
export declare const REPORT_TITLE = "Reticle compare";
|
|
4
|
+
/** The report page for one comparison. */
|
|
5
|
+
export declare function compareReportHtml(report: CompareReport): string;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reticle compare --report <file>`: the comparison as one HTML page a person opens, shares or attaches to CI. It is
|
|
3
|
+
* built from the `--json` object, so the page and the JSON never disagree. It is self-contained: no script, no external
|
|
4
|
+
* style, no font. Every value is escaped, because the lines come from the pages under test: a page that shows
|
|
5
|
+
* `<script>` must not run it inside the report.
|
|
6
|
+
*/
|
|
7
|
+
import { CompareVerdict } from './behaviour-diff.js';
|
|
8
|
+
import { DifferenceKind } from './compare-diff.js';
|
|
9
|
+
export const REPORT_TITLE = 'Reticle compare';
|
|
10
|
+
const TXT = {
|
|
11
|
+
reference: 'Reference',
|
|
12
|
+
candidate: 'Candidate',
|
|
13
|
+
base: 'Base',
|
|
14
|
+
compared: 'sequences compared',
|
|
15
|
+
ignored: 'lines ignored (they also changed between two runs of the reference)',
|
|
16
|
+
unstable: 'not compared: the reference did not do them the same way twice',
|
|
17
|
+
referenceFailed: 'not compared: the reference could not do them',
|
|
18
|
+
steps: 'Steps',
|
|
19
|
+
onlyReference: 'Only the reference shows',
|
|
20
|
+
onlyCandidate: 'Only the candidate shows',
|
|
21
|
+
unreached: 'The candidate could not do this step',
|
|
22
|
+
same: 'No difference in what users see, in the sequences compared.',
|
|
23
|
+
invalid: 'This run proves nothing, so nothing was proved. Reason:',
|
|
24
|
+
generated: 'Generated by reticle compare. Nothing was installed in either app.',
|
|
25
|
+
};
|
|
26
|
+
const VERDICT_TEXT = {
|
|
27
|
+
[CompareVerdict.SAME]: 'SAME',
|
|
28
|
+
[CompareVerdict.DIFFERENT]: 'DIFFERENT',
|
|
29
|
+
[CompareVerdict.INVALID]: 'INVALID',
|
|
30
|
+
};
|
|
31
|
+
/** Design values, in one place (the report is outside the app's design system). */
|
|
32
|
+
const STYLE = `
|
|
33
|
+
:root { --bg:#fbfbfa; --fg:#1c1c1a; --muted:#6b6b66; --line:#e4e3df; --card:#ffffff;
|
|
34
|
+
--same:#1f7a45; --diff:#b42318; --invalid:#8a6d00; --add-bg:#e9f6ee; --rem-bg:#fdecec; --mono:ui-monospace,SFMono-Regular,Menlo,monospace; }
|
|
35
|
+
@media (prefers-color-scheme: dark) { :root { --bg:#141413; --fg:#ecebe7; --muted:#a3a29c; --line:#2c2c29; --card:#1c1c1a;
|
|
36
|
+
--same:#4cc27e; --diff:#ff7b6b; --invalid:#e0c04a; --add-bg:#16301f; --rem-bg:#3a1a17; } }
|
|
37
|
+
* { box-sizing:border-box } body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.5 system-ui,sans-serif }
|
|
38
|
+
main { max-width:960px; margin:0 auto; padding:24px 16px 48px }
|
|
39
|
+
h1 { font-size:20px; margin:0 0 4px } .verdict { font-weight:700; letter-spacing:.04em }
|
|
40
|
+
.SAME { color:var(--same) } .DIFFERENT { color:var(--diff) } .INVALID { color:var(--invalid) }
|
|
41
|
+
.urls, .counts { color:var(--muted); margin:4px 0 } .urls code { color:var(--fg); overflow-wrap:anywhere }
|
|
42
|
+
.card { background:var(--card); border:1px solid var(--line); border-radius:8px; padding:12px 14px; margin:14px 0 }
|
|
43
|
+
.card h2 { font-size:14px; margin:0 0 6px; color:var(--muted) }
|
|
44
|
+
ol.steps { margin:4px 0 8px 18px; padding:0 } pre { margin:4px 0; padding:8px; border-radius:6px; white-space:pre-wrap;
|
|
45
|
+
overflow-wrap:anywhere; font:13px/1.45 var(--mono) } .rem { background:var(--rem-bg) } .add { background:var(--add-bg) }
|
|
46
|
+
.side { font-size:13px; color:var(--muted); margin-top:6px } footer { color:var(--muted); font-size:13px; margin-top:28px }
|
|
47
|
+
`;
|
|
48
|
+
const esc = (s) => String(s)
|
|
49
|
+
.replaceAll('&', '&')
|
|
50
|
+
.replaceAll('<', '<')
|
|
51
|
+
.replaceAll('>', '>')
|
|
52
|
+
.replaceAll('"', '"')
|
|
53
|
+
.replaceAll("'", ''');
|
|
54
|
+
function differenceCard(d, n) {
|
|
55
|
+
const steps = `<ol class="steps">${d.actions.map((a) => `<li>${esc(a)}</li>`).join('')}</ol>`;
|
|
56
|
+
const seen = `Seen in ${String(d.seen)} sequence${1 === d.seen ? '' : 's'}`;
|
|
57
|
+
if (d.kind === DifferenceKind.UNREACHED) {
|
|
58
|
+
return `<section class="card"><h2>#${String(n)} · ${esc(seen)}</h2><div class="side">${TXT.steps}</div>${steps}<div class="side">${TXT.unreached}</div><pre class="rem">${esc(d.failedAt ?? '')}</pre></section>`;
|
|
59
|
+
}
|
|
60
|
+
const side = (label, cls, lines) => 0 === lines.length
|
|
61
|
+
? ''
|
|
62
|
+
: `<div class="side">${label}</div><pre class="${cls}">${esc(lines.join('\n'))}</pre>`;
|
|
63
|
+
return `<section class="card"><h2>#${String(n)} · ${esc(seen)}</h2><div class="side">${TXT.steps}</div>${steps}${side(TXT.onlyReference, 'rem', d.removed)}${side(TXT.onlyCandidate, 'add', d.added)}</section>`;
|
|
64
|
+
}
|
|
65
|
+
/** The report page for one comparison. */
|
|
66
|
+
export function compareReportHtml(report) {
|
|
67
|
+
const verdict = VERDICT_TEXT[report.verdict] ?? String(report.verdict);
|
|
68
|
+
const urls = [
|
|
69
|
+
`${TXT.reference}: <code>${esc(report.reference)}</code>`,
|
|
70
|
+
`${TXT.candidate}: <code>${esc(report.candidate)}</code>`,
|
|
71
|
+
...(report.base === undefined ? [] : [`${TXT.base}: <code>${esc(report.base)}</code>`]),
|
|
72
|
+
];
|
|
73
|
+
const counts = [
|
|
74
|
+
`<b>${String(report.compared)}</b> of ${String(report.sequences)} ${TXT.compared}`,
|
|
75
|
+
`<b>${String(report.ignored)}</b> ${TXT.ignored}`,
|
|
76
|
+
...(report.unstable > 0 ? [`<b>${String(report.unstable)}</b> ${TXT.unstable}`] : []),
|
|
77
|
+
...(report.referenceFailed > 0
|
|
78
|
+
? [`<b>${String(report.referenceFailed)}</b> ${TXT.referenceFailed}`]
|
|
79
|
+
: []),
|
|
80
|
+
];
|
|
81
|
+
let body;
|
|
82
|
+
if (report.verdict === CompareVerdict.INVALID) {
|
|
83
|
+
body = `<section class="card"><p>${TXT.invalid}</p><pre>${esc(report.reason ?? '')}</pre></section>`;
|
|
84
|
+
}
|
|
85
|
+
else if (0 === report.differences.length) {
|
|
86
|
+
body = `<section class="card"><p>${TXT.same}</p></section>`;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
body = report.differences.map((d, i) => differenceCard(d, i + 1)).join('\n');
|
|
90
|
+
}
|
|
91
|
+
return [
|
|
92
|
+
'<!doctype html>',
|
|
93
|
+
'<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">',
|
|
94
|
+
`<title>${REPORT_TITLE}: ${verdict}</title><style>${STYLE}</style></head><body><main>`,
|
|
95
|
+
`<h1>${REPORT_TITLE}: <span class="verdict ${verdict}">${verdict}</span></h1>`,
|
|
96
|
+
...urls.map((u) => `<p class="urls">${u}</p>`),
|
|
97
|
+
`<p class="counts">${counts.join(' · ')}</p>`,
|
|
98
|
+
body,
|
|
99
|
+
`<footer>${TXT.generated}</footer>`,
|
|
100
|
+
'</main></body></html>',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CompareResult } from './compare-run.js';
|
|
2
|
+
/** The version of the `--results-json` file this writes. */
|
|
3
|
+
export declare const COMPARE_SCHEMA_VERSION = 1;
|
|
4
|
+
interface Urls {
|
|
5
|
+
reference: string;
|
|
6
|
+
candidate: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function compareExit(result: CompareResult): number;
|
|
9
|
+
export declare function compareLines(urls: Urls, result: CompareResult): string[];
|
|
10
|
+
/** The `--results-json` file. Every count the text gives is here too, so nothing is dropped silently. */
|
|
11
|
+
export declare function compareJson(urls: Urls, result: CompareResult): Urls & CompareResult & {
|
|
12
|
+
schemaVersion: number;
|
|
13
|
+
identical: boolean;
|
|
14
|
+
};
|
|
15
|
+
export {};
|