@reticlehq/server 2.11.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +26 -2
- package/dist/bridge/bridge.d.ts +0 -13
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/capsule/capsule-store.d.ts +2 -1
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +8 -0
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-parse.d.ts +3 -1
- package/dist/cli/cli-parse.js +29 -7
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +1 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/cloud-cli.js +12 -6
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +3 -1
- package/dist/cli/doctor-rows.js +3 -1
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +143 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli.js +37 -110
- package/dist/cloud/cloud-config.d.ts +2 -1
- package/dist/cloud/cloud-sync.d.ts +11 -0
- package/dist/cloud/cloud-sync.js +32 -0
- package/dist/crawl/crawl.d.ts +9 -13
- package/dist/crawl/crawl.js +20 -3
- package/dist/daemon/daemon-resolve.d.ts +79 -0
- package/dist/daemon/daemon-resolve.js +118 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/dev/stale-issue-guard.d.ts +0 -2
- package/dist/dev/stale-issue-guard.js +1 -1
- package/dist/events/contradictions.d.ts +41 -0
- package/dist/events/contradictions.js +125 -12
- package/dist/events/declared.d.ts +14 -0
- package/dist/events/declared.js +33 -0
- package/dist/events/echo-mismatch.js +14 -0
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +13 -0
- package/dist/events/predicate-asks.js +22 -0
- package/dist/events/predicate-eval.js +64 -22
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +43 -3
- package/dist/events/predicate-schema.d.ts +44 -0
- package/dist/events/predicate-schema.js +126 -23
- package/dist/events/predicate.d.ts +15 -0
- package/dist/events/predicate.js +41 -2
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +0 -2
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-replay-run.js +27 -4
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +32 -4
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +6 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +4 -1
- package/dist/honesty/blind-spots.d.ts +0 -17
- package/dist/honesty/blind-spots.js +1 -1
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/instrumentation-gaps.d.ts +30 -6
- package/dist/honesty/instrumentation-gaps.js +61 -5
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +15 -0
- package/dist/honesty/verified.js +68 -4
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +1 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-store.d.ts +0 -5
- package/dist/impact/impact-store.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/init/agent-rules.d.ts +17 -0
- package/dist/init/agent-rules.js +31 -1
- package/dist/init/capabilities.d.ts +0 -2
- package/dist/init/capabilities.js +1 -1
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +0 -25
- package/dist/init/confirm.js +3 -3
- package/dist/init/cra.d.ts +14 -1
- package/dist/init/cra.js +43 -12
- package/dist/init/dev-script.d.ts +48 -0
- package/dist/init/dev-script.js +63 -0
- package/dist/init/mcp.d.ts +16 -5
- package/dist/init/mcp.js +26 -10
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +83 -9
- package/dist/init/plan.d.ts +10 -30
- package/dist/init/plan.js +30 -15
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +0 -26
- package/dist/init/run.js +14 -87
- package/dist/init/snippets.js +53 -10
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +3 -2
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/intent/inline-intent.d.ts +46 -0
- package/dist/intent/inline-intent.js +103 -0
- package/dist/intent/intent-store.d.ts +37 -0
- package/dist/intent/intent-store.js +112 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +80 -0
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +25 -0
- package/dist/journal/envelope.d.ts +26 -26
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/mcp/attach-memory.d.ts +67 -0
- package/dist/mcp/attach-memory.js +142 -0
- package/dist/mcp/mcp-proxy.d.ts +0 -7
- package/dist/mcp/mcp-proxy.js +14 -2
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +45 -23
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +10 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +12 -1
- package/dist/pool/browser-pool.js +31 -1
- package/dist/pool/playwright-launcher.js +3 -1
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.js +6 -1
- package/dist/project/project-tools.js +3 -32
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/verification-sync.js +2 -2
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +10 -0
- package/dist/session/no-session-diagnosis.js +24 -7
- package/dist/session/no-session-next-action.d.ts +5 -0
- package/dist/session/no-session-next-action.js +35 -3
- package/dist/session/no-session-watch.d.ts +8 -0
- package/dist/session/no-session-watch.js +82 -64
- package/dist/session/output-budget.js +4 -1
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-manager.js +50 -1
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +78 -0
- package/dist/session/session.d.ts +31 -1
- package/dist/session/session.js +75 -2
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/project-profile.d.ts +0 -8
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +126 -21
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +20 -3
- package/dist/tools/assert-verdict.js +36 -4
- package/dist/tools/browser-tools.js +0 -20
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +123 -69
- package/dist/tools/dynamic-tools.js +29 -8
- package/dist/tools/error-recovery.d.ts +24 -14
- package/dist/tools/error-recovery.js +70 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +62 -2
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +5 -1
- package/dist/tools/lease-tools.js +22 -8
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/observe-tools.js +63 -26
- package/dist/tools/read-tools.js +5 -3
- package/dist/tools/real-input-attempt.d.ts +22 -0
- package/dist/tools/real-input-attempt.js +153 -1
- package/dist/tools/reconcile-tools.js +18 -4
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +16 -0
- package/dist/tools/tool-names.d.ts +3 -0
- package/dist/tools/tool-names.js +3 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools.js +22 -4
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/update-nudge.d.ts +7 -10
- package/dist/update/update-nudge.js +12 -3
- package/dist/update/updater.d.ts +1 -1
- package/docs/README.md +2 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/verify.mdx +4 -5
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +2 -2
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +3 -2
- package/package.json +2 -2
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { IntentState, JournalVerdictEffectSchema, RUN_ESTABLISHED_CAP, } from '@reticlehq/core';
|
|
2
|
+
import { subjectOf } from '../runs/run-context.js';
|
|
3
|
+
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
|
+
/**
|
|
5
|
+
* Were `reticle_context` and `reticle_intent` used at all — and did using them change anything?
|
|
6
|
+
*
|
|
7
|
+
* Both shipped with the same pre-registered disproof: *if agents never call it, cut it*. Nothing
|
|
8
|
+
* recorded whether they were called, so the disproof could never be run, and a feature whose
|
|
9
|
+
* disproof cannot be run is one nobody can kill. This is the smallest thing that makes the question
|
|
10
|
+
* answerable, per session, locally — an instrument, not a scold. The numbers exist so a FEATURE can
|
|
11
|
+
* be removed, never so an agent can be blamed for how it drove.
|
|
12
|
+
*
|
|
13
|
+
* ## Fold first, record only what nothing records
|
|
14
|
+
*
|
|
15
|
+
* Three of the four questions are already answerable from what is on disk. The intent ledger says
|
|
16
|
+
* what was declared; the journal says what was driven and what verdicts were drawn. Those are folded
|
|
17
|
+
* here and never re-stored, for the reason `run-context.ts` gives at length: two ways to compute one
|
|
18
|
+
* number is worse than one way.
|
|
19
|
+
*
|
|
20
|
+
* The journal does NOT record read-only calls — it holds actions and verdicts only. So "was
|
|
21
|
+
* `reticle_context` called" has no ledger to fold, and that one thing is recorded, in memory, on the
|
|
22
|
+
* session, by `CaptureLedger`. It is the whole of the new state.
|
|
23
|
+
*
|
|
24
|
+
* ## Absent is not zero
|
|
25
|
+
*
|
|
26
|
+
* A session this daemon has recorded nothing for — journaling off, a session adopted after the fact
|
|
27
|
+
* — reports `observed: false` and states no counts at all. It has not established that the features
|
|
28
|
+
* went unused; it has established that it was not watching. This repo has shipped the other bug
|
|
29
|
+
* twice.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The calls this instrument needs and the journal does not keep.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately a short explicit list rather than "everything that is not an action". Under-inclusion
|
|
35
|
+
* is the safe direction: a read tool missing from here lowers the hit side and the miss side by the
|
|
36
|
+
* same call, so the comparison the disproof turns on stays fair. Over-inclusion is not — a lifecycle
|
|
37
|
+
* or disk-side tool landing in here would be counted as a "read" the agent made of the app, which it
|
|
38
|
+
* was not.
|
|
39
|
+
*/
|
|
40
|
+
export const CAPTURED_TOOLS = new Set([
|
|
41
|
+
ReticleTool.CONTEXT,
|
|
42
|
+
ReticleTool.SNAPSHOT,
|
|
43
|
+
ReticleTool.QUERY,
|
|
44
|
+
ReticleTool.INSPECT,
|
|
45
|
+
ReticleTool.STATE,
|
|
46
|
+
ReticleTool.STORAGE,
|
|
47
|
+
ReticleTool.NETWORK,
|
|
48
|
+
ReticleTool.CONSOLE,
|
|
49
|
+
ReticleTool.OBSERVE,
|
|
50
|
+
ReticleTool.EXPLORE,
|
|
51
|
+
]);
|
|
52
|
+
/**
|
|
53
|
+
* How many calls the ledger holds before it starts dropping the oldest.
|
|
54
|
+
*
|
|
55
|
+
* A verification loop is 50–200 calls, so this is not reached in normal use; it exists so a runaway
|
|
56
|
+
* session cannot grow a per-session array without bound. When it IS reached the report says
|
|
57
|
+
* `truncated` rather than presenting a short count as a complete one.
|
|
58
|
+
*/
|
|
59
|
+
const CAPTURE_CAP = 2000;
|
|
60
|
+
/**
|
|
61
|
+
* The one thing that is stored rather than folded: the read-only calls the journal never sees.
|
|
62
|
+
*
|
|
63
|
+
* In memory and per session, the same shape as `GapLedger`. It is not durable on purpose — the
|
|
64
|
+
* question is about one run, and a second file on disk would be a second thing to reconcile with the
|
|
65
|
+
* journal.
|
|
66
|
+
*/
|
|
67
|
+
export class CaptureLedger {
|
|
68
|
+
#calls = [];
|
|
69
|
+
#dropped = 0;
|
|
70
|
+
#tools = new Map();
|
|
71
|
+
/**
|
|
72
|
+
* One call, under the name of the tool that RAN — every tool, not just the read set above.
|
|
73
|
+
*
|
|
74
|
+
* Separate from `note` because it answers a different question and is bounded differently: this
|
|
75
|
+
* map cannot outgrow the tool table however long the session runs, so it needs no cap and never
|
|
76
|
+
* has to report a floor. See honesty/tool-hit-rate.ts for what it feeds.
|
|
77
|
+
*/
|
|
78
|
+
noteTool(tool) {
|
|
79
|
+
this.#tools.set(tool, (this.#tools.get(tool) ?? 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
/** How many times each tool was called this session, in first-call order. */
|
|
82
|
+
toolCalls() {
|
|
83
|
+
return this.#tools;
|
|
84
|
+
}
|
|
85
|
+
note(call) {
|
|
86
|
+
this.#calls.push(call);
|
|
87
|
+
if (this.#calls.length > CAPTURE_CAP) {
|
|
88
|
+
this.#calls.shift();
|
|
89
|
+
this.#dropped += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
calls() {
|
|
93
|
+
return this.#calls;
|
|
94
|
+
}
|
|
95
|
+
/** How many calls fell off the front. Non-zero means the counts below are a floor. */
|
|
96
|
+
get dropped() {
|
|
97
|
+
return this.#dropped;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Record one captured call against the session that made it.
|
|
102
|
+
*
|
|
103
|
+
* Tolerates a session that carries no ledger, exactly as `noteSessionGaps` does and for the same
|
|
104
|
+
* reason: `Session` is satisfied structurally, and many specs — plus any consumer embedding this
|
|
105
|
+
* engine — build one as an object literal. Such a session is a real caller that simply keeps no
|
|
106
|
+
* ledger, and an instrument must never be the reason a tool call throws. Its calls are then absent
|
|
107
|
+
* rather than counted as zero, which is what `observed: false` exists to say.
|
|
108
|
+
*/
|
|
109
|
+
export function noteCapturedCall(session, call) {
|
|
110
|
+
session.capture?.note({ ...call, afterActions: session.actionCount ?? 0 });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Count one call against the session that made it, whichever tool it was.
|
|
114
|
+
*
|
|
115
|
+
* Tolerant of a ledger-less session for exactly the reason `noteCapturedCall` is: its calls are then
|
|
116
|
+
* absent rather than counted as zero, which is what `observed: false` exists to say.
|
|
117
|
+
*/
|
|
118
|
+
export function noteToolDispatched(session, tool) {
|
|
119
|
+
session.capture?.noteTool(tool);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Every point in the run at which each subject became an established fact.
|
|
123
|
+
*
|
|
124
|
+
* "Established" is not a second definition: it is the rule `establishedFromJournal` folds by — an
|
|
125
|
+
* action with an observed settle outcome, filed under the ref or target it named — and `subjectOf`
|
|
126
|
+
* is imported from there rather than rewritten, so the two cannot drift apart.
|
|
127
|
+
*/
|
|
128
|
+
function establishedIndexes(actions) {
|
|
129
|
+
const out = new Map();
|
|
130
|
+
actions.forEach((action, index) => {
|
|
131
|
+
if (undefined === action.settled)
|
|
132
|
+
return;
|
|
133
|
+
const key = subjectOf(action);
|
|
134
|
+
if (undefined === key)
|
|
135
|
+
return;
|
|
136
|
+
const at = out.get(key);
|
|
137
|
+
if (at === undefined)
|
|
138
|
+
out.set(key, [index]);
|
|
139
|
+
else
|
|
140
|
+
at.push(index);
|
|
141
|
+
});
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/** Was this subject already an established fact by the time a call at `afterActions` was made? */
|
|
145
|
+
function wasEstablished(index, subject, afterActions) {
|
|
146
|
+
if (undefined === subject)
|
|
147
|
+
return false;
|
|
148
|
+
const at = index.get(subject);
|
|
149
|
+
if (at === undefined)
|
|
150
|
+
return false;
|
|
151
|
+
// Action `i` has been dispatched once the counter reads `i + 1`, so `i + 1 <= afterActions` is
|
|
152
|
+
// "already true when the call was made" — which is what keeps a FIRST look off this count. The
|
|
153
|
+
// lower bound is the run context's own window: a fact that has scrolled past RUN_ESTABLISHED_CAP
|
|
154
|
+
// is not something `reticle_context` would have handed back, so re-reading it is not a re-fetch.
|
|
155
|
+
return at.some((i) => i + 1 <= afterActions && i + 1 > afterActions - RUN_ESTABLISHED_CAP);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Did calling `reticle_context` change what happened next?
|
|
159
|
+
*
|
|
160
|
+
* The mechanical proxy, stated so it can be argued with: look at the very next recorded call. If an
|
|
161
|
+
* action was dispatched before it — the action counter moved — the agent ACTED, and the context
|
|
162
|
+
* plausibly helped. If instead the next call is a read of a subject the context had just supplied,
|
|
163
|
+
* the agent re-fetched what it had been handed, and the context did not help.
|
|
164
|
+
*
|
|
165
|
+
* ## What this proxy CANNOT see
|
|
166
|
+
*
|
|
167
|
+
* - Whether the agent READ the response, reasoned over it, or wrote a better answer from it. There
|
|
168
|
+
* is no signal for that anywhere, and this must not be mistaken for one.
|
|
169
|
+
* - Anything that happens between two Reticle calls: source edits, calls to other MCP servers, a
|
|
170
|
+
* turn ending, a human interrupting. A long gap is invisible and reads as adjacency.
|
|
171
|
+
* - Reads that name no ref or target (a whole-page snapshot). They land in `readOther`, never in
|
|
172
|
+
* `refetched`, so a snapshot that duplicated the context is under-counted, not over-counted.
|
|
173
|
+
* - Causation. `acted` is proximity in a call sequence, nothing more. An agent that would have acted
|
|
174
|
+
* anyway is indistinguishable from one the context unblocked.
|
|
175
|
+
* - `reticle_context` called with no session connected — it answers there by design, and there is no
|
|
176
|
+
* per-session ledger to record it against, so those calls are missing entirely.
|
|
177
|
+
* - Two agents driving one session. Their calls interleave into one sequence and "next call" stops
|
|
178
|
+
* meaning "next thing that agent did".
|
|
179
|
+
*/
|
|
180
|
+
export function foldFeatureCapture(input) {
|
|
181
|
+
const { calls, actions, intents } = input;
|
|
182
|
+
if (0 === calls.length && 0 === actions.length)
|
|
183
|
+
return { observed: false };
|
|
184
|
+
const established = establishedIndexes(actions);
|
|
185
|
+
const isRefetch = (call) => ReticleTool.CONTEXT !== call.tool &&
|
|
186
|
+
wasEstablished(established, call.subject, call.afterActions);
|
|
187
|
+
let acted = 0;
|
|
188
|
+
let refetched = 0;
|
|
189
|
+
let readOther = 0;
|
|
190
|
+
let nothingAfter = 0;
|
|
191
|
+
const atSteps = [];
|
|
192
|
+
calls.forEach((call, i) => {
|
|
193
|
+
if (ReticleTool.CONTEXT !== call.tool)
|
|
194
|
+
return;
|
|
195
|
+
atSteps.push(call.afterActions);
|
|
196
|
+
const next = calls[i + 1];
|
|
197
|
+
if (next === undefined) {
|
|
198
|
+
if (input.finalActions > call.afterActions)
|
|
199
|
+
acted += 1;
|
|
200
|
+
else
|
|
201
|
+
nothingAfter += 1;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (next.afterActions > call.afterActions)
|
|
205
|
+
acted += 1;
|
|
206
|
+
else if (isRefetch(next))
|
|
207
|
+
refetched += 1;
|
|
208
|
+
else
|
|
209
|
+
readOther += 1;
|
|
210
|
+
});
|
|
211
|
+
// A verdict is any journal action carrying the bounded verdict effect both verification tools
|
|
212
|
+
// write. Parsed with core's own schema rather than a local shape, for the usual reason.
|
|
213
|
+
const verdicts = actions.filter((a) => JournalVerdictEffectSchema.safeParse(a.effect).success);
|
|
214
|
+
return {
|
|
215
|
+
observed: true,
|
|
216
|
+
...(input.dropped > 0 ? { truncated: true } : {}),
|
|
217
|
+
context: { calls: atSteps.length, atSteps, acted, refetched, readOther, nothingAfter },
|
|
218
|
+
intents: {
|
|
219
|
+
declared: intents.length,
|
|
220
|
+
open: intents.filter((it) => IntentState.PROVED !== it.state).length,
|
|
221
|
+
},
|
|
222
|
+
missed: {
|
|
223
|
+
// The coarse question, asked honestly. Nothing binds a verdict to a specific intent outside
|
|
224
|
+
// flow replay, so "covered" cannot mean "this intent proves this verdict" without inventing a
|
|
225
|
+
// link Reticle cannot observe — the same reason `undeclared-change.ts` asks only whether
|
|
226
|
+
// anything at all was declared. So this counts verdicts drawn against an EMPTY ledger, and
|
|
227
|
+
// stops at zero once anything is declared. That zero is not a claim that every verdict was
|
|
228
|
+
// covered; it is the end of what this can see.
|
|
229
|
+
verdictsWithNoIntentDeclared: 0 === intents.length ? verdicts.length : 0,
|
|
230
|
+
refetchedEstablished: calls.filter(isRefetch).length,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -29,20 +29,28 @@ export interface ActionInstrumentationFacts {
|
|
|
29
29
|
/** True when the outcome was positively PROVED rather than inferred. */
|
|
30
30
|
proved?: boolean;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* The `file:line` of the element this verdict is about, when the build plugin stamped one.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* the
|
|
34
|
+
* Was a boolean, on the reasoning that the gap only needed to know WHETHER the agent could be
|
|
35
|
+
* pointed at code. That was true while the only gap reading it was the one that fires when the
|
|
36
|
+
* pointer is missing. It stopped being true the moment a gap wanted to CARRY the pointer: a gap is
|
|
37
|
+
* read back later, out of the ledger, by which time its `ref` is very likely dead, and a boolean
|
|
38
|
+
* cannot be turned back into a location. Both callers already hold this as a formatted string.
|
|
38
39
|
*/
|
|
39
|
-
|
|
40
|
+
source?: string | undefined;
|
|
40
41
|
/** The ref that was driven, for the report. */
|
|
41
42
|
ref?: string | undefined;
|
|
42
43
|
/** Did the caller's predicate ask about registered state? */
|
|
43
44
|
stateAsked: boolean;
|
|
44
45
|
/** True when the session has no store registered to read. */
|
|
45
46
|
stateUnwatched: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Did the app declare any capabilities at all?
|
|
49
|
+
*
|
|
50
|
+
* Undefined means NOT KNOWN, and not-known must never become an accusation — callers that predate
|
|
51
|
+
* this field keep their behaviour rather than start reporting a gap they cannot substantiate.
|
|
52
|
+
*/
|
|
53
|
+
hasCapabilities?: boolean | undefined;
|
|
46
54
|
/** Did the DOM move in this action's window? */
|
|
47
55
|
domMutated: boolean;
|
|
48
56
|
/** How many app signals fired in the window. */
|
|
@@ -51,5 +59,21 @@ export interface ActionInstrumentationFacts {
|
|
|
51
59
|
routeChanged: boolean;
|
|
52
60
|
/** Did anything signal that route change? */
|
|
53
61
|
routeSignalFired: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Did a code change land since the last verdict with nothing declaring what it was for?
|
|
64
|
+
*
|
|
65
|
+
* Arrives already decided, because the decision needs the intent ledger and this stays pure. Its
|
|
66
|
+
* gate — and the reasoning that keeps it from becoming a nag — lives in `isChangeUndeclared`, and
|
|
67
|
+
* that is the only place allowed to answer it.
|
|
68
|
+
*/
|
|
69
|
+
changeUndeclared?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* How many intents the ledger still holds undischarged. Undefined when nothing consulted it.
|
|
72
|
+
*
|
|
73
|
+
* A count rather than a boolean because the message names the number: "one thing still unproved"
|
|
74
|
+
* and "four things still unproved" are different situations for an agent deciding whether it is
|
|
75
|
+
* finished, and a bare flag makes them read the same.
|
|
76
|
+
*/
|
|
77
|
+
openIntentCount?: number;
|
|
54
78
|
}
|
|
55
79
|
export declare function gapsForAction(facts: ActionInstrumentationFacts): InstrumentationGap[];
|
|
@@ -3,19 +3,75 @@ export function gapsForAction(facts) {
|
|
|
3
3
|
const gaps = [];
|
|
4
4
|
// A red verdict names the control and cannot name the line that renders it. That is the round trip
|
|
5
5
|
// the agent is about to spend, and the one a build plugin removes permanently.
|
|
6
|
-
if (!facts.pass &&
|
|
6
|
+
if (!facts.pass && facts.source === undefined) {
|
|
7
7
|
gaps.push(instrumentationGap(InstrumentationGapKind.NO_SOURCE_MAPPING, 'the control that was driven carries no source mapping', 'this verdict can name the control but not the file and line that render it, so finding the code is a separate search', { ...(facts.ref === undefined ? {} : { ref: facts.ref }) }));
|
|
8
8
|
}
|
|
9
|
-
// Asked about state, and there is no state channel to answer from.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Asked about state, and there is no state channel to answer from. Deliberately UNLOCATED: a store
|
|
10
|
+
// is registered once at app setup, nowhere near the control that was driven, and nothing in this
|
|
11
|
+
// window knows where that setup lives. Borrowing the acted line would send the agent to a file
|
|
12
|
+
// that cannot hold the fix — worse than no pointer, because it costs the trip as well.
|
|
13
|
+
// An app that declared NOTHING is told once, without having to be asked.
|
|
14
|
+
//
|
|
15
|
+
// The rule below fires only when an assertion about state could not be answered, which is correct
|
|
16
|
+
// for that gap and leaves the common case silent: an app whose capabilities file registers
|
|
17
|
+
// nothing drives fine, produces verdicts, and never mentions being under-instrumented — unless
|
|
18
|
+
// the agent happens to ask about state, which an agent with no reason to suspect a problem will
|
|
19
|
+
// not do. The session has known the answer the whole time and no verdict ever consulted it.
|
|
20
|
+
//
|
|
21
|
+
// Reported as the same kind, because it is the same missing thing seen from the other side, and
|
|
22
|
+
// guarded so the two conditions cannot report it twice.
|
|
23
|
+
if (false === facts.hasCapabilities || (facts.stateAsked && facts.stateUnwatched)) {
|
|
24
|
+
// Same missing thing, two ways of meeting it — and the sentence has to say which. Describing an
|
|
25
|
+
// assertion about state to an agent that made no such assertion is a false explanation, and a
|
|
26
|
+
// gap nobody can act on is worse than no gap: it costs the trip and teaches the wrong lesson.
|
|
27
|
+
const declaredNothing = false === facts.hasCapabilities;
|
|
28
|
+
gaps.push(instrumentationGap(InstrumentationGapKind.NO_STORE_REGISTERED, declaredNothing
|
|
29
|
+
? 'this app declared no capabilities at all, so no state can be read from it'
|
|
30
|
+
: 'no store is registered, and this assertion was about state', declaredNothing
|
|
31
|
+
? 'every verdict here rests on what the DOM happens to show, and reticle_state will stay empty however many flows are driven'
|
|
32
|
+
: 'the assertion could not be answered from the deterministic channel and had to fall back to what the DOM happens to show'));
|
|
12
33
|
}
|
|
13
34
|
// The app moved and said nothing, and Reticle could not prove the outcome another way.
|
|
14
35
|
if (facts.domMutated && 0 === facts.signalsFired && true !== facts.proved) {
|
|
15
|
-
gaps.push(instrumentationGap(InstrumentationGapKind.NO_SIGNAL_ON_MUTATION, 'the DOM changed and no signal fired for it', 'the outcome had to be inferred from the DOM instead of read from the app asserting its own success, which is the strongest evidence available',
|
|
36
|
+
gaps.push(instrumentationGap(InstrumentationGapKind.NO_SIGNAL_ON_MUTATION, 'the DOM changed and no signal fired for it', 'the outcome had to be inferred from the DOM instead of read from the app asserting its own success, which is the strongest evidence available',
|
|
37
|
+
// The one gap here that can be LOCATED. It is about the element that was driven, and that
|
|
38
|
+
// element is exactly what `source` describes — so the pointer is a fact, not a guess. It
|
|
39
|
+
// also outlives the `ref` beside it, which is what makes this gap still actionable when
|
|
40
|
+
// coverage reads it back long after the page has re-rendered.
|
|
41
|
+
{
|
|
42
|
+
...(facts.ref === undefined ? {} : { ref: facts.ref }),
|
|
43
|
+
...(facts.source === undefined ? {} : { source: facts.source }),
|
|
44
|
+
}));
|
|
16
45
|
}
|
|
46
|
+
// Also unlocated, for the same reason: the remedy is a router adapter wired app-wide, not a line
|
|
47
|
+
// at the control that happened to trigger this navigation.
|
|
17
48
|
if (facts.routeChanged && !facts.routeSignalFired) {
|
|
18
49
|
gaps.push(instrumentationGap(InstrumentationGapKind.NO_ROUTE_SIGNAL, 'the route changed and nothing signalled it', 'route consequences cannot be asserted on this app, so a navigation can only be checked by what rendered afterwards'));
|
|
19
50
|
}
|
|
51
|
+
// The one gap here that is not about the app. Deliberately UNLOCATED and deliberately generic: the
|
|
52
|
+
// only honest thing to report is that the intent is absent. Naming the file that changed, or the
|
|
53
|
+
// control that was driven, would invite the reader to treat a guess as the declaration — and a
|
|
54
|
+
// guessed intent reads as the developer's own words, which is strictly worse than honest absence.
|
|
55
|
+
if (true === facts.changeUndeclared) {
|
|
56
|
+
gaps.push(instrumentationGap(InstrumentationGapKind.UNDECLARED_CHANGE, 'code changed since the last verdict and no intent says what the change was for', 'this verdict can only check the app against itself, so a green here means "nothing visibly broke" rather than "the change did what it was meant to"'));
|
|
57
|
+
}
|
|
58
|
+
// A green that does not settle what the run OWES.
|
|
59
|
+
//
|
|
60
|
+
// The mirror of `changeUndeclared` above, and the more expensive half: that fires when nothing was
|
|
61
|
+
// declared, this when something was and no verdict ever proved it. Measured on the bench fixture —
|
|
62
|
+
// an agent drew SEVEN green verdicts and reported FIXED on a form that still accepted a
|
|
63
|
+
// whitespace-only service, quoting its own patch as the evidence. Every verdict was honestly
|
|
64
|
+
// green; none of them was about the thing claimed, and nothing in the result said so.
|
|
65
|
+
//
|
|
66
|
+
// Only on a PASS: a failing verdict proved nothing and already says that, so adding this would be
|
|
67
|
+
// two sentences for one fact. It downgrades nothing either — the assertion did hold — it just
|
|
68
|
+
// names the debt on the result the agent is already reading, while it is deciding if it is done.
|
|
69
|
+
// `proved`, not `pass`, because a bare `{ settled }` wait scores the predicate true while proving
|
|
70
|
+
// nothing — the verdict comes back `no-fault`, and keying on `pass` attached "this verdict passed"
|
|
71
|
+
// to a result whose own `because` says it is not verification.
|
|
72
|
+
const owed = facts.openIntentCount ?? 0;
|
|
73
|
+
if (facts.pass && true === facts.proved && 0 < owed) {
|
|
74
|
+
gaps.push(instrumentationGap(InstrumentationGapKind.INTENT_UNDISCHARGED, `this verdict passed, and ${String(owed)} declared intent(s) are still unproved`, 'a green settles what it asserted, not what the run set out to do — reporting done here rests on the change looking right rather than on anything having checked it'));
|
|
75
|
+
}
|
|
20
76
|
return dedupeGaps(gaps);
|
|
21
77
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tools this session called, and how reachable each one was.
|
|
3
|
+
*
|
|
4
|
+
* The generalisation of the capture ledger: it recorded a short list of read tools to answer one
|
|
5
|
+
* feature's disproof, and the question that followed is the same question asked of the whole table —
|
|
6
|
+
* most of the surface has never been observed being called at all, and a decision about what to cut
|
|
7
|
+
* or consolidate is about to be made. This is the instrument for that decision, and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* ## Why the count alone is not enough
|
|
10
|
+
*
|
|
11
|
+
* A tool that goes uncalled because nobody was told it exists and a tool that goes uncalled because
|
|
12
|
+
* nobody wants it look identical in a call count. They are opposite conclusions. So every count is
|
|
13
|
+
* reported beside the tool's REACHABILITY — advertised on the default surface, advertised only on
|
|
14
|
+
* the extended one, or reachable solely by naming it to `reticle_run` — and the interesting cell is
|
|
15
|
+
* the one where an agent found a run-only tool anyway.
|
|
16
|
+
*
|
|
17
|
+
* ## The tiers are derived, never restated
|
|
18
|
+
*
|
|
19
|
+
* `tierOfTool` reads `CORE_TOOL_NAMES` and `EXTENDED_TOOL_NAMES` directly. A tool promoted or
|
|
20
|
+
* demoted between surfaces changes tier here with nothing edited. A local copy of either list would
|
|
21
|
+
* go stale on the first move and would then be wrong about the exact axis this exists to measure —
|
|
22
|
+
* and it would be wrong silently, in a report used to justify deleting things.
|
|
23
|
+
*
|
|
24
|
+
* ## What these numbers do NOT establish
|
|
25
|
+
*
|
|
26
|
+
* - **One session is not a hit rate.** This is evidence about ONE run by ONE agent against ONE app.
|
|
27
|
+
* A hit rate is a distribution over many sessions; a single session quoted as one is an anecdote
|
|
28
|
+
* with a denominator drawn on it.
|
|
29
|
+
* - **Never-called is not useless.** A tool one team reaches for once a month in an emergency and a
|
|
30
|
+
* tool nobody has ever called are the same zero here. Frequency alone cannot separate them, so a
|
|
31
|
+
* cut decided on frequency alone will delete the wrong things.
|
|
32
|
+
* - **Absent is not zero.** A session with no ledger reports `observed: false`. It has not
|
|
33
|
+
* established that no tools were called; it has established that nothing was watching.
|
|
34
|
+
* - The tools that ran BEFORE this instrument existed in the process, or against a session this
|
|
35
|
+
* daemon adopted after the fact, are simply missing rather than counted low.
|
|
36
|
+
*/
|
|
37
|
+
/** How an agent could have reached a tool. Derived from the surface sets — see above. */
|
|
38
|
+
export declare const ToolTier: {
|
|
39
|
+
/** Advertised directly on the surface every user gets. */
|
|
40
|
+
readonly DEFAULT: "default";
|
|
41
|
+
/** Advertised only when the full schema-carrying surface is switched on. */
|
|
42
|
+
readonly EXTENDED: "extended";
|
|
43
|
+
/** Advertised nowhere: reachable only by naming it to `reticle_run`. */
|
|
44
|
+
readonly RUN_ONLY: "run-only";
|
|
45
|
+
};
|
|
46
|
+
export type ToolTier = (typeof ToolTier)[keyof typeof ToolTier];
|
|
47
|
+
/** Which surface would have put this tool in front of an agent, read off that surface's own set. */
|
|
48
|
+
export declare function tierOfTool(tool: string): ToolTier;
|
|
49
|
+
/** How many tools fell in each tier on one side of the called/never-called split. */
|
|
50
|
+
export interface TierCounts {
|
|
51
|
+
default: number;
|
|
52
|
+
extended: number;
|
|
53
|
+
runOnly: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ToolHitRate {
|
|
56
|
+
/** False when this session carries no ledger — "not watched", never "nothing was called". */
|
|
57
|
+
observed: boolean;
|
|
58
|
+
/** The four-plus-two cells: every tier, called and never-called. Omitted when not observed. */
|
|
59
|
+
cells?: {
|
|
60
|
+
called: TierCounts;
|
|
61
|
+
neverCalled: TierCounts;
|
|
62
|
+
};
|
|
63
|
+
/** Each tool called this session, most-called first. */
|
|
64
|
+
called?: readonly {
|
|
65
|
+
tool: string;
|
|
66
|
+
tier: ToolTier;
|
|
67
|
+
calls: number;
|
|
68
|
+
}[];
|
|
69
|
+
/** Each tool in the table this session never called, by name. */
|
|
70
|
+
neverCalled?: readonly {
|
|
71
|
+
tool: string;
|
|
72
|
+
tier: ToolTier;
|
|
73
|
+
}[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Cross one session's per-tool counts with the tool table.
|
|
77
|
+
*
|
|
78
|
+
* `allTools` is the DISPATCHABLE table — the names `reticle_run` will accept — passed in rather than
|
|
79
|
+
* imported, because the table is assembled in the module that owns this tool and importing it back
|
|
80
|
+
* would close a cycle. A tool counted but absent from the table still appears, so a name the table
|
|
81
|
+
* lost is visible rather than dropped.
|
|
82
|
+
*/
|
|
83
|
+
export declare function foldToolHitRate(input: {
|
|
84
|
+
calls: ReadonlyMap<string, number> | undefined;
|
|
85
|
+
allTools: readonly string[];
|
|
86
|
+
}): ToolHitRate;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { CORE_TOOL_NAMES, EXTENDED_TOOL_NAMES } from '../tools/tool-surface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which tools this session called, and how reachable each one was.
|
|
4
|
+
*
|
|
5
|
+
* The generalisation of the capture ledger: it recorded a short list of read tools to answer one
|
|
6
|
+
* feature's disproof, and the question that followed is the same question asked of the whole table —
|
|
7
|
+
* most of the surface has never been observed being called at all, and a decision about what to cut
|
|
8
|
+
* or consolidate is about to be made. This is the instrument for that decision, and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* ## Why the count alone is not enough
|
|
11
|
+
*
|
|
12
|
+
* A tool that goes uncalled because nobody was told it exists and a tool that goes uncalled because
|
|
13
|
+
* nobody wants it look identical in a call count. They are opposite conclusions. So every count is
|
|
14
|
+
* reported beside the tool's REACHABILITY — advertised on the default surface, advertised only on
|
|
15
|
+
* the extended one, or reachable solely by naming it to `reticle_run` — and the interesting cell is
|
|
16
|
+
* the one where an agent found a run-only tool anyway.
|
|
17
|
+
*
|
|
18
|
+
* ## The tiers are derived, never restated
|
|
19
|
+
*
|
|
20
|
+
* `tierOfTool` reads `CORE_TOOL_NAMES` and `EXTENDED_TOOL_NAMES` directly. A tool promoted or
|
|
21
|
+
* demoted between surfaces changes tier here with nothing edited. A local copy of either list would
|
|
22
|
+
* go stale on the first move and would then be wrong about the exact axis this exists to measure —
|
|
23
|
+
* and it would be wrong silently, in a report used to justify deleting things.
|
|
24
|
+
*
|
|
25
|
+
* ## What these numbers do NOT establish
|
|
26
|
+
*
|
|
27
|
+
* - **One session is not a hit rate.** This is evidence about ONE run by ONE agent against ONE app.
|
|
28
|
+
* A hit rate is a distribution over many sessions; a single session quoted as one is an anecdote
|
|
29
|
+
* with a denominator drawn on it.
|
|
30
|
+
* - **Never-called is not useless.** A tool one team reaches for once a month in an emergency and a
|
|
31
|
+
* tool nobody has ever called are the same zero here. Frequency alone cannot separate them, so a
|
|
32
|
+
* cut decided on frequency alone will delete the wrong things.
|
|
33
|
+
* - **Absent is not zero.** A session with no ledger reports `observed: false`. It has not
|
|
34
|
+
* established that no tools were called; it has established that nothing was watching.
|
|
35
|
+
* - The tools that ran BEFORE this instrument existed in the process, or against a session this
|
|
36
|
+
* daemon adopted after the fact, are simply missing rather than counted low.
|
|
37
|
+
*/
|
|
38
|
+
/** How an agent could have reached a tool. Derived from the surface sets — see above. */
|
|
39
|
+
export const ToolTier = {
|
|
40
|
+
/** Advertised directly on the surface every user gets. */
|
|
41
|
+
DEFAULT: 'default',
|
|
42
|
+
/** Advertised only when the full schema-carrying surface is switched on. */
|
|
43
|
+
EXTENDED: 'extended',
|
|
44
|
+
/** Advertised nowhere: reachable only by naming it to `reticle_run`. */
|
|
45
|
+
RUN_ONLY: 'run-only',
|
|
46
|
+
};
|
|
47
|
+
/** Which surface would have put this tool in front of an agent, read off that surface's own set. */
|
|
48
|
+
export function tierOfTool(tool) {
|
|
49
|
+
if (CORE_TOOL_NAMES.has(tool))
|
|
50
|
+
return ToolTier.DEFAULT;
|
|
51
|
+
if (EXTENDED_TOOL_NAMES.has(tool))
|
|
52
|
+
return ToolTier.EXTENDED;
|
|
53
|
+
return ToolTier.RUN_ONLY;
|
|
54
|
+
}
|
|
55
|
+
function tally(tools) {
|
|
56
|
+
return {
|
|
57
|
+
default: tools.filter((tier) => ToolTier.DEFAULT === tier).length,
|
|
58
|
+
extended: tools.filter((tier) => ToolTier.EXTENDED === tier).length,
|
|
59
|
+
runOnly: tools.filter((tier) => ToolTier.RUN_ONLY === tier).length,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Cross one session's per-tool counts with the tool table.
|
|
64
|
+
*
|
|
65
|
+
* `allTools` is the DISPATCHABLE table — the names `reticle_run` will accept — passed in rather than
|
|
66
|
+
* imported, because the table is assembled in the module that owns this tool and importing it back
|
|
67
|
+
* would close a cycle. A tool counted but absent from the table still appears, so a name the table
|
|
68
|
+
* lost is visible rather than dropped.
|
|
69
|
+
*/
|
|
70
|
+
export function foldToolHitRate(input) {
|
|
71
|
+
const { calls } = input;
|
|
72
|
+
if (calls === undefined)
|
|
73
|
+
return { observed: false };
|
|
74
|
+
const called = [...calls]
|
|
75
|
+
.map(([tool, count]) => ({ tool, tier: tierOfTool(tool), calls: count }))
|
|
76
|
+
.sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
|
|
77
|
+
const neverCalled = input.allTools
|
|
78
|
+
.filter((tool) => !calls.has(tool))
|
|
79
|
+
.map((tool) => ({ tool, tier: tierOfTool(tool) }))
|
|
80
|
+
.sort((a, b) => a.tool.localeCompare(b.tool));
|
|
81
|
+
return {
|
|
82
|
+
observed: true,
|
|
83
|
+
cells: {
|
|
84
|
+
called: tally(called.map((entry) => entry.tier)),
|
|
85
|
+
neverCalled: tally(neverCalled.map((entry) => entry.tier)),
|
|
86
|
+
},
|
|
87
|
+
called,
|
|
88
|
+
neverCalled,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this verdict being drawn over code nobody said anything about?
|
|
3
|
+
*
|
|
4
|
+
* Declaring intent is opt-in, and nothing has ever noticed that a change landed and nobody said what
|
|
5
|
+
* it was supposed to make true. That is the moment it matters: a change with no declared intent can
|
|
6
|
+
* only be verified against itself, which is the definition of a false green.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this fires on EVERY verdict, not once per change
|
|
9
|
+
*
|
|
10
|
+
* The house rule is that a gap nobody hit is a backlog, and a backlog reported as a finding is how
|
|
11
|
+
* an agent learns to stop reading findings. This clears that bar on the same test every other
|
|
12
|
+
* instrumentation gap clears it on: **did the absence make THIS verdict weaker?** An unwatched store
|
|
13
|
+
* weakens every state assertion made over it, not merely the first, so it is reported on every one.
|
|
14
|
+
* An undeclared change is the same shape — while nothing says what the edit was for, each verdict
|
|
15
|
+
* drawn after it is checked against nothing but itself, and that is true of the fifth one exactly as
|
|
16
|
+
* it was of the first. Reporting it once and then falling silent would leave four verdicts carrying
|
|
17
|
+
* a weakness they no longer disclose, which is the failure mode this whole surface exists to prevent.
|
|
18
|
+
*
|
|
19
|
+
* It is also not a backlog, because it is not a survey of the project: it is answered from what this
|
|
20
|
+
* session observed (an epoch that moved) about the verdict being returned right now, and it is
|
|
21
|
+
* closed by one call the agent can make immediately. A finding that stays open only because the
|
|
22
|
+
* agent has not made that call is a finding about the current verdict, not a chore.
|
|
23
|
+
*
|
|
24
|
+
* ## The silences, and why each one is required
|
|
25
|
+
*
|
|
26
|
+
* - **An edit was actually OBSERVED.** The epoch degrades to `NO_EDITS_OBSERVED` outside Vite, and
|
|
27
|
+
* that means "no edits observed", never "no edits happened" — a finding built on an absence
|
|
28
|
+
* Reticle cannot see would be a fabrication.
|
|
29
|
+
* - **The ledger holds nothing undischarged.** An open intent IS the declaration; silence is the
|
|
30
|
+
* correct response to it, and declaring one is what turns this off from the next verdict on.
|
|
31
|
+
*
|
|
32
|
+
* ## Why "covers the work" is answered as "anything open at all"
|
|
33
|
+
*
|
|
34
|
+
* The tempting answer is to match the intent's `surface` against the files that changed. Reticle
|
|
35
|
+
* cannot do that honestly: it observes that an epoch advanced, not WHAT advanced — the hot-update
|
|
36
|
+
* channel reports a counter, not a diff — so any file-level matching would be inference dressed as a
|
|
37
|
+
* fact, and it would fire on an agent that had declared its intent perfectly well. Over-reporting on
|
|
38
|
+
* a correct agent is the direction of error that trains it to stop reading findings.
|
|
39
|
+
*
|
|
40
|
+
* So the question asked is the coarse one, and it is asked honestly: has the agent declared anything
|
|
41
|
+
* that is still outstanding? If it has, it is working to a stated purpose and this stays quiet.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* `openIntents` is a thunk rather than a list because reading the ledger touches disk, and the epoch
|
|
45
|
+
* check answers without it for every page that never reported a hot update — which is most of them.
|
|
46
|
+
* Once an edit HAS been observed the read happens per verdict, and that is the intended cost: the
|
|
47
|
+
* ledger is the only thing that can end it, so it has to be re-asked each time.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isChangeUndeclared(
|
|
50
|
+
/** The epoch the session is under now. Undefined when the page never stamped one. */
|
|
51
|
+
currentEpoch: number | undefined, openIntents: () => Promise<readonly unknown[]>): Promise<boolean>;
|