@reticlehq/server 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/SKILL.md +130 -778
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/causal-summary.d.ts +36 -1
- package/dist/capsule/causal-summary.js +33 -7
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +65 -34
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +32 -0
- package/dist/cli/cli-port.js +82 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +47 -3
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +69 -0
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +119 -39
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/events/contradictions.d.ts +46 -0
- package/dist/events/contradictions.js +35 -22
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -72
- package/dist/events/predicate-eval.js +117 -236
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +296 -22
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +21 -5
- package/dist/flows/heal-run.js +1 -1
- package/dist/flows/replay.js +3 -0
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +10 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +49 -0
- package/dist/honesty/unsettled.js +82 -0
- package/dist/honesty/verified.d.ts +36 -3
- package/dist/honesty/verified.js +54 -11
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +47 -10
- package/dist/init/agent-rules.d.ts +36 -0
- package/dist/init/agent-rules.js +132 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +38 -6
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +114 -0
- package/dist/init/confirm.js +165 -0
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +15 -0
- package/dist/init/mcp.js +19 -0
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +78 -16
- package/dist/init/plan.d.ts +47 -6
- package/dist/init/plan.js +228 -69
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +157 -45
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +54 -6
- package/dist/init/snippets.d.ts +45 -5
- package/dist/init/snippets.js +194 -42
- package/dist/init/vite-config.js +53 -5
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +37 -5
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +35 -30
- package/dist/mcp/mcp-proxy.js +306 -104
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +70 -34
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +11 -2
- package/dist/mcp/proxy-lifecycle.js +12 -3
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +67 -2
- package/dist/session/no-session-diagnosis.js +236 -27
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +174 -8
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +15 -17
- package/dist/telemetry/bug-found.js +52 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +56 -0
- package/dist/telemetry/install-source.js +74 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +15 -0
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +26 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +209 -28
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +49 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +73 -9
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +159 -26
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +40 -1
- package/dist/tools/lease-tools.js +181 -83
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +10 -4
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +52 -0
- package/dist/tools/react-grace.js +90 -0
- package/dist/tools/read-tools.js +67 -5
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +94 -0
- package/dist/tools/settle-in-flight.js +146 -0
- package/dist/tools/snapshot-delta.d.ts +10 -0
- package/dist/tools/snapshot-delta.js +194 -9
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +86 -1
- package/dist/tools/tools.js +61 -8
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +55 -11
- package/docs/actions.mdx +120 -0
- package/docs/agent-cheatsheet.md +95 -29
- package/docs/architecture.md +52 -35
- package/docs/benchmarks.md +127 -75
- package/docs/best-practices.mdx +216 -0
- package/docs/capabilities.mdx +534 -0
- package/docs/cli/affected.mdx +53 -0
- package/docs/cli/capsules.mdx +70 -0
- package/docs/cli/cloud.mdx +153 -0
- package/docs/cli/doctor.mdx +109 -0
- package/docs/cli/drive.mdx +74 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +94 -0
- package/docs/cli/hunt.mdx +73 -0
- package/docs/cli/identify.mdx +76 -0
- package/docs/cli/init.mdx +121 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +74 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +69 -0
- package/docs/cli/status.mdx +88 -0
- package/docs/cli/stop.mdx +64 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +76 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +62 -0
- package/docs/cli.mdx +241 -0
- package/docs/debugging.md +16 -10
- package/docs/deploy-checks.md +45 -12
- package/docs/desktop.mdx +384 -0
- package/docs/docs.json +364 -0
- package/docs/enterprise.md +22 -16
- package/docs/faq.mdx +102 -0
- package/docs/fixtures.md +13 -7
- package/docs/flows.md +29 -23
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +238 -0
- package/docs/gate-plan.md +48 -42
- package/docs/gates.md +28 -22
- package/docs/getting-started.md +129 -77
- package/docs/human-control.md +24 -18
- package/docs/index.mdx +143 -0
- package/docs/install-agentic.mdx +224 -0
- package/docs/install-manual.mdx +196 -0
- package/docs/instrumentation.mdx +238 -0
- package/docs/integration-patterns.md +41 -35
- package/docs/local-registry.md +19 -13
- package/docs/multi-agent-testing.md +19 -13
- package/docs/packages/babel-plugin.mdx +79 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +116 -0
- package/docs/packages/electron.mdx +85 -0
- package/docs/packages/eslint-plugin.mdx +86 -0
- package/docs/packages/next.mdx +89 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +119 -0
- package/docs/packages/vite-plugin.mdx +94 -0
- package/docs/packages.mdx +218 -0
- package/docs/platform-integration.md +35 -26
- package/docs/predicates.mdx +248 -0
- package/docs/quickstart.mdx +257 -0
- package/docs/recipes.mdx +282 -0
- package/docs/skill-file.mdx +107 -0
- package/docs/state-management.mdx +199 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +50 -33
- package/docs/telemetry-contract.md +106 -52
- package/docs/telemetry-events.mdx +475 -0
- package/docs/telemetry.md +63 -55
- package/docs/testing.md +44 -6
- package/docs/token-efficiency.md +37 -27
- package/docs/tools-act-and-wait.mdx +173 -0
- package/docs/tools-act-sequence.mdx +137 -0
- package/docs/tools-act.mdx +95 -0
- package/docs/tools-assert.mdx +129 -0
- package/docs/tools-console.mdx +63 -0
- package/docs/tools-inspect.mdx +88 -0
- package/docs/tools-navigate.mdx +99 -0
- package/docs/tools-network.mdx +119 -0
- package/docs/tools-observe.mdx +145 -0
- package/docs/tools-overview.mdx +160 -0
- package/docs/tools-query.mdx +126 -0
- package/docs/tools-session-and-feedback.mdx +112 -0
- package/docs/tools-sessions.mdx +94 -0
- package/docs/tools-snapshot.mdx +129 -0
- package/docs/tools-state.mdx +104 -0
- package/docs/tools-tools-and-run.mdx +156 -0
- package/docs/tools-wait-for.mdx +85 -0
- package/docs/troubleshooting.mdx +263 -0
- package/docs/usage.md +209 -165
- package/docs/vs-chrome-devtools-mcp.mdx +123 -0
- package/docs/vs-playwright-mcp.mdx +125 -0
- package/docs/vs-screenshots.mdx +137 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +109 -0
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -256
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
package/dist/flows/flow-tools.js
CHANGED
|
@@ -255,7 +255,9 @@ export const FLOW_TOOLS = [
|
|
|
255
255
|
'stale ref. On an anchor MISS returns legible DRIFT { step, anchor, drift:{ reasonKind, reason, ' +
|
|
256
256
|
'nearest } } (the closest surviving testid) and stops — the "whose fault is it" contract. ' +
|
|
257
257
|
'Returns { name, status: ok|drift|error, steps:[...] }; missing/malformed files and action ' +
|
|
258
|
-
'failures are status:error with a structured code (distinct from contract-changed drift).'
|
|
258
|
+
'failures are status:error with a structured code (distinct from contract-changed drift). ' +
|
|
259
|
+
'An ok replay of a flow that asserts no consequence also carries unverifiable:{reason} — it ' +
|
|
260
|
+
'would read ok even if the feature were broken, so do NOT treat that ok as proof.',
|
|
259
261
|
inputSchema: {
|
|
260
262
|
flow: z
|
|
261
263
|
.string()
|
|
@@ -285,6 +287,12 @@ export const FLOW_TOOLS = [
|
|
|
285
287
|
.optional()
|
|
286
288
|
.describe('Push-default deviation report over the replay segments: ranked deviations vs the learned envelope + a nominal count, or a fall-back note below 3 runs.'),
|
|
287
289
|
error: z.object({ code: z.string(), message: z.string() }).optional(),
|
|
290
|
+
// Declared here or a validating profile strips it -- the same way `name` was stripped once.
|
|
291
|
+
// A field the handler sets and the schema omits arrives as nothing, silently.
|
|
292
|
+
unverifiable: z
|
|
293
|
+
.object({ reason: z.string() })
|
|
294
|
+
.optional()
|
|
295
|
+
.describe('Set on an ok replay that cannot fail: the flow asserts no observable consequence, so it passes even when the feature is broken. Same reason reticle_flow_verify reports.'),
|
|
288
296
|
decision: z
|
|
289
297
|
.object({
|
|
290
298
|
verdict: z.string(),
|
|
@@ -367,23 +375,31 @@ export const FLOW_TOOLS = [
|
|
|
367
375
|
requested.length > 0) {
|
|
368
376
|
const concurrency = resolveConcurrency(requested.length, pool.capacity(), parallelArg);
|
|
369
377
|
const outcomes = await mapWithConcurrency(requested, concurrency, async (flowName) => {
|
|
370
|
-
const
|
|
378
|
+
const start = deps.now();
|
|
379
|
+
const lease = await acquireLeasedSession(pool, deps.sessions, appUrl, projectId);
|
|
371
380
|
try {
|
|
372
|
-
|
|
381
|
+
const replay = await replayNamedFlow(deps, { flowName, sessionId: lease.sessionId });
|
|
382
|
+
return { replay, durationMs: deps.now() - start };
|
|
373
383
|
}
|
|
374
384
|
finally {
|
|
375
|
-
// Always release: a crashed flow must never leak a slot and starve the rest of the suite.
|
|
376
385
|
await lease.release().catch(() => undefined);
|
|
377
386
|
}
|
|
378
387
|
});
|
|
379
388
|
const parallelRuns = await Promise.all(outcomes.map(async (o, i) => {
|
|
380
389
|
const name = requested[i] ?? '';
|
|
381
|
-
const replay = o.ok && o.value !== undefined ? o.value : leaseFailureReplay(name, o.error);
|
|
390
|
+
const replay = o.ok && o.value !== undefined ? o.value.replay : leaseFailureReplay(name, o.error);
|
|
382
391
|
const loaded = await deps.flows.load(name, projectId).catch(() => null);
|
|
383
392
|
const flow = loaded !== null && loaded.ok ? loaded.value : undefined;
|
|
384
393
|
return flow === undefined ? { replay } : { replay, flow };
|
|
385
394
|
}));
|
|
395
|
+
const timed = outcomes.map((o, i) => ({
|
|
396
|
+
replay: o.ok && o.value !== undefined
|
|
397
|
+
? o.value.replay
|
|
398
|
+
: leaseFailureReplay(requested[i] ?? '', o.error),
|
|
399
|
+
durationMs: o.ok && o.value !== undefined ? o.value.durationMs : 0,
|
|
400
|
+
}));
|
|
386
401
|
const flaky = await recordSuiteFlakes(deps.fs, deps.reticleRoot, parallelRuns);
|
|
402
|
+
await persistAndSyncVerificationRun(deps, timed, projectId);
|
|
387
403
|
const verdict = buildSuiteVerdict(parallelRuns);
|
|
388
404
|
return flaky.length > 0 ? { ...verdict, flaky: [...flaky] } : verdict;
|
|
389
405
|
}
|
package/dist/flows/heal-run.js
CHANGED
|
@@ -16,7 +16,7 @@ const HEAL_MESSAGES = {
|
|
|
16
16
|
NOTHING: 'nothing to heal — every anchor resolved on replay',
|
|
17
17
|
HEALED: "rewrote drifted testid anchors to their nearest surviving match and re-verified the flow's success consequence still fires",
|
|
18
18
|
DRIFT_DRY: 'confident rebind(s) proposed — re-run with apply:true to write them to disk',
|
|
19
|
-
UNHEALABLE: `drift found, but no nearest match cleared the confidence floor (HEAL_CONFIDENCE_MIN=${HEAL_CONFIDENCE_MIN}); file left untouched
|
|
19
|
+
UNHEALABLE: `drift found, but no nearest match cleared the confidence floor (HEAL_CONFIDENCE_MIN=${HEAL_CONFIDENCE_MIN}); file left untouched; add a data-testid or fix the flow by hand`,
|
|
20
20
|
HEALED_UNVERIFIED: 'rewrote drifted testid anchors — but this flow declares no success consequence, so the rebind resolves a locator without proving the intent still holds. Add a success-state assertion (reticle_annotate) so future heals can be verified.',
|
|
21
21
|
CONSEQUENCE_BROKEN: 'rebind resolves the drifted locator to a surviving element, but the healed flow no longer satisfies its success consequence — refusing to write (a heal that loses the intent would ship a green-but-dead test). Fix by hand and verify',
|
|
22
22
|
};
|
package/dist/flows/replay.js
CHANGED
|
@@ -247,6 +247,9 @@ export async function replayProgram(session, program, confirmDangerous = false)
|
|
|
247
247
|
session.beginAction(ReticleTool.REPLAY, { steps: liveSteps.length });
|
|
248
248
|
let r;
|
|
249
249
|
try {
|
|
250
|
+
// DIVERGENCE: replay sends one batched ACT_SEQUENCE command to the browser;
|
|
251
|
+
// live (tools/act-tools.ts) sends N individual ACT commands for per-step timeout +
|
|
252
|
+
// progress. A bug in either is invisible from the other.
|
|
250
253
|
r = await session.command(ReticleCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
251
254
|
}
|
|
252
255
|
finally {
|
|
@@ -10,5 +10,8 @@ import { type ReticleEvent } from '@reticlehq/core';
|
|
|
10
10
|
* optimistically rendered "dispatched", the page settled, every channel agreed — and the server
|
|
11
11
|
* REVERTED it to `held` 1.2 s later. The verdict was not wrong about what it observed. It was early,
|
|
12
12
|
* and nothing in the response said so except the status code nobody was reading.
|
|
13
|
+
*
|
|
14
|
+
* The DEV TOOLCHAIN's own traffic is excluded, on the same rule every other check applies: a verdict
|
|
15
|
+
* about the app must not be decided by a request the app did not make.
|
|
13
16
|
*/
|
|
14
17
|
export declare function hasAcceptedWrite(events: readonly ReticleEvent[]): boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EventType, HTTP_ACCEPTED } from '@reticlehq/core';
|
|
2
|
-
import { asNumber } from '../tools/tools-helpers.js';
|
|
1
|
+
import { EventType, HTTP_ACCEPTED, isDevToolingUrl } from '@reticlehq/core';
|
|
2
|
+
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
3
3
|
/**
|
|
4
4
|
* Did a write in this window answer `202 Accepted`?
|
|
5
5
|
*
|
|
@@ -11,7 +11,12 @@ import { asNumber } from '../tools/tools-helpers.js';
|
|
|
11
11
|
* optimistically rendered "dispatched", the page settled, every channel agreed — and the server
|
|
12
12
|
* REVERTED it to `held` 1.2 s later. The verdict was not wrong about what it observed. It was early,
|
|
13
13
|
* and nothing in the response said so except the status code nobody was reading.
|
|
14
|
+
*
|
|
15
|
+
* The DEV TOOLCHAIN's own traffic is excluded, on the same rule every other check applies: a verdict
|
|
16
|
+
* about the app must not be decided by a request the app did not make.
|
|
14
17
|
*/
|
|
15
18
|
export function hasAcceptedWrite(events) {
|
|
16
|
-
return events.some((e) => e.type === EventType.NET_REQUEST &&
|
|
19
|
+
return events.some((e) => e.type === EventType.NET_REQUEST &&
|
|
20
|
+
asNumber(e.data['status']) === HTTP_ACCEPTED &&
|
|
21
|
+
!isDevToolingUrl(asString(e.data['url'])));
|
|
17
22
|
}
|
|
@@ -17,8 +17,23 @@
|
|
|
17
17
|
import { PredicateKind } from '@reticlehq/core';
|
|
18
18
|
export function readsDomState(predicate) {
|
|
19
19
|
switch (predicate.kind) {
|
|
20
|
+
// ROUTE is here, and it did not used to be. It was purely event-based — satisfiable only by a
|
|
21
|
+
// navigation inside the act's window, and therefore unable to be answered by the past — so it
|
|
22
|
+
// sat with the floored kinds below.
|
|
23
|
+
//
|
|
24
|
+
// It gained a fallback to the CURRENT route for the case where the window holds no route change,
|
|
25
|
+
// which is what makes "did the session survive a reload?" answerable at all. Without a
|
|
26
|
+
// before-check that fallback is a guaranteed green: the app is already on /login, the click's
|
|
27
|
+
// handler is broken, nothing navigates, and an assertion that the action reached /login passes
|
|
28
|
+
// on the strength of it having been there all along. A bare `{ kind: 'route' }` becomes
|
|
29
|
+
// unconditionally true, because there is always a current route.
|
|
30
|
+
//
|
|
31
|
+
// One extra query on the act path buys `already_true`, reported as UNKNOWN with the reason
|
|
32
|
+
// rather than as a pass. The fallback removed a false red; this is what stops it becoming a
|
|
33
|
+
// false green, and that is the trade this codebase never makes.
|
|
20
34
|
case PredicateKind.ELEMENT:
|
|
21
35
|
case PredicateKind.TEXT:
|
|
36
|
+
case PredicateKind.ROUTE:
|
|
22
37
|
return true;
|
|
23
38
|
case PredicateKind.ALL_OF:
|
|
24
39
|
case PredicateKind.ANY_OF:
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The kind enum lives in core (it crosses the wire in a BLIND_SPOT event); re-exported here so existing
|
|
8
8
|
// honesty-side imports keep working.
|
|
9
9
|
export { BlindSpotKind } from '@reticlehq/core';
|
|
10
|
-
import { BlindSpotKind, EventType } from '@reticlehq/core';
|
|
10
|
+
import { BlindSpotKind, EventType, ReticleEnv, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
11
11
|
/**
|
|
12
12
|
* Whether the observation window saw everything it was asked about.
|
|
13
13
|
*
|
|
@@ -29,13 +29,21 @@ const LABEL = {
|
|
|
29
29
|
[BlindSpotKind.VIRTUALIZED_UNMOUNTED]: (n) => `${String(n)} virtualized unmounted row${1 === n ? '' : 's'} unobserved`,
|
|
30
30
|
// Not "some rows we could not see" — the events never reached the observer, so this window is a
|
|
31
31
|
// SAMPLE of what the app did. Phrased as a caveat on what the whole result MEANS.
|
|
32
|
-
|
|
32
|
+
// Names a NUMBER, not a variable. "Raise it for a busy app" leaves the reader to invent a value,
|
|
33
|
+
// and the one who reported this had to guess before finding one that worked. The default was
|
|
34
|
+
// raised in the same change, so a page still hitting the cap after that is genuinely unusual and
|
|
35
|
+
// deserves a concrete next value rather than an adjective.
|
|
36
|
+
[BlindSpotKind.RATE_LIMITED]: (n) => `${String(n)} event${1 === n ? '' : 's'} dropped by the bridge rate cap, so this window is SAMPLED. Set ${ReticleEnv.MAX_MESSAGES_PER_SECOND}=${String(TRANSPORT_LIMITS.MAX_MESSAGES_PER_SECOND * 5)} and restart the daemon, then re-run this drive`,
|
|
33
37
|
// Not a count of things — a single fact about the page. Phrased so the coverage line reads as a
|
|
34
38
|
// caveat on what the network view MEANS, not as a tally.
|
|
35
39
|
// A count of unverifiable ACTIONS, not of things we failed to look at. The distinction matters:
|
|
36
40
|
// nothing here was missed, there was simply never a verdict to observe, and no amount of extra
|
|
37
41
|
// instrumentation would produce one.
|
|
38
42
|
[BlindSpotKind.VERDICTLESS_SEND]: (n) => `${String(n)} one-way IPC send${1 === n ? '' : 's'} dispatched with NO verdict — the renderer never learns whether the main process handled ${1 === n ? 'it' : 'them'}, so this cannot be confirmed from the page`,
|
|
43
|
+
// Not a tally — a fact about the app's wiring, phrased as what the EMPTY state channel means. An
|
|
44
|
+
// agent reading `stateDiffs: []` otherwise reads "the app changed no state", which is a claim
|
|
45
|
+
// nothing here can support. Names the one-line fix, because the reader is the one who can apply it.
|
|
46
|
+
[BlindSpotKind.UNWATCHED_STATE]: () => 'no subscribable store is registered, so NO state change is observed — an empty stateDiffs here means unwatched, not unchanged (register the store itself in your reticle-dev module: registerStore(name, store), not a getter)',
|
|
39
47
|
[BlindSpotKind.WRAPPED_NETWORK]: () => 'fetch was already wrapped before Reticle, so recorded requests may differ from what was sent',
|
|
40
48
|
// A fact about the app's wiring, not a tally. Says what the empty network view MEANS, because an
|
|
41
49
|
// empty one here is indistinguishable from "this app made no backend calls" — and that reading is
|
|
@@ -33,6 +33,15 @@ interface HonestyInputs {
|
|
|
33
33
|
* knows whether a blind-spot note came from the browser transport or from a boundary in the page.
|
|
34
34
|
*/
|
|
35
35
|
losses?: readonly CaptureLoss[];
|
|
36
|
+
/**
|
|
37
|
+
* Did the page go quiet inside the action's window? Omitted when nothing measured it.
|
|
38
|
+
*
|
|
39
|
+
* Settlement stopped being a veto over a declared consequence that held (see `verified.ts`), and a
|
|
40
|
+
* fact that no longer decides the verdict must not therefore disappear: a page that never goes
|
|
41
|
+
* idle is worth knowing about, and a caller who cares can still gate on it. Reported here rather
|
|
42
|
+
* than beside `verified`, where the answer belongs to the rule and not to its inputs.
|
|
43
|
+
*/
|
|
44
|
+
settled?: boolean;
|
|
36
45
|
}
|
|
37
46
|
export interface HonestyBlock {
|
|
38
47
|
grade: HonestyGrade;
|
|
@@ -52,6 +61,8 @@ export interface HonestyBlock {
|
|
|
52
61
|
issues: string[];
|
|
53
62
|
losses?: CaptureLoss[];
|
|
54
63
|
};
|
|
64
|
+
/** Whether the page went quiet in this window — present only when it was measured. */
|
|
65
|
+
settled?: boolean;
|
|
55
66
|
}
|
|
56
67
|
export declare function buildHonestyBlock(inputs: HonestyInputs): HonestyBlock;
|
|
57
68
|
interface HonestyBar {
|
package/dist/honesty/honesty.js
CHANGED
|
@@ -46,6 +46,7 @@ export function buildHonestyBlock(inputs) {
|
|
|
46
46
|
? {}
|
|
47
47
|
: { envelope: { samples, sufficient: samples >= MIN_ENVELOPE_SAMPLES } }),
|
|
48
48
|
coverage: { ...(pct === undefined ? {} : { pct }), partial },
|
|
49
|
+
...(inputs.settled === undefined ? {} : { settled: inputs.settled }),
|
|
49
50
|
integrity: {
|
|
50
51
|
clean: 0 === issues.length,
|
|
51
52
|
issues,
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* every other field in the record agrees is a clean 200.
|
|
9
9
|
*
|
|
10
10
|
* So an omission is declared rather than left to inference, with the one line that fixes it. Present
|
|
11
|
-
* only when a body-bearing call was returned AND
|
|
12
|
-
* captures bodies pays nothing and the field's
|
|
11
|
+
* only when a body-bearing call was returned AND no body — request or response — was recorded
|
|
12
|
+
* anywhere in the result, so an app that already captures bodies pays nothing and the field's
|
|
13
|
+
* PRESENCE is the warning.
|
|
13
14
|
*/
|
|
14
15
|
export declare function bodiesNotCaptured(calls: {
|
|
15
16
|
method?: string;
|
|
16
17
|
requestBody?: string;
|
|
18
|
+
responseBody?: string;
|
|
17
19
|
}[]): {
|
|
18
20
|
bodiesNotCaptured?: string;
|
|
19
21
|
};
|
|
@@ -10,13 +10,20 @@ const BODY_BEARING_METHODS = new Set(['POST', 'PUT', 'PATCH']);
|
|
|
10
10
|
* every other field in the record agrees is a clean 200.
|
|
11
11
|
*
|
|
12
12
|
* So an omission is declared rather than left to inference, with the one line that fixes it. Present
|
|
13
|
-
* only when a body-bearing call was returned AND
|
|
14
|
-
* captures bodies pays nothing and the field's
|
|
13
|
+
* only when a body-bearing call was returned AND no body — request or response — was recorded
|
|
14
|
+
* anywhere in the result, so an app that already captures bodies pays nothing and the field's
|
|
15
|
+
* PRESENCE is the warning.
|
|
15
16
|
*/
|
|
16
17
|
export function bodiesNotCaptured(calls) {
|
|
17
18
|
const bodyBearing = calls.filter((c) => BODY_BEARING_METHODS.has((c.method ?? '').toUpperCase()));
|
|
18
19
|
if (0 === bodyBearing.length)
|
|
19
20
|
return {};
|
|
21
|
+
// A recorded response body anywhere in the result proves capture is ON. An absent request body
|
|
22
|
+
// then means this payload was not stringified (a multipart upload, a body the SDK skipped), not
|
|
23
|
+
// that recording is off — and firing the note would contradict the body sitting in the same
|
|
24
|
+
// result, exactly on the question the note exists to answer. (#394)
|
|
25
|
+
if (calls.some((c) => c.responseBody !== undefined))
|
|
26
|
+
return {};
|
|
20
27
|
if (bodyBearing.some((c) => c.requestBody !== undefined))
|
|
21
28
|
return {};
|
|
22
29
|
return {
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import { type ReticleEvent } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* The writes whose outcome went unread, as "METHOD url".
|
|
4
|
+
*
|
|
5
|
+
* Named rather than counted, because a caveat an agent cannot locate is a caveat it learns to skip.
|
|
6
|
+
* The sentence used to say "a write returned 2xx with a response body that was never recorded" and
|
|
7
|
+
* name no write, which is unactionable on any page that makes more than one call.
|
|
8
|
+
*/
|
|
9
|
+
export declare function unreadWriteLabels(events: readonly ReticleEvent[]): string[];
|
|
2
10
|
export declare function hasUnreadWriteOutcome(events: readonly ReticleEvent[]): boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { EventType, MUTATING_METHODS, NetInitiator } from '@reticlehq/core';
|
|
1
|
+
import { EventType, MUTATING_METHODS, NetInitiator, isDevToolingUrl, } from '@reticlehq/core';
|
|
2
|
+
import { asString } from '../tools/tools-helpers.js';
|
|
2
3
|
const IPC_METHOD = NetInitiator.IPC.toUpperCase();
|
|
3
4
|
/**
|
|
4
5
|
* A write whose outcome is sitting in a payload nobody read.
|
|
@@ -29,22 +30,48 @@ const IPC_METHOD = NetInitiator.IPC.toUpperCase();
|
|
|
29
30
|
* The residual gap is real and narrower: a handler that RESOLVES with a failure inside its payload
|
|
30
31
|
* is invisible without body capture. That case is `findBodyFailures`, which reads the payload and
|
|
31
32
|
* catches it precisely — with capture on, where it belongs.
|
|
33
|
+
*
|
|
34
|
+
* - **the app's own traffic only.** The DEV TOOLCHAIN writes too: Next's overlay answers
|
|
35
|
+
* `POST /__nextjs_original-stack-frames` with a JSON body the moment the app logs one React
|
|
36
|
+
* warning, and nothing captures it. Judged as the app's write, that made every verdict in a Next
|
|
37
|
+
* dev app `unknown` — decided by a channel the caller never asked about, on a request the app did
|
|
38
|
+
* not make. `findContradictions` has split this traffic out for the same reason since the overlay
|
|
39
|
+
* turned correct navigations red; this rule was reading the unsplit window.
|
|
32
40
|
*/
|
|
33
41
|
const OK_MIN = 200;
|
|
34
42
|
const OK_MAX = 300;
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The writes whose outcome went unread, as "METHOD url".
|
|
45
|
+
*
|
|
46
|
+
* Named rather than counted, because a caveat an agent cannot locate is a caveat it learns to skip.
|
|
47
|
+
* The sentence used to say "a write returned 2xx with a response body that was never recorded" and
|
|
48
|
+
* name no write, which is unactionable on any page that makes more than one call.
|
|
49
|
+
*/
|
|
50
|
+
export function unreadWriteLabels(events) {
|
|
51
|
+
const labels = [];
|
|
52
|
+
for (const event of events) {
|
|
37
53
|
if (event.type !== EventType.NET_REQUEST)
|
|
38
|
-
|
|
54
|
+
continue;
|
|
39
55
|
const method = ('string' === typeof event.data['method'] ? event.data['method'] : '').toUpperCase();
|
|
40
56
|
if (method === IPC_METHOD || !MUTATING_METHODS.includes(method))
|
|
41
|
-
|
|
57
|
+
continue;
|
|
58
|
+
const url = asString(event.data['url']);
|
|
59
|
+
if (isDevToolingUrl(url))
|
|
60
|
+
continue;
|
|
42
61
|
const status = event.data['status'];
|
|
43
62
|
if (typeof status !== 'number' || status < OK_MIN || status >= OK_MAX)
|
|
44
|
-
|
|
63
|
+
continue;
|
|
45
64
|
if (event.data['responseBody'] !== undefined)
|
|
46
|
-
|
|
65
|
+
continue; // read — findBodyFailures judged it
|
|
47
66
|
const size = event.data['responseSize'];
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
if ('number' !== typeof size || size <= 0)
|
|
68
|
+
continue;
|
|
69
|
+
const label = `${method} ${url ?? ''}`.trim();
|
|
70
|
+
if (!labels.includes(label))
|
|
71
|
+
labels.push(label);
|
|
72
|
+
}
|
|
73
|
+
return labels;
|
|
74
|
+
}
|
|
75
|
+
export function hasUnreadWriteOutcome(events) {
|
|
76
|
+
return unreadWriteLabels(events).length > 0;
|
|
50
77
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an `unsettled` verdict was waiting for, and what its window held instead.
|
|
3
|
+
*
|
|
4
|
+
* `unsettled` is the commonest reason a verdict comes back `unknown`, and all it said was that the
|
|
5
|
+
* page "never settled" — a sentence equally true of a page that polls, a page that animates, and a
|
|
6
|
+
* page whose write is still open. Those need three different next moves, and an agent handed the
|
|
7
|
+
* sentence cannot tell which it is holding, so the drive stops there. An `unknown` that explains
|
|
8
|
+
* itself is a retry; one that does not is a dead end.
|
|
9
|
+
*
|
|
10
|
+
* Two releases already tuned this window and neither is undone here. The budget is still spent in
|
|
11
|
+
* full before anything is called unsettled (settle-in-flight.ts), and the app is still given its
|
|
12
|
+
* reaction window before any accusation (waitForReaction). This only describes what was true at the
|
|
13
|
+
* moment the budget genuinely ran out.
|
|
14
|
+
*
|
|
15
|
+
* It rides in `because`, beside `verifiedReason`, rather than in a field of its own: the verdict has
|
|
16
|
+
* one sentence an agent is known to read, and a caveat parked anywhere else is a caveat nobody reads.
|
|
17
|
+
*/
|
|
18
|
+
import type { Predicate } from '../events/predicate.js';
|
|
19
|
+
export interface UnsettledWindow {
|
|
20
|
+
/** What the wait was for, in the caller's own terms — see `describeWaitTarget`. */
|
|
21
|
+
waitedFor: string;
|
|
22
|
+
/** Requests that started in this window and had not come back, as "METHOD url". */
|
|
23
|
+
stillInFlight: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Endpoints called more than once in this window, as "METHOD url ×N" — see `repeatedRequestLabels`.
|
|
26
|
+
*
|
|
27
|
+
* The case `stillInFlight` structurally cannot see: a retry loop against a dead backend completes
|
|
28
|
+
* every attempt, so nothing is ever outstanding, and the verdict fell through to a sentence that
|
|
29
|
+
* blamed unnamed churn on an app whose whole problem was one endpoint being hammered. Optional —
|
|
30
|
+
* a caller that does not supply it gets exactly the sentence that shipped before.
|
|
31
|
+
*/
|
|
32
|
+
repeated?: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The wait, in one phrase.
|
|
36
|
+
*
|
|
37
|
+
* The default matters most: a call that declares no `until` waits for the page to go IDLE, and an
|
|
38
|
+
* agent that never chose that wait cannot be expected to recognise it in a timeout about it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function describeWaitTarget(predicate: Predicate): string;
|
|
41
|
+
/**
|
|
42
|
+
* The `because` sentence for an unsettled verdict: the wait, the observation, the next move.
|
|
43
|
+
*
|
|
44
|
+
* The two branches are the point. An outstanding write is worth waiting for and worth asserting
|
|
45
|
+
* directly; page churn is neither, and telling an agent to raise its timeout on churn sells it a
|
|
46
|
+
* slower dead end. Without the detail the sentence is the one that shipped before, so a caller that
|
|
47
|
+
* cannot supply the facts loses nothing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function unsettledBecause(base: string, window: UnsettledWindow | undefined): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an `unsettled` verdict was waiting for, and what its window held instead.
|
|
3
|
+
*
|
|
4
|
+
* `unsettled` is the commonest reason a verdict comes back `unknown`, and all it said was that the
|
|
5
|
+
* page "never settled" — a sentence equally true of a page that polls, a page that animates, and a
|
|
6
|
+
* page whose write is still open. Those need three different next moves, and an agent handed the
|
|
7
|
+
* sentence cannot tell which it is holding, so the drive stops there. An `unknown` that explains
|
|
8
|
+
* itself is a retry; one that does not is a dead end.
|
|
9
|
+
*
|
|
10
|
+
* Two releases already tuned this window and neither is undone here. The budget is still spent in
|
|
11
|
+
* full before anything is called unsettled (settle-in-flight.ts), and the app is still given its
|
|
12
|
+
* reaction window before any accusation (waitForReaction). This only describes what was true at the
|
|
13
|
+
* moment the budget genuinely ran out.
|
|
14
|
+
*
|
|
15
|
+
* It rides in `because`, beside `verifiedReason`, rather than in a field of its own: the verdict has
|
|
16
|
+
* one sentence an agent is known to read, and a caveat parked anywhere else is a caveat nobody reads.
|
|
17
|
+
*/
|
|
18
|
+
import { PredicateKind } from '@reticlehq/core';
|
|
19
|
+
/** Enough to recognise the one that is stuck; not so many that a chatty window eats the verdict. */
|
|
20
|
+
const MAX_LISTED_REQUESTS = 3;
|
|
21
|
+
/**
|
|
22
|
+
* The wait, in one phrase.
|
|
23
|
+
*
|
|
24
|
+
* The default matters most: a call that declares no `until` waits for the page to go IDLE, and an
|
|
25
|
+
* agent that never chose that wait cannot be expected to recognise it in a timeout about it.
|
|
26
|
+
*/
|
|
27
|
+
export function describeWaitTarget(predicate) {
|
|
28
|
+
switch (predicate.kind) {
|
|
29
|
+
case PredicateKind.SETTLED:
|
|
30
|
+
return 'the page to go idle';
|
|
31
|
+
case PredicateKind.SIGNAL:
|
|
32
|
+
return predicate.name === undefined ? 'any signal' : `signal '${predicate.name}'`;
|
|
33
|
+
case PredicateKind.NET:
|
|
34
|
+
return `a request${predicate.method === undefined ? '' : ` ${predicate.method}`}${predicate.urlContains === undefined ? '' : ` matching '${predicate.urlContains}'`}`;
|
|
35
|
+
case PredicateKind.ROUTE:
|
|
36
|
+
return `a route change${predicate.pathname === undefined ? '' : ` to '${predicate.pathname}'`}${predicate.contains === undefined ? '' : ` containing '${predicate.contains}'`}`;
|
|
37
|
+
case PredicateKind.STATE:
|
|
38
|
+
return `state at '${predicate.path}'`;
|
|
39
|
+
case PredicateKind.TEXT:
|
|
40
|
+
return `text containing '${predicate.contains}'`;
|
|
41
|
+
case PredicateKind.ELEMENT:
|
|
42
|
+
return `an element matching ${JSON.stringify(predicate.query)}`;
|
|
43
|
+
case PredicateKind.CONSOLE:
|
|
44
|
+
return 'a console message';
|
|
45
|
+
case PredicateKind.ANIMATION:
|
|
46
|
+
return predicate.name === undefined ? 'an animation' : `animation '${predicate.name}'`;
|
|
47
|
+
case PredicateKind.ALL_OF:
|
|
48
|
+
return `all of (${predicate.predicates.map(describeWaitTarget).join('; ')})`;
|
|
49
|
+
case PredicateKind.ANY_OF:
|
|
50
|
+
return `any of (${predicate.predicates.map(describeWaitTarget).join('; ')})`;
|
|
51
|
+
case PredicateKind.NOT:
|
|
52
|
+
return `the absence of ${describeWaitTarget(predicate.predicate)}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The `because` sentence for an unsettled verdict: the wait, the observation, the next move.
|
|
57
|
+
*
|
|
58
|
+
* The two branches are the point. An outstanding write is worth waiting for and worth asserting
|
|
59
|
+
* directly; page churn is neither, and telling an agent to raise its timeout on churn sells it a
|
|
60
|
+
* slower dead end. Without the detail the sentence is the one that shipped before, so a caller that
|
|
61
|
+
* cannot supply the facts loses nothing.
|
|
62
|
+
*/
|
|
63
|
+
export function unsettledBecause(base, window) {
|
|
64
|
+
if (window === undefined)
|
|
65
|
+
return `${base} — re-check, or assert a consequence that settles`;
|
|
66
|
+
if (0 === window.stillInFlight.length) {
|
|
67
|
+
const repeated = window.repeated ?? [];
|
|
68
|
+
return (`${base}. It waited for ${window.waitedFor}, and no request started in this window was left ` +
|
|
69
|
+
'open — what kept the page busy was its own churn ' +
|
|
70
|
+
(0 === repeated.length
|
|
71
|
+
? '(a poll, a timer, an animation)'
|
|
72
|
+
: `(these fired repeatedly: ${repeated.join(', ')} — a poll or a retrying query)`) +
|
|
73
|
+
', so waiting longer will not help. Assert the consequence you care about (a signal, a route ' +
|
|
74
|
+
'change, or store state) instead of waiting for idle, and re-check if the app is genuinely slow');
|
|
75
|
+
}
|
|
76
|
+
const shown = window.stillInFlight.slice(0, MAX_LISTED_REQUESTS);
|
|
77
|
+
const rest = window.stillInFlight.length - shown.length;
|
|
78
|
+
return (`${base}. It waited for ${window.waitedFor}, and ${String(window.stillInFlight.length)} ` +
|
|
79
|
+
`request(s) started in this window had not come back: ${shown.join(', ')}` +
|
|
80
|
+
`${rest > 0 ? `, and ${String(rest)} more` : ''} — re-check once they land, raise timeout_ms, ` +
|
|
81
|
+
'or assert the request itself so the wait ends on the response rather than on idle');
|
|
82
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Verified, VerifiedReason } from '@reticlehq/core';
|
|
2
2
|
import { type HonestyBlock } from './honesty.js';
|
|
3
|
+
import { type UnsettledWindow } from './unsettled.js';
|
|
3
4
|
/**
|
|
4
5
|
* The decision rule: eight trust dimensions in, one answer out.
|
|
5
6
|
*
|
|
@@ -41,16 +42,48 @@ interface VerifiedInputs {
|
|
|
41
42
|
}[];
|
|
42
43
|
/** Did a real frame flush before the wait gave up? */
|
|
43
44
|
settled?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The caller NAMED the expected consequence before acting — an `until`/`predicate` of its own,
|
|
47
|
+
* rather than the default "wait for the page to go idle".
|
|
48
|
+
*
|
|
49
|
+
* This is the epistemic core of the tool: a declaration made before the action cannot be
|
|
50
|
+
* rationalised after it. When such a declaration HOLDS, settlement is corroboration, not a veto —
|
|
51
|
+
* so the two clauses that answer "the app had not gone quiet" step aside for it. Measured in the
|
|
52
|
+
* field on three different apps: the expected text was on screen, the write returned 204, the
|
|
53
|
+
* nested verdict passed with evidence, and the verdict was `unknown` because the SPA polls, or
|
|
54
|
+
* because the tab was hidden and a hidden tab never fires the animation frame `settled` is read
|
|
55
|
+
* from. A hidden tab is the NORMAL state for agent-driven verification, so a signal that is always
|
|
56
|
+
* false there cannot be a precondition for a verdict.
|
|
57
|
+
*
|
|
58
|
+
* Narrow on purpose. It steps aside for settlement ONLY: an absence-derived finding about what the
|
|
59
|
+
* action DID — a duplicate write, a dead control, a route that rendered nothing, a response
|
|
60
|
+
* nothing consumed — still downgrades, and every positively OBSERVED contradiction still wins
|
|
61
|
+
* outright. Fixing a false negative must not open a path to a false positive.
|
|
62
|
+
*/
|
|
63
|
+
declaredConsequence?: boolean;
|
|
44
64
|
/**
|
|
45
65
|
* A write in this window answered `202 Accepted` — the server took the request and has NOT
|
|
46
66
|
* finished processing it.
|
|
47
67
|
*/
|
|
48
68
|
outcomePending?: boolean;
|
|
49
69
|
/**
|
|
50
|
-
*
|
|
51
|
-
* never read. See `
|
|
70
|
+
* Writes in this window that returned 2xx with a payload that was never recorded, as "METHOD url",
|
|
71
|
+
* so their outcome was never read. See `unreadWriteLabels` — the status line describes the
|
|
72
|
+
* transport, not the result.
|
|
73
|
+
*
|
|
74
|
+
* The LABELS rather than a flag: every other clause names the evidence that decided it, and this
|
|
75
|
+
* one named nothing, so an agent holding the caveat could not tell which call it was about.
|
|
76
|
+
* Empty means nothing went unread.
|
|
77
|
+
*/
|
|
78
|
+
outcomeUnread?: readonly string[];
|
|
79
|
+
/**
|
|
80
|
+
* What the wait was for and what the window held when it ended — read ONLY by the two clauses that
|
|
81
|
+
* answer UNSETTLED, which is the commonest reason a verdict comes back `unknown` and was also the
|
|
82
|
+
* least actionable. Optional: without it those clauses say exactly what they said before.
|
|
52
83
|
*/
|
|
53
|
-
|
|
84
|
+
unsettled?: UnsettledWindow;
|
|
85
|
+
/** A passing absence assertion targeted a region that the current capture could not observe. */
|
|
86
|
+
absenceBlindSpot?: string;
|
|
54
87
|
}
|
|
55
88
|
interface VerifiedVerdict {
|
|
56
89
|
verified: Verified;
|
package/dist/honesty/verified.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { Verified, VerifiedReason, isAbsenceDerived } from '@reticlehq/core';
|
|
1
|
+
import { ContradictionKind, MUTATING_METHODS, Verified, VerifiedReason, isAbsenceDerived, } from '@reticlehq/core';
|
|
2
2
|
import { HonestyGrade } from './honesty.js';
|
|
3
|
+
import { unsettledBecause } from './unsettled.js';
|
|
3
4
|
export function decideVerified(inputs) {
|
|
4
5
|
const { pass, honesty, contradictions = [], settled, outcomePending, outcomeUnread } = inputs;
|
|
6
|
+
/** The caller named a consequence before acting and it held — see `declaredConsequence`. */
|
|
7
|
+
const declaredHeld = true === inputs.declaredConsequence && true === pass;
|
|
5
8
|
// Ahead of the failure clause, because a failure is only the most actionable fact when there WAS
|
|
6
9
|
// one. An assertion nobody could evaluate is not a defect in the app, and calling it one was
|
|
7
10
|
// putting the agent's own malformed calls into the bug count.
|
|
@@ -63,13 +66,40 @@ export function decideVerified(inputs) {
|
|
|
63
66
|
// Absence-derived only: report it, do not assert failure. UNKNOWN is the honest answer — Reticle
|
|
64
67
|
// drove the app and could not yet tell. The finding still rides out in `contradictions`, so an
|
|
65
68
|
// agent that wants to wait and re-check has everything it needs.
|
|
66
|
-
|
|
69
|
+
//
|
|
70
|
+
// ...except when the only finding is that a READ had not come back AND the caller declared the
|
|
71
|
+
// consequence that did. `request-never-settled` is a statement about when we stopped looking; the
|
|
72
|
+
// declaration is a statement about what was supposed to happen, made before it could. On a page
|
|
73
|
+
// that polls, the first is permanently true and vetoed every verdict the second earned.
|
|
74
|
+
//
|
|
75
|
+
// An open WRITE is the exception to the exception, and it is where the false-green defence lives:
|
|
76
|
+
// "the toast said Saved while the POST was still open" is the archetype this product exists to
|
|
77
|
+
// catch, and its outcome genuinely does not exist yet. A poll or a prefetch left hanging says
|
|
78
|
+
// nothing about the action; an unfinished mutation says the answer is not in yet. Read off the
|
|
79
|
+
// labels the window already carries — and when there are no labels to read, nothing is softened.
|
|
80
|
+
const openWrite = (inputs.unsettled?.stillInFlight ?? []).some((label) => MUTATING_METHODS.includes((label.split(' ')[0] ?? '').toUpperCase()));
|
|
81
|
+
const settlementOnly = declaredHeld &&
|
|
82
|
+
inputs.unsettled !== undefined &&
|
|
83
|
+
!openWrite &&
|
|
84
|
+
contradictions.every((c) => c.kind === ContradictionKind.REQUEST_NEVER_SETTLED);
|
|
85
|
+
if (contradictions.length > 0 && !settlementOnly) {
|
|
67
86
|
const kinds = contradictions.map((c) => c.kind).join(', ');
|
|
68
87
|
return {
|
|
69
88
|
verified: Verified.UNKNOWN,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
// NOT `unsettled`: this clause fires whether or not the page went idle, and naming it after
|
|
90
|
+
// idle produced verdicts that read `unsettled` beside `settled: true`.
|
|
91
|
+
verifiedReason: VerifiedReason.EVIDENCE_INCOMPLETE,
|
|
92
|
+
because: unsettledBecause(`the assertion held, but this window closed before the app finished (${kinds})`, inputs.unsettled),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// An absence assertion over a region Reticle cannot observe is not disproved, but it is not proved
|
|
96
|
+
// either. This is narrower than general partial coverage: the target itself is scoped to the blind
|
|
97
|
+
// region, so the DOM result cannot answer the question the caller asked.
|
|
98
|
+
if (inputs.absenceBlindSpot !== undefined) {
|
|
99
|
+
return {
|
|
100
|
+
verified: Verified.UNKNOWN,
|
|
101
|
+
verifiedReason: VerifiedReason.ABSENCE_BLIND_SPOT,
|
|
102
|
+
because: inputs.absenceBlindSpot,
|
|
73
103
|
};
|
|
74
104
|
}
|
|
75
105
|
// Below the contradiction clause, and for the same reason a contradiction outranks a dirty
|
|
@@ -132,19 +162,26 @@ export function decideVerified(inputs) {
|
|
|
132
162
|
//
|
|
133
163
|
// UNKNOWN, not NO: nothing is known to have failed. The remedy is in the sentence, because an
|
|
134
164
|
// agent that cannot act on a caveat will learn to skip it.
|
|
135
|
-
if (
|
|
165
|
+
if (outcomeUnread !== undefined && outcomeUnread.length > 0) {
|
|
136
166
|
return {
|
|
137
167
|
verified: Verified.UNKNOWN,
|
|
138
168
|
verifiedReason: VerifiedReason.OUTCOME_UNREAD,
|
|
139
|
-
because:
|
|
169
|
+
because: `a write returned 2xx with a response body that was never recorded (${outcomeUnread.join('; ')}), so its outcome is unread` +
|
|
170
|
+
' — a 200 describes the transport, not the result (a batch reports per-item failures in the body, and every GraphQL error is a 200). Enable it where your app calls connect(): `reticle.connect({ captureNetworkBodies: true })`, then re-run',
|
|
140
171
|
};
|
|
141
172
|
}
|
|
142
173
|
// Never settled: the page may still be moving, so the observation window may have closed early.
|
|
143
|
-
|
|
174
|
+
//
|
|
175
|
+
// Skipped when the caller DECLARED the consequence and it held. `settled` here is "a real
|
|
176
|
+
// animation frame fired within the budget", which a hidden or throttled tab never does — and a
|
|
177
|
+
// hidden tab is the ordinary case for agent-driven verification. It is corroboration; it is not
|
|
178
|
+
// the claim. It is still reported: `honesty.settled` carries the fact, and the sentence below says
|
|
179
|
+
// so in words.
|
|
180
|
+
if (false === settled && !declaredHeld) {
|
|
144
181
|
return {
|
|
145
182
|
verified: Verified.UNKNOWN,
|
|
146
183
|
verifiedReason: VerifiedReason.UNSETTLED,
|
|
147
|
-
because: 'the page never settled, so the reaction window may have closed before the app finished',
|
|
184
|
+
because: unsettledBecause('the page never settled, so the reaction window may have closed before the app finished', inputs.unsettled),
|
|
148
185
|
};
|
|
149
186
|
}
|
|
150
187
|
// Partial coverage is a real caveat but not a blocker — it is reported in `honesty.coverage` and
|
|
@@ -155,11 +192,17 @@ export function decideVerified(inputs) {
|
|
|
155
192
|
// partial` — the prose contradicting the evidence block next to it. Whichever a reader believes,
|
|
156
193
|
// one of them lied, and the whole point of this layer is that the sentence can be trusted on its
|
|
157
194
|
// own. Seen on a one-way IPC send: coverage said the outcome was unobservable, `because` said clean.
|
|
195
|
+
// A green that rests on the declaration rather than on idle says so. The page not going quiet is a
|
|
196
|
+
// real fact about the app — a poll, a timer, a throttled tab — and a caller who cares must be able
|
|
197
|
+
// to see it without re-deriving it, so it is stated here and carried in `honesty.settled`.
|
|
198
|
+
const notIdle = false === settled
|
|
199
|
+
? ', though the page never went idle — this rests on the consequence you declared, not on the page going quiet'
|
|
200
|
+
: '';
|
|
158
201
|
return {
|
|
159
202
|
verified: Verified.YES,
|
|
160
203
|
verifiedReason: VerifiedReason.PROVED,
|
|
161
204
|
because: true === honesty.coverage?.partial
|
|
162
|
-
? `assertion held at ${honesty.grade} grade with no channel disagreeing, but coverage was PARTIAL — see \`coverage\` for what went unobserved`
|
|
163
|
-
: `assertion held at ${honesty.grade} grade over a clean capture with no channel disagreeing`,
|
|
205
|
+
? `assertion held at ${honesty.grade} grade with no channel disagreeing, but coverage was PARTIAL — see \`coverage\` for what went unobserved${notIdle}`
|
|
206
|
+
: `assertion held at ${honesty.grade} grade over a clean capture with no channel disagreeing${notIdle}`,
|
|
164
207
|
};
|
|
165
208
|
}
|