@reticlehq/server 2.11.0 → 2.12.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 +26 -2
- package/dist/bridge/bridge.d.ts +0 -13
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/capsule/capsule-store.d.ts +2 -1
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +8 -0
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-parse.d.ts +3 -1
- package/dist/cli/cli-parse.js +29 -7
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +1 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/cloud-cli.js +12 -6
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +3 -1
- package/dist/cli/doctor-rows.js +3 -1
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +143 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli.js +37 -110
- package/dist/cloud/cloud-config.d.ts +2 -1
- package/dist/cloud/cloud-sync.d.ts +11 -0
- package/dist/cloud/cloud-sync.js +32 -0
- package/dist/crawl/crawl.d.ts +9 -13
- package/dist/crawl/crawl.js +20 -3
- package/dist/daemon/daemon-resolve.d.ts +79 -0
- package/dist/daemon/daemon-resolve.js +118 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/dev/stale-issue-guard.d.ts +0 -2
- package/dist/dev/stale-issue-guard.js +1 -1
- package/dist/events/contradictions.d.ts +41 -0
- package/dist/events/contradictions.js +125 -12
- package/dist/events/declared.d.ts +14 -0
- package/dist/events/declared.js +33 -0
- package/dist/events/echo-mismatch.js +14 -0
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +13 -0
- package/dist/events/predicate-asks.js +22 -0
- package/dist/events/predicate-eval.js +64 -22
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +43 -3
- package/dist/events/predicate-schema.d.ts +44 -0
- package/dist/events/predicate-schema.js +126 -23
- package/dist/events/predicate.d.ts +15 -0
- package/dist/events/predicate.js +41 -2
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +0 -2
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-replay-run.js +27 -4
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +32 -4
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +6 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +4 -1
- package/dist/honesty/blind-spots.d.ts +0 -17
- package/dist/honesty/blind-spots.js +1 -1
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/instrumentation-gaps.d.ts +30 -6
- package/dist/honesty/instrumentation-gaps.js +61 -5
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +15 -0
- package/dist/honesty/verified.js +68 -4
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +1 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-store.d.ts +0 -5
- package/dist/impact/impact-store.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/init/agent-rules.d.ts +17 -0
- package/dist/init/agent-rules.js +31 -1
- package/dist/init/capabilities.d.ts +0 -2
- package/dist/init/capabilities.js +1 -1
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +0 -25
- package/dist/init/confirm.js +3 -3
- package/dist/init/cra.d.ts +14 -1
- package/dist/init/cra.js +43 -12
- package/dist/init/dev-script.d.ts +48 -0
- package/dist/init/dev-script.js +63 -0
- package/dist/init/mcp.d.ts +16 -5
- package/dist/init/mcp.js +26 -10
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +83 -9
- package/dist/init/plan.d.ts +10 -30
- package/dist/init/plan.js +30 -15
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +0 -26
- package/dist/init/run.js +14 -87
- package/dist/init/snippets.js +53 -10
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +3 -2
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/intent/inline-intent.d.ts +46 -0
- package/dist/intent/inline-intent.js +103 -0
- package/dist/intent/intent-store.d.ts +37 -0
- package/dist/intent/intent-store.js +112 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +80 -0
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +25 -0
- package/dist/journal/envelope.d.ts +26 -26
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/mcp/attach-memory.d.ts +67 -0
- package/dist/mcp/attach-memory.js +142 -0
- package/dist/mcp/mcp-proxy.d.ts +0 -7
- package/dist/mcp/mcp-proxy.js +14 -2
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +45 -23
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +10 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +12 -1
- package/dist/pool/browser-pool.js +31 -1
- package/dist/pool/playwright-launcher.js +3 -1
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.js +6 -1
- package/dist/project/project-tools.js +3 -32
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/verification-sync.js +2 -2
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +10 -0
- package/dist/session/no-session-diagnosis.js +24 -7
- package/dist/session/no-session-next-action.d.ts +5 -0
- package/dist/session/no-session-next-action.js +35 -3
- package/dist/session/no-session-watch.d.ts +8 -0
- package/dist/session/no-session-watch.js +82 -64
- package/dist/session/output-budget.js +4 -1
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-manager.js +50 -1
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +78 -0
- package/dist/session/session.d.ts +31 -1
- package/dist/session/session.js +75 -2
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/project-profile.d.ts +0 -8
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +126 -21
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +20 -3
- package/dist/tools/assert-verdict.js +36 -4
- package/dist/tools/browser-tools.js +0 -20
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +123 -69
- package/dist/tools/dynamic-tools.js +29 -8
- package/dist/tools/error-recovery.d.ts +24 -14
- package/dist/tools/error-recovery.js +70 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +62 -2
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +5 -1
- package/dist/tools/lease-tools.js +22 -8
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/observe-tools.js +63 -26
- package/dist/tools/read-tools.js +5 -3
- package/dist/tools/real-input-attempt.d.ts +22 -0
- package/dist/tools/real-input-attempt.js +153 -1
- package/dist/tools/reconcile-tools.js +18 -4
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +16 -0
- package/dist/tools/tool-names.d.ts +3 -0
- package/dist/tools/tool-names.js +3 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools.js +22 -4
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/update-nudge.d.ts +7 -10
- package/dist/update/update-nudge.js +12 -3
- package/dist/update/updater.d.ts +1 -1
- package/docs/README.md +2 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/verify.mdx +4 -5
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +2 -2
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +3 -2
- package/package.json +2 -2
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
package/dist/tools/act-tools.js
CHANGED
|
@@ -18,12 +18,15 @@ import { causalSummary } from '../capsule/causal-summary.js';
|
|
|
18
18
|
import { findContradictions } from '../events/contradictions.js';
|
|
19
19
|
import { gapsForAction } from '../honesty/instrumentation-gaps.js';
|
|
20
20
|
import { noteSessionGaps } from '../honesty/gap-ledger.js';
|
|
21
|
+
import { isChangeUndeclared } from '../honesty/undeclared-change.js';
|
|
22
|
+
import { openSessionIntents } from '../intent/open-intents.js';
|
|
23
|
+
import { dischargeInlineIntent, inlineVerdictId, linkInlineIntent, } from '../intent/inline-intent.js';
|
|
21
24
|
import { declaresState } from '../events/predicate-asks.js';
|
|
22
25
|
import { isStateUnwatched } from '../honesty/blind-spots.js';
|
|
23
26
|
import { inFlightRequestLabels, repeatedRequestLabels, waitForInFlight, } from './settle-in-flight.js';
|
|
24
27
|
import { waitForReaction } from './react-grace.js';
|
|
25
28
|
import { decideVerified } from '../honesty/verified.js';
|
|
26
|
-
import { declaredExpectations } from '../events/declared.js';
|
|
29
|
+
import { declaredExpectations, declaresBodyIndependentChannel } from '../events/declared.js';
|
|
27
30
|
import { readsDomState } from '../honesty/already-true.js';
|
|
28
31
|
import { describeWaitTarget } from '../honesty/unsettled.js';
|
|
29
32
|
import { saveFailedAssertCapsule } from './act-capsule.js';
|
|
@@ -34,12 +37,31 @@ import { buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage
|
|
|
34
37
|
import { hasAcceptedWrite } from '../honesty/accepted-write.js';
|
|
35
38
|
import { unreadWriteLabels } from '../honesty/unread-outcome.js';
|
|
36
39
|
import { evaluatePredicate, waitForPredicate, provenExpectedLinks, PredicateSchema, } from '../events/predicate.js';
|
|
40
|
+
import { awaitDocumentSuccessor } from '../session/session-successor.js';
|
|
37
41
|
import { healthEnvelope, refuseIfThrottled } from '../session/session-health.js';
|
|
38
42
|
import { pausedShortCircuit, pausedOutputShape, withControl, PAUSED_NO_VERDICT, } from '../session/control-envelope.js';
|
|
39
43
|
import { asString, asNumber, asRecord, sourceOf } from './tools-helpers.js';
|
|
40
|
-
import { sessionIdShape } from './tool-kit.js';
|
|
44
|
+
import { intentArg, sessionIdShape } from './tool-kit.js';
|
|
41
45
|
import { asActionType, gradeOf } from './act-helpers.js';
|
|
42
|
-
import { tryRealInput } from './real-input-attempt.js';
|
|
46
|
+
import { tryRealInput, rewriteUploadArgs } from './real-input-attempt.js';
|
|
47
|
+
/**
|
|
48
|
+
* Single dispatch point for every ACT and ACT_SEQUENCE command.
|
|
49
|
+
*
|
|
50
|
+
* This is the seam the reviewer asked for: instead of wiring rewriteUploadArgs at three separate
|
|
51
|
+
* call sites (reticle_act, reticle_act_and_wait, reticle_act_sequence) we intercept once here,
|
|
52
|
+
* which also covers flow-replay, crawl, and any future dispatch site that goes through this helper.
|
|
53
|
+
*
|
|
54
|
+
* captureAct is called AFTER this, so the flow-recording captures the pre-rewrite args (the path
|
|
55
|
+
* the agent actually wrote) rather than the base64 blob — replay would otherwise send 750 KiB of
|
|
56
|
+
* base64 to the browser as if it were an inline content call.
|
|
57
|
+
*/
|
|
58
|
+
async function actCommand(deps, session, actArgs, timeoutMs) {
|
|
59
|
+
const rewritten = await rewriteUploadArgs(deps, 'string' === typeof actArgs['action'] ? actArgs['action'] : '', asRecord(actArgs['args']));
|
|
60
|
+
const bridgeArgs = { ...actArgs, args: rewritten };
|
|
61
|
+
return timeoutMs !== undefined
|
|
62
|
+
? session.command(ReticleCommand.ACT, bridgeArgs, timeoutMs)
|
|
63
|
+
: session.command(ReticleCommand.ACT, bridgeArgs);
|
|
64
|
+
}
|
|
43
65
|
/**
|
|
44
66
|
* Narrow the wire's `action` to a real ActionType, or undefined.
|
|
45
67
|
*
|
|
@@ -113,7 +135,7 @@ export const ACT_TOOLS = [
|
|
|
113
135
|
args: z
|
|
114
136
|
.record(z.unknown())
|
|
115
137
|
.optional()
|
|
116
|
-
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration.'),
|
|
138
|
+
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
|
|
117
139
|
refuseWhenThrottled: z
|
|
118
140
|
.boolean()
|
|
119
141
|
.optional()
|
|
@@ -179,7 +201,7 @@ export const ACT_TOOLS = [
|
|
|
179
201
|
settledOutcome = real.settled ?? undefined;
|
|
180
202
|
// Native input reports no synthetic effect block, so nothing measured in-target: undefined
|
|
181
203
|
// (the weaker empty-window test), never a fabricated zero.
|
|
182
|
-
session.lastAct.markActed(since, action, undefined);
|
|
204
|
+
session.lastAct.markActed(since, action, undefined, asString(args['ref']));
|
|
183
205
|
return withControl(session, {
|
|
184
206
|
since,
|
|
185
207
|
inputMode: InputMode.REAL,
|
|
@@ -190,7 +212,9 @@ export const ACT_TOOLS = [
|
|
|
190
212
|
...healthEnvelope(session),
|
|
191
213
|
});
|
|
192
214
|
}
|
|
193
|
-
|
|
215
|
+
// actCommand is the single interception point: it rewrites upload+path args to real bytes
|
|
216
|
+
// before any ACT command crosses the bridge, covering this call site and all others.
|
|
217
|
+
const result = await actCommand(deps, session, {
|
|
194
218
|
ref: targetRef.ref,
|
|
195
219
|
action: args['action'],
|
|
196
220
|
args: args['args'] ?? {},
|
|
@@ -205,7 +229,7 @@ export const ACT_TOOLS = [
|
|
|
205
229
|
// Keep what only this call measured, so the observe that judges this window can ask whether
|
|
206
230
|
// anything happened INSIDE the target — the one fact that separates a dead control from a
|
|
207
231
|
// page that was merely busy with something else.
|
|
208
|
-
session.lastAct.markActed(since, action, mutatedWithin(r));
|
|
232
|
+
session.lastAct.markActed(since, action, mutatedWithin(r), asString(args['ref']));
|
|
209
233
|
return withControl(session, {
|
|
210
234
|
since,
|
|
211
235
|
inputMode: InputMode.SYNTHETIC,
|
|
@@ -280,7 +304,7 @@ export const ACT_TOOLS = [
|
|
|
280
304
|
for (let i = 0; i < inputSteps.length; i++) {
|
|
281
305
|
const step = asRecord(inputSteps[i]);
|
|
282
306
|
try {
|
|
283
|
-
const result = await
|
|
307
|
+
const result = await actCommand(deps, session, { ref: step['ref'], action: step['action'], args: step['args'] ?? {} }, perStepTimeout);
|
|
284
308
|
if (!result.ok) {
|
|
285
309
|
stalledAt = i;
|
|
286
310
|
stepResults.push({
|
|
@@ -372,7 +396,7 @@ export const ACT_TOOLS = [
|
|
|
372
396
|
args: z
|
|
373
397
|
.record(z.unknown())
|
|
374
398
|
.optional()
|
|
375
|
-
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control.'),
|
|
399
|
+
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control. For upload: { path } is a path on disk (absolute or relative to project root; daemon reads real bytes) or { name, content?, type? } for inline bytes.'),
|
|
376
400
|
predicate: PredicateSchema.optional().describe('Alias for `until` (the name reticle_assert / reticle_wait_for use).'),
|
|
377
401
|
until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as reticle_assert). OMIT to wait for the page to SETTLE — network + DOM idle — the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
|
|
378
402
|
timeout_ms: z
|
|
@@ -383,6 +407,7 @@ export const ACT_TOOLS = [
|
|
|
383
407
|
.boolean()
|
|
384
408
|
.optional()
|
|
385
409
|
.describe('Throw if the tab is throttled. Default: false.'),
|
|
410
|
+
intent: intentArg,
|
|
386
411
|
...sessionIdShape,
|
|
387
412
|
},
|
|
388
413
|
outputSchema: {
|
|
@@ -446,6 +471,10 @@ export const ACT_TOOLS = [
|
|
|
446
471
|
since: z
|
|
447
472
|
.number()
|
|
448
473
|
.describe('Cursor for this act — pass to reticle_observe/reticle_assert for the full timeline.'),
|
|
474
|
+
sessionId: z
|
|
475
|
+
.string()
|
|
476
|
+
.optional()
|
|
477
|
+
.describe('The session that answered, when a full-document navigation replaced the one that was acted on. Absent when the original session survived.'),
|
|
449
478
|
session: z
|
|
450
479
|
.object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
|
|
451
480
|
.optional(),
|
|
@@ -453,7 +482,9 @@ export const ACT_TOOLS = [
|
|
|
453
482
|
...pausedOutputShape,
|
|
454
483
|
},
|
|
455
484
|
handler: async (deps, args) => {
|
|
456
|
-
|
|
485
|
+
let session = deps.sessions.resolve(asString(args['sessionId']));
|
|
486
|
+
const acted = session;
|
|
487
|
+
const actedSessionId = session.id;
|
|
457
488
|
// Live-control: refuse to drive the page (no act, no predicate eval) while paused.
|
|
458
489
|
//
|
|
459
490
|
// A VERDICT rides out with the refusal. This is the one tool here that promises `verified`,
|
|
@@ -472,6 +503,10 @@ export const ACT_TOOLS = [
|
|
|
472
503
|
? parsePredicate(withUntil['until'])
|
|
473
504
|
: { kind: PredicateKind.SETTLED };
|
|
474
505
|
const timeout = asNumber(args['timeout_ms']) ?? DEFAULT_ASSERT_TIMEOUT_MS;
|
|
506
|
+
// An intent declared here lands in the ledger BEFORE the verdict is drawn, which is what makes
|
|
507
|
+
// the undeclared-change gap silent on THIS verdict rather than the next one: it reads the
|
|
508
|
+
// ledger below and finds something open. The discharge comes after that read.
|
|
509
|
+
const intentId = await linkInlineIntent(deps, asString(args['sessionId']), asString(args['intent']), PredicateKind.SETTLED === until.kind ? undefined : until);
|
|
475
510
|
// Before anything is driven: this path cannot honour a native-input request, and taking the
|
|
476
511
|
// argument and ignoring it told the agent its trusted click had happened. See act-danger.
|
|
477
512
|
assertNativeInputSupported(asRecord(args['args']));
|
|
@@ -480,12 +515,17 @@ export const ACT_TOOLS = [
|
|
|
480
515
|
const resolved = await resolveActTarget(session, args);
|
|
481
516
|
if ('error' === resolved.kind)
|
|
482
517
|
throw new Error(resolved.message);
|
|
483
|
-
|
|
518
|
+
let since = session.elapsed();
|
|
519
|
+
const actedSince = since;
|
|
484
520
|
// The cursor + effect are marked after the act dispatches (below) — a refused act leaves none.
|
|
485
521
|
// The attribution window stays open across the settle wait below, so post-dispatch async events
|
|
486
522
|
// (the whole point of act_and_wait) attribute to this action. finishAction fires after the wait.
|
|
487
523
|
session.beginAction(ReticleTool.ACT_AND_WAIT, asRecord(args));
|
|
488
524
|
let settledOutcome;
|
|
525
|
+
// The verdict, written into the action record so a LATER turn can read what this one proved.
|
|
526
|
+
// A verdict that lives only in the response lives only in the agent's context window, which is
|
|
527
|
+
// exactly the copy a compaction destroys — see runs/run-context.ts.
|
|
528
|
+
let verdictEffect;
|
|
489
529
|
// Was the declared consequence ALREADY TRUE? Only asked for predicates that read live DOM
|
|
490
530
|
// state — event-based ones are floored at this act's cursor and cannot be satisfied by the
|
|
491
531
|
// past, so they need no pre-check and pay nothing. One extra query, on the path where a green
|
|
@@ -494,7 +534,8 @@ export const ACT_TOOLS = [
|
|
|
494
534
|
? (await evaluatePredicate(session, until, since, false)).pass
|
|
495
535
|
: false;
|
|
496
536
|
try {
|
|
497
|
-
|
|
537
|
+
// actCommand is the single interception point for upload+path rewrite.
|
|
538
|
+
const actResult = await actCommand(deps, session, {
|
|
498
539
|
ref: resolved.ref,
|
|
499
540
|
action: args['action'],
|
|
500
541
|
args: args['args'] ?? {},
|
|
@@ -504,12 +545,31 @@ export const ACT_TOOLS = [
|
|
|
504
545
|
captureAct(deps.recordings, args, actResult.result);
|
|
505
546
|
// Dispatched — now this act owns the cursor and the effect. Marking its OWN measurement also
|
|
506
547
|
// stops the spread below from inheriting an earlier reticle_act's action and mutation count.
|
|
507
|
-
session.lastAct.markActed(since, asString(args['action']), mutatedWithin(asRecord(actResult.result)));
|
|
548
|
+
session.lastAct.markActed(since, asString(args['action']), mutatedWithin(asRecord(actResult.result)), asString(args['ref']));
|
|
508
549
|
// Honesty: floor the predicate at this act's cursor so a stale buffered event can't satisfy it.
|
|
509
550
|
const predicateStarted = session.elapsed();
|
|
510
|
-
|
|
551
|
+
let verdict = timeout > 0
|
|
511
552
|
? await waitForPredicate(session, until, timeout, since)
|
|
512
553
|
: await evaluatePredicate(session, until, since);
|
|
554
|
+
// A full-document navigation tears the SDK down mid-wait. `navigate` already waits for the
|
|
555
|
+
// HELLO and returns the new id; this path used to grade `observation_lost` and leave the
|
|
556
|
+
// agent holding a dead id, so the next assert failed even though the new page had loaded.
|
|
557
|
+
// Follow the unique same-origin successor (the awaitArrival idea, without a known URL) and
|
|
558
|
+
// evaluate there. Two live tabs at that origin is still a guess — we do not follow then.
|
|
559
|
+
if (true === verdict.observationLost && timeout > 0) {
|
|
560
|
+
const remaining = timeout - (session.elapsed() - predicateStarted);
|
|
561
|
+
const next = remaining > 0 ? await awaitDocumentSuccessor(deps.sessions, session, remaining) : null;
|
|
562
|
+
if (next !== null) {
|
|
563
|
+
// Compute leftover on the departed session before we point `session` at the successor —
|
|
564
|
+
// the new session's elapsed() starts at 0 and would refund the whole budget.
|
|
565
|
+
const leftover = timeout - (session.elapsed() - predicateStarted);
|
|
566
|
+
session = next;
|
|
567
|
+
since = 0;
|
|
568
|
+
if (leftover > 0) {
|
|
569
|
+
verdict = await waitForPredicate(session, until, leftover, since);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
513
573
|
// The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is
|
|
514
574
|
// while the write is still in flight — so the verdict was taken over a window the app had not
|
|
515
575
|
// finished, and came back `unknown / unsettled` asking the CALLER to re-check. A login that
|
|
@@ -547,7 +607,8 @@ export const ACT_TOOLS = [
|
|
|
547
607
|
const actedSource = sourceOf(r['source']);
|
|
548
608
|
// Remembered on the session so a LATER assertion can name a file even when its failure has no
|
|
549
609
|
// element to point at — a signal that never fired, a request that was never made.
|
|
550
|
-
|
|
610
|
+
const actedSourceLabel = actedSource === undefined ? undefined : `${actedSource.file}:${String(actedSource.line)}`;
|
|
611
|
+
session.lastAct.markSource(actedSourceLabel);
|
|
551
612
|
const windowEvents = session.eventsSince(since);
|
|
552
613
|
const trace = summarizeReaction(buildReactionReport(windowEvents, session.elapsed() - since));
|
|
553
614
|
// The bounded Tier-1 causal summary — net/console/state/storage diffs the trace's counts miss.
|
|
@@ -590,8 +651,11 @@ export const ACT_TOOLS = [
|
|
|
590
651
|
...(gapNote === undefined ? [] : [CaptureLoss.TRANSPORT_GAP]),
|
|
591
652
|
...(impeaching.note === undefined ? [] : [CaptureLoss.BLIND_SPOT]),
|
|
592
653
|
];
|
|
654
|
+
// Named because the discharge below records the same grade the verdict reports, rather than a
|
|
655
|
+
// second reading of the same evidence that could drift from it.
|
|
656
|
+
const grade = gradeOf(gradedLinks);
|
|
593
657
|
const honesty = buildHonestyBlock({
|
|
594
|
-
grade
|
|
658
|
+
grade,
|
|
595
659
|
attribution: 'window',
|
|
596
660
|
// Did the buffer lose scarce evidence FROM THIS WINDOW — not "did it evict anything while
|
|
597
661
|
// the action ran", which was the previous rule and which is true on essentially every live
|
|
@@ -644,6 +708,8 @@ export const ACT_TOOLS = [
|
|
|
644
708
|
// not evidence the destination rendered either — `alreadyTrue` decides that, once.
|
|
645
709
|
renderProved: verdict.pass && !alreadyTrue && declared.rendersContent,
|
|
646
710
|
...session.lastAct.effect(),
|
|
711
|
+
currentDocumentId: session.currentDocumentId,
|
|
712
|
+
currentEditEpoch: session.currentEditEpoch,
|
|
647
713
|
});
|
|
648
714
|
// The single field an agent reads. Everything below it is the evidence it was derived from;
|
|
649
715
|
// this is the only one that has to be interpreted, and now it interprets itself.
|
|
@@ -656,6 +722,9 @@ export const ACT_TOOLS = [
|
|
|
656
722
|
// overriding it — see `declaredConsequence`. An explicit `{ kind: "settled" }` is not a
|
|
657
723
|
// declaration about the app's behaviour, it IS the idle wait, so it does not count.
|
|
658
724
|
declaredConsequence: until.kind !== PredicateKind.SETTLED,
|
|
725
|
+
// A body-independent declaration that held is a channel the unread payload does not own.
|
|
726
|
+
// Omit when false: a net-only `until` must still hit `outcome_unread`.
|
|
727
|
+
...(declaresBodyIndependentChannel(until) ? { independentOfBody: true } : {}),
|
|
659
728
|
...(alreadyTrue ? { alreadyTrue } : {}),
|
|
660
729
|
// An assertion nobody could evaluate must not be reported as one the app failed.
|
|
661
730
|
...(verdict.inconclusive === undefined ? {} : { inconclusive: verdict.inconclusive }),
|
|
@@ -683,18 +752,55 @@ export const ACT_TOOLS = [
|
|
|
683
752
|
// Computed once: the verdict block reports it, and the instrumentation gaps are a second
|
|
684
753
|
// reading of the same evidence rather than a new observation.
|
|
685
754
|
const actionSummary = causalSummary(windowEvents, { stateUnwatched });
|
|
755
|
+
// Asked of every verdict drawn after an observed edit, not once per edit — see
|
|
756
|
+
// isChangeUndeclared for why repeating it is disclosure rather than nagging.
|
|
757
|
+
// Read ONCE and used twice: `changeUndeclared` asks whether the ledger is empty, and the
|
|
758
|
+
// undischarged-intent gap asks how much it still holds. Two reads would be two chances for
|
|
759
|
+
// them to disagree about the same file.
|
|
760
|
+
const openIntents = await openSessionIntents(deps, asString(args['sessionId']));
|
|
761
|
+
const changeUndeclared = await isChangeUndeclared(session.currentEditEpoch, () => Promise.resolve(openIntents));
|
|
686
762
|
const gaps = gapsForAction({
|
|
687
763
|
pass: verdict.pass,
|
|
688
764
|
proved: decision.verifiedReason === VerifiedReason.PROVED,
|
|
689
|
-
|
|
765
|
+
changeUndeclared,
|
|
766
|
+
source: actedSourceLabel,
|
|
690
767
|
ref: asString(args['ref']),
|
|
691
768
|
stateAsked: declaresState(until),
|
|
692
769
|
stateUnwatched,
|
|
770
|
+
// What the app DECLARED, so an under-instrumented one is told without having to be asked.
|
|
771
|
+
hasCapabilities: session.hasCapabilities,
|
|
772
|
+
// What the run still owes. A green that leaves this above zero is not the same as done.
|
|
773
|
+
// MINUS the one this verdict is about to discharge.
|
|
774
|
+
//
|
|
775
|
+
// The discharge happens below, after the gaps are built, so a straight `openIntents.length`
|
|
776
|
+
// counts the intent this very call proves. Measured live on the first drive: an inline
|
|
777
|
+
// intent was declared, asserted and PROVED by the same verdict, and the result still said
|
|
778
|
+
// "1 declared intent(s) are still unproved" while the ledger recorded it `proved`. A gap
|
|
779
|
+
// that fires on the one path doing everything right is noise, and noise is what gets
|
|
780
|
+
// filtered out — taking the honest gaps with it.
|
|
781
|
+
openIntentCount: openIntents.filter((i) => !(intentId !== undefined && Verified.YES === decision.verified && i.id === intentId)).length,
|
|
693
782
|
domMutated: (session.lastAct.effect().mutatedWithin ?? 0) > 0,
|
|
694
783
|
signalsFired: actionSummary.signals.length,
|
|
695
784
|
routeChanged: actionSummary.route !== undefined,
|
|
696
785
|
routeSignalFired: actionSummary.signals.some((name) => name.startsWith('route')),
|
|
697
786
|
});
|
|
787
|
+
// The verdict IS the proof attempt, so a green one discharges the intent it was drawn for.
|
|
788
|
+
// Only a green: a red proved nothing, and `dischargeIntent` refuses an unbound intent anyway,
|
|
789
|
+
// so a bare settle leaves it open rather than collecting a proof nothing earned. The id is
|
|
790
|
+
// checked here rather than only inside the helper so a caller that declared no intent touches
|
|
791
|
+
// nothing at all, not even the clock.
|
|
792
|
+
if (intentId !== undefined && Verified.YES === decision.verified) {
|
|
793
|
+
await dischargeInlineIntent(deps, asString(args['sessionId']), intentId, {
|
|
794
|
+
verdictId: inlineVerdictId(ReticleTool.ACT_AND_WAIT, deps.now()),
|
|
795
|
+
grade,
|
|
796
|
+
at: deps.now(),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
verdictEffect = {
|
|
800
|
+
claim: describeWaitTarget(until),
|
|
801
|
+
verified: decision.verified,
|
|
802
|
+
...(actedSourceLabel === undefined ? {} : { source: actedSourceLabel }),
|
|
803
|
+
};
|
|
698
804
|
// Recorded on the session, so a later "am I done?" can answer with what is STILL missing
|
|
699
805
|
// rather than with everything that was ever missing. An empty list closes a gap, which is
|
|
700
806
|
// why it is noted rather than skipped.
|
|
@@ -706,9 +812,7 @@ export const ACT_TOOLS = [
|
|
|
706
812
|
// Promoted out of `effect` on red only. On green nobody needs it and it is noise; on red it
|
|
707
813
|
// is the first thing the agent wants, and burying a file:line inside the effect block is
|
|
708
814
|
// most of the way to not reporting it at all.
|
|
709
|
-
...(verdict.pass ||
|
|
710
|
-
? {}
|
|
711
|
-
: { source: `${actedSource.file}:${String(actedSource.line)}` }),
|
|
815
|
+
...(verdict.pass || actedSourceLabel === undefined ? {} : { source: actedSourceLabel }),
|
|
712
816
|
trace,
|
|
713
817
|
...(capsuleSaved === undefined ? {} : { capsuleSaved }),
|
|
714
818
|
// The window cannot say whether anything was WATCHING state — that is a level fact the
|
|
@@ -730,11 +834,12 @@ export const ACT_TOOLS = [
|
|
|
730
834
|
honesty,
|
|
731
835
|
...(capsule === undefined ? {} : { capsule }),
|
|
732
836
|
since,
|
|
837
|
+
...(session.id === actedSessionId ? {} : { sessionId: session.id }),
|
|
733
838
|
...healthEnvelope(session),
|
|
734
839
|
});
|
|
735
840
|
}
|
|
736
841
|
finally {
|
|
737
|
-
|
|
842
|
+
acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
|
|
738
843
|
}
|
|
739
844
|
},
|
|
740
845
|
},
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Predicate } from '../events/predicate-schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* The `file:line` an ASSERTION is entitled to report — its own evidence, or nothing.
|
|
4
|
+
*
|
|
5
|
+
* `reticle_assert` and `reticle_wait_for` drive nothing, so the last act's source is whatever
|
|
6
|
+
* unrelated action ran before them. Journaling it made an assert on one component claim it had
|
|
7
|
+
* proven something about another, and an assert that matched nothing claim a location at all — a
|
|
8
|
+
* wrong file:line in front of an agent, persisted, and read back by `reticle_context` long after
|
|
9
|
+
* the turn that produced it.
|
|
10
|
+
*
|
|
11
|
+
* The one borrow that survives is the documented one, and only on a failure: a predicate with no DOM
|
|
12
|
+
* clause has no element to point at, and the handler that should have fired the missing signal or
|
|
13
|
+
* made the missing request lives with the control that was last driven. That is what the tools'
|
|
14
|
+
* output schemas describe, and it is a pointer to the code under test rather than a guess about
|
|
15
|
+
* which code this verdict was about.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assertSource(facts: {
|
|
18
|
+
predicate: Predicate;
|
|
19
|
+
evidence: unknown;
|
|
20
|
+
pass: boolean;
|
|
21
|
+
lastActSource: string | undefined;
|
|
22
|
+
}): string | undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { declaresDom } from '../events/predicate-asks.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where an assertion's OWN evidence is written, as `file:line`.
|
|
4
|
+
*
|
|
5
|
+
* The element descriptors an oracle returns already carry the nearest `data-reticle-source` the build
|
|
6
|
+
* plugin stamped, so a predicate that matched an element knows exactly where that element lives. It
|
|
7
|
+
* is only reported when every matched descriptor agrees: two loci and no way to choose is the same
|
|
8
|
+
* problem as no locus at all, and a guessed pointer costs the agent the trip AND leaves it further
|
|
9
|
+
* from the code.
|
|
10
|
+
*
|
|
11
|
+
* Near-miss evidence is deliberately NOT read. A near miss is a DIFFERENT element than the one the
|
|
12
|
+
* caller asked about, so its line is a suggestion dressed as a fact.
|
|
13
|
+
*/
|
|
14
|
+
function evidenceSource(evidence) {
|
|
15
|
+
if (!Array.isArray(evidence) || 0 === evidence.length)
|
|
16
|
+
return undefined;
|
|
17
|
+
let agreed;
|
|
18
|
+
for (const entry of evidence) {
|
|
19
|
+
if ('object' !== typeof entry || null === entry)
|
|
20
|
+
return undefined;
|
|
21
|
+
const found = entry.source;
|
|
22
|
+
if ('string' !== typeof found || 0 === found.length)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (agreed === undefined)
|
|
25
|
+
agreed = found;
|
|
26
|
+
else if (agreed !== found)
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return agreed;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The `file:line` an ASSERTION is entitled to report — its own evidence, or nothing.
|
|
33
|
+
*
|
|
34
|
+
* `reticle_assert` and `reticle_wait_for` drive nothing, so the last act's source is whatever
|
|
35
|
+
* unrelated action ran before them. Journaling it made an assert on one component claim it had
|
|
36
|
+
* proven something about another, and an assert that matched nothing claim a location at all — a
|
|
37
|
+
* wrong file:line in front of an agent, persisted, and read back by `reticle_context` long after
|
|
38
|
+
* the turn that produced it.
|
|
39
|
+
*
|
|
40
|
+
* The one borrow that survives is the documented one, and only on a failure: a predicate with no DOM
|
|
41
|
+
* clause has no element to point at, and the handler that should have fired the missing signal or
|
|
42
|
+
* made the missing request lives with the control that was last driven. That is what the tools'
|
|
43
|
+
* output schemas describe, and it is a pointer to the code under test rather than a guess about
|
|
44
|
+
* which code this verdict was about.
|
|
45
|
+
*/
|
|
46
|
+
export function assertSource(facts) {
|
|
47
|
+
const own = evidenceSource(facts.evidence);
|
|
48
|
+
if (own !== undefined)
|
|
49
|
+
return own;
|
|
50
|
+
if (facts.pass || declaresDom(facts.predicate))
|
|
51
|
+
return undefined;
|
|
52
|
+
return facts.lastActSource;
|
|
53
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InstrumentationGap } from '@reticlehq/core';
|
|
1
|
+
import type { InstrumentationGap, JournalVerdictEffect } from '@reticlehq/core';
|
|
2
2
|
import type { Predicate } from '../events/predicate.js';
|
|
3
3
|
import type { Session } from '../session/session.js';
|
|
4
4
|
import { type Contradiction } from '../events/contradictions.js';
|
|
@@ -15,7 +15,12 @@ import { type Contradiction } from '../events/contradictions.js';
|
|
|
15
15
|
* consult it. Asserting a moment earlier — while the POST was still open — is the same story with a
|
|
16
16
|
* different name: `request-never-settled`, also unreported.
|
|
17
17
|
*/
|
|
18
|
-
export declare function assertVerdict(session: Session, predicate: Predicate, pass: boolean,
|
|
18
|
+
export declare function assertVerdict(session: Session, predicate: Predicate, pass: boolean,
|
|
19
|
+
/**
|
|
20
|
+
* What the oracle actually looked at. The only thing this verdict is entitled to point at — see
|
|
21
|
+
* `assertSource`, which reads the matched element descriptors' own `file:line`.
|
|
22
|
+
*/
|
|
23
|
+
evidence: unknown, since: number,
|
|
19
24
|
/** Set when the assertion was never evaluated — see honesty/verified.ts. */
|
|
20
25
|
inconclusive?: string,
|
|
21
26
|
/**
|
|
@@ -23,9 +28,21 @@ inconclusive?: string,
|
|
|
23
28
|
* `reticle_wait_for` reach the same disconnect path as the act tools, and a verdict that blames
|
|
24
29
|
* the app for a lost connection is no more honest on this route than on that one.
|
|
25
30
|
*/
|
|
26
|
-
observationLost?: boolean
|
|
31
|
+
observationLost?: boolean,
|
|
32
|
+
/**
|
|
33
|
+
* Set when code changed since the last verdict with nothing declaring what it was for. Decided by
|
|
34
|
+
* `isChangeUndeclared` at the call site, which is the only place that can reach the intent ledger —
|
|
35
|
+
* this path takes a session and no `deps`.
|
|
36
|
+
*/
|
|
37
|
+
changeUndeclared?: boolean): Promise<{
|
|
27
38
|
decision: Record<string, unknown>;
|
|
28
39
|
contradictions: Contradiction[];
|
|
29
40
|
coverage: Record<string, unknown>;
|
|
30
41
|
gaps: InstrumentationGap[];
|
|
42
|
+
/**
|
|
43
|
+
* The bounded verdict, in the SAME shape `act_and_wait` writes into its journal action — so the
|
|
44
|
+
* journal holds one verdict shape rather than two and the run fold that reads it needs no second
|
|
45
|
+
* case. Built here because this is the one place that holds the typed decision and the predicate.
|
|
46
|
+
*/
|
|
47
|
+
verdictEffect: JournalVerdictEffect;
|
|
31
48
|
}>;
|
|
@@ -4,7 +4,7 @@ import { noteSessionGaps } from '../honesty/gap-ledger.js';
|
|
|
4
4
|
import { declaresState } from '../events/predicate-asks.js';
|
|
5
5
|
import { isStateUnwatched } from '../honesty/blind-spots.js';
|
|
6
6
|
import { findContradictions } from '../events/contradictions.js';
|
|
7
|
-
import { declaredExpectations } from '../events/declared.js';
|
|
7
|
+
import { declaredExpectations, declaresBodyIndependentChannel } from '../events/declared.js';
|
|
8
8
|
import { blindSpotsFromState, buildCoverageStatement, Coverage, impeachesCapture, transportGapNote, } from '../honesty/blind-spots.js';
|
|
9
9
|
import { buildHonestyBlock } from '../honesty/honesty.js';
|
|
10
10
|
import { hasAcceptedWrite } from '../honesty/accepted-write.js';
|
|
@@ -13,6 +13,7 @@ import { decideVerified } from '../honesty/verified.js';
|
|
|
13
13
|
import { describeWaitTarget } from '../honesty/unsettled.js';
|
|
14
14
|
import { inFlightRequestLabels, repeatedRequestLabels } from './settle-in-flight.js';
|
|
15
15
|
import { gradeOfPredicate } from './assert-grade.js';
|
|
16
|
+
import { assertSource } from './assert-source.js';
|
|
16
17
|
function absenceBlindSpotNote(predicate, spots) {
|
|
17
18
|
if (predicate.kind !== 'element' || predicate.absent !== true)
|
|
18
19
|
return undefined;
|
|
@@ -37,7 +38,12 @@ function absenceBlindSpotNote(predicate, spots) {
|
|
|
37
38
|
* consult it. Asserting a moment earlier — while the POST was still open — is the same story with a
|
|
38
39
|
* different name: `request-never-settled`, also unreported.
|
|
39
40
|
*/
|
|
40
|
-
export async function assertVerdict(session, predicate, pass,
|
|
41
|
+
export async function assertVerdict(session, predicate, pass,
|
|
42
|
+
/**
|
|
43
|
+
* What the oracle actually looked at. The only thing this verdict is entitled to point at — see
|
|
44
|
+
* `assertSource`, which reads the matched element descriptors' own `file:line`.
|
|
45
|
+
*/
|
|
46
|
+
evidence, since,
|
|
41
47
|
/** Set when the assertion was never evaluated — see honesty/verified.ts. */
|
|
42
48
|
inconclusive,
|
|
43
49
|
/**
|
|
@@ -45,7 +51,13 @@ inconclusive,
|
|
|
45
51
|
* `reticle_wait_for` reach the same disconnect path as the act tools, and a verdict that blames
|
|
46
52
|
* the app for a lost connection is no more honest on this route than on that one.
|
|
47
53
|
*/
|
|
48
|
-
observationLost
|
|
54
|
+
observationLost,
|
|
55
|
+
/**
|
|
56
|
+
* Set when code changed since the last verdict with nothing declaring what it was for. Decided by
|
|
57
|
+
* `isChangeUndeclared` at the call site, which is the only place that can reach the intent ledger —
|
|
58
|
+
* this path takes a session and no `deps`.
|
|
59
|
+
*/
|
|
60
|
+
changeUndeclared) {
|
|
49
61
|
// Scope caveat, stated because it is a real limitation and not a bug: blind spots are tracked per
|
|
50
62
|
// SESSION, not per assertion window, so a cross-origin iframe seen once marks every later verdict
|
|
51
63
|
// in that session partial — including ones about a region it cannot affect. That errs toward
|
|
@@ -80,6 +92,8 @@ observationLost) {
|
|
|
80
92
|
const declared = declaredExpectations(predicate);
|
|
81
93
|
const contradictions = findContradictions(windowEvents, {
|
|
82
94
|
prior,
|
|
95
|
+
currentDocumentId: session.currentDocumentId,
|
|
96
|
+
currentEditEpoch: session.currentEditEpoch,
|
|
83
97
|
expectedFailures: declared.netFailures,
|
|
84
98
|
renderProved: pass && declared.rendersContent,
|
|
85
99
|
...(actCursor !== undefined && actCursor >= since ? { actionSince: actCursor } : {}),
|
|
@@ -100,6 +114,7 @@ observationLost) {
|
|
|
100
114
|
// not override it. A fix that lived on one half of the verdict surface would leave the other
|
|
101
115
|
// half broken, and this is the tool agents call most.
|
|
102
116
|
declaredConsequence: predicate.kind !== PredicateKind.SETTLED,
|
|
117
|
+
...(declaresBodyIndependentChannel(predicate) ? { independentOfBody: true } : {}),
|
|
103
118
|
...(inconclusive === undefined ? {} : { inconclusive }),
|
|
104
119
|
...(true === observationLost ? { observationLost: true } : {}),
|
|
105
120
|
...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
|
|
@@ -131,19 +146,36 @@ observationLost) {
|
|
|
131
146
|
// state assertion against an app that registers no store.
|
|
132
147
|
const gaps = gapsForAction({
|
|
133
148
|
pass,
|
|
134
|
-
|
|
149
|
+
...(changeUndeclared === undefined ? {} : { changeUndeclared }),
|
|
150
|
+
source: session.lastAct.source(),
|
|
135
151
|
stateAsked: declaresState(predicate),
|
|
136
152
|
stateUnwatched: isStateUnwatched(spots),
|
|
153
|
+
// What the app DECLARED, so an under-instrumented one is told without having to be asked.
|
|
154
|
+
hasCapabilities: session.hasCapabilities,
|
|
137
155
|
domMutated: false,
|
|
138
156
|
signalsFired: 0,
|
|
139
157
|
routeChanged: false,
|
|
140
158
|
routeSignalFired: false,
|
|
141
159
|
});
|
|
142
160
|
noteSessionGaps(session, gaps);
|
|
161
|
+
// The one file:line this verdict is entitled to: its own evidence, or — for a failure with no DOM
|
|
162
|
+
// clause to point at — the control last driven. Returned so the RESPONSE can echo the same value
|
|
163
|
+
// the journal keeps; one verdict must never carry two different pointers.
|
|
164
|
+
const source = assertSource({
|
|
165
|
+
predicate,
|
|
166
|
+
evidence,
|
|
167
|
+
pass,
|
|
168
|
+
lastActSource: session.lastAct.source(),
|
|
169
|
+
});
|
|
143
170
|
return {
|
|
144
171
|
decision: decision,
|
|
145
172
|
contradictions,
|
|
146
173
|
coverage,
|
|
147
174
|
gaps,
|
|
175
|
+
verdictEffect: {
|
|
176
|
+
claim: describeWaitTarget(predicate),
|
|
177
|
+
verified: decision.verified,
|
|
178
|
+
...(source === undefined ? {} : { source }),
|
|
179
|
+
},
|
|
148
180
|
};
|
|
149
181
|
}
|
|
@@ -88,24 +88,4 @@ export const BROWSER_TOOLS = [
|
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
90
|
},
|
|
91
|
-
{
|
|
92
|
-
name: ReticleTool.REFRESH,
|
|
93
|
-
description: 'Reload the connected browser tab. Pass { hard: true } to bypass the browser cache (equivalent to Cmd+Shift+R). The SDK reconnects automatically after the reload.',
|
|
94
|
-
inputSchema: {
|
|
95
|
-
hard: z
|
|
96
|
-
.boolean()
|
|
97
|
-
.optional()
|
|
98
|
-
.describe('Set true to bypass the browser cache. Default: false (normal reload).'),
|
|
99
|
-
...sessionIdShape,
|
|
100
|
-
},
|
|
101
|
-
outputSchema: {
|
|
102
|
-
ok: z.boolean(),
|
|
103
|
-
},
|
|
104
|
-
handler: async (deps, args) => {
|
|
105
|
-
await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.REFRESH, {
|
|
106
|
-
hard: true === args['hard'],
|
|
107
|
-
});
|
|
108
|
-
return { ok: true };
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
91
|
];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finish a truncated snapshot, or say it could not be finished.
|
|
3
|
+
*
|
|
4
|
+
* The snapshot walk stops at its node cap and returns a document-order PREFIX with `truncated:true`.
|
|
5
|
+
* That flag has always been honest and has never been actionable: it says the read stopped, not
|
|
6
|
+
* where, so a caller could receive half a page and draw a NEGATIVE conclusion from it — "no error is
|
|
7
|
+
* shown", "the row is gone" — when the thing was simply past the cut. A read that under-reports is a
|
|
8
|
+
* verification result that is quietly wrong.
|
|
9
|
+
*
|
|
10
|
+
* The walk now names its own frontier (`unread`: refs of the subtrees it never entered), so the cut
|
|
11
|
+
* is recoverable: re-read each branch at its own path and the union is the whole tree. This is that
|
|
12
|
+
* loop, plus the half that matters — when the union CANNOT be assembled it says so, and the caller
|
|
13
|
+
* must not turn an empty result over it into an absence.
|
|
14
|
+
*
|
|
15
|
+
* The assembled tree is a UNION of subtrees, not a re-render: each appended chunk is indented from
|
|
16
|
+
* its own root. Callers scan it line by line (that is what every consumer of `tree` does), so the
|
|
17
|
+
* indentation restart costs nothing and re-walking the whole page from scratch would cost a lot.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* How many snapshot round-trips one completion may spend.
|
|
21
|
+
*
|
|
22
|
+
* Each is a real command to the page, so this is the cost ceiling of a completed read, not a safety
|
|
23
|
+
* net. Twelve covers a page whose cut frontier is a handful of branches a couple of levels deep —
|
|
24
|
+
* the shape a node cap actually produces — while keeping the worst case near a dozen round-trips
|
|
25
|
+
* rather than a walk of the DOM one subtree at a time.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_COMPLETION_READS = 12;
|
|
28
|
+
/**
|
|
29
|
+
* How many times the completion may follow a frontier into a new one.
|
|
30
|
+
*
|
|
31
|
+
* A branch re-read can itself truncate and name further branches, so this recursion needs a floor
|
|
32
|
+
* under it independent of the read budget: a hostile or pathologically deep DOM can hand back a new
|
|
33
|
+
* frontier every round forever. Four rounds (the first read plus three levels of re-reads) is deeper
|
|
34
|
+
* than a node cap reaches on a real page, and terminates on one that is not real.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MAX_COMPLETION_DEPTH = 4;
|
|
37
|
+
/** Reads one snapshot — whole page when `scope` is undefined, that branch when it is not. */
|
|
38
|
+
export type SnapshotReader = (scope: string | undefined) => Promise<unknown>;
|
|
39
|
+
export interface CompletedRead {
|
|
40
|
+
/** The union of every subtree that was read. Never a lie about what it contains — only about how much. */
|
|
41
|
+
tree: string;
|
|
42
|
+
/** True only when every branch the page cut was read back. A negative conclusion needs this. */
|
|
43
|
+
complete: boolean;
|
|
44
|
+
reads: number;
|
|
45
|
+
/** Why completion stopped short, for the caller to quote. Present ONLY when `complete` is false. */
|
|
46
|
+
incompleteBecause?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function readCompleteTree(read: SnapshotReader): Promise<CompletedRead>;
|