@reticlehq/server 2.8.0 → 2.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 +130 -813
- 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/capsule-store.d.ts +2 -2
- package/dist/capsule/causal-summary.d.ts +16 -1
- package/dist/capsule/causal-summary.js +2 -1
- package/dist/cli/cli-doctor.js +41 -13
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-launch.d.ts +0 -17
- package/dist/cli/cli-launch.js +0 -28
- package/dist/cli/cli-parse.d.ts +1 -1
- package/dist/cli/cli-parse.js +1 -1
- package/dist/cli/cli-port.d.ts +24 -0
- package/dist/cli/cli-port.js +66 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +45 -1
- 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.js +6 -3
- 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/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +65 -70
- package/dist/crawl/crawl.d.ts +7 -2
- package/dist/crawl/crawl.js +52 -9
- package/dist/ee/audit-log.js +2 -2
- package/dist/events/body-failures.d.ts +2 -2
- package/dist/events/contradiction-folds.d.ts +45 -0
- package/dist/events/contradiction-folds.js +42 -0
- package/dist/events/contradictions.d.ts +51 -1
- package/dist/events/contradictions.js +26 -7
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +9 -3
- 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 -91
- package/dist/events/predicate-eval.js +85 -326
- 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 +127 -5
- package/dist/events/stale-response.d.ts +2 -3
- package/dist/events/stale-response.js +49 -2
- package/dist/events/unit-mismatch.d.ts +2 -2
- 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-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +9 -1
- package/dist/flows/replay.js +3 -0
- package/dist/flows/verify-change-tools.js +1 -1
- 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 +4 -0
- 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 +9 -0
- package/dist/honesty/unsettled.js +7 -3
- package/dist/honesty/verified.d.ts +29 -3
- package/dist/honesty/verified.js +67 -8
- 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 +25 -0
- package/dist/index.js +60 -10
- package/dist/init/agent-rules.d.ts +20 -0
- package/dist/init/agent-rules.js +36 -0
- package/dist/init/astro-patch.js +27 -1
- package/dist/init/confirm.d.ts +21 -3
- package/dist/init/confirm.js +32 -5
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- 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 +16 -2
- package/dist/init/mcp.js +23 -1
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +25 -1
- package/dist/init/plan.d.ts +7 -2
- package/dist/init/plan.js +66 -30
- package/dist/init/run.js +11 -5
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +12 -4
- package/dist/init/snippets.d.ts +1 -1
- package/dist/init/snippets.js +35 -7
- package/dist/init/vite-config.js +24 -2
- package/dist/input/network-mock-tools.js +8 -1
- 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.d.ts +69 -4
- package/dist/license/license.js +84 -15
- package/dist/mcp/mcp-proxy.d.ts +7 -93
- package/dist/mcp/mcp-proxy.js +183 -250
- package/dist/mcp/mcp.d.ts +27 -2
- package/dist/mcp/mcp.js +77 -36
- package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
- package/dist/mcp/proxy-daemon-probe.js +48 -0
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +110 -3
- package/dist/mcp/proxy-lifecycle.d.ts +10 -1
- package/dist/mcp/proxy-lifecycle.js +11 -2
- 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/mcp/sse-frame-parser.d.ts +19 -0
- package/dist/mcp/sse-frame-parser.js +41 -0
- 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 +55 -2
- package/dist/session/no-session-diagnosis.js +199 -22
- 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 +172 -11
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session-readiness.js +1 -1
- 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/bug-found.d.ts +18 -0
- package/dist/telemetry/bug-found.js +14 -0
- package/dist/telemetry/daemon-telemetry.js +29 -0
- package/dist/telemetry/install-source.d.ts +20 -0
- package/dist/telemetry/install-source.js +34 -0
- package/dist/telemetry/instrumentation-stall.d.ts +57 -0
- package/dist/telemetry/instrumentation-stall.js +103 -0
- package/dist/telemetry/license-activation.d.ts +48 -0
- package/dist/telemetry/license-activation.js +57 -0
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +10 -0
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +23 -3
- package/dist/tools/act-tools.js +166 -29
- package/dist/tools/assert-verdict.js +32 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/dynamic-tools.js +17 -8
- package/dist/tools/error-recovery.js +10 -6
- package/dist/tools/feedback-tools.js +31 -6
- package/dist/tools/invoke-tool.d.ts +1 -1
- package/dist/tools/invoke-tool.js +30 -8
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +11 -0
- package/dist/tools/lease-tools.js +141 -96
- package/dist/tools/observe-tools.js +9 -3
- package/dist/tools/react-grace.d.ts +4 -0
- package/dist/tools/react-grace.js +6 -3
- package/dist/tools/read-tools.js +47 -1
- 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 +15 -0
- package/dist/tools/settle-in-flight.js +42 -2
- package/dist/tools/snapshot-delta.d.ts +3 -0
- package/dist/tools/snapshot-delta.js +67 -6
- package/dist/tools/tool-kit.d.ts +38 -3
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +84 -19
- package/dist/tools/tools.js +61 -5
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +1 -2
- package/docs/actions.mdx +41 -8
- package/docs/agent-cheatsheet.md +6 -4
- package/docs/architecture.md +6 -0
- package/docs/benchmarks.md +60 -12
- package/docs/best-practices.mdx +11 -6
- package/docs/capabilities.mdx +204 -57
- package/docs/cli/affected.mdx +6 -4
- package/docs/cli/capsules.mdx +9 -8
- package/docs/cli/cloud.mdx +30 -4
- package/docs/cli/doctor.mdx +41 -8
- package/docs/cli/drive.mdx +34 -15
- package/docs/cli/gate.mdx +6 -6
- package/docs/cli/help.mdx +8 -6
- package/docs/cli/hunt.mdx +20 -4
- package/docs/cli/identify.mdx +16 -2
- package/docs/cli/init.mdx +35 -8
- package/docs/cli/kill.mdx +5 -5
- package/docs/cli/license.mdx +1 -1
- package/docs/cli/open.mdx +6 -0
- package/docs/cli/restart.mdx +6 -6
- package/docs/cli/serve.mdx +10 -3
- package/docs/cli/status.mdx +29 -5
- package/docs/cli/stop.mdx +8 -0
- package/docs/cli/verify.mdx +20 -1
- package/docs/cli/version.mdx +1 -1
- package/docs/cli/watch.mdx +9 -2
- package/docs/cli.mdx +44 -22
- package/docs/debugging.md +3 -1
- package/docs/deploy-checks.md +31 -2
- package/docs/desktop.mdx +276 -80
- package/docs/docs.json +13 -12
- package/docs/enterprise.md +9 -3
- package/docs/faq.mdx +49 -35
- package/docs/fixtures.md +2 -0
- package/docs/flows.md +7 -5
- package/docs/for-agents.md +3 -3
- package/docs/frameworks.mdx +46 -7
- package/docs/gate-plan.md +4 -2
- package/docs/gates.md +8 -6
- package/docs/getting-started.md +53 -12
- package/docs/human-control.md +2 -0
- package/docs/index.mdx +43 -1
- package/docs/install-agentic.mdx +13 -4
- package/docs/install-manual.mdx +6 -4
- package/docs/instrumentation.mdx +7 -1
- package/docs/integration-patterns.md +3 -1
- package/docs/local-registry.md +4 -2
- package/docs/multi-agent-testing.md +3 -1
- package/docs/packages/babel-plugin.mdx +15 -6
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/core.mdx +26 -16
- package/docs/packages/electron.mdx +5 -7
- package/docs/packages/eslint-plugin.mdx +5 -3
- package/docs/packages/next.mdx +3 -5
- package/docs/packages/react.mdx +3 -3
- package/docs/packages/server.mdx +2 -2
- package/docs/packages/tauri.mdx +6 -6
- package/docs/packages/test.mdx +18 -7
- package/docs/packages/vite-plugin.mdx +4 -6
- package/docs/packages.mdx +14 -13
- package/docs/platform-integration.md +2 -0
- package/docs/predicates.mdx +76 -7
- package/docs/quickstart.mdx +51 -5
- package/docs/recipes.mdx +63 -23
- package/docs/skill-file.mdx +3 -1
- package/docs/state-management.mdx +2 -0
- package/docs/system-map.md +3 -1
- package/docs/telemetry-contract.md +33 -5
- package/docs/telemetry-events.mdx +63 -18
- package/docs/telemetry.md +8 -5
- package/docs/testing.md +35 -1
- package/docs/token-efficiency.md +8 -2
- package/docs/tools-act-and-wait.mdx +64 -14
- package/docs/tools-act-sequence.mdx +34 -17
- package/docs/tools-act.mdx +24 -11
- package/docs/tools-assert.mdx +69 -10
- package/docs/tools-console.mdx +11 -7
- package/docs/tools-inspect.mdx +11 -8
- package/docs/tools-navigate.mdx +41 -5
- package/docs/tools-network.mdx +42 -9
- package/docs/tools-observe.mdx +66 -10
- package/docs/tools-overview.mdx +26 -23
- package/docs/tools-query.mdx +52 -8
- package/docs/tools-session-and-feedback.mdx +35 -9
- package/docs/tools-sessions.mdx +39 -10
- package/docs/tools-snapshot.mdx +57 -3
- package/docs/tools-state.mdx +24 -1
- package/docs/tools-tools-and-run.mdx +55 -24
- package/docs/tools-wait-for.mdx +34 -15
- package/docs/troubleshooting.mdx +80 -10
- package/docs/usage.md +46 -24
- package/docs/vs-chrome-devtools-mcp.mdx +41 -0
- package/docs/vs-playwright-mcp.mdx +42 -2
- package/docs/vs-screenshots.mdx +42 -1
- package/docs/what-is-reticle.mdx +1 -1
- package/docs/why-reticle.mdx +3 -1
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -265
|
@@ -91,6 +91,49 @@ function flights(events) {
|
|
|
91
91
|
*/
|
|
92
92
|
const MAX_FLIGHTS_PER_PATH = 60;
|
|
93
93
|
/** The one contradiction, or none. Reported once per window — a race is a fact about the window. */
|
|
94
|
+
/**
|
|
95
|
+
* How long after a superseded response lands we look for the app reacting to it.
|
|
96
|
+
*
|
|
97
|
+
* A handler runs on the microtask that resolves the fetch; the render follows within a frame or two.
|
|
98
|
+
* Beyond this the connection between the response and any change is speculation.
|
|
99
|
+
*/
|
|
100
|
+
const APPLY_WINDOW_MS = 500;
|
|
101
|
+
/**
|
|
102
|
+
* How far BEFORE the completion stamp a reaction still counts as reacting to it.
|
|
103
|
+
*
|
|
104
|
+
* Not a fudge factor — a correction for which of the two is stamped first. `NET_REQUEST` is emitted
|
|
105
|
+
* when the observer's wrapper sees the fetch settle, and the app's own handler runs on the same
|
|
106
|
+
* microtask; in practice the app's state write, signal and DOM text land at or a millisecond BEFORE
|
|
107
|
+
* the completion event that caused them.
|
|
108
|
+
*
|
|
109
|
+
* So a strictly-forward window could not see the application at all, and the rule could never fire on
|
|
110
|
+
* real traffic however plainly the app misbehaved. Sixteen passing unit tests did not catch that,
|
|
111
|
+
* because synthetic events are written with the reaction placed after the cause.
|
|
112
|
+
*/
|
|
113
|
+
const APPLY_SLACK_MS = 100;
|
|
114
|
+
/**
|
|
115
|
+
* Did the app REACT to this response — did anything user-visible move after it landed?
|
|
116
|
+
*
|
|
117
|
+
* The question the rule could not previously ask, and the difference between a defect and a
|
|
118
|
+
* correctly-handled race. An app that guards against superseded responses still RACES: two requests
|
|
119
|
+
* overlap and settle out of order exactly as before. What it does not do is act on the loser.
|
|
120
|
+
*
|
|
121
|
+
* Without this check the rule fires identically on both, which is worse than not having it: a
|
|
122
|
+
* detector that accuses correct code teaches people to ignore it, and it takes the true positives
|
|
123
|
+
* down with it.
|
|
124
|
+
*/
|
|
125
|
+
function appliedAfter(events, settledAt) {
|
|
126
|
+
return events.some((event) => {
|
|
127
|
+
if (event.t < settledAt - APPLY_SLACK_MS || event.t > settledAt + APPLY_WINDOW_MS)
|
|
128
|
+
return false;
|
|
129
|
+
return (event.type === EventType.STATE_CHANGE ||
|
|
130
|
+
event.type === EventType.SIGNAL ||
|
|
131
|
+
event.type === EventType.DOM_TEXT ||
|
|
132
|
+
event.type === EventType.DOM_ADDED ||
|
|
133
|
+
event.type === EventType.DOM_REMOVED ||
|
|
134
|
+
event.type === EventType.DOM_ATTR);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
94
137
|
export function findStaleResponses(events) {
|
|
95
138
|
// Grouped by path first: two reads of DIFFERENT endpoints can never race, and comparing them was
|
|
96
139
|
// the bulk of the work on any real page, which talks to many endpoints at once.
|
|
@@ -105,13 +148,13 @@ export function findStaleResponses(events) {
|
|
|
105
148
|
}
|
|
106
149
|
for (const group of byPath.values()) {
|
|
107
150
|
const reads = group.slice(-MAX_FLIGHTS_PER_PATH);
|
|
108
|
-
const found = raceIn(reads);
|
|
151
|
+
const found = raceIn(reads, events);
|
|
109
152
|
if (found !== undefined)
|
|
110
153
|
return [found];
|
|
111
154
|
}
|
|
112
155
|
return [];
|
|
113
156
|
}
|
|
114
|
-
function raceIn(reads) {
|
|
157
|
+
function raceIn(reads, events) {
|
|
115
158
|
for (const first of reads) {
|
|
116
159
|
for (const second of reads) {
|
|
117
160
|
if (first.id === second.id)
|
|
@@ -127,6 +170,10 @@ function raceIn(reads) {
|
|
|
127
170
|
// Parallel pagination is not a race — see differsOnlyByEnumeration.
|
|
128
171
|
if (differsOnlyByEnumeration(queryOf(first.url), queryOf(second.url)))
|
|
129
172
|
continue;
|
|
173
|
+
// The race happened. Whether it MATTERS is whether the app acted on the loser — an app that
|
|
174
|
+
// drops superseded responses races just as visibly and is not broken.
|
|
175
|
+
if (!appliedAfter(events, first.settledAt))
|
|
176
|
+
continue;
|
|
130
177
|
return {
|
|
131
178
|
kind: ContradictionKind.STALE_RESPONSE_APPLIED,
|
|
132
179
|
claim: `both reads of ${pathOf(first.url)} returned successfully and the page settled`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReticleEvent } from '@reticlehq/core';
|
|
2
|
-
import type {
|
|
2
|
+
import type { OwnContradiction } from './contradictions.js';
|
|
3
3
|
/**
|
|
4
4
|
* @param prior events from BEFORE the window, used only to learn what the API has stated.
|
|
5
5
|
*
|
|
@@ -13,4 +13,4 @@ import type { Contradiction } from './contradictions.js';
|
|
|
13
13
|
* every other rule blame this action for events it did not cause, and would re-report every earlier
|
|
14
14
|
* mistake on every assert.
|
|
15
15
|
*/
|
|
16
|
-
export declare function findUnitMismatches(events: readonly ReticleEvent[], prior?: readonly ReticleEvent[]):
|
|
16
|
+
export declare function findUnitMismatches(events: readonly ReticleEvent[], prior?: readonly ReticleEvent[]): OwnContradiction[];
|
|
@@ -118,6 +118,9 @@ export const ANNOTATE_TOOLS = [
|
|
|
118
118
|
recovery: z.string().optional(),
|
|
119
119
|
target: z.string().optional(),
|
|
120
120
|
compiled: z.string().optional(),
|
|
121
|
+
// Set when the compiled annotation would overstate what a replay can check — e.g. a
|
|
122
|
+
// success-state that flow_save will grade assertion-free (#395).
|
|
123
|
+
note: z.string().optional(),
|
|
121
124
|
code: z.string().optional(),
|
|
122
125
|
},
|
|
123
126
|
handler: (deps, args) => {
|
package/dist/flows/annotate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnnotationErrorCode, AnnotationKind, AnnotationTarget, COMPILED_PREDICATE_PREFIX, } from '@reticlehq/core';
|
|
1
|
+
import { AnnotationErrorCode, AnnotationKind, AnnotationTarget, COMPILED_PREDICATE_PREFIX, flowExpectHasConsequence, flowExpectIsPresenceOnly, } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* The PURE compiler at the heart of the annotation facet. A structured annotation +
|
|
4
4
|
* the current captured-step count → an AnnotateOutcome (a result envelope + the patch to apply to
|
|
@@ -96,11 +96,25 @@ function stepPatch(a, stepCount, stepExpect) {
|
|
|
96
96
|
patch: { stepIndex: stepCount - 1, stepExpect },
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
+
// A success end-condition that is not an observable CONSEQUENCE (signal / net / state) cannot lift
|
|
100
|
+
// the flow above the grade flow_save will give it. Saying "will succeed when ..." without that
|
|
101
|
+
// caveat is the #395 contradiction: annotate promises a check that flow_save then grades
|
|
102
|
+
// assertion-free, so a flow that only checks the console replays green through any regression. Name
|
|
103
|
+
// the grade here, using the same consequence/presence classification every grader shares.
|
|
104
|
+
const SUCCESS_ASSERTION_FREE_NOTE = 'This success-state is not an observable consequence, so flow_save will grade the flow assertion-free — it passes even when the feature is broken. Use a signal / net / state success-state to give the flow something that can fail.';
|
|
105
|
+
const SUCCESS_PRESENCE_ONLY_NOTE = 'This success-state only checks element presence, so flow_save will grade the flow presence-only — a locator healed to the wrong element still passes it. Use a signal / net / state success-state to assert an observable consequence.';
|
|
99
106
|
function flowSuccess(a, success) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
const result = {
|
|
108
|
+
ok: true,
|
|
109
|
+
target: AnnotationTarget.FLOW,
|
|
110
|
+
compiled: describeCompiled(a),
|
|
103
111
|
};
|
|
112
|
+
if (!flowExpectHasConsequence(success)) {
|
|
113
|
+
result.note = flowExpectIsPresenceOnly(success)
|
|
114
|
+
? SUCCESS_PRESENCE_ONLY_NOTE
|
|
115
|
+
: SUCCESS_ASSERTION_FREE_NOTE;
|
|
116
|
+
}
|
|
117
|
+
return { result, patch: { success } };
|
|
104
118
|
}
|
|
105
119
|
/**
|
|
106
120
|
* Human-readable confirmation text for the recorder strip / tool result,
|
package/dist/flows/coverage.d.ts
CHANGED
|
@@ -9,11 +9,18 @@ interface DeclaredSurface {
|
|
|
9
9
|
signals: readonly string[];
|
|
10
10
|
flows: readonly string[];
|
|
11
11
|
}
|
|
12
|
-
interface CoverageDimension {
|
|
12
|
+
export interface CoverageDimension {
|
|
13
13
|
total: number;
|
|
14
14
|
covered: number;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Percent covered, rounded. PRESENT ONLY WHEN IT WAS MEASURED — absent when nothing is declared,
|
|
17
|
+
* same rule as the honesty layer's `{ pct?: number; partial: boolean }`.
|
|
18
|
+
*
|
|
19
|
+
* This used to report 100 for an empty surface ("vacuously complete"), which made
|
|
20
|
+
* `covered: 0, total: 0, pct: 100` — a contradiction the caller had to notice unaided, in output an
|
|
21
|
+
* agent reads to decide whether its change is covered.
|
|
22
|
+
*/
|
|
23
|
+
pct?: number;
|
|
17
24
|
/** Declared-but-never-exercised members — what "everything" is silently missing. */
|
|
18
25
|
uncovered: string[];
|
|
19
26
|
}
|
|
@@ -21,8 +28,34 @@ export interface Coverage {
|
|
|
21
28
|
testids: CoverageDimension;
|
|
22
29
|
signals: CoverageDimension;
|
|
23
30
|
flows: CoverageDimension;
|
|
24
|
-
/** Overall percent across all three dimensions combined. */
|
|
25
|
-
overallPct
|
|
31
|
+
/** Overall percent across all three dimensions combined; absent when nothing is declared. */
|
|
32
|
+
overallPct?: number;
|
|
26
33
|
}
|
|
34
|
+
/** The gate's outcome when the project has recorded no flows at all — not a pass, and not a failure. */
|
|
35
|
+
export declare const NO_FLOWS = "no_flows";
|
|
36
|
+
/**
|
|
37
|
+
* What to do about it, since the empty state is otherwise indistinguishable from success.
|
|
38
|
+
*
|
|
39
|
+
* Names the TOOLS, not a `reticle record` CLI verb: this CLI does not dispatch one, and a suggestion
|
|
40
|
+
* that errors is a second dead end on top of the first (see suggested-commands-exist.test.ts). Same
|
|
41
|
+
* wording as the empty-suite verdict, which is the same situation reached from `reticle verify`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const NO_FLOWS_NOTE = "no flows recorded \u2014 nothing was checked. Record one with reticle_record { action: \"start\" }, then reticle_flow_save.";
|
|
44
|
+
/** The gate's flow-coverage line. `pct` and `outcome` are mutually exclusive by construction. */
|
|
45
|
+
export interface FlowCoverageReport {
|
|
46
|
+
covered: number;
|
|
47
|
+
total: number;
|
|
48
|
+
pct?: number;
|
|
49
|
+
outcome?: typeof NO_FLOWS;
|
|
50
|
+
note?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Report flow coverage so that an EMPTY suite cannot be read as a passing one.
|
|
54
|
+
*
|
|
55
|
+
* Same rule `buildSuiteVerdict` already applies to `reticle verify`, where zero flows reports
|
|
56
|
+
* `unverifiable` rather than "all 0 flows pass": a green that cannot go red is not a pass. Callers
|
|
57
|
+
* must treat a present `outcome` as not-passing.
|
|
58
|
+
*/
|
|
59
|
+
export declare function flowCoverageReport(flows: CoverageDimension): FlowCoverageReport;
|
|
27
60
|
export declare function computeCoverage(declared: DeclaredSurface, exercised: DeclaredSurface): Coverage;
|
|
28
61
|
export {};
|
package/dist/flows/coverage.js
CHANGED
|
@@ -4,11 +4,36 @@
|
|
|
4
4
|
* (contract testids/signals/flows) actually touched by passing verification, so "everything" is honest
|
|
5
5
|
* about what everything means. Pure set math; the deviation report + gate carry the number.
|
|
6
6
|
*/
|
|
7
|
+
/** The gate's outcome when the project has recorded no flows at all — not a pass, and not a failure. */
|
|
8
|
+
export const NO_FLOWS = 'no_flows';
|
|
9
|
+
/**
|
|
10
|
+
* What to do about it, since the empty state is otherwise indistinguishable from success.
|
|
11
|
+
*
|
|
12
|
+
* Names the TOOLS, not a `reticle record` CLI verb: this CLI does not dispatch one, and a suggestion
|
|
13
|
+
* that errors is a second dead end on top of the first (see suggested-commands-exist.test.ts). Same
|
|
14
|
+
* wording as the empty-suite verdict, which is the same situation reached from `reticle verify`.
|
|
15
|
+
*/
|
|
16
|
+
export const NO_FLOWS_NOTE = 'no flows recorded — nothing was checked. Record one with reticle_record { action: "start" }, then reticle_flow_save.';
|
|
17
|
+
/**
|
|
18
|
+
* Report flow coverage so that an EMPTY suite cannot be read as a passing one.
|
|
19
|
+
*
|
|
20
|
+
* Same rule `buildSuiteVerdict` already applies to `reticle verify`, where zero flows reports
|
|
21
|
+
* `unverifiable` rather than "all 0 flows pass": a green that cannot go red is not a pass. Callers
|
|
22
|
+
* must treat a present `outcome` as not-passing.
|
|
23
|
+
*/
|
|
24
|
+
export function flowCoverageReport(flows) {
|
|
25
|
+
const { covered, total } = flows;
|
|
26
|
+
if (0 === total)
|
|
27
|
+
return { covered, total, outcome: NO_FLOWS, note: NO_FLOWS_NOTE };
|
|
28
|
+
return { covered, total, ...(flows.pct === undefined ? {} : { pct: flows.pct }) };
|
|
29
|
+
}
|
|
7
30
|
function dimension(declared, exercised) {
|
|
8
31
|
const uncovered = declared.filter((d) => !exercised.has(d));
|
|
9
32
|
const covered = declared.length - uncovered.length;
|
|
10
|
-
const
|
|
11
|
-
|
|
33
|
+
const base = { total: declared.length, covered, uncovered };
|
|
34
|
+
if (0 === declared.length)
|
|
35
|
+
return base;
|
|
36
|
+
return { ...base, pct: Math.round((covered / declared.length) * 100) };
|
|
12
37
|
}
|
|
13
38
|
export function computeCoverage(declared, exercised) {
|
|
14
39
|
const testids = dimension(declared.testids, new Set(exercised.testids));
|
|
@@ -16,6 +41,7 @@ export function computeCoverage(declared, exercised) {
|
|
|
16
41
|
const flows = dimension(declared.flows, new Set(exercised.flows));
|
|
17
42
|
const total = testids.total + signals.total + flows.total;
|
|
18
43
|
const covered = testids.covered + signals.covered + flows.covered;
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
if (0 === total)
|
|
45
|
+
return { testids, signals, flows };
|
|
46
|
+
return { testids, signals, flows, overallPct: Math.round((covered / total) * 100) };
|
|
21
47
|
}
|
package/dist/flows/decision.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { type FlowFile, type FlowReplayResult, type ReplayDecision, type SuiteVerdict } from '@reticlehq/core';
|
|
2
2
|
export declare function buildDecision(result: FlowReplayResult, flow?: FlowFile): ReplayDecision;
|
|
3
|
+
/**
|
|
4
|
+
* Aggregate per-flow replays into one suite verdict — the autonomous loop's consolidated answer
|
|
5
|
+
* after a build: did anything break, and what's the prioritized fix list. Pure: pass the already
|
|
6
|
+
* decision-annotated replay results + their flows. Passing flows are counted; only failures carry
|
|
7
|
+
* detail (token-cheap), each with the actionable decision so the agent fixes without re-querying.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Why a green replay still verified nothing, or undefined when it genuinely did.
|
|
11
|
+
*
|
|
12
|
+
* A flow with no steps, or one that asserts no observable consequence, replays green no matter what
|
|
13
|
+
* the app does — the grader already knows this at save time and says so. Counting it as `passed`
|
|
14
|
+
* turned that warning into "all 1 flow pass", which is a false green in the exact feature sold as
|
|
15
|
+
* the regression suite. Classified only when the flow file is available; never guessed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function unverifiableReason(flow: FlowFile | undefined): string | undefined;
|
|
3
18
|
export declare function buildSuiteVerdict(runs: ReadonlyArray<{
|
|
4
19
|
replay: FlowReplayResult;
|
|
5
20
|
flow?: FlowFile;
|
package/dist/flows/decision.js
CHANGED
|
@@ -100,7 +100,7 @@ function suiteVerdictOf(status) {
|
|
|
100
100
|
* turned that warning into "all 1 flow pass", which is a false green in the exact feature sold as
|
|
101
101
|
* the regression suite. Classified only when the flow file is available; never guessed.
|
|
102
102
|
*/
|
|
103
|
-
function unverifiableReason(flow) {
|
|
103
|
+
export function unverifiableReason(flow) {
|
|
104
104
|
if (flow === undefined)
|
|
105
105
|
return undefined;
|
|
106
106
|
if (0 === flow.steps.length) {
|
|
@@ -2,7 +2,7 @@ import { EventType, FLOW_SIGNAL_TIMEOUT_MS, FlowErrorCode, RecordedFlowSchema, R
|
|
|
2
2
|
import { asString } from '../tools/tools-helpers.js';
|
|
3
3
|
import { replayFlow } from './flow-replay.js';
|
|
4
4
|
import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js';
|
|
5
|
-
import { buildDecision } from './decision.js';
|
|
5
|
+
import { buildDecision, unverifiableReason } from './decision.js';
|
|
6
6
|
import { waitForPredicate } from '../events/predicate.js';
|
|
7
7
|
import { computeSegments } from '../journal/rollups.js';
|
|
8
8
|
import { AssertionTiersStore } from './assertion-tiers-store.js';
|
|
@@ -174,6 +174,13 @@ export async function replayNamedFlow(deps, args) {
|
|
|
174
174
|
return errored;
|
|
175
175
|
}
|
|
176
176
|
const result = { name, status, steps };
|
|
177
|
+
// A green that cannot go red is not a pass. `reticle_flow_verify` already refuses to count these,
|
|
178
|
+
// via this same function -- a single-flow caller saw a bare `ok` and had no way to learn the flow
|
|
179
|
+
// asserts nothing. Derived from the same helper on purpose: two copies of this judgement would
|
|
180
|
+
// drift, and the sibling tools would then disagree about the same flow.
|
|
181
|
+
const cannotFail = status === ReplayStatus.OK ? unverifiableReason(loaded.value) : undefined;
|
|
182
|
+
if (cannotFail !== undefined)
|
|
183
|
+
result.unverifiable = { reason: cannotFail };
|
|
177
184
|
if (status !== ReplayStatus.OK)
|
|
178
185
|
result.decision = buildDecision(result, loaded.value);
|
|
179
186
|
applyStartPathHint(result, startPathHint);
|
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_verify{action:"flows"} reports.'),
|
|
288
296
|
decision: z
|
|
289
297
|
.object({
|
|
290
298
|
verdict: z.string(),
|
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 {
|
|
@@ -97,7 +97,7 @@ export const VERIFY_CHANGE_TOOLS = [
|
|
|
97
97
|
const cursor = cursorBeforeReplay(deps, args);
|
|
98
98
|
const verify = FLOW_TOOLS.find((tool) => tool.name === ReticleTool.FLOW_VERIFY);
|
|
99
99
|
if (verify === undefined)
|
|
100
|
-
throw new Error('
|
|
100
|
+
throw new Error('the whole-suite replay tool is not registered');
|
|
101
101
|
const suite = asRecord(await verify.handler(deps, {
|
|
102
102
|
names: affected,
|
|
103
103
|
...(asNumber(args['parallel']) === undefined ? {} : { parallel: args['parallel'] }),
|
|
@@ -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:
|
|
@@ -40,6 +40,10 @@ const LABEL = {
|
|
|
40
40
|
// nothing here was missed, there was simply never a verdict to observe, and no amount of extra
|
|
41
41
|
// instrumentation would produce one.
|
|
42
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)',
|
|
43
47
|
[BlindSpotKind.WRAPPED_NETWORK]: () => 'fetch was already wrapped before Reticle, so recorded requests may differ from what was sent',
|
|
44
48
|
// A fact about the app's wiring, not a tally. Says what the empty network view MEANS, because an
|
|
45
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
|
}
|
|
@@ -21,6 +21,15 @@ export interface UnsettledWindow {
|
|
|
21
21
|
waitedFor: string;
|
|
22
22
|
/** Requests that started in this window and had not come back, as "METHOD url". */
|
|
23
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[];
|
|
24
33
|
}
|
|
25
34
|
/**
|
|
26
35
|
* The wait, in one phrase.
|
|
@@ -64,10 +64,14 @@ export function unsettledBecause(base, window) {
|
|
|
64
64
|
if (window === undefined)
|
|
65
65
|
return `${base} — re-check, or assert a consequence that settles`;
|
|
66
66
|
if (0 === window.stillInFlight.length) {
|
|
67
|
+
const repeated = window.repeated ?? [];
|
|
67
68
|
return (`${base}. It waited for ${window.waitedFor}, and no request started in this window was left ` +
|
|
68
|
-
'open — what kept the page busy was its own churn
|
|
69
|
-
|
|
70
|
-
|
|
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');
|
|
71
75
|
}
|
|
72
76
|
const shown = window.stillInFlight.slice(0, MAX_LISTED_REQUESTS);
|
|
73
77
|
const rest = window.stillInFlight.length - shown.length;
|