@reticlehq/server 2.10.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 +73 -11
- 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 +41 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- 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 +30 -0
- package/dist/events/predicate-asks.js +48 -0
- package/dist/events/predicate-eval.js +254 -25
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +51 -5
- package/dist/events/predicate-schema.d.ts +62 -0
- package/dist/events/predicate-schema.js +162 -4
- 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 +65 -8
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +14 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -2
- package/dist/honesty/blind-spots.d.ts +8 -16
- package/dist/honesty/blind-spots.js +18 -5
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +79 -0
- package/dist/honesty/instrumentation-gaps.js +77 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- 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 -2
- package/dist/index.js +41 -0
- 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/node-io.js +3 -0
- 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 +2 -10
- package/dist/init/run.js +26 -68
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- 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 +2 -2
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license.d.ts +8 -2
- package/dist/license/license.js +10 -15
- 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/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +93 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/project-tools.js +3 -32
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -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/run-tools.js +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/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +20 -0
- package/dist/session/no-session-diagnosis.js +41 -12
- 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 +83 -65
- 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 +40 -1
- package/dist/session/session.js +84 -2
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.js +12 -23
- 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/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +12 -3
- package/dist/telemetry/telemetry.js +25 -5
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +159 -24
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +21 -2
- package/dist/tools/assert-verdict.js +59 -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/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +124 -37
- 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 +70 -27
- 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 +28 -1
- 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 +3 -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 +3 -3
- package/docs/enterprise.md +3 -1
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +145 -0
- 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/telemetry-contract.md +1 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- 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/telemetry/instrumentation-stall.d.ts +0 -57
- package/dist/telemetry/instrumentation-stall.js +0 -103
package/dist/tools/act-tools.js
CHANGED
|
@@ -8,7 +8,7 @@ import { resolveTargetRef } from './resolve-target.js';
|
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { aliasParam } from './alias-args.js';
|
|
10
10
|
import { captureAct, compileSequenceStep } from '../flows/replay.js';
|
|
11
|
-
import { ActionType, ActionWarning, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, ReticleCommand, Verified, PredicateKind, } from '@reticlehq/core';
|
|
11
|
+
import { ActionType, ActionWarning, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, ReticleCommand, Verified, VerifiedReason, PredicateKind, } from '@reticlehq/core';
|
|
12
12
|
import { assertNativeInputSupported } from './act-danger.js';
|
|
13
13
|
import { leanActResult, mutatedWithin } from './act-view.js';
|
|
14
14
|
import { ReticleTool } from './tool-names.js';
|
|
@@ -16,26 +16,52 @@ import { buildReactionReport, summarizeReaction } from '../events/reaction.js';
|
|
|
16
16
|
import { parsePredicate } from '../events/predicate-parse.js';
|
|
17
17
|
import { causalSummary } from '../capsule/causal-summary.js';
|
|
18
18
|
import { findContradictions } from '../events/contradictions.js';
|
|
19
|
+
import { gapsForAction } from '../honesty/instrumentation-gaps.js';
|
|
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';
|
|
24
|
+
import { declaresState } from '../events/predicate-asks.js';
|
|
25
|
+
import { isStateUnwatched } from '../honesty/blind-spots.js';
|
|
19
26
|
import { inFlightRequestLabels, repeatedRequestLabels, waitForInFlight, } from './settle-in-flight.js';
|
|
20
27
|
import { waitForReaction } from './react-grace.js';
|
|
21
28
|
import { decideVerified } from '../honesty/verified.js';
|
|
22
|
-
import { declaredExpectations } from '../events/declared.js';
|
|
29
|
+
import { declaredExpectations, declaresBodyIndependentChannel } from '../events/declared.js';
|
|
23
30
|
import { readsDomState } from '../honesty/already-true.js';
|
|
24
31
|
import { describeWaitTarget } from '../honesty/unsettled.js';
|
|
25
32
|
import { saveFailedAssertCapsule } from './act-capsule.js';
|
|
26
33
|
import { buildDivergenceCapsule } from '../capsule/capsule.js';
|
|
27
34
|
import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
|
|
28
35
|
import { buildHonestyBlock } from '../honesty/honesty.js';
|
|
29
|
-
import {
|
|
36
|
+
import { buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage, impeachesCapture, } from '../honesty/blind-spots.js';
|
|
30
37
|
import { hasAcceptedWrite } from '../honesty/accepted-write.js';
|
|
31
38
|
import { unreadWriteLabels } from '../honesty/unread-outcome.js';
|
|
32
39
|
import { evaluatePredicate, waitForPredicate, provenExpectedLinks, PredicateSchema, } from '../events/predicate.js';
|
|
40
|
+
import { awaitDocumentSuccessor } from '../session/session-successor.js';
|
|
33
41
|
import { healthEnvelope, refuseIfThrottled } from '../session/session-health.js';
|
|
34
42
|
import { pausedShortCircuit, pausedOutputShape, withControl, PAUSED_NO_VERDICT, } from '../session/control-envelope.js';
|
|
35
43
|
import { asString, asNumber, asRecord, sourceOf } from './tools-helpers.js';
|
|
36
|
-
import { sessionIdShape } from './tool-kit.js';
|
|
44
|
+
import { intentArg, sessionIdShape } from './tool-kit.js';
|
|
37
45
|
import { asActionType, gradeOf } from './act-helpers.js';
|
|
38
|
-
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
|
+
}
|
|
39
65
|
/**
|
|
40
66
|
* Narrow the wire's `action` to a real ActionType, or undefined.
|
|
41
67
|
*
|
|
@@ -109,7 +135,7 @@ export const ACT_TOOLS = [
|
|
|
109
135
|
args: z
|
|
110
136
|
.record(z.unknown())
|
|
111
137
|
.optional()
|
|
112
|
-
.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.'),
|
|
113
139
|
refuseWhenThrottled: z
|
|
114
140
|
.boolean()
|
|
115
141
|
.optional()
|
|
@@ -175,7 +201,7 @@ export const ACT_TOOLS = [
|
|
|
175
201
|
settledOutcome = real.settled ?? undefined;
|
|
176
202
|
// Native input reports no synthetic effect block, so nothing measured in-target: undefined
|
|
177
203
|
// (the weaker empty-window test), never a fabricated zero.
|
|
178
|
-
session.lastAct.markActed(since, action, undefined);
|
|
204
|
+
session.lastAct.markActed(since, action, undefined, asString(args['ref']));
|
|
179
205
|
return withControl(session, {
|
|
180
206
|
since,
|
|
181
207
|
inputMode: InputMode.REAL,
|
|
@@ -186,7 +212,9 @@ export const ACT_TOOLS = [
|
|
|
186
212
|
...healthEnvelope(session),
|
|
187
213
|
});
|
|
188
214
|
}
|
|
189
|
-
|
|
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, {
|
|
190
218
|
ref: targetRef.ref,
|
|
191
219
|
action: args['action'],
|
|
192
220
|
args: args['args'] ?? {},
|
|
@@ -201,7 +229,7 @@ export const ACT_TOOLS = [
|
|
|
201
229
|
// Keep what only this call measured, so the observe that judges this window can ask whether
|
|
202
230
|
// anything happened INSIDE the target — the one fact that separates a dead control from a
|
|
203
231
|
// page that was merely busy with something else.
|
|
204
|
-
session.lastAct.markActed(since, action, mutatedWithin(r));
|
|
232
|
+
session.lastAct.markActed(since, action, mutatedWithin(r), asString(args['ref']));
|
|
205
233
|
return withControl(session, {
|
|
206
234
|
since,
|
|
207
235
|
inputMode: InputMode.SYNTHETIC,
|
|
@@ -276,7 +304,7 @@ export const ACT_TOOLS = [
|
|
|
276
304
|
for (let i = 0; i < inputSteps.length; i++) {
|
|
277
305
|
const step = asRecord(inputSteps[i]);
|
|
278
306
|
try {
|
|
279
|
-
const result = await
|
|
307
|
+
const result = await actCommand(deps, session, { ref: step['ref'], action: step['action'], args: step['args'] ?? {} }, perStepTimeout);
|
|
280
308
|
if (!result.ok) {
|
|
281
309
|
stalledAt = i;
|
|
282
310
|
stepResults.push({
|
|
@@ -368,7 +396,7 @@ export const ACT_TOOLS = [
|
|
|
368
396
|
args: z
|
|
369
397
|
.record(z.unknown())
|
|
370
398
|
.optional()
|
|
371
|
-
.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.'),
|
|
372
400
|
predicate: PredicateSchema.optional().describe('Alias for `until` (the name reticle_assert / reticle_wait_for use).'),
|
|
373
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" }] }.'),
|
|
374
402
|
timeout_ms: z
|
|
@@ -379,6 +407,7 @@ export const ACT_TOOLS = [
|
|
|
379
407
|
.boolean()
|
|
380
408
|
.optional()
|
|
381
409
|
.describe('Throw if the tab is throttled. Default: false.'),
|
|
410
|
+
intent: intentArg,
|
|
382
411
|
...sessionIdShape,
|
|
383
412
|
},
|
|
384
413
|
outputSchema: {
|
|
@@ -432,12 +461,20 @@ export const ACT_TOOLS = [
|
|
|
432
461
|
.array(z.unknown())
|
|
433
462
|
.optional()
|
|
434
463
|
.describe('Channels that DISAGREE about this action — the UI advanced while its write failed, a success signal fired over a failed request, a response changed nothing, a duplicate fired, a request never settled. OMITTED when clean. Treat any entry as a finding even when the verdict is green: a passing assertion and a contradicted channel is exactly the false green this exists to catch.'),
|
|
464
|
+
instrumentationGaps: z
|
|
465
|
+
.array(z.unknown())
|
|
466
|
+
.optional()
|
|
467
|
+
.describe('What the APP did not tell Reticle, and the one change that would fix it — each entry is { kind, missing, cost, fix, source?, ref? }. Reported ONLY where an absence made THIS verdict weaker (a red that cannot name a file:line, a state assertion with no registered store, a DOM change no signal announced, a route change nothing signalled); never a survey of the page, so an entry is always work worth doing now. OMITTED when the app told Reticle everything it needed. Applying `fix` makes every later verdict on this app stronger, not just this one.'),
|
|
435
468
|
honesty: z
|
|
436
469
|
.unknown()
|
|
437
470
|
.describe('The verdict trust block { grade, attribution, coverage, integrity, envelope? } — a green never looks stronger than this. Gate on grade ≥ net AND integrity.clean. Fields that were not measured are OMITTED rather than reported as zero, so treat an absent `envelope` as "not sampled", never as a failure.'),
|
|
438
471
|
since: z
|
|
439
472
|
.number()
|
|
440
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.'),
|
|
441
478
|
session: z
|
|
442
479
|
.object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
|
|
443
480
|
.optional(),
|
|
@@ -445,7 +482,9 @@ export const ACT_TOOLS = [
|
|
|
445
482
|
...pausedOutputShape,
|
|
446
483
|
},
|
|
447
484
|
handler: async (deps, args) => {
|
|
448
|
-
|
|
485
|
+
let session = deps.sessions.resolve(asString(args['sessionId']));
|
|
486
|
+
const acted = session;
|
|
487
|
+
const actedSessionId = session.id;
|
|
449
488
|
// Live-control: refuse to drive the page (no act, no predicate eval) while paused.
|
|
450
489
|
//
|
|
451
490
|
// A VERDICT rides out with the refusal. This is the one tool here that promises `verified`,
|
|
@@ -464,6 +503,10 @@ export const ACT_TOOLS = [
|
|
|
464
503
|
? parsePredicate(withUntil['until'])
|
|
465
504
|
: { kind: PredicateKind.SETTLED };
|
|
466
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);
|
|
467
510
|
// Before anything is driven: this path cannot honour a native-input request, and taking the
|
|
468
511
|
// argument and ignoring it told the agent its trusted click had happened. See act-danger.
|
|
469
512
|
assertNativeInputSupported(asRecord(args['args']));
|
|
@@ -472,12 +515,17 @@ export const ACT_TOOLS = [
|
|
|
472
515
|
const resolved = await resolveActTarget(session, args);
|
|
473
516
|
if ('error' === resolved.kind)
|
|
474
517
|
throw new Error(resolved.message);
|
|
475
|
-
|
|
518
|
+
let since = session.elapsed();
|
|
519
|
+
const actedSince = since;
|
|
476
520
|
// The cursor + effect are marked after the act dispatches (below) — a refused act leaves none.
|
|
477
521
|
// The attribution window stays open across the settle wait below, so post-dispatch async events
|
|
478
522
|
// (the whole point of act_and_wait) attribute to this action. finishAction fires after the wait.
|
|
479
523
|
session.beginAction(ReticleTool.ACT_AND_WAIT, asRecord(args));
|
|
480
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;
|
|
481
529
|
// Was the declared consequence ALREADY TRUE? Only asked for predicates that read live DOM
|
|
482
530
|
// state — event-based ones are floored at this act's cursor and cannot be satisfied by the
|
|
483
531
|
// past, so they need no pre-check and pay nothing. One extra query, on the path where a green
|
|
@@ -486,7 +534,8 @@ export const ACT_TOOLS = [
|
|
|
486
534
|
? (await evaluatePredicate(session, until, since, false)).pass
|
|
487
535
|
: false;
|
|
488
536
|
try {
|
|
489
|
-
|
|
537
|
+
// actCommand is the single interception point for upload+path rewrite.
|
|
538
|
+
const actResult = await actCommand(deps, session, {
|
|
490
539
|
ref: resolved.ref,
|
|
491
540
|
action: args['action'],
|
|
492
541
|
args: args['args'] ?? {},
|
|
@@ -496,12 +545,31 @@ export const ACT_TOOLS = [
|
|
|
496
545
|
captureAct(deps.recordings, args, actResult.result);
|
|
497
546
|
// Dispatched — now this act owns the cursor and the effect. Marking its OWN measurement also
|
|
498
547
|
// stops the spread below from inheriting an earlier reticle_act's action and mutation count.
|
|
499
|
-
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']));
|
|
500
549
|
// Honesty: floor the predicate at this act's cursor so a stale buffered event can't satisfy it.
|
|
501
550
|
const predicateStarted = session.elapsed();
|
|
502
|
-
|
|
551
|
+
let verdict = timeout > 0
|
|
503
552
|
? await waitForPredicate(session, until, timeout, since)
|
|
504
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
|
+
}
|
|
505
573
|
// The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is
|
|
506
574
|
// while the write is still in flight — so the verdict was taken over a window the app had not
|
|
507
575
|
// finished, and came back `unknown / unsettled` asking the CALLER to re-check. A login that
|
|
@@ -539,7 +607,8 @@ export const ACT_TOOLS = [
|
|
|
539
607
|
const actedSource = sourceOf(r['source']);
|
|
540
608
|
// Remembered on the session so a LATER assertion can name a file even when its failure has no
|
|
541
609
|
// element to point at — a signal that never fired, a request that was never made.
|
|
542
|
-
|
|
610
|
+
const actedSourceLabel = actedSource === undefined ? undefined : `${actedSource.file}:${String(actedSource.line)}`;
|
|
611
|
+
session.lastAct.markSource(actedSourceLabel);
|
|
543
612
|
const windowEvents = session.eventsSince(since);
|
|
544
613
|
const trace = summarizeReaction(buildReactionReport(windowEvents, session.elapsed() - since));
|
|
545
614
|
// The bounded Tier-1 causal summary — net/console/state/storage diffs the trace's counts miss.
|
|
@@ -561,7 +630,7 @@ export const ACT_TOOLS = [
|
|
|
561
630
|
const coverage = buildCoverageStatement(spots);
|
|
562
631
|
// Nothing subscribed ⇒ the state channel is dark, and the summary must say so rather than
|
|
563
632
|
// report an empty diff list that reads like a fact about the app. See CausalSummary.
|
|
564
|
-
const stateUnwatched = spots
|
|
633
|
+
const stateUnwatched = isStateUnwatched(spots);
|
|
565
634
|
// Only a spot that IMPEACHES the capture belongs in integrity — see impeachesCapture. A
|
|
566
635
|
// structural boundary (virtualized rows, a cross-origin frame) is reported as coverage and
|
|
567
636
|
// must not downgrade a verdict about what WAS observed.
|
|
@@ -582,8 +651,11 @@ export const ACT_TOOLS = [
|
|
|
582
651
|
...(gapNote === undefined ? [] : [CaptureLoss.TRANSPORT_GAP]),
|
|
583
652
|
...(impeaching.note === undefined ? [] : [CaptureLoss.BLIND_SPOT]),
|
|
584
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);
|
|
585
657
|
const honesty = buildHonestyBlock({
|
|
586
|
-
grade
|
|
658
|
+
grade,
|
|
587
659
|
attribution: 'window',
|
|
588
660
|
// Did the buffer lose scarce evidence FROM THIS WINDOW — not "did it evict anything while
|
|
589
661
|
// the action ran", which was the previous rule and which is true on essentially every live
|
|
@@ -636,6 +708,8 @@ export const ACT_TOOLS = [
|
|
|
636
708
|
// not evidence the destination rendered either — `alreadyTrue` decides that, once.
|
|
637
709
|
renderProved: verdict.pass && !alreadyTrue && declared.rendersContent,
|
|
638
710
|
...session.lastAct.effect(),
|
|
711
|
+
currentDocumentId: session.currentDocumentId,
|
|
712
|
+
currentEditEpoch: session.currentEditEpoch,
|
|
639
713
|
});
|
|
640
714
|
// The single field an agent reads. Everything below it is the evidence it was derived from;
|
|
641
715
|
// this is the only one that has to be interpreted, and now it interprets itself.
|
|
@@ -648,6 +722,9 @@ export const ACT_TOOLS = [
|
|
|
648
722
|
// overriding it — see `declaredConsequence`. An explicit `{ kind: "settled" }` is not a
|
|
649
723
|
// declaration about the app's behaviour, it IS the idle wait, so it does not count.
|
|
650
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 } : {}),
|
|
651
728
|
...(alreadyTrue ? { alreadyTrue } : {}),
|
|
652
729
|
// An assertion nobody could evaluate must not be reported as one the app failed.
|
|
653
730
|
...(verdict.inconclusive === undefined ? {} : { inconclusive: verdict.inconclusive }),
|
|
@@ -672,6 +749,62 @@ export const ACT_TOOLS = [
|
|
|
672
749
|
repeated: repeatedRequestLabels(windowEvents),
|
|
673
750
|
},
|
|
674
751
|
});
|
|
752
|
+
// Computed once: the verdict block reports it, and the instrumentation gaps are a second
|
|
753
|
+
// reading of the same evidence rather than a new observation.
|
|
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));
|
|
762
|
+
const gaps = gapsForAction({
|
|
763
|
+
pass: verdict.pass,
|
|
764
|
+
proved: decision.verifiedReason === VerifiedReason.PROVED,
|
|
765
|
+
changeUndeclared,
|
|
766
|
+
source: actedSourceLabel,
|
|
767
|
+
ref: asString(args['ref']),
|
|
768
|
+
stateAsked: declaresState(until),
|
|
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,
|
|
782
|
+
domMutated: (session.lastAct.effect().mutatedWithin ?? 0) > 0,
|
|
783
|
+
signalsFired: actionSummary.signals.length,
|
|
784
|
+
routeChanged: actionSummary.route !== undefined,
|
|
785
|
+
routeSignalFired: actionSummary.signals.some((name) => name.startsWith('route')),
|
|
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
|
+
};
|
|
804
|
+
// Recorded on the session, so a later "am I done?" can answer with what is STILL missing
|
|
805
|
+
// rather than with everything that was ever missing. An empty list closes a gap, which is
|
|
806
|
+
// why it is noted rather than skipped.
|
|
807
|
+
noteSessionGaps(session, gaps);
|
|
675
808
|
return withControl(session, {
|
|
676
809
|
...decision,
|
|
677
810
|
effect: leanActResult(actResult.result),
|
|
@@ -679,14 +812,15 @@ export const ACT_TOOLS = [
|
|
|
679
812
|
// Promoted out of `effect` on red only. On green nobody needs it and it is noise; on red it
|
|
680
813
|
// is the first thing the agent wants, and burying a file:line inside the effect block is
|
|
681
814
|
// most of the way to not reporting it at all.
|
|
682
|
-
...(verdict.pass ||
|
|
683
|
-
? {}
|
|
684
|
-
: { source: `${actedSource.file}:${String(actedSource.line)}` }),
|
|
815
|
+
...(verdict.pass || actedSourceLabel === undefined ? {} : { source: actedSourceLabel }),
|
|
685
816
|
trace,
|
|
686
817
|
...(capsuleSaved === undefined ? {} : { capsuleSaved }),
|
|
687
818
|
// The window cannot say whether anything was WATCHING state — that is a level fact the
|
|
688
819
|
// session holds. Without it an empty `stateDiffs` reads as "the app changed nothing".
|
|
689
|
-
summary:
|
|
820
|
+
summary: actionSummary,
|
|
821
|
+
// What the APP did not tell Reticle, and the one change that would fix it. Reported only
|
|
822
|
+
// where an absence made this very verdict weaker — never as a survey of the page.
|
|
823
|
+
...(gaps.length > 0 ? { instrumentationGaps: gaps } : {}),
|
|
690
824
|
// Cross-channel disagreement, reported WITH the action that caused it.
|
|
691
825
|
//
|
|
692
826
|
// This is the one finding here a human structurally cannot make — they watch one channel,
|
|
@@ -700,11 +834,12 @@ export const ACT_TOOLS = [
|
|
|
700
834
|
honesty,
|
|
701
835
|
...(capsule === undefined ? {} : { capsule }),
|
|
702
836
|
since,
|
|
837
|
+
...(session.id === actedSessionId ? {} : { sessionId: session.id }),
|
|
703
838
|
...healthEnvelope(session),
|
|
704
839
|
});
|
|
705
840
|
}
|
|
706
841
|
finally {
|
|
707
|
-
|
|
842
|
+
acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
|
|
708
843
|
}
|
|
709
844
|
},
|
|
710
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,3 +1,4 @@
|
|
|
1
|
+
import type { InstrumentationGap, JournalVerdictEffect } from '@reticlehq/core';
|
|
1
2
|
import type { Predicate } from '../events/predicate.js';
|
|
2
3
|
import type { Session } from '../session/session.js';
|
|
3
4
|
import { type Contradiction } from '../events/contradictions.js';
|
|
@@ -14,7 +15,12 @@ import { type Contradiction } from '../events/contradictions.js';
|
|
|
14
15
|
* consult it. Asserting a moment earlier — while the POST was still open — is the same story with a
|
|
15
16
|
* different name: `request-never-settled`, also unreported.
|
|
16
17
|
*/
|
|
17
|
-
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,
|
|
18
24
|
/** Set when the assertion was never evaluated — see honesty/verified.ts. */
|
|
19
25
|
inconclusive?: string,
|
|
20
26
|
/**
|
|
@@ -22,8 +28,21 @@ inconclusive?: string,
|
|
|
22
28
|
* `reticle_wait_for` reach the same disconnect path as the act tools, and a verdict that blames
|
|
23
29
|
* the app for a lost connection is no more honest on this route than on that one.
|
|
24
30
|
*/
|
|
25
|
-
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<{
|
|
26
38
|
decision: Record<string, unknown>;
|
|
27
39
|
contradictions: Contradiction[];
|
|
28
40
|
coverage: Record<string, unknown>;
|
|
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;
|
|
29
48
|
}>;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { BlindSpotKind, CaptureLoss, PredicateKind } from '@reticlehq/core';
|
|
2
|
+
import { gapsForAction } from '../honesty/instrumentation-gaps.js';
|
|
3
|
+
import { noteSessionGaps } from '../honesty/gap-ledger.js';
|
|
4
|
+
import { declaresState } from '../events/predicate-asks.js';
|
|
5
|
+
import { isStateUnwatched } from '../honesty/blind-spots.js';
|
|
2
6
|
import { findContradictions } from '../events/contradictions.js';
|
|
3
|
-
import { declaredExpectations } from '../events/declared.js';
|
|
7
|
+
import { declaredExpectations, declaresBodyIndependentChannel } from '../events/declared.js';
|
|
4
8
|
import { blindSpotsFromState, buildCoverageStatement, Coverage, impeachesCapture, transportGapNote, } from '../honesty/blind-spots.js';
|
|
5
9
|
import { buildHonestyBlock } from '../honesty/honesty.js';
|
|
6
10
|
import { hasAcceptedWrite } from '../honesty/accepted-write.js';
|
|
@@ -9,6 +13,7 @@ import { decideVerified } from '../honesty/verified.js';
|
|
|
9
13
|
import { describeWaitTarget } from '../honesty/unsettled.js';
|
|
10
14
|
import { inFlightRequestLabels, repeatedRequestLabels } from './settle-in-flight.js';
|
|
11
15
|
import { gradeOfPredicate } from './assert-grade.js';
|
|
16
|
+
import { assertSource } from './assert-source.js';
|
|
12
17
|
function absenceBlindSpotNote(predicate, spots) {
|
|
13
18
|
if (predicate.kind !== 'element' || predicate.absent !== true)
|
|
14
19
|
return undefined;
|
|
@@ -33,7 +38,12 @@ function absenceBlindSpotNote(predicate, spots) {
|
|
|
33
38
|
* consult it. Asserting a moment earlier — while the POST was still open — is the same story with a
|
|
34
39
|
* different name: `request-never-settled`, also unreported.
|
|
35
40
|
*/
|
|
36
|
-
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,
|
|
37
47
|
/** Set when the assertion was never evaluated — see honesty/verified.ts. */
|
|
38
48
|
inconclusive,
|
|
39
49
|
/**
|
|
@@ -41,7 +51,13 @@ inconclusive,
|
|
|
41
51
|
* `reticle_wait_for` reach the same disconnect path as the act tools, and a verdict that blames
|
|
42
52
|
* the app for a lost connection is no more honest on this route than on that one.
|
|
43
53
|
*/
|
|
44
|
-
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) {
|
|
45
61
|
// Scope caveat, stated because it is a real limitation and not a bug: blind spots are tracked per
|
|
46
62
|
// SESSION, not per assertion window, so a cross-origin iframe seen once marks every later verdict
|
|
47
63
|
// in that session partial — including ones about a region it cannot affect. That errs toward
|
|
@@ -76,6 +92,8 @@ observationLost) {
|
|
|
76
92
|
const declared = declaredExpectations(predicate);
|
|
77
93
|
const contradictions = findContradictions(windowEvents, {
|
|
78
94
|
prior,
|
|
95
|
+
currentDocumentId: session.currentDocumentId,
|
|
96
|
+
currentEditEpoch: session.currentEditEpoch,
|
|
79
97
|
expectedFailures: declared.netFailures,
|
|
80
98
|
renderProved: pass && declared.rendersContent,
|
|
81
99
|
...(actCursor !== undefined && actCursor >= since ? { actionSince: actCursor } : {}),
|
|
@@ -96,6 +114,7 @@ observationLost) {
|
|
|
96
114
|
// not override it. A fix that lived on one half of the verdict surface would leave the other
|
|
97
115
|
// half broken, and this is the tool agents call most.
|
|
98
116
|
declaredConsequence: predicate.kind !== PredicateKind.SETTLED,
|
|
117
|
+
...(declaresBodyIndependentChannel(predicate) ? { independentOfBody: true } : {}),
|
|
99
118
|
...(inconclusive === undefined ? {} : { inconclusive }),
|
|
100
119
|
...(true === observationLost ? { observationLost: true } : {}),
|
|
101
120
|
...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
|
|
@@ -122,5 +141,41 @@ observationLost) {
|
|
|
122
141
|
repeated: repeatedRequestLabels(windowEvents),
|
|
123
142
|
},
|
|
124
143
|
});
|
|
125
|
-
|
|
144
|
+
// The same rule the act path uses, fed by what THIS path knows. An assertion drives nothing, so
|
|
145
|
+
// only two of the four gates can fire here: a red with no remembered source to point at, and a
|
|
146
|
+
// state assertion against an app that registers no store.
|
|
147
|
+
const gaps = gapsForAction({
|
|
148
|
+
pass,
|
|
149
|
+
...(changeUndeclared === undefined ? {} : { changeUndeclared }),
|
|
150
|
+
source: session.lastAct.source(),
|
|
151
|
+
stateAsked: declaresState(predicate),
|
|
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,
|
|
155
|
+
domMutated: false,
|
|
156
|
+
signalsFired: 0,
|
|
157
|
+
routeChanged: false,
|
|
158
|
+
routeSignalFired: false,
|
|
159
|
+
});
|
|
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
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
decision: decision,
|
|
172
|
+
contradictions,
|
|
173
|
+
coverage,
|
|
174
|
+
gaps,
|
|
175
|
+
verdictEffect: {
|
|
176
|
+
claim: describeWaitTarget(predicate),
|
|
177
|
+
verified: decision.verified,
|
|
178
|
+
...(source === undefined ? {} : { source }),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
126
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
|
];
|