@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
|
@@ -3,6 +3,10 @@ import { asString } from '../tools/tools-helpers.js';
|
|
|
3
3
|
import { replayFlow } from './flow-replay.js';
|
|
4
4
|
import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js';
|
|
5
5
|
import { buildDecision, unverifiableReason } from './decision.js';
|
|
6
|
+
import { classifyFlowAssertions } from './flow-classify.js';
|
|
7
|
+
import { dischargeFlowIntent, flowIntentStatement, flowReplayVerdictId } from './flow-intent.js';
|
|
8
|
+
import { IntentStore } from '../intent/intent-store.js';
|
|
9
|
+
import { sessionRoot } from '../project/session-root.js';
|
|
6
10
|
import { waitForPredicate } from '../events/predicate.js';
|
|
7
11
|
import { computeSegments } from '../journal/rollups.js';
|
|
8
12
|
import { AssertionTiersStore } from './assertion-tiers-store.js';
|
|
@@ -10,7 +14,7 @@ import { toFlowSources } from './flow-sources.js';
|
|
|
10
14
|
import { reportAndAccumulate } from '../journal/deviation-service.js';
|
|
11
15
|
import { EnvelopeStore } from '../journal/envelope-store.js';
|
|
12
16
|
import { homedir } from 'node:os';
|
|
13
|
-
import { syncRunRecordToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
17
|
+
import { cloudFetch, syncRunRecordToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
14
18
|
import { resolveProjectCloud } from '../cloud/cloud-config.js';
|
|
15
19
|
import { log } from '../log.js';
|
|
16
20
|
export function latestRecordedFlow(events) {
|
|
@@ -66,7 +70,7 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs, proje
|
|
|
66
70
|
const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
|
|
67
71
|
if (null === cloud.config || !cloud.policy.memory)
|
|
68
72
|
return; // not attached / memory disabled → local only
|
|
69
|
-
const result = await syncRunRecordToCloud({ kind: RunKind.FLOW_REPLAY, name, status: runStatus, at: deps.now(), durationMs }, projectId, cloud.config,
|
|
73
|
+
const result = await syncRunRecordToCloud({ kind: RunKind.FLOW_REPLAY, name, status: runStatus, at: deps.now(), durationMs }, projectId, cloud.config, cloudFetch);
|
|
70
74
|
if (result.outcome !== SyncOutcome.SYNCED) {
|
|
71
75
|
log('cloud-run-record-sync-failed', { flow: name, status: result.status, error: result.error });
|
|
72
76
|
}
|
|
@@ -120,6 +124,13 @@ export async function replayNamedFlow(deps, args) {
|
|
|
120
124
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) },
|
|
121
125
|
};
|
|
122
126
|
}
|
|
127
|
+
// What this flow is FOR, from the shared ledger — so a failure can report the business outcome
|
|
128
|
+
// that stopped being true before the step that stopped being green. Undefined when nothing
|
|
129
|
+
// declared one, which the decision then says plainly rather than inventing a goal from step names.
|
|
130
|
+
const intents = new IntentStore(deps.fs, sessionRoot(deps, asString(args['sessionId'])), {
|
|
131
|
+
now: deps.now,
|
|
132
|
+
});
|
|
133
|
+
const intentSaid = await flowIntentStatement(intents, loaded.value);
|
|
123
134
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
124
135
|
// Captured before replay: if the tab isn't on the flow's start page, a step-1 drift is a wrong-page
|
|
125
136
|
// symptom, not a regression — surface that on the decision instead of a bare "a step no longer matches".
|
|
@@ -156,6 +167,18 @@ export async function replayNamedFlow(deps, args) {
|
|
|
156
167
|
})),
|
|
157
168
|
// Record what this flow COVERS so a later deletion can be scoped to the files that changed.
|
|
158
169
|
toFlowSources([{ name, steps: loaded.value.steps }])[0]?.sources ?? []);
|
|
170
|
+
// The ledger learns what this run proved. Only a flow that COULD have gone red discharges: an
|
|
171
|
+
// assertion-free flow is never bound, and `dischargeIntent` refuses an unbound intent, so the
|
|
172
|
+
// guard here is the cheap half of a rule the ledger already enforces. `grade` is what makes a
|
|
173
|
+
// later weakening visible — an intent re-proved by a weaker flow says so in the git diff.
|
|
174
|
+
if (unverifiableReason(loaded.value) === undefined) {
|
|
175
|
+
const provedAt = deps.now();
|
|
176
|
+
await dischargeFlowIntent(intents, loaded.value, {
|
|
177
|
+
verdictId: flowReplayVerdictId(name, provedAt),
|
|
178
|
+
grade: classifyFlowAssertions(loaded.value).grade,
|
|
179
|
+
at: provedAt,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
159
182
|
}
|
|
160
183
|
// Push-default: the deviation report over this drive's segments, learned across runs. Best-effort.
|
|
161
184
|
const deviation = await computeReplayDeviation(deps, session, replayFloor);
|
|
@@ -167,7 +190,7 @@ export async function replayNamedFlow(deps, args) {
|
|
|
167
190
|
steps,
|
|
168
191
|
error: { code: ReplayStatus.ERROR, message: failed.error ?? 'flow action failed' },
|
|
169
192
|
};
|
|
170
|
-
errored.decision = buildDecision(errored, loaded.value);
|
|
193
|
+
errored.decision = buildDecision(errored, loaded.value, intentSaid);
|
|
171
194
|
applyStartPathHint(errored, startPathHint);
|
|
172
195
|
if (deviation !== undefined)
|
|
173
196
|
errored.deviation = deviation;
|
|
@@ -182,7 +205,7 @@ export async function replayNamedFlow(deps, args) {
|
|
|
182
205
|
if (cannotFail !== undefined)
|
|
183
206
|
result.unverifiable = { reason: cannotFail };
|
|
184
207
|
if (status !== ReplayStatus.OK)
|
|
185
|
-
result.decision = buildDecision(result, loaded.value);
|
|
208
|
+
result.decision = buildDecision(result, loaded.value, intentSaid);
|
|
186
209
|
applyStartPathHint(result, startPathHint);
|
|
187
210
|
if (deviation !== undefined)
|
|
188
211
|
result.deviation = deviation;
|
|
@@ -38,9 +38,21 @@ export function successLabel(success) {
|
|
|
38
38
|
export function successToPredicate(success, dynamic) {
|
|
39
39
|
const parts = [];
|
|
40
40
|
if (success.signal !== undefined) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const signal = {
|
|
42
|
+
kind: PredicateKind.SIGNAL,
|
|
43
|
+
name: success.signal,
|
|
44
|
+
};
|
|
45
|
+
if (success.signalData !== undefined)
|
|
46
|
+
signal.dataMatches = success.signalData;
|
|
47
|
+
if (success.signalCount !== undefined) {
|
|
48
|
+
signal.count = success.signalCount;
|
|
49
|
+
// Same post-settle reasoning as net.count: the success waiter is wait-until-true, so an exact
|
|
50
|
+
// count is transiently satisfied the instant the FIRST matching signal fires — before a
|
|
51
|
+
// double-fire's duplicate arrives. Gating on `settled` forces the count to be read only after
|
|
52
|
+
// the app goes quiet, by which point the duplicate IS counted and the over-count fails.
|
|
53
|
+
parts.push({ kind: PredicateKind.SETTLED });
|
|
54
|
+
}
|
|
55
|
+
parts.push(signal);
|
|
44
56
|
}
|
|
45
57
|
if (success.net !== undefined) {
|
|
46
58
|
const net = { kind: PredicateKind.NET };
|
package/dist/flows/flow-tools.js
CHANGED
|
@@ -6,7 +6,7 @@ import { recordSuiteFlakes } from './suite-flakes.js';
|
|
|
6
6
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
7
7
|
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
8
8
|
import { log } from '../log.js';
|
|
9
|
-
import { syncFlowToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
9
|
+
import { cloudFetch, syncFlowToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
10
10
|
import { mapWithConcurrency, resolveConcurrency } from './parallel-suite.js';
|
|
11
11
|
import { acquireLeasedSession } from '../tools/lease-tools.js';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
@@ -14,6 +14,7 @@ import { resolveProjectCloud } from '../cloud/cloud-config.js';
|
|
|
14
14
|
import { buildSuiteVerdict } from './decision.js';
|
|
15
15
|
import { classifyFlowAssertions } from './flow-classify.js';
|
|
16
16
|
import { isValidFlowName, flowPath } from '../project/reticle-dir.js';
|
|
17
|
+
import { FlowStore } from './flows.js';
|
|
17
18
|
import { replayNamedFlow, flowErrorMessage, latestRecordedFlow, sessionProjectId, } from './flow-replay-run.js';
|
|
18
19
|
import { persistAndSyncVerificationRun } from '../runs/verification-sync.js';
|
|
19
20
|
import { runServerVerify } from './server-verify.js';
|
|
@@ -29,7 +30,7 @@ async function syncSavedFlowToCloud(deps, flow, projectId) {
|
|
|
29
30
|
const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
|
|
30
31
|
if (null === cloud.config || !cloud.policy.flows)
|
|
31
32
|
return; // not attached / flows disabled → local only
|
|
32
|
-
const result = await syncFlowToCloud(flow, cloud.config, projectId,
|
|
33
|
+
const result = await syncFlowToCloud(flow, cloud.config, projectId, cloudFetch);
|
|
33
34
|
if (result.outcome !== SyncOutcome.SYNCED) {
|
|
34
35
|
log('cloud-flow-sync-failed', { flow: flow.name, status: result.status, error: result.error });
|
|
35
36
|
}
|
|
@@ -67,6 +68,35 @@ export function leaseFailureReplay(name, error) {
|
|
|
67
68
|
}
|
|
68
69
|
/** Stand-in path for a flow whose name fails the path-segment guard — never a real join. */
|
|
69
70
|
const INVALID_FLOW_NAME_PATH = '(invalid flow name — not a usable path)';
|
|
71
|
+
/**
|
|
72
|
+
* The flow store a SESSION's artifacts belong in, and the root it resolved to.
|
|
73
|
+
*
|
|
74
|
+
* `deps.flows` is bound to the daemon's own root at construction, which is right only when the
|
|
75
|
+
* daemon happens to have been started in the project it is driving. When a resolver is wired and it
|
|
76
|
+
* names a different project, the write has to go there instead — so the store is rebound for this
|
|
77
|
+
* call. Rebinding is cheap: a FlowStore is a filesystem port, a root and a clock.
|
|
78
|
+
*
|
|
79
|
+
* Returns `deps.flows` unchanged when nothing is wired or the roots already agree, so the common
|
|
80
|
+
* case allocates nothing and behaves exactly as before.
|
|
81
|
+
*/
|
|
82
|
+
function flowsForSession(deps, projectId) {
|
|
83
|
+
const resolved = deps.artifactRootFor?.(projectId);
|
|
84
|
+
if (resolved === undefined || resolved.root === deps.reticleRoot) {
|
|
85
|
+
return { flows: deps.flows, root: deps.reticleRoot };
|
|
86
|
+
}
|
|
87
|
+
return { flows: new FlowStore(deps.fs, resolved.root, { now: deps.now }), root: resolved.root };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* How a save reports that nobody said what the flow is for.
|
|
91
|
+
*
|
|
92
|
+
* Declared once and shared by every save tool: an undeclared field is stripped from
|
|
93
|
+
* `structuredContent` by a schema-strict MCP client, so a save path that forgot this line would
|
|
94
|
+
* report the nudge to the unit tests and to nobody else.
|
|
95
|
+
*/
|
|
96
|
+
const INTENT_GAP_FIELD = z
|
|
97
|
+
.unknown()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('Present ONLY when the flow was saved with nothing saying what it is for: { kind, missing, cost, fix }. The flow is on disk either way — this never blocks a save. Close it by saving again with `intent`, or by setting the flow file’s `intentId` to an intent already in the ledger. Nothing here is ever derived from the flow name, the steps or the assertions: a guessed goal reads as the author’s own words.');
|
|
70
100
|
export const FLOW_TOOLS = [
|
|
71
101
|
{
|
|
72
102
|
name: ReticleTool.FLOW_SAVE,
|
|
@@ -79,6 +109,10 @@ export const FLOW_TOOLS = [
|
|
|
79
109
|
flowName: z
|
|
80
110
|
.string()
|
|
81
111
|
.describe('Name for the flow file (saved to .reticle/flows/<flowName>.json). Use again in reticle_flow{action:"load"} / reticle_flow_replay.'),
|
|
112
|
+
intent: z
|
|
113
|
+
.string()
|
|
114
|
+
.optional()
|
|
115
|
+
.describe('What this flow is FOR, in prose: which user does what, and what should become true. Answer it here rather than in a second call. Without it the flow still saves, but the day it goes red the report can only name the step that broke, not the thing that stopped being true.'),
|
|
82
116
|
sessionId: z
|
|
83
117
|
.string()
|
|
84
118
|
.optional()
|
|
@@ -105,6 +139,7 @@ export const FLOW_TOOLS = [
|
|
|
105
139
|
warning: z.string().optional(),
|
|
106
140
|
})
|
|
107
141
|
.optional(),
|
|
142
|
+
intentGap: INTENT_GAP_FIELD,
|
|
108
143
|
error: z.string().optional(),
|
|
109
144
|
code: z.string().optional(),
|
|
110
145
|
},
|
|
@@ -119,7 +154,9 @@ export const FLOW_TOOLS = [
|
|
|
119
154
|
}
|
|
120
155
|
// fold any structured annotations (expect/dynamic/success/intent) onto the saved flow.
|
|
121
156
|
const success = deps.annotations.success(name);
|
|
122
|
-
|
|
157
|
+
// The inline argument wins over an annotation left from an earlier call: it is the more recent
|
|
158
|
+
// statement, and it is the one the author just typed.
|
|
159
|
+
const intent = asString(args['intent']) ?? deps.annotations.intent(name);
|
|
123
160
|
const annotations = {
|
|
124
161
|
stepExpect: deps.annotations.stepExpect(name),
|
|
125
162
|
dynamic: deps.annotations.dynamic(name),
|
|
@@ -132,16 +169,26 @@ export const FLOW_TOOLS = [
|
|
|
132
169
|
const emptyRefusal = emptyFlowRefusal(program.steps.length, name);
|
|
133
170
|
if (emptyRefusal !== undefined)
|
|
134
171
|
return Promise.resolve(emptyRefusal);
|
|
135
|
-
|
|
172
|
+
const { flows, root } = flowsForSession(deps, projectId);
|
|
173
|
+
return flows.save(program, annotations, projectId).then(async (res) => {
|
|
136
174
|
if (!res.ok)
|
|
137
175
|
return { error: flowErrorMessage(res.code), code: res.code };
|
|
138
176
|
deps.annotations.clear(name);
|
|
139
177
|
// Grade the saved flow's assertions so the agent learns immediately if it just saved a flow
|
|
140
178
|
// that asserts nothing observable (passes even when the feature is broken).
|
|
141
|
-
const loaded = await
|
|
179
|
+
const loaded = await flows.load(res.value.name, projectId);
|
|
180
|
+
// The absolute path, always. A save that reports success and not WHERE cost a reporter a
|
|
181
|
+
// `find` across their disk to discover it had landed in an unrelated repo. The name came
|
|
182
|
+
// back from a save that succeeded, so the guard is a type narrowing rather than a doubt.
|
|
183
|
+
const saved = res.value.name;
|
|
184
|
+
const path = isValidFlowName(saved) ? flowPath(root, saved, projectId) : undefined;
|
|
142
185
|
return loaded.ok
|
|
143
|
-
? {
|
|
144
|
-
|
|
186
|
+
? {
|
|
187
|
+
...res.value,
|
|
188
|
+
...(path === undefined ? {} : { path }),
|
|
189
|
+
assertions: classifyFlowAssertions(loaded.value),
|
|
190
|
+
}
|
|
191
|
+
: { ...res.value, ...(path === undefined ? {} : { path }) };
|
|
145
192
|
});
|
|
146
193
|
},
|
|
147
194
|
},
|
|
@@ -448,6 +495,10 @@ export const FLOW_TOOLS = [
|
|
|
448
495
|
.string()
|
|
449
496
|
.optional()
|
|
450
497
|
.describe('Override the flow name embedded in the recorded flow. Omit to use the recorder-assigned name.'),
|
|
498
|
+
intent: z
|
|
499
|
+
.string()
|
|
500
|
+
.optional()
|
|
501
|
+
.describe('What this flow is FOR, in prose: which user does what, and what should become true. The recorder captures clicks, not goals, so this is the only place the goal can come from. Without it the flow still saves, but the day it goes red the report can only name the step that broke.'),
|
|
451
502
|
...{
|
|
452
503
|
sessionId: z
|
|
453
504
|
.string()
|
|
@@ -462,6 +513,7 @@ export const FLOW_TOOLS = [
|
|
|
462
513
|
stepCount: z.number().optional(),
|
|
463
514
|
degraded: z.number().optional(),
|
|
464
515
|
empty: z.boolean().optional(),
|
|
516
|
+
intentGap: INTENT_GAP_FIELD,
|
|
465
517
|
error: z.string().optional(),
|
|
466
518
|
code: z.string().optional(),
|
|
467
519
|
},
|
|
@@ -475,7 +527,12 @@ export const FLOW_TOOLS = [
|
|
|
475
527
|
};
|
|
476
528
|
}
|
|
477
529
|
const override = asString(aliasParam(args, 'flowName', ['flow'])['flowName']);
|
|
478
|
-
const
|
|
530
|
+
const intent = asString(args['intent']);
|
|
531
|
+
const flow = {
|
|
532
|
+
...recorded.flow,
|
|
533
|
+
...(override === undefined ? {} : { name: override }),
|
|
534
|
+
...(intent === undefined ? {} : { intent }),
|
|
535
|
+
};
|
|
479
536
|
// The store stamps the project into the file AND routes it to the per-project subdir (a shared
|
|
480
537
|
// daemon serves many apps), so location and content agree from one source of truth.
|
|
481
538
|
const emptyRecorded = emptyFlowRefusal(flow.steps.length, flow.name);
|
package/dist/flows/flows.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FlowErrorCode } from '@reticlehq/core';
|
|
2
|
-
import type { FlowAnchor, FlowExpect, FlowFile, FlowStep, HealChange } from '@reticlehq/core';
|
|
2
|
+
import type { FlowAnchor, FlowExpect, FlowFile, FlowStep, HealChange, InstrumentationGap } from '@reticlehq/core';
|
|
3
3
|
import type { CompiledProgram, RecordedStep } from './recordings.js';
|
|
4
4
|
import type { FileSystemPort } from '../project/fs-port.js';
|
|
5
5
|
/** A monotonic clock injected for createdAt — never call Date.now inside the store (rule 7). */
|
|
@@ -30,6 +30,11 @@ interface SaveSummary {
|
|
|
30
30
|
stepCount: number;
|
|
31
31
|
degraded: number;
|
|
32
32
|
empty: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Present only when the flow was saved with nothing saying what it is for. Never blocks the save,
|
|
35
|
+
* and absent the moment the flow carries prose or an `intentId`. See `flowIntentGap`.
|
|
36
|
+
*/
|
|
37
|
+
intentGap?: InstrumentationGap;
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* The structured annotations folded onto a flow at save time: per-step
|
package/dist/flows/flows.js
CHANGED
|
@@ -3,6 +3,8 @@ import { AnchorKind, DEGRADED_ANCHOR_ROLE, FLOW_FILE_VERSION, FlowErrorCode, Flo
|
|
|
3
3
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
4
|
import { asString, asRecord } from '../tools/tools-helpers.js';
|
|
5
5
|
import { applyHealChanges } from './heal.js';
|
|
6
|
+
import { flowIntentGap, linkFlowIntent } from './flow-intent.js';
|
|
7
|
+
import { IntentStore } from '../intent/intent-store.js';
|
|
6
8
|
import { flowDir, flowPath, reticleDirPaths, isValidFlowName } from '../project/reticle-dir.js';
|
|
7
9
|
/**
|
|
8
10
|
* A projectId only scopes storage when it's a safe single path segment (it's stamped from the
|
|
@@ -168,6 +170,34 @@ export class FlowStore {
|
|
|
168
170
|
#serialize(flow) {
|
|
169
171
|
return `${JSON.stringify(flow, null, JSON_INDENT)}\n`;
|
|
170
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Register the flow's business goal in the intent ledger and stamp the row's id onto the flow.
|
|
175
|
+
*
|
|
176
|
+
* Both save paths route through here — the compiled-recording one and the in-page recorder one —
|
|
177
|
+
* because a flow's goal must land in the ledger whichever way the flow arrived. A flow with no
|
|
178
|
+
* goal is returned untouched and nothing is written, so an older flow keeps its exact bytes.
|
|
179
|
+
*/
|
|
180
|
+
#linkIntent(flow) {
|
|
181
|
+
return linkFlowIntent(new IntentStore(this.#fs, this.#root, this.#clock), flow);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* What a caller is told about a flow that just landed on disk.
|
|
185
|
+
*
|
|
186
|
+
* Both save paths build it here rather than each assembling their own object, because the two used
|
|
187
|
+
* to be duplicates and the intent nudge is exactly the kind of field that gets added to one of a
|
|
188
|
+
* pair. A flow saved by the recorder and a flow saved from a compiled recording are the same
|
|
189
|
+
* regression test, and must not report differently for having arrived by a different door.
|
|
190
|
+
*/
|
|
191
|
+
#summary(flow) {
|
|
192
|
+
const gap = flowIntentGap(flow);
|
|
193
|
+
return {
|
|
194
|
+
name: flow.name,
|
|
195
|
+
stepCount: flow.steps.length,
|
|
196
|
+
degraded: flow.steps.filter((s) => true === s.degraded).length,
|
|
197
|
+
empty: 0 === flow.steps.length,
|
|
198
|
+
...(gap === undefined ? {} : { intentGap: gap }),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
171
201
|
/**
|
|
172
202
|
* Convert a CompiledProgram (testid-normalized) into an anchored, on-disk flow + write it.
|
|
173
203
|
* Optionally fold structured annotations (per-step expect, dynamic[], success) onto
|
|
@@ -186,19 +216,10 @@ export class FlowStore {
|
|
|
186
216
|
createdAt: this.#clock.now(),
|
|
187
217
|
steps,
|
|
188
218
|
};
|
|
189
|
-
const flow = withAnnotations(base, annotations);
|
|
219
|
+
const flow = await this.#linkIntent(withAnnotations(base, annotations));
|
|
190
220
|
await this.#fs.mkdir(flowDir(this.#root, pid));
|
|
191
221
|
await this.#fs.writeFile(flowPath(this.#root, program.name, pid), this.#serialize(flow));
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
ok: true,
|
|
195
|
-
value: {
|
|
196
|
-
name: program.name,
|
|
197
|
-
stepCount: flow.steps.length,
|
|
198
|
-
degraded,
|
|
199
|
-
empty: 0 === flow.steps.length,
|
|
200
|
-
},
|
|
201
|
-
};
|
|
222
|
+
return { ok: true, value: this.#summary(flow) };
|
|
202
223
|
}
|
|
203
224
|
/**
|
|
204
225
|
* Persist an already-anchored FlowFile captured in-page (no recompilation). The
|
|
@@ -215,19 +236,10 @@ export class FlowStore {
|
|
|
215
236
|
const parsed = FlowFileSchema.safeParse(stamped);
|
|
216
237
|
if (!parsed.success)
|
|
217
238
|
return { ok: false, code: FlowErrorCode.PARSE_FAILED };
|
|
218
|
-
const valid = parsed.data;
|
|
239
|
+
const valid = await this.#linkIntent(parsed.data);
|
|
219
240
|
await this.#fs.mkdir(flowDir(this.#root, pid));
|
|
220
241
|
await this.#fs.writeFile(flowPath(this.#root, asFlowName(valid.name), pid), this.#serialize(valid));
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
ok: true,
|
|
224
|
-
value: {
|
|
225
|
-
name: valid.name,
|
|
226
|
-
stepCount: valid.steps.length,
|
|
227
|
-
degraded,
|
|
228
|
-
empty: 0 === valid.steps.length,
|
|
229
|
-
},
|
|
230
|
-
};
|
|
242
|
+
return { ok: true, value: this.#summary(valid) };
|
|
231
243
|
}
|
|
232
244
|
/**
|
|
233
245
|
* Apply confident testid rebinds to an on-disk flow (the reticle_flow_heal
|
|
@@ -8,9 +8,12 @@ export function predicateToExpect(predicate) {
|
|
|
8
8
|
case PredicateKind.SIGNAL: {
|
|
9
9
|
if (predicate.name === undefined)
|
|
10
10
|
return undefined;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const signal = { signal: predicate.name };
|
|
12
|
+
if (predicate.dataMatches !== undefined)
|
|
13
|
+
signal.signalData = predicate.dataMatches;
|
|
14
|
+
if (predicate.count !== undefined)
|
|
15
|
+
signal.signalCount = predicate.count;
|
|
16
|
+
return signal;
|
|
14
17
|
}
|
|
15
18
|
case PredicateKind.NET: {
|
|
16
19
|
const net = {};
|
|
@@ -25,6 +28,14 @@ export function predicateToExpect(predicate) {
|
|
|
25
28
|
return 0 === Object.keys(net).length ? undefined : { net };
|
|
26
29
|
}
|
|
27
30
|
case PredicateKind.CONSOLE: {
|
|
31
|
+
// `contains` has no representation in FlowExpect.console (core keeps `level` and `absent`
|
|
32
|
+
// and nothing else). Copying the rest through anyway would record an assertion the agent
|
|
33
|
+
// never made: {console, level:'warn', contains:'no-op', absent:true} would save "no warn
|
|
34
|
+
// entries at all", a false red on any unrelated warning, and in the presence direction it
|
|
35
|
+
// saves a strictly weaker claim than the one chosen. This file's own rule applies: record
|
|
36
|
+
// nothing rather than something different, so the flow stays assertion-free there instead.
|
|
37
|
+
if (predicate.contains !== undefined)
|
|
38
|
+
return undefined;
|
|
28
39
|
const console_ = {};
|
|
29
40
|
if (predicate.level !== undefined)
|
|
30
41
|
console_.level = predicate.level;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { VerifyMode } from '../cloud/cloud-config.js';
|
|
2
|
-
import { submitServerVerification } from '../cloud/cloud-sync.js';
|
|
2
|
+
import { CLOUD_VERIFY_TIMEOUT_MS, cloudFetch, submitServerVerification, } from '../cloud/cloud-sync.js';
|
|
3
|
+
import { log } from '../log.js';
|
|
3
4
|
const PASS = 'pass';
|
|
4
5
|
const UNVERIFIED = 'unverified';
|
|
5
6
|
const SOURCE = 'reticle-mcp';
|
|
@@ -31,7 +32,20 @@ export async function runServerVerify(deps, cloud, sessionId, flows) {
|
|
|
31
32
|
// The server hits the URL itself; with no URL (or a localhost one it can't reach) fall back to local.
|
|
32
33
|
if (previewUrl === undefined || 0 === previewUrl.length)
|
|
33
34
|
return null;
|
|
34
|
-
|
|
35
|
+
// The submit blocks while the hosted runner drives a real browser, so it gets the long budget — but a
|
|
36
|
+
// budget it must have: this is awaited inside the verify tool, and an unbounded wait here is an MCP
|
|
37
|
+
// call that never returns. A timeout is reported and then falls back to the local replay below.
|
|
38
|
+
const report = await submitServerVerification({ previewUrl, flows, source: SOURCE }, cloud.config, async (url, init) => {
|
|
39
|
+
try {
|
|
40
|
+
return await cloudFetch(url, init, CLOUD_VERIFY_TIMEOUT_MS);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
log('cloud-server-verify-failed', {
|
|
44
|
+
error: error instanceof Error ? error.message : String(error),
|
|
45
|
+
});
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
35
49
|
if (null === report)
|
|
36
50
|
return null;
|
|
37
51
|
// The hosted runner said it couldn't actually verify (e.g. it's not enabled yet). Never surface that as
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { sessionRoot } from '../project/session-root.js';
|
|
2
3
|
import { verdictForSuite } from './verify-change-verdict.js';
|
|
3
4
|
import { attributedFailures } from './attributed-failure.js';
|
|
4
5
|
import { Verified } from '@reticlehq/core';
|
|
@@ -62,7 +63,10 @@ export const VERIFY_CHANGE_TOOLS = [
|
|
|
62
63
|
: [];
|
|
63
64
|
const since = asString(args['since']);
|
|
64
65
|
const changedFiles = await resolveChangedFiles(files, since);
|
|
65
|
-
|
|
66
|
+
// Same root flow_save wrote to. A read that resolved differently would report "no flows
|
|
67
|
+
// covered this change" over flows that exist, which reads as a clean result rather than
|
|
68
|
+
// an error, so the disagreement would be invisible.
|
|
69
|
+
const flows = await loadNamedFlows(deps.fs, sessionRoot(deps, asString(args['sessionId'])));
|
|
66
70
|
const { affected, unknownProvenance } = affectedSavedFlows(flows, changedFiles);
|
|
67
71
|
if (0 === changedFiles.length) {
|
|
68
72
|
return {
|
|
@@ -193,7 +197,10 @@ async function inspectAfterReplay(deps, args, cursor) {
|
|
|
193
197
|
}
|
|
194
198
|
try {
|
|
195
199
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
196
|
-
const contradictions = findContradictions(session.eventsSince(cursor)
|
|
200
|
+
const contradictions = findContradictions(session.eventsSince(cursor), {
|
|
201
|
+
currentDocumentId: session.currentDocumentId,
|
|
202
|
+
currentEditEpoch: session.currentEditEpoch,
|
|
203
|
+
});
|
|
197
204
|
const snapshot = await session.command(ReticleCommand.SNAPSHOT, {
|
|
198
205
|
mode: SnapshotMode.INTERACTIVE,
|
|
199
206
|
});
|
|
@@ -65,22 +65,14 @@ export declare function blindSpotsFromEvents(events: readonly ReticleEvent[]): B
|
|
|
65
65
|
* from what happened to be said recently.
|
|
66
66
|
*/
|
|
67
67
|
export declare function blindSpotsFromState(state: Readonly<Record<string, number>>): BlindSpot[];
|
|
68
|
+
/** The impeaching note for a transport gap, or undefined when the window was intact. */
|
|
69
|
+
export declare function transportGapNote(events: readonly ReticleEvent[]): string | undefined;
|
|
68
70
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `RATE_LIMITED` — the BRIDGE sampling because events arrived faster than its per-second cap — is
|
|
72
|
-
* already the one blind spot that `impeachesCapture`, on the reasoning that a green over a window
|
|
73
|
-
* you did not fully see "would only describe what was observed". `TRANSPORT_OVERFLOW` is the exact
|
|
74
|
-
* same loss on the other side of the wire, and it was read in journal rollups and NOWHERE on the
|
|
75
|
-
* verdict path. So the identical condition downgraded a verdict from one side of the socket and was
|
|
76
|
-
* invisible from the other: a window that dropped 34 events graded `proved`, in a sentence whose own
|
|
77
|
-
* words were "over a clean capture".
|
|
71
|
+
* Is the state channel dark?
|
|
78
72
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
73
|
+
* Nothing subscribed means NO state change is observed, so an empty `stateDiffs` is a statement
|
|
74
|
+
* about Reticle rather than about the app. Derived here, once, because two callers now need it —
|
|
75
|
+
* the act path's causal summary and the instrumentation-gap surface — and a second spelling of this
|
|
76
|
+
* predicate would eventually disagree with the first about what "unwatched" means.
|
|
83
77
|
*/
|
|
84
|
-
export declare function
|
|
85
|
-
/** The impeaching note for a transport gap, or undefined when the window was intact. */
|
|
86
|
-
export declare function transportGapNote(events: readonly ReticleEvent[]): string | undefined;
|
|
78
|
+
export declare function isStateUnwatched(spots: readonly BlindSpot[]): boolean;
|
|
@@ -45,10 +45,12 @@ const LABEL = {
|
|
|
45
45
|
// nothing here can support. Names the one-line fix, because the reader is the one who can apply it.
|
|
46
46
|
[BlindSpotKind.UNWATCHED_STATE]: () => 'no subscribable store is registered, so NO state change is observed — an empty stateDiffs here means unwatched, not unchanged (register the store itself in your reticle-dev module: registerStore(name, store), not a getter)',
|
|
47
47
|
[BlindSpotKind.WRAPPED_NETWORK]: () => 'fetch was already wrapped before Reticle, so recorded requests may differ from what was sent',
|
|
48
|
-
// A fact about the app's wiring, not a tally. Says what
|
|
49
|
-
//
|
|
50
|
-
// the false green: every IPC call went unseen and every `assert { net }` over
|
|
51
|
-
|
|
48
|
+
// A fact about the app's wiring, not a tally. Says what a network view WITHOUT ipc:// records
|
|
49
|
+
// MEANS, because that state here is indistinguishable from "this app made no backend calls" —
|
|
50
|
+
// and that reading is the false green: every IPC call went unseen and every `assert { net }` over
|
|
51
|
+
// them is vacuous. Document-initiated loads ARE seen since subresource observation (#447), so
|
|
52
|
+
// the note names the missing half instead of claiming the whole view is blind.
|
|
53
|
+
[BlindSpotKind.UNOBSERVED_IPC]: () => "this Electron renderer has no Reticle preload, so NO ipcRenderer.invoke is observed; document-initiated loads may still appear, so a view with no ipc:// records means unseen IPC, not none (add require('@reticlehq/electron/preload') to your preload)",
|
|
52
54
|
};
|
|
53
55
|
/**
|
|
54
56
|
* Blind spots that IMPEACH the capture, as opposed to bounding what it covers.
|
|
@@ -133,7 +135,7 @@ export function blindSpotsFromState(state) {
|
|
|
133
135
|
* every real app, and a caveat that is always present is one nobody reads. TRANSPORT_OVERFLOW is the
|
|
134
136
|
* honest "arbitrary events are gone" marker — it cannot say which.
|
|
135
137
|
*/
|
|
136
|
-
|
|
138
|
+
function droppedByTransport(events) {
|
|
137
139
|
let dropped = 0;
|
|
138
140
|
for (const e of events) {
|
|
139
141
|
if (e.type !== EventType.TRANSPORT_OVERFLOW)
|
|
@@ -150,3 +152,14 @@ export function transportGapNote(events) {
|
|
|
150
152
|
? undefined
|
|
151
153
|
: `the browser dropped ${String(dropped)} event(s) in this window (transport queue overflow), so events that would have contradicted this may never have arrived`;
|
|
152
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Is the state channel dark?
|
|
157
|
+
*
|
|
158
|
+
* Nothing subscribed means NO state change is observed, so an empty `stateDiffs` is a statement
|
|
159
|
+
* about Reticle rather than about the app. Derived here, once, because two callers now need it —
|
|
160
|
+
* the act path's causal summary and the instrumentation-gap surface — and a second spelling of this
|
|
161
|
+
* predicate would eventually disagree with the first about what "unwatched" means.
|
|
162
|
+
*/
|
|
163
|
+
export function isStateUnwatched(spots) {
|
|
164
|
+
return spots.some((spot) => spot.kind === BlindSpotKind.UNWATCHED_STATE && spot.count > 0);
|
|
165
|
+
}
|