@reticlehq/server 3.9.0 → 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/SKILL.md +13 -12
- 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 -187
- 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/cli-doctor.js +1 -1
- package/dist/command/cli/cli-flow-commands.js +2 -9
- package/dist/command/cli/cli-kill.js +1 -1
- 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 +17 -15
- package/dist/command/cli/cli-parse.js +44 -27
- 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 +58 -29
- package/dist/command/cli/cli-verify.d.ts +11 -2
- package/dist/command/cli/cli-verify.js +27 -8
- 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/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor-repairs.js +1 -1
- 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/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
- package/dist/command/cli/ports/port-holder.d.ts +9 -0
- package/dist/command/cli/ports/port-holder.js +27 -3
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +22 -0
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +7 -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 +46 -41
- 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 +12 -1
- package/dist/command/daemon/daemon.js +35 -4
- package/dist/command/daemon/lifetime/death-cause.js +21 -2
- package/dist/command/daemon/lifetime/heartbeat.js +7 -2
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- 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-runtime.js +6 -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 +6 -3
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/version-skew.js +1 -1
- 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/visual/visual-tools.js +66 -19
- package/dist/index.js +7 -4
- 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/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- 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 +44 -0
- 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 +19 -3
- package/dist/language/flows/recording/tape/recordings.js +71 -5
- package/dist/language/flows/replay.d.ts +19 -5
- package/dist/language/flows/replay.js +99 -7
- 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/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/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/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 +28 -9
- 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 +22 -20
- package/dist/portal/input/real-input.js +41 -31
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +75 -18
- package/dist/portal/pool/browser-pool.d.ts +11 -29
- package/dist/portal/pool/browser-pool.js +35 -17
- package/dist/portal/pool/context-permissions.d.ts +17 -3
- package/dist/portal/pool/context-permissions.js +36 -5
- 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 +44 -17
- package/dist/portal/pool/lease-page-error.js +147 -5
- package/dist/portal/pool/page-error-journal.d.ts +7 -1
- package/dist/portal/pool/page-error-journal.js +27 -0
- package/dist/portal/pool/playwright-launcher.js +51 -8
- package/dist/portal/pool/pool-contract.d.ts +27 -1
- package/dist/portal/pool/pool-lease.d.ts +36 -1
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +23 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
- package/dist/portal/session/dev-server/dev-command.js +61 -15
- 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 +67 -25
- package/dist/portal/session/no-session-next-action.d.ts +2 -0
- package/dist/portal/session/no-session-next-action.js +12 -3
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +38 -3
- 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 +174 -63
- package/dist/start-wiring.d.ts +2 -1
- package/dist/start-wiring.js +2 -1
- package/dist/surface/mcp/mcp.js +3 -2
- 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 +1 -1
- 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/after-match.d.ts +4 -1
- package/dist/surface/tools/act/after-match.js +8 -1
- 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 +15 -1
- package/dist/surface/tools/act/resolve-target.js +24 -1
- package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
- package/dist/surface/tools/act/sequence-grade.js +37 -26
- 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 +78 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +74 -86
- 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 +16 -37
- package/dist/surface/tools/browser-tools.js +12 -6
- 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 +9 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +91 -21
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +3 -2
- 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 +35 -1
- package/dist/surface/tools/lease-hint.js +42 -0
- package/dist/surface/tools/lease-readiness.d.ts +26 -0
- package/dist/surface/tools/lease-readiness.js +79 -0
- package/dist/surface/tools/lease-seed.d.ts +14 -1
- package/dist/surface/tools/lease-seed.js +35 -3
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +62 -110
- 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/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +6 -0
- package/dist/surface/tools/tool-kit.js +2 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +28 -5
- 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 +1 -0
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +8 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- 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 +1 -1
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/license.mdx +9 -7
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- 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 +33 -23
- package/docs/deploy-checks.md +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +2 -2
- package/docs/environment.mdx +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 +1 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +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 +2 -2
- 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 +13 -13
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -6,11 +6,22 @@
|
|
|
6
6
|
* rejected. So every call reads the CURRENT session, and anything that may have navigated waits for
|
|
7
7
|
* the successor before it answers — the same wait `navigate { reload }` does.
|
|
8
8
|
*/
|
|
9
|
-
import { ActionType, ReticleCommand, ReticleTool, SnapshotMode, asRecord, asString, } from '@reticlehq/core';
|
|
9
|
+
import { ActionType, EventType, ReticleCommand, ReticleTool, SnapshotMode, asNumber, asRecord, asString, } from '@reticlehq/core';
|
|
10
10
|
import { RELOAD_RECONNECT_TIMEOUT_MS, waitForReconnect, } from '../../portal/session/session-reconnect.js';
|
|
11
11
|
import { isSessionReplacedError } from '../../portal/session/facts/session-replaced.js';
|
|
12
|
+
import { FIELD_ATTRS } from './explorer.js';
|
|
13
|
+
/** How long a press waits for the requests it started to answer. */
|
|
14
|
+
export const PENDING_WAIT_MS = 10_000;
|
|
15
|
+
const PENDING_POLL_MS = 100;
|
|
16
|
+
/** A request that was sent and has not answered: a `net.pending` with no `net.request` of its id. */
|
|
17
|
+
function inFlight(events) {
|
|
18
|
+
const answered = new Set(events.filter((e) => EventType.NET_REQUEST === e.type).map((e) => asString(e.data['id'])));
|
|
19
|
+
return events.some((e) => EventType.NET_PENDING === e.type && !answered.has(asString(e.data['id'])));
|
|
20
|
+
}
|
|
12
21
|
export function sessionPort(input) {
|
|
13
22
|
let current = input.start;
|
|
23
|
+
/** The URL the page had before the last action, for `back`. */
|
|
24
|
+
let lastUrl;
|
|
14
25
|
/** Wait for the document that replaced `before`. False when none arrived in time. */
|
|
15
26
|
const successor = async (before) => {
|
|
16
27
|
const back = await waitForReconnect({
|
|
@@ -21,77 +32,135 @@ export function sessionPort(input) {
|
|
|
21
32
|
sleep: input.sleep,
|
|
22
33
|
});
|
|
23
34
|
const next = input.sessions.get(before.id);
|
|
24
|
-
if (!back || next === undefined)
|
|
35
|
+
if (!back || next === undefined) {
|
|
25
36
|
return false;
|
|
37
|
+
}
|
|
26
38
|
current = next;
|
|
27
39
|
return true;
|
|
28
40
|
};
|
|
29
|
-
const
|
|
41
|
+
const read = async (mode) => {
|
|
30
42
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
31
43
|
try {
|
|
32
|
-
const res = await current.command(ReticleCommand.SNAPSHOT, {
|
|
33
|
-
mode: SnapshotMode.INTERACTIVE,
|
|
34
|
-
});
|
|
44
|
+
const res = await current.command(ReticleCommand.SNAPSHOT, { mode });
|
|
35
45
|
const body = asRecord(res.result);
|
|
36
46
|
const route = asString(asRecord(body['status'])['route']);
|
|
37
47
|
return { tree: asString(body['tree']) ?? '', ...(route === undefined ? {} : { route }) };
|
|
38
48
|
}
|
|
39
49
|
catch (error) {
|
|
40
50
|
// A click that followed a real link leaves the next read on a dead document.
|
|
41
|
-
if (!isSessionReplacedError(error) || !(await successor(current)))
|
|
51
|
+
if (!isSessionReplacedError(error) || !(await successor(current))) {
|
|
42
52
|
break;
|
|
53
|
+
}
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
return { tree: '' };
|
|
46
57
|
};
|
|
58
|
+
/** Send a command that replaces the document, then wait for the new one and let it settle. */
|
|
59
|
+
const load = async (name, args) => {
|
|
60
|
+
const was = current;
|
|
61
|
+
try {
|
|
62
|
+
const res = await was.command(name, args);
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (!isSessionReplacedError(error)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!(await successor(was))) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
await input.sleep(input.settleMs);
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
47
78
|
return {
|
|
48
79
|
current: () => current,
|
|
49
|
-
look,
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
look: () => read(SnapshotMode.INTERACTIVE),
|
|
81
|
+
lookFull: () => read(SnapshotMode.FULL),
|
|
82
|
+
reset: () => load(ReticleCommand.NAVIGATE, { url: input.startUrl }),
|
|
83
|
+
reload: () => load(ReticleCommand.REFRESH, {}),
|
|
84
|
+
// ponytail: a load of the URL the page had before the last action, not the browser's history
|
|
85
|
+
// back, which no page command sends. An app that keeps state in its history entries comes back
|
|
86
|
+
// fresh here; add a history command if a finding needs it.
|
|
87
|
+
back: () => lastUrl === undefined
|
|
88
|
+
? Promise.resolve(false)
|
|
89
|
+
: load(ReticleCommand.NAVIGATE, { url: lastUrl }),
|
|
90
|
+
async fieldAttrs(control) {
|
|
91
|
+
const [, role, name] = /^(\S+)(?:\s+"((?:[^"\\]|\\.)*)")?/.exec(control) ?? [];
|
|
92
|
+
if (role === undefined) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
52
95
|
try {
|
|
53
|
-
const res = await
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
const res = await current.command(ReticleCommand.QUERY, {
|
|
97
|
+
role,
|
|
98
|
+
...(name === undefined ? {} : { name }),
|
|
99
|
+
attrs: [...FIELD_ATTRS],
|
|
100
|
+
});
|
|
101
|
+
const first = asRecord(res.result)['elements'];
|
|
102
|
+
const attrs = Array.isArray(first) ? asRecord(asRecord(first[0])['attrs']) : {};
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
105
|
+
if ('string' === typeof value) {
|
|
106
|
+
out[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
56
110
|
}
|
|
57
|
-
catch
|
|
58
|
-
|
|
59
|
-
return false;
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
60
113
|
}
|
|
61
|
-
if (!(await successor(before)))
|
|
62
|
-
return false;
|
|
63
|
-
await input.sleep(input.settleMs);
|
|
64
|
-
return true;
|
|
65
114
|
},
|
|
66
|
-
async act(ref, action, value) {
|
|
115
|
+
async act(ref, action, value, opts) {
|
|
67
116
|
const session = current;
|
|
117
|
+
lastUrl = session.url;
|
|
68
118
|
const since = session.elapsed();
|
|
69
119
|
session.beginAction?.(ReticleTool.CRAWL, { ref, action });
|
|
70
120
|
let ok = false;
|
|
71
121
|
let replaced = false;
|
|
122
|
+
let source;
|
|
72
123
|
try {
|
|
73
124
|
const res = await session.command(ReticleCommand.ACT, {
|
|
74
125
|
ref,
|
|
75
126
|
action: 'fill' === action ? ActionType.FILL : ActionType.CLICK,
|
|
76
127
|
args: value === undefined ? {} : { value },
|
|
77
128
|
});
|
|
78
|
-
|
|
129
|
+
const body = asRecord(res.result);
|
|
130
|
+
ok = res.ok && false !== body['dispatched'];
|
|
131
|
+
const src = asRecord(body['source']);
|
|
132
|
+
const file = asString(src['file']);
|
|
133
|
+
const line = asNumber(src['line']);
|
|
134
|
+
source = file === undefined || line === undefined ? undefined : { file, line };
|
|
79
135
|
}
|
|
80
136
|
catch (error) {
|
|
81
137
|
// Following a real link replaces the document mid-command: the click WORKED.
|
|
82
|
-
if (!isSessionReplacedError(error))
|
|
138
|
+
if (!isSessionReplacedError(error)) {
|
|
83
139
|
throw error;
|
|
140
|
+
}
|
|
84
141
|
replaced = true;
|
|
85
142
|
ok = true;
|
|
86
143
|
}
|
|
87
144
|
finally {
|
|
88
145
|
session.finishAction?.();
|
|
89
146
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
147
|
+
let events = session.eventsSince(since);
|
|
148
|
+
if (false !== opts?.settle) {
|
|
149
|
+
await input.sleep(input.settleMs);
|
|
150
|
+
events = session.eventsSince(since);
|
|
151
|
+
// A slow write answers long after the page settles. Wait for the requests this press
|
|
152
|
+
// started, up to a bound, so its answer, its error and a second copy are in its events.
|
|
153
|
+
const poll = Math.max(input.settleMs, PENDING_POLL_MS);
|
|
154
|
+
for (let i = 0; i < Math.floor(PENDING_WAIT_MS / poll) && inFlight(events); i += 1) {
|
|
155
|
+
await input.sleep(poll);
|
|
156
|
+
events = session.eventsSince(since);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const found = source === undefined ? {} : { source };
|
|
160
|
+
if (replaced && (await successor(session))) {
|
|
161
|
+
return { ok, events: [...events, ...current.eventsSince(0)], ...found };
|
|
162
|
+
}
|
|
163
|
+
return { ok, events, ...found };
|
|
95
164
|
},
|
|
96
165
|
...(input.setMocks === undefined
|
|
97
166
|
? {}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check a run's final answer against a value read from the app when the grade runs. Pure.
|
|
3
|
+
*
|
|
4
|
+
* A fixed answer string goes stale when the data changes. Here the expected answer is the output of
|
|
5
|
+
* a declared source (a database query, an endpoint), read at grade time. A source with no rows means
|
|
6
|
+
* that the task cannot be done, and the answer must be one of the `none` words (default `N/A`).
|
|
7
|
+
*
|
|
8
|
+
* Match kinds. Text is folded first: Unicode normal form, lower case, spacing, quotes and a final
|
|
9
|
+
* full stop.
|
|
10
|
+
* - `exact`: the answer is the one expected value. With more than one row, it is `set`.
|
|
11
|
+
* - `number`: the answer is one number, and nothing else. `$`, `%` and digit-group commas are
|
|
12
|
+
* removed. It equals the first value within 0.005.
|
|
13
|
+
* - `set`: the answer is a list, split on commas, semicolons and new lines, or also on " and ". It
|
|
14
|
+
* holds each expected value one time and nothing more. Order is not checked.
|
|
15
|
+
* - `includes`: the answer contains each expected value. A long answer that names every record
|
|
16
|
+
* also passes, so use it only where the answer cannot list all choices.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
export declare const AnswerMatch: {
|
|
20
|
+
readonly EXACT: "exact";
|
|
21
|
+
readonly NUMBER: "number";
|
|
22
|
+
readonly SET: "set";
|
|
23
|
+
readonly INCLUDES: "includes";
|
|
24
|
+
};
|
|
25
|
+
export type AnswerMatch = (typeof AnswerMatch)[keyof typeof AnswerMatch];
|
|
26
|
+
/** The default answer for a task that cannot be done. */
|
|
27
|
+
export declare const DEFAULT_NONE = "N/A";
|
|
28
|
+
export declare const AnswerSpecSchema: z.ZodObject<{
|
|
29
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
30
|
+
kind: z.ZodLiteral<"command">;
|
|
31
|
+
label: z.ZodString;
|
|
32
|
+
command: z.ZodString;
|
|
33
|
+
container: z.ZodOptional<z.ZodString>;
|
|
34
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
35
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
36
|
+
kind: z.ZodLiteral<"http">;
|
|
37
|
+
label: z.ZodString;
|
|
38
|
+
url: z.ZodString;
|
|
39
|
+
path: z.ZodOptional<z.ZodString>;
|
|
40
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
41
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
42
|
+
kind: z.ZodLiteral<"file">;
|
|
43
|
+
label: z.ZodString;
|
|
44
|
+
path: z.ZodString;
|
|
45
|
+
at: z.ZodOptional<z.ZodString>;
|
|
46
|
+
container: z.ZodOptional<z.ZodString>;
|
|
47
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
48
|
+
}, z.core.$strict>], "kind">;
|
|
49
|
+
match: z.ZodDefault<z.ZodEnum<{
|
|
50
|
+
number: "number";
|
|
51
|
+
includes: "includes";
|
|
52
|
+
set: "set";
|
|
53
|
+
exact: "exact";
|
|
54
|
+
}>>;
|
|
55
|
+
none: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
56
|
+
}, z.core.$strict>;
|
|
57
|
+
export type AnswerSpec = z.infer<typeof AnswerSpecSchema>;
|
|
58
|
+
/** The `--expect` file of `reticle grade judge`. */
|
|
59
|
+
export declare const ExpectSchema: z.ZodObject<{
|
|
60
|
+
answer: z.ZodObject<{
|
|
61
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
62
|
+
kind: z.ZodLiteral<"command">;
|
|
63
|
+
label: z.ZodString;
|
|
64
|
+
command: z.ZodString;
|
|
65
|
+
container: z.ZodOptional<z.ZodString>;
|
|
66
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
67
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
68
|
+
kind: z.ZodLiteral<"http">;
|
|
69
|
+
label: z.ZodString;
|
|
70
|
+
url: z.ZodString;
|
|
71
|
+
path: z.ZodOptional<z.ZodString>;
|
|
72
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
73
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
74
|
+
kind: z.ZodLiteral<"file">;
|
|
75
|
+
label: z.ZodString;
|
|
76
|
+
path: z.ZodString;
|
|
77
|
+
at: z.ZodOptional<z.ZodString>;
|
|
78
|
+
container: z.ZodOptional<z.ZodString>;
|
|
79
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
80
|
+
}, z.core.$strict>], "kind">;
|
|
81
|
+
match: z.ZodDefault<z.ZodEnum<{
|
|
82
|
+
number: "number";
|
|
83
|
+
includes: "includes";
|
|
84
|
+
set: "set";
|
|
85
|
+
exact: "exact";
|
|
86
|
+
}>>;
|
|
87
|
+
none: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
88
|
+
}, z.core.$strict>;
|
|
89
|
+
}, z.core.$strict>;
|
|
90
|
+
export interface AnswerCheck {
|
|
91
|
+
ok: boolean;
|
|
92
|
+
/** The expected answer, as text. */
|
|
93
|
+
expected: string;
|
|
94
|
+
/** Why the answer did not match. */
|
|
95
|
+
reason?: string;
|
|
96
|
+
}
|
|
97
|
+
export declare function foldText(text: string): string;
|
|
98
|
+
/** Check `answer` against the rows the answer source gave at grade time. */
|
|
99
|
+
export declare function checkAnswer(rows: readonly string[], answer: string | undefined, spec: Pick<AnswerSpec, 'match' | 'none'>): AnswerCheck;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check a run's final answer against a value read from the app when the grade runs. Pure.
|
|
3
|
+
*
|
|
4
|
+
* A fixed answer string goes stale when the data changes. Here the expected answer is the output of
|
|
5
|
+
* a declared source (a database query, an endpoint), read at grade time. A source with no rows means
|
|
6
|
+
* that the task cannot be done, and the answer must be one of the `none` words (default `N/A`).
|
|
7
|
+
*
|
|
8
|
+
* Match kinds. Text is folded first: Unicode normal form, lower case, spacing, quotes and a final
|
|
9
|
+
* full stop.
|
|
10
|
+
* - `exact`: the answer is the one expected value. With more than one row, it is `set`.
|
|
11
|
+
* - `number`: the answer is one number, and nothing else. `$`, `%` and digit-group commas are
|
|
12
|
+
* removed. It equals the first value within 0.005.
|
|
13
|
+
* - `set`: the answer is a list, split on commas, semicolons and new lines, or also on " and ". It
|
|
14
|
+
* holds each expected value one time and nothing more. Order is not checked.
|
|
15
|
+
* - `includes`: the answer contains each expected value. A long answer that names every record
|
|
16
|
+
* also passes, so use it only where the answer cannot list all choices.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { StateSourceSchema } from '../compare/outside/state-sources.js';
|
|
20
|
+
export const AnswerMatch = {
|
|
21
|
+
EXACT: 'exact',
|
|
22
|
+
NUMBER: 'number',
|
|
23
|
+
SET: 'set',
|
|
24
|
+
INCLUDES: 'includes',
|
|
25
|
+
};
|
|
26
|
+
/** The default answer for a task that cannot be done. */
|
|
27
|
+
export const DEFAULT_NONE = 'N/A';
|
|
28
|
+
/** Two numbers closer than this are equal: money has two decimals. */
|
|
29
|
+
const NUMBER_TOLERANCE = 0.005;
|
|
30
|
+
export const AnswerSpecSchema = z.strictObject({
|
|
31
|
+
source: StateSourceSchema,
|
|
32
|
+
match: z
|
|
33
|
+
.enum([AnswerMatch.EXACT, AnswerMatch.NUMBER, AnswerMatch.SET, AnswerMatch.INCLUDES])
|
|
34
|
+
.default(AnswerMatch.EXACT),
|
|
35
|
+
none: z.array(z.string().min(1)).min(1).default([DEFAULT_NONE]),
|
|
36
|
+
});
|
|
37
|
+
/** The `--expect` file of `reticle grade judge`. */
|
|
38
|
+
export const ExpectSchema = z.strictObject({
|
|
39
|
+
answer: AnswerSpecSchema,
|
|
40
|
+
});
|
|
41
|
+
const COLUMN_BREAK = /\t/g;
|
|
42
|
+
const LIST_BREAK = /\s*[,;\n]\s*/;
|
|
43
|
+
const LIST_BREAK_AND = /\s*(?:[,;\n]|\band\b)\s*/;
|
|
44
|
+
const NUMBER_NOISE = /[$%\s]|,(?=\d{3}\b)/g;
|
|
45
|
+
const NUMBER_ONLY = /^[-+]?(?:\d+\.?\d*|\.\d+)$/;
|
|
46
|
+
export function foldText(text) {
|
|
47
|
+
return text
|
|
48
|
+
.normalize('NFKC')
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim()
|
|
52
|
+
.replace(/^["'`]+|["'`]+$/g, '')
|
|
53
|
+
.replace(/\.$/, '')
|
|
54
|
+
.trim();
|
|
55
|
+
}
|
|
56
|
+
/** The number the text is, or undefined when it is not exactly one number. */
|
|
57
|
+
function numberOf(text) {
|
|
58
|
+
const bare = foldText(text).replace(NUMBER_NOISE, '');
|
|
59
|
+
return NUMBER_ONLY.test(bare) ? Number(bare) : undefined;
|
|
60
|
+
}
|
|
61
|
+
const items = (text, breaks) => text
|
|
62
|
+
.split(breaks)
|
|
63
|
+
.map(foldText)
|
|
64
|
+
.filter((item) => '' !== item);
|
|
65
|
+
/** A list split on commas, or also on "and": a value such as "Black and White Tee" keeps its "and". */
|
|
66
|
+
const sameList = (values, answer) => sameSet(values, items(answer, LIST_BREAK)) || sameSet(values, items(answer, LIST_BREAK_AND));
|
|
67
|
+
function sameSet(expected, got) {
|
|
68
|
+
if (expected.length !== got.length) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const left = [...got];
|
|
72
|
+
return expected.every((value) => {
|
|
73
|
+
const at = left.indexOf(value);
|
|
74
|
+
if (at < 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
left.splice(at, 1);
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Check `answer` against the rows the answer source gave at grade time. */
|
|
82
|
+
export function checkAnswer(rows, answer, spec) {
|
|
83
|
+
const values = rows.map((row) => foldText(row.replace(COLUMN_BREAK, ' '))).filter((v) => v);
|
|
84
|
+
const expected = 0 === values.length ? (spec.none[0] ?? DEFAULT_NONE) : values.join(', ');
|
|
85
|
+
if (answer === undefined) {
|
|
86
|
+
return { ok: false, expected, reason: 'no answer was given' };
|
|
87
|
+
}
|
|
88
|
+
const got = foldText(answer);
|
|
89
|
+
if (0 === values.length) {
|
|
90
|
+
const ok = spec.none.some((word) => foldText(word) === got);
|
|
91
|
+
return ok ? { ok, expected } : { ok, expected, reason: 'the task cannot be done' };
|
|
92
|
+
}
|
|
93
|
+
let ok;
|
|
94
|
+
switch (spec.match) {
|
|
95
|
+
case AnswerMatch.NUMBER: {
|
|
96
|
+
const want = numberOf(values[0] ?? '');
|
|
97
|
+
const have = numberOf(answer);
|
|
98
|
+
ok = want !== undefined && have !== undefined && Math.abs(want - have) < NUMBER_TOLERANCE;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case AnswerMatch.INCLUDES:
|
|
102
|
+
ok = values.every((value) => got.includes(value));
|
|
103
|
+
break;
|
|
104
|
+
case AnswerMatch.SET:
|
|
105
|
+
ok = sameList(values, answer);
|
|
106
|
+
break;
|
|
107
|
+
case AnswerMatch.EXACT:
|
|
108
|
+
ok = 1 === values.length ? values[0] === got : sameList(values, answer);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
return ok ? { ok, expected } : { ok, expected, reason: `the answer is not ${expected}` };
|
|
112
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Noise by cell, not by line. Pure.
|
|
3
|
+
*
|
|
4
|
+
* An app writes the time of a run into a row (`updated_at`). The two correct runs then differ in that
|
|
5
|
+
* cell, and the line rule ignores those two lines. But the candidate writes a third time, so its
|
|
6
|
+
* line is new, and a correct run failed. Here, for each row (table and id) that differs between the
|
|
7
|
+
* two correct runs, the cells that differ are noise. A removed line and an added line of the same row
|
|
8
|
+
* that differ only in noise cells are not a difference. A cell the two correct runs agree on (a date
|
|
9
|
+
* the task sets) is never noise.
|
|
10
|
+
*/
|
|
11
|
+
import { type Row } from './grade-renumber.js';
|
|
12
|
+
export interface NoiseCells {
|
|
13
|
+
/** The reference's own line for each noisy row. */
|
|
14
|
+
reference: Map<string, Row>;
|
|
15
|
+
/** The cells that differ, for each noisy row. */
|
|
16
|
+
cells: Map<string, Set<number>>;
|
|
17
|
+
}
|
|
18
|
+
/** For each row the two correct runs wrote differently, the cells that differ. */
|
|
19
|
+
export declare function noiseCells(referenceOnly: readonly string[], againOnly: readonly string[]): NoiseCells;
|
|
20
|
+
/**
|
|
21
|
+
* The added lines less those that differ from the reference's line of the same row only in noise
|
|
22
|
+
* cells. The reference's line is already noise, so no removed line pairs with it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function withoutNoiseCells(added: readonly string[], noise: NoiseCells): {
|
|
25
|
+
added: string[];
|
|
26
|
+
ignored: number;
|
|
27
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Noise by cell, not by line. Pure.
|
|
3
|
+
*
|
|
4
|
+
* An app writes the time of a run into a row (`updated_at`). The two correct runs then differ in that
|
|
5
|
+
* cell, and the line rule ignores those two lines. But the candidate writes a third time, so its
|
|
6
|
+
* line is new, and a correct run failed. Here, for each row (table and id) that differs between the
|
|
7
|
+
* two correct runs, the cells that differ are noise. A removed line and an added line of the same row
|
|
8
|
+
* that differ only in noise cells are not a difference. A cell the two correct runs agree on (a date
|
|
9
|
+
* the task sets) is never noise.
|
|
10
|
+
*/
|
|
11
|
+
import { parse } from './grade-renumber.js';
|
|
12
|
+
const rowKey = (row) => `${row.table}\t${row.id}`;
|
|
13
|
+
/** The cells that differ between the same row of two lines. Undefined when the rows differ in shape. */
|
|
14
|
+
function changedCells(a, b) {
|
|
15
|
+
if (a.cells.length !== b.cells.length) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return a.cells.flatMap((cell, i) => (cell === b.cells[i] ? [] : [i]));
|
|
19
|
+
}
|
|
20
|
+
/** For each row the two correct runs wrote differently, the cells that differ. */
|
|
21
|
+
export function noiseCells(referenceOnly, againOnly) {
|
|
22
|
+
const again = new Map();
|
|
23
|
+
for (const line of againOnly) {
|
|
24
|
+
const row = parse(line);
|
|
25
|
+
if (row !== undefined) {
|
|
26
|
+
again.set(rowKey(row), row);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const out = { reference: new Map(), cells: new Map() };
|
|
30
|
+
for (const line of referenceOnly) {
|
|
31
|
+
const row = parse(line);
|
|
32
|
+
const other = row === undefined ? undefined : again.get(rowKey(row));
|
|
33
|
+
const cells = row === undefined || other === undefined ? undefined : changedCells(row, other);
|
|
34
|
+
if (row !== undefined && cells !== undefined) {
|
|
35
|
+
out.reference.set(rowKey(row), row);
|
|
36
|
+
out.cells.set(rowKey(row), new Set(cells));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The added lines less those that differ from the reference's line of the same row only in noise
|
|
43
|
+
* cells. The reference's line is already noise, so no removed line pairs with it.
|
|
44
|
+
*/
|
|
45
|
+
export function withoutNoiseCells(added, noise) {
|
|
46
|
+
const kept = added.filter((line) => {
|
|
47
|
+
const row = parse(line);
|
|
48
|
+
const key = row === undefined ? '' : rowKey(row);
|
|
49
|
+
const ours = noise.reference.get(key);
|
|
50
|
+
const allowed = noise.cells.get(key);
|
|
51
|
+
const cells = row === undefined || ours === undefined ? undefined : changedCells(ours, row);
|
|
52
|
+
return cells === undefined || allowed === undefined || !cells.every((c) => allowed.has(c));
|
|
53
|
+
});
|
|
54
|
+
return { added: kept, ignored: added.length - kept.length };
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Row {
|
|
2
|
+
/** `state <label>`: the table. */
|
|
3
|
+
table: string;
|
|
4
|
+
id: string;
|
|
5
|
+
cells: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function parse(line: string): Row | undefined;
|
|
8
|
+
export declare function renumber(removed: readonly string[], added: readonly string[], reference: readonly string[], candidate: readonly string[]): {
|
|
9
|
+
removed: string[];
|
|
10
|
+
added: string[];
|
|
11
|
+
renumbered: number;
|
|
12
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match rows that an app deleted and wrote again with a new id. Pure.
|
|
3
|
+
*
|
|
4
|
+
* Many apps save a record by deleting its child rows and inserting them again. The rows hold the
|
|
5
|
+
* same values, but the database gives them new auto-increment ids. A run that saves a record two
|
|
6
|
+
* times then differs from a run that saves it one time, on every child row, although the data
|
|
7
|
+
* means the same. That made a correct run fail.
|
|
8
|
+
*
|
|
9
|
+
* A text row is `state <label> <id>\t<cell>\t...`. A removed row and an added row of the same
|
|
10
|
+
* table are one record when all their cells after the id are equal, after each id already matched
|
|
11
|
+
* is replaced by its new id. The match records old id -> new id, so a row that refers to the
|
|
12
|
+
* old id (a link to the deleted row) matches next. The loop stops when no row matches.
|
|
13
|
+
*
|
|
14
|
+
* A match needs two integer ids, an old id that the candidate no longer has in that table, and a
|
|
15
|
+
* new id that the reference did not have in that table. So a changed value in a first column that
|
|
16
|
+
* is not an id is not hidden. The caller reports how many rows matched, because this removes
|
|
17
|
+
* lines from the difference.
|
|
18
|
+
*/
|
|
19
|
+
const CELL = '\t';
|
|
20
|
+
const INTEGER = /^\d+$/;
|
|
21
|
+
export function parse(line) {
|
|
22
|
+
const [first, ...cells] = line.split(CELL);
|
|
23
|
+
if (first === undefined || 0 === cells.length) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const space = first.lastIndexOf(' ');
|
|
27
|
+
const id = first.slice(space + 1);
|
|
28
|
+
return space < 0 || !INTEGER.test(id) ? undefined : { table: first.slice(0, space), id, cells };
|
|
29
|
+
}
|
|
30
|
+
/** The ids each table has in a snapshot. */
|
|
31
|
+
function idsOf(lines) {
|
|
32
|
+
const out = new Map();
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const row = parse(line);
|
|
35
|
+
if (row !== undefined) {
|
|
36
|
+
const ids = out.get(row.table) ?? new Set();
|
|
37
|
+
ids.add(row.id);
|
|
38
|
+
out.set(row.table, ids);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
const keyOf = (table, cells) => [table, ...cells].join(CELL);
|
|
44
|
+
export function renumber(removed, added, reference, candidate) {
|
|
45
|
+
const before = idsOf(reference);
|
|
46
|
+
const after = idsOf(candidate);
|
|
47
|
+
const newId = new Map();
|
|
48
|
+
const left = removed.map(parse);
|
|
49
|
+
const right = added.map(parse);
|
|
50
|
+
const gone = new Set();
|
|
51
|
+
const come = new Set();
|
|
52
|
+
let progress = true;
|
|
53
|
+
while (progress) {
|
|
54
|
+
progress = false;
|
|
55
|
+
const open = new Map();
|
|
56
|
+
right.forEach((row, j) => {
|
|
57
|
+
if (row !== undefined && !come.has(j)) {
|
|
58
|
+
const key = keyOf(row.table, row.cells);
|
|
59
|
+
open.set(key, [...(open.get(key) ?? []), j]);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
left.forEach((row, i) => {
|
|
63
|
+
if (row === undefined || gone.has(i) || (after.get(row.table)?.has(row.id) ?? false)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const key = keyOf(row.table, row.cells.map((cell) => newId.get(cell) ?? cell));
|
|
67
|
+
const j = (open.get(key) ?? []).find((k) => {
|
|
68
|
+
const id = right[k]?.id ?? '';
|
|
69
|
+
return !come.has(k) && !(before.get(row.table)?.has(id) ?? false);
|
|
70
|
+
});
|
|
71
|
+
const to = j === undefined ? undefined : right[j]?.id;
|
|
72
|
+
if (j === undefined || to === undefined || (newId.get(row.id) ?? to) !== to) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
newId.set(row.id, to);
|
|
76
|
+
gone.add(i);
|
|
77
|
+
come.add(j);
|
|
78
|
+
progress = true;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
removed: removed.filter((_, i) => !gone.has(i)),
|
|
83
|
+
added: added.filter((_, j) => !come.has(j)),
|
|
84
|
+
renumbered: gone.size,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grade a finished run of an app from snapshots of its stored data. Pure.
|
|
3
|
+
*
|
|
4
|
+
* A snapshot is the state lines of `readStateSources`, and optionally a `url` line for the page the
|
|
5
|
+
* run ended on. Three snapshots go in: the reference (the app after the correct steps), again (the
|
|
6
|
+
* app after the same correct steps, from the same start), and the candidate (the app after the steps
|
|
7
|
+
* to grade). A line that differs between the two reference snapshots is noise and is ignored, and so
|
|
8
|
+
* is a change to a cell of a row that the two references wrote differently (see `grade-noise.ts`). Any
|
|
9
|
+
* other line that differs between the reference and the candidate is a difference, and the grade
|
|
10
|
+
* fails. A source that could not be read makes the grade unverified, never a pass.
|
|
11
|
+
*
|
|
12
|
+
* A row the candidate deleted and wrote again with a new id and the same values is not a difference
|
|
13
|
+
* (see `grade-renumber.ts`). The grade counts these rows.
|
|
14
|
+
*
|
|
15
|
+
* State lines are sorted rows, so the diff is a count of each line, not an ordered diff. That keeps
|
|
16
|
+
* the cost linear in the number of rows, and a source of many thousand rows is cheap.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
export declare const GradeVerdict: {
|
|
20
|
+
readonly PASS: "pass";
|
|
21
|
+
readonly FAIL: "fail";
|
|
22
|
+
/** The grade proves nothing: a source could not be read, or the answer could not be checked. */
|
|
23
|
+
readonly UNVERIFIED: "unverified";
|
|
24
|
+
};
|
|
25
|
+
export type GradeVerdict = (typeof GradeVerdict)[keyof typeof GradeVerdict];
|
|
26
|
+
export declare const SnapshotSchema: z.ZodObject<{
|
|
27
|
+
lines: z.ZodArray<z.ZodString>;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
export type Snapshot = z.infer<typeof SnapshotSchema>;
|
|
30
|
+
/** The line that holds the page a run ended on. */
|
|
31
|
+
export declare const URL_PREFIX = "url";
|
|
32
|
+
/**
|
|
33
|
+
* The page a run ended on, as one line: the origin and path without a final slash, and the query
|
|
34
|
+
* values sorted by name. The fragment is removed. Text that is not a URL is kept as it is.
|
|
35
|
+
*/
|
|
36
|
+
export declare function urlLine(url: string): string;
|
|
37
|
+
export interface StateGrade {
|
|
38
|
+
verdict: GradeVerdict;
|
|
39
|
+
/** Lines only the reference has, less noise. */
|
|
40
|
+
removed: string[];
|
|
41
|
+
/** Lines only the candidate has, less noise. */
|
|
42
|
+
added: string[];
|
|
43
|
+
/** Lines that differed but were ignored, because they also differed between the two references. */
|
|
44
|
+
ignored: number;
|
|
45
|
+
/** The reads that failed, on any snapshot. */
|
|
46
|
+
unread: string[];
|
|
47
|
+
/** Rows the candidate deleted and wrote again with a new id and the same values. Not differences. */
|
|
48
|
+
renumbered: number;
|
|
49
|
+
}
|
|
50
|
+
export declare function gradeState(reference: Snapshot, again: Snapshot, candidate: Snapshot): StateGrade;
|