@reticlehq/server 3.2.0 → 3.3.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 +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* tool and the `@reticlehq/test` spec runner share one implementation — no divergent oracle.
|
|
13
13
|
* Pure: no IO, no clock.
|
|
14
14
|
*/
|
|
15
|
-
import { AnchorKind, PredicateKind } from '@reticlehq/core';
|
|
15
|
+
import { AnchorKind, PredicateKind, clauseOfKind, flowExpectToPredicate, withoutClauses, } from '@reticlehq/core';
|
|
16
16
|
/** The dynamic (LLM-output) testids whose presence is never asserted — same rule replay uses. */
|
|
17
17
|
export function dynamicTestids(flow) {
|
|
18
18
|
return new Set((flow.dynamic ?? [])
|
|
@@ -23,145 +23,59 @@ export function dynamicTestids(flow) {
|
|
|
23
23
|
export const SUCCESS_STEP_TOOL = 'success';
|
|
24
24
|
/** A short human label for the success end-condition, for result rows. */
|
|
25
25
|
export function successLabel(success) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
/*
|
|
27
|
+
* NOT `expectLabel`, and the difference is the reader.
|
|
28
|
+
*
|
|
29
|
+
* That one names a clause for a drift row, where `signal:` tells you which channel failed. This
|
|
30
|
+
* one is `mustHold` in the domain model an agent reads BEFORE testing — a list of what each flow
|
|
31
|
+
* guarantees — and there the prefix is noise on the commonest case. A signal's own name is the
|
|
32
|
+
* sentence: "order:placed" is what must hold.
|
|
33
|
+
*/
|
|
34
|
+
const signal = clauseOfKind(success, PredicateKind.SIGNAL);
|
|
35
|
+
if (signal?.name !== undefined)
|
|
36
|
+
return signal.name;
|
|
37
|
+
const net = clauseOfKind(success, PredicateKind.NET);
|
|
38
|
+
if (net !== undefined)
|
|
39
|
+
return net.urlContains ?? net.method ?? 'net';
|
|
40
|
+
const console_ = clauseOfKind(success, PredicateKind.CONSOLE);
|
|
41
|
+
if (console_ !== undefined) {
|
|
42
|
+
return `console:${true === console_.absent ? 'clean' : (console_.level ?? 'error')}`;
|
|
32
43
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
const state = clauseOfKind(success, PredicateKind.STATE);
|
|
45
|
+
if (state !== undefined)
|
|
46
|
+
return `state:${state.path}`;
|
|
47
|
+
const text = clauseOfKind(success, PredicateKind.TEXT);
|
|
48
|
+
if (text?.contains !== undefined)
|
|
49
|
+
return `text:${text.contains}`;
|
|
50
|
+
const element = clauseOfKind(success, PredicateKind.ELEMENT);
|
|
51
|
+
const q = element?.query;
|
|
52
|
+
return q?.testid ?? q?.name ?? q?.role ?? 'success';
|
|
38
53
|
}
|
|
39
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* What a flow's success oracle actually asserts, once the file has been read and the dynamic
|
|
56
|
+
* (LLM-output) testids have been taken out of it.
|
|
57
|
+
*
|
|
58
|
+
* Most of what this function used to do is gone, and that is the migration working as intended: it
|
|
59
|
+
* compiled a flat `FlowExpect` into a `Predicate`, and a step's `expect` IS a predicate now, lifted
|
|
60
|
+
* by the schema when the file is read. It keeps the name and the signature because it is on
|
|
61
|
+
* `@reticlehq/test`'s published surface, and it still accepts the flat shape for the same reason a
|
|
62
|
+
* v1 file still loads — a spec written against the old spelling is not wrong, it is old.
|
|
63
|
+
*
|
|
64
|
+
* What remains is the DYNAMIC SKIP, and it is not cosmetic. A testid whose content a model writes is
|
|
65
|
+
* asserted for PRESENCE by the step layer and never for content; a success oracle bound to one would
|
|
66
|
+
* assert exactly what the flow has already declared unassertable. Dropping every such clause can
|
|
67
|
+
* leave nothing at all, and `undefined` is the honest answer — vacuously met, the same as declaring
|
|
68
|
+
* no success, because nothing here was checkable.
|
|
69
|
+
*/
|
|
40
70
|
export function successToPredicate(success, dynamic) {
|
|
41
|
-
|
|
42
|
-
if (success.signal !== undefined) {
|
|
43
|
-
const signal = {
|
|
44
|
-
kind: PredicateKind.SIGNAL,
|
|
45
|
-
name: success.signal,
|
|
46
|
-
};
|
|
47
|
-
if (success.signalData !== undefined)
|
|
48
|
-
signal.dataMatches = success.signalData;
|
|
49
|
-
if (success.signalCount !== undefined) {
|
|
50
|
-
signal.count = success.signalCount;
|
|
51
|
-
// Same post-settle reasoning as net.count: the success waiter is wait-until-true, so an exact
|
|
52
|
-
// count is transiently satisfied the instant the FIRST matching signal fires — before a
|
|
53
|
-
// double-fire's duplicate arrives. Gating on `settled` forces the count to be read only after
|
|
54
|
-
// the app goes quiet, by which point the duplicate IS counted and the over-count fails.
|
|
55
|
-
parts.push({ kind: PredicateKind.SETTLED });
|
|
56
|
-
}
|
|
57
|
-
parts.push(signal);
|
|
58
|
-
}
|
|
59
|
-
if (success.route !== undefined) {
|
|
60
|
-
// The mirror of `predicateToExpect`'s ROUTE case. Replay evaluates predicates, so carrying the
|
|
61
|
-
// route back here is the whole of what makes a recorded navigation able to fail.
|
|
62
|
-
const route = {
|
|
63
|
-
kind: PredicateKind.ROUTE,
|
|
64
|
-
};
|
|
65
|
-
if (success.route.pathname !== undefined)
|
|
66
|
-
route.pathname = success.route.pathname;
|
|
67
|
-
if (success.route.contains !== undefined)
|
|
68
|
-
route.contains = success.route.contains;
|
|
69
|
-
return route;
|
|
70
|
-
}
|
|
71
|
-
if (success.net !== undefined) {
|
|
72
|
-
const net = { kind: PredicateKind.NET };
|
|
73
|
-
if (success.net.method !== undefined)
|
|
74
|
-
net.method = success.net.method;
|
|
75
|
-
if (success.net.urlContains !== undefined)
|
|
76
|
-
net.urlContains = success.net.urlContains;
|
|
77
|
-
if (success.net.status !== undefined)
|
|
78
|
-
net.status = success.net.status;
|
|
79
|
-
if (success.net.count !== undefined) {
|
|
80
|
-
net.count = success.net.count;
|
|
81
|
-
// A cardinality assertion is inherently POST-SETTLE. The success waiter is wait-until-true, so an
|
|
82
|
-
// exact count (e.g. 1) is transiently satisfied the instant the FIRST matching request lands —
|
|
83
|
-
// before a double-submit's duplicate arrives. Gating on `settled` forces the count to be read
|
|
84
|
-
// only after the network has gone quiet, so the duplicate IS counted and the over-count fails.
|
|
85
|
-
parts.push({ kind: PredicateKind.SETTLED });
|
|
86
|
-
}
|
|
87
|
-
parts.push(net);
|
|
88
|
-
}
|
|
89
|
-
if (success.console !== undefined) {
|
|
90
|
-
const con = {
|
|
91
|
-
kind: PredicateKind.CONSOLE,
|
|
92
|
-
};
|
|
93
|
-
if (success.console.level !== undefined)
|
|
94
|
-
con.level = success.console.level;
|
|
95
|
-
if (success.console.absent !== undefined) {
|
|
96
|
-
con.absent = success.console.absent;
|
|
97
|
-
// An `absent` assertion is post-settle, same as net.count: a wait-until-true waiter sees "no
|
|
98
|
-
// error yet" at the first poll and passes BEFORE the action's error fires. Gate on `settled` so
|
|
99
|
-
// the console is read only after the page quiets, by which point any error is in the buffer.
|
|
100
|
-
if (success.console.absent)
|
|
101
|
-
parts.push({ kind: PredicateKind.SETTLED });
|
|
102
|
-
}
|
|
103
|
-
parts.push(con);
|
|
104
|
-
}
|
|
105
|
-
const state = success.state;
|
|
106
|
-
if (state !== undefined) {
|
|
107
|
-
const part = {
|
|
108
|
-
kind: PredicateKind.STATE,
|
|
109
|
-
path: state.path,
|
|
110
|
-
};
|
|
111
|
-
if (state.store !== undefined)
|
|
112
|
-
part.store = state.store;
|
|
113
|
-
if (state.equals !== undefined)
|
|
114
|
-
part.equals = state.equals;
|
|
115
|
-
// `hold` = an INVARIANT ("this state must still hold after the action settles"), vs the default
|
|
116
|
-
// wait-for-change. A wait-until-true waiter would pass the instant the path already equals the
|
|
117
|
-
// value — which, for "an unrelated path stayed put", is true BEFORE a side-effect leak fires. Gate
|
|
118
|
-
// on `settled` so the read happens after the page quiets, by which point the leak has landed.
|
|
119
|
-
if (true === state.hold)
|
|
120
|
-
parts.push({ kind: PredicateKind.SETTLED });
|
|
121
|
-
parts.push(part);
|
|
122
|
-
}
|
|
123
|
-
const element = success.element;
|
|
124
|
-
if (element !== undefined) {
|
|
125
|
-
const testid = element.testid;
|
|
126
|
-
// A dynamic-marked testid is NOT asserted as a success condition (presence-only).
|
|
127
|
-
if (testid === undefined || !dynamic.has(testid)) {
|
|
128
|
-
const query = {};
|
|
129
|
-
if (testid !== undefined)
|
|
130
|
-
query['testid'] = testid;
|
|
131
|
-
if (element.role !== undefined)
|
|
132
|
-
query['role'] = element.role;
|
|
133
|
-
if (element.name !== undefined)
|
|
134
|
-
query['name'] = element.name;
|
|
135
|
-
if (Object.keys(query).length > 0)
|
|
136
|
-
parts.push({ kind: PredicateKind.ELEMENT, query });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const text = success.text;
|
|
140
|
-
if (text !== undefined) {
|
|
141
|
-
const part = {
|
|
142
|
-
kind: PredicateKind.TEXT,
|
|
143
|
-
contains: text.contains,
|
|
144
|
-
};
|
|
145
|
-
if (text.scope !== undefined)
|
|
146
|
-
part.scope = text.scope;
|
|
147
|
-
if (text.visible !== undefined)
|
|
148
|
-
part.visible = text.visible;
|
|
149
|
-
if (true === text.absent) {
|
|
150
|
-
part.absent = true;
|
|
151
|
-
// Same post-settle reasoning as `console.absent` and `state.hold`: a wait-until-true waiter
|
|
152
|
-
// reads "not there yet" on the first poll and passes BEFORE the text it is meant to see
|
|
153
|
-
// disappear has even been rendered. Gate on `settled` so the read happens after the page
|
|
154
|
-
// quiets, by which point a text that was going to appear has.
|
|
155
|
-
parts.push({ kind: PredicateKind.SETTLED });
|
|
156
|
-
}
|
|
157
|
-
parts.push(part);
|
|
158
|
-
}
|
|
159
|
-
const [first] = parts;
|
|
160
|
-
if (0 === parts.length)
|
|
71
|
+
if (success === undefined)
|
|
161
72
|
return undefined;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
73
|
+
const predicate = 'kind' in success ? success : flowExpectToPredicate(success);
|
|
74
|
+
if (predicate === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
return withoutClauses(predicate, (clause) => PredicateKind.ELEMENT === clause.kind &&
|
|
77
|
+
clause.query.testid !== undefined &&
|
|
78
|
+
dynamic.has(clause.query.testid));
|
|
165
79
|
}
|
|
166
80
|
/**
|
|
167
81
|
* Assert a flow's success end-condition after replay. Delegates to the injected waitForSignal (the
|
|
@@ -170,8 +84,6 @@ export function successToPredicate(success, dynamic) {
|
|
|
170
84
|
* the injected timeout. Never reads the wall clock.
|
|
171
85
|
*/
|
|
172
86
|
export async function assertSuccess(session, success, dynamic, waitForSignal, timeoutMs, since = 0) {
|
|
173
|
-
if (success === undefined)
|
|
174
|
-
return { pass: true };
|
|
175
87
|
const predicate = successToPredicate(success, dynamic);
|
|
176
88
|
if (predicate === undefined)
|
|
177
89
|
return { pass: true };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type ProjectId } from '@reticlehq/core';
|
|
1
|
+
import { type Predicate, type ProjectId } from '@reticlehq/core';
|
|
2
2
|
export { REDACTED_FILL } from './fields/flow-secret-field.js';
|
|
3
3
|
import { type FlowResult } from './flow-result.js';
|
|
4
4
|
export type { FlowResult } from './flow-result.js';
|
|
5
|
-
import type { FlowAnchor,
|
|
5
|
+
import type { FlowAnchor, FlowFile, FlowStep, HealChange, InstrumentationGap } from '@reticlehq/core';
|
|
6
6
|
import type { CompiledProgram, RecordedStep } from './recording/tape/recordings.js';
|
|
7
7
|
import type { FileSystemPort } from '../../memory/project/fs/fs-port.js';
|
|
8
8
|
import type { Clock } from '../../machine/clock.js';
|
|
@@ -49,9 +49,9 @@ interface SaveSummary {
|
|
|
49
49
|
* the same bytes as before.
|
|
50
50
|
*/
|
|
51
51
|
export interface FlowAnnotations {
|
|
52
|
-
stepExpect: Map<number,
|
|
52
|
+
stepExpect: Map<number, Predicate>;
|
|
53
53
|
dynamic: string[];
|
|
54
|
-
success?:
|
|
54
|
+
success?: Predicate;
|
|
55
55
|
/** The flow's declared business goal (intent annotation). */
|
|
56
56
|
intent?: string;
|
|
57
57
|
}
|
|
@@ -75,6 +75,10 @@ export declare class FlowStore {
|
|
|
75
75
|
recordLearned(name: string, learned: NonNullable<FlowFile['learned']>, projectId?: ProjectId): Promise<FlowResult<{
|
|
76
76
|
name: string;
|
|
77
77
|
}>>;
|
|
78
|
+
/** Give sourceless steps the files a clean replay resolved them to, merged onto the file as it is now. */
|
|
79
|
+
recordSources(name: string, sources: ReadonlyMap<string, NonNullable<FlowStep['source']>>, projectId?: ProjectId): Promise<FlowResult<{
|
|
80
|
+
name: string;
|
|
81
|
+
}>>;
|
|
78
82
|
/**
|
|
79
83
|
* Apply confident testid rebinds to an on-disk flow (the `reticle_flow_heal` apply path),
|
|
80
84
|
* rewriting ONLY the named steps' anchors and preserving every other field. Loading, the name
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asFlowName, asProjectId } from '@reticlehq/core';
|
|
1
|
+
import { asFlowName, asProjectId, } from '@reticlehq/core';
|
|
2
2
|
import { REDACTED_FILL } from './fields/flow-secret-field.js';
|
|
3
3
|
export { REDACTED_FILL } from './fields/flow-secret-field.js';
|
|
4
4
|
import { safeProjectId } from './flow-result.js';
|
|
@@ -7,6 +7,7 @@ import { AnchorKind, DEGRADED_ANCHOR_ROLE, FLOW_FILE_VERSION, FlowErrorCode, Flo
|
|
|
7
7
|
import { ReticleTool } from '@reticlehq/core';
|
|
8
8
|
import { asRecord, asString } from '@reticlehq/core';
|
|
9
9
|
import { applyHealChanges } from './heal.js';
|
|
10
|
+
import { withLearnedSources } from './learned-sources.js';
|
|
10
11
|
import { flowIntentGap, linkFlowIntent } from './flow-intent.js';
|
|
11
12
|
import { IntentStore } from '../../memory/intent/intent-store.js';
|
|
12
13
|
import { flowDir, flowParentDir, flowPath, reticleDirPaths, isValidFlowName, } from '../../memory/project/dir/reticle-dir.js';
|
|
@@ -351,6 +352,13 @@ export class FlowStore {
|
|
|
351
352
|
value: { name },
|
|
352
353
|
}));
|
|
353
354
|
}
|
|
355
|
+
/** Give sourceless steps the files a clean replay resolved them to, merged onto the file as it is now. */
|
|
356
|
+
async recordSources(name, sources, projectId) {
|
|
357
|
+
return await this.#changeInPlace(name, projectId, (flow) => ({
|
|
358
|
+
next: withLearnedSources(flow, sources) ?? flow,
|
|
359
|
+
value: { name },
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
354
362
|
/**
|
|
355
363
|
* Apply confident testid rebinds to an on-disk flow (the `reticle_flow_heal` apply path),
|
|
356
364
|
* rewriting ONLY the named steps' anchors and preserving every other field. Loading, the name
|
|
@@ -362,7 +370,8 @@ export class FlowStore {
|
|
|
362
370
|
*/
|
|
363
371
|
async heal(name, changes, projectId) {
|
|
364
372
|
return await this.#changeInPlace(name, projectId, (flow) => {
|
|
365
|
-
|
|
373
|
+
// The store's injected clock, so a written flow records when the rebind actually happened.
|
|
374
|
+
const { flow: next, applied } = applyHealChanges(flow, changes, () => this.#clock.now());
|
|
366
375
|
return { next, value: { name, changed: applied } };
|
|
367
376
|
});
|
|
368
377
|
}
|
|
@@ -25,7 +25,15 @@ export declare function confidenceFor(from: string, to: string): number;
|
|
|
25
25
|
* skipped (idempotent / defensive), never throwing. Shared by the on-disk writer (FlowStore.heal)
|
|
26
26
|
* and the in-memory pre-write verification in reticle_flow_heal, so both rewrite identically.
|
|
27
27
|
*/
|
|
28
|
-
export declare function applyHealChanges(flow: FlowFile, changes: HealChange[]
|
|
28
|
+
export declare function applyHealChanges(flow: FlowFile, changes: HealChange[],
|
|
29
|
+
/**
|
|
30
|
+
* When the rebind happened. Injected rather than read here, because this function is pure and a
|
|
31
|
+
* clock inside it would make the flow it returns depend on when a test ran.
|
|
32
|
+
*
|
|
33
|
+
* Optional so every existing caller keeps working; without it the step still records WHICH anchor
|
|
34
|
+
* was replaced, which is the half a reader actually acts on.
|
|
35
|
+
*/
|
|
36
|
+
now?: () => number): {
|
|
29
37
|
flow: FlowFile;
|
|
30
38
|
applied: HealChange[];
|
|
31
39
|
};
|
|
@@ -38,7 +38,15 @@ export function confidenceFor(from, to) {
|
|
|
38
38
|
* skipped (idempotent / defensive), never throwing. Shared by the on-disk writer (FlowStore.heal)
|
|
39
39
|
* and the in-memory pre-write verification in reticle_flow_heal, so both rewrite identically.
|
|
40
40
|
*/
|
|
41
|
-
export function applyHealChanges(flow, changes
|
|
41
|
+
export function applyHealChanges(flow, changes,
|
|
42
|
+
/**
|
|
43
|
+
* When the rebind happened. Injected rather than read here, because this function is pure and a
|
|
44
|
+
* clock inside it would make the flow it returns depend on when a test ran.
|
|
45
|
+
*
|
|
46
|
+
* Optional so every existing caller keeps working; without it the step still records WHICH anchor
|
|
47
|
+
* was replaced, which is the half a reader actually acts on.
|
|
48
|
+
*/
|
|
49
|
+
now) {
|
|
42
50
|
const byStep = new Map();
|
|
43
51
|
for (const change of changes)
|
|
44
52
|
byStep.set(change.step, change);
|
|
@@ -51,7 +59,21 @@ export function applyHealChanges(flow, changes) {
|
|
|
51
59
|
return step;
|
|
52
60
|
}
|
|
53
61
|
applied.push(change);
|
|
54
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
...step,
|
|
64
|
+
anchor: { kind: AnchorKind.TESTID, value: change.to },
|
|
65
|
+
/*
|
|
66
|
+
* `from` is the ORIGINAL recorded anchor, kept across later heals.
|
|
67
|
+
*
|
|
68
|
+
* A step healed twice has an intermediate name nobody ever chose, so overwriting with the
|
|
69
|
+
* newest would throw away the only anchor a reader wants — the one that was recorded by
|
|
70
|
+
* driving. The timestamp is the LATEST heal, because that is when this anchor arrived.
|
|
71
|
+
*/
|
|
72
|
+
healed: {
|
|
73
|
+
from: step.healed?.from ?? change.from,
|
|
74
|
+
at: now?.() ?? step.healed?.at ?? 0,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
55
77
|
});
|
|
56
78
|
return { flow: { ...flow, steps }, applied };
|
|
57
79
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learn a flow step's source file from the element its anchor resolved to on a replay.
|
|
3
|
+
*
|
|
4
|
+
* A step recorded before the build stamped the app names no file, so a scoped re-verify cannot say
|
|
5
|
+
* which edits it covers. Every replay already asks the page for each anchor's element, and that
|
|
6
|
+
* element carries its stamp now: this records the answer at the one place every resolution passes
|
|
7
|
+
* through (the session's QUERY), rather than threading it through each step runner.
|
|
8
|
+
*/
|
|
9
|
+
import { type FlowFile, type FlowStep } from '@reticlehq/core';
|
|
10
|
+
import type { FlowReplaySession } from './flow-replay-types.js';
|
|
11
|
+
type StepSource = NonNullable<FlowStep['source']>;
|
|
12
|
+
/** `src/Pay.tsx:12:4` or `src/Pay.tsx:12` → `{file, line}`; anything else is not a location. */
|
|
13
|
+
export declare function parseCompactSource(value: string): StepSource | undefined;
|
|
14
|
+
/** Wrap a replay session so every anchor QUERY that resolves to one stamped element is remembered. */
|
|
15
|
+
export declare function recordingSources(inner: FlowReplaySession): {
|
|
16
|
+
session: FlowReplaySession;
|
|
17
|
+
sources: ReadonlyMap<string, StepSource>;
|
|
18
|
+
};
|
|
19
|
+
/** The flow with every sourceless step given the file its anchor resolved to; undefined if none was. */
|
|
20
|
+
export declare function withLearnedSources(flow: FlowFile, sources: ReadonlyMap<string, StepSource>): FlowFile | undefined;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learn a flow step's source file from the element its anchor resolved to on a replay.
|
|
3
|
+
*
|
|
4
|
+
* A step recorded before the build stamped the app names no file, so a scoped re-verify cannot say
|
|
5
|
+
* which edits it covers. Every replay already asks the page for each anchor's element, and that
|
|
6
|
+
* element carries its stamp now: this records the answer at the one place every resolution passes
|
|
7
|
+
* through (the session's QUERY), rather than threading it through each step runner.
|
|
8
|
+
*/
|
|
9
|
+
import { ReticleCommand, asRecord, asString } from '@reticlehq/core';
|
|
10
|
+
import { anchorQueryArgs } from './flow-step-runners.js';
|
|
11
|
+
/** `src/Pay.tsx:12:4` or `src/Pay.tsx:12` → `{file, line}`; anything else is not a location. */
|
|
12
|
+
export function parseCompactSource(value) {
|
|
13
|
+
const match = /^(.+?):(\d+)(?::\d+)?$/.exec(value);
|
|
14
|
+
const file = match?.[1];
|
|
15
|
+
const line = match?.[2];
|
|
16
|
+
if (file === undefined || line === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
return { file, line: Number(line) };
|
|
19
|
+
}
|
|
20
|
+
function keyOf(args) {
|
|
21
|
+
if (args === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
const by = asString(args['by']);
|
|
24
|
+
const value = asString(args['value']);
|
|
25
|
+
if (by === undefined || value === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
return JSON.stringify([by, value, asString(args['name']) ?? null]);
|
|
28
|
+
}
|
|
29
|
+
/** The one element a query found, when exactly one was found and it carries a stamp. */
|
|
30
|
+
function soleSource(result) {
|
|
31
|
+
const elements = asRecord(result)?.['elements'];
|
|
32
|
+
if (!Array.isArray(elements) || elements.length !== 1)
|
|
33
|
+
return undefined;
|
|
34
|
+
const source = asString(asRecord(elements[0])?.['source']);
|
|
35
|
+
return source === undefined ? undefined : parseCompactSource(source);
|
|
36
|
+
}
|
|
37
|
+
/** Wrap a replay session so every anchor QUERY that resolves to one stamped element is remembered. */
|
|
38
|
+
export function recordingSources(inner) {
|
|
39
|
+
const sources = new Map();
|
|
40
|
+
const session = new Proxy(inner, {
|
|
41
|
+
// Bound to the target, never the proxy: a real Session keeps `#private` state, and a method
|
|
42
|
+
// called with the proxy as `this` throws reading it.
|
|
43
|
+
get(target, prop) {
|
|
44
|
+
if (prop !== 'command') {
|
|
45
|
+
const value = Reflect.get(target, prop, target);
|
|
46
|
+
return 'function' === typeof value ? value.bind(target) : value;
|
|
47
|
+
}
|
|
48
|
+
return async (name, args) => {
|
|
49
|
+
const result = await target.command(name, args);
|
|
50
|
+
const key = name === ReticleCommand.QUERY ? keyOf(args) : undefined;
|
|
51
|
+
const source = result.ok ? soleSource(result.result) : undefined;
|
|
52
|
+
if (key !== undefined && source !== undefined)
|
|
53
|
+
sources.set(key, source);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return { session, sources };
|
|
59
|
+
}
|
|
60
|
+
function learnSteps(steps, sources) {
|
|
61
|
+
let changed = false;
|
|
62
|
+
const next = steps.map((step) => {
|
|
63
|
+
const nested = step.steps === undefined ? undefined : learnSteps(step.steps, sources);
|
|
64
|
+
const learned = step.source === undefined
|
|
65
|
+
? sources.get(keyOf(anchorQueryArgs(step.anchor) ?? undefined) ?? '')
|
|
66
|
+
: undefined;
|
|
67
|
+
if (learned === undefined && nested?.changed !== true)
|
|
68
|
+
return step;
|
|
69
|
+
changed = true;
|
|
70
|
+
return {
|
|
71
|
+
...step,
|
|
72
|
+
...(learned === undefined ? {} : { source: learned }),
|
|
73
|
+
...(nested === undefined ? {} : { steps: nested.steps }),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
return { steps: next, changed };
|
|
77
|
+
}
|
|
78
|
+
/** The flow with every sourceless step given the file its anchor resolved to; undefined if none was. */
|
|
79
|
+
export function withLearnedSources(flow, sources) {
|
|
80
|
+
if (0 === sources.size)
|
|
81
|
+
return undefined;
|
|
82
|
+
const { steps, changed } = learnSteps(flow.steps, sources);
|
|
83
|
+
return changed ? { ...flow, steps } : undefined;
|
|
84
|
+
}
|
|
@@ -7,13 +7,6 @@
|
|
|
7
7
|
* stop/save can warn instead of writing a flow that fails on the next click.
|
|
8
8
|
*/
|
|
9
9
|
import { type ReticleEvent } from '@reticlehq/core';
|
|
10
|
-
/**
|
|
11
|
-
* The page a recording step sat on, comparable across path routers and hash routers.
|
|
12
|
-
*
|
|
13
|
-
* A startPath is stored as pathname+hash (`/#/search`); a ROUTE_CHANGE carries them separately.
|
|
14
|
-
* Without this, every hash-router page compares as `/` and a backtrack is invisible.
|
|
15
|
-
*/
|
|
16
|
-
export declare function recordingRouteOf(pathname: string, hash?: string): string;
|
|
17
10
|
/**
|
|
18
11
|
* Warn when the journey left a page and came back. Linear A→B→C is silent: that still cannot
|
|
19
12
|
* replay either if a later click needs a previous page, but the reported failure is specifically
|
|
@@ -17,7 +17,7 @@ function asString(value) {
|
|
|
17
17
|
* A startPath is stored as pathname+hash (`/#/search`); a ROUTE_CHANGE carries them separately.
|
|
18
18
|
* Without this, every hash-router page compares as `/` and a backtrack is invisible.
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
function recordingRouteOf(pathname, hash = '') {
|
|
21
21
|
const hashIdx = pathname.indexOf('#/');
|
|
22
22
|
if (hashIdx >= 0) {
|
|
23
23
|
return routePathOf(pathname.slice(0, hashIdx) || '/', pathname.slice(hashIdx));
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { FlowStepResult } from '@reticlehq/core';
|
|
14
14
|
/** Set on a replay that stopped early; omitted entirely otherwise, so a clean pass stays flat. */
|
|
15
|
-
|
|
15
|
+
interface ReplayHalt {
|
|
16
16
|
/** Index of the step that stopped the run. */
|
|
17
17
|
atStep: number;
|
|
18
18
|
/** How many of the flow's steps were never attempted because of it. */
|
|
@@ -24,3 +24,4 @@ export interface ReplayHalt {
|
|
|
24
24
|
* defect restated as its own fix.
|
|
25
25
|
*/
|
|
26
26
|
export declare function haltedFrom(results: readonly FlowStepResult[], total: number): ReplayHalt | undefined;
|
|
27
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Predicate } from '@reticlehq/core';
|
|
2
2
|
/** One captured agent action, normalized for replay. */
|
|
3
3
|
export interface RecordedStep {
|
|
4
4
|
/** ReticleTool.ACT | ReticleTool.ACT_SEQUENCE */
|
|
@@ -8,7 +8,7 @@ export interface RecordedStep {
|
|
|
8
8
|
/** false if any ref could not be normalized to a testid (replay only valid in-session). */
|
|
9
9
|
stable: boolean;
|
|
10
10
|
/** Optional post-condition annotation carried into the on-disk flow's expect. */
|
|
11
|
-
expect?:
|
|
11
|
+
expect?: Predicate;
|
|
12
12
|
/**
|
|
13
13
|
* The route this step ran on. RECORDER-INTERNAL: it never reaches the on-disk flow.
|
|
14
14
|
*
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { REDACTED_FILL, secretEnvKey } from './fields/flow-secret-field.js';
|
|
2
|
+
import { FlowPredicateSchema } from '@reticlehq/core';
|
|
2
3
|
import { DANGEROUS_ACTION_CONFIRM_ARG, ReticleCommand, QueryBy, } from '@reticlehq/core';
|
|
3
4
|
import { ReticleTool } from '@reticlehq/core';
|
|
4
5
|
import { formatStepAddress } from 'open-verification';
|
|
5
|
-
import { predicateToExpect, enforcedOnReplay } from '../../judgement/outcome/predicate-to-expect.js';
|
|
6
|
-
import { PredicateSchema } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
7
6
|
import { asRecord, asString } from '@reticlehq/core';
|
|
8
7
|
/**
|
|
9
8
|
* The note attached when a testid resolves to multiple live elements (the first match is used).
|
|
@@ -105,16 +104,21 @@ route) {
|
|
|
105
104
|
/**
|
|
106
105
|
* A declared predicate, as a recorded expectation — or nothing.
|
|
107
106
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
107
|
+
* It is now the SAME OBJECT. A step's `expect` is a `Predicate`, so what the agent declared in
|
|
108
|
+
* `until` is what the flow file stores, and there is no conversion left to lose anything in.
|
|
109
|
+
*
|
|
110
|
+
* What was here before was a lossy round trip through a flat struct with one slot per kind. An
|
|
111
|
+
* `allOf[netA, netB]` saved as one of the two with nothing said; anything the struct could not
|
|
112
|
+
* express was dropped, and a filter then removed the kinds replay would not have checked anyway.
|
|
113
|
+
* Every one of those was a claim the agent made and the file did not keep.
|
|
112
114
|
*/
|
|
113
115
|
function enforceableExpect(raw) {
|
|
114
116
|
if (raw === undefined)
|
|
115
117
|
return undefined;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
// The flow file's own schema, so capture keeps exactly what a load would accept — a predicate
|
|
119
|
+
// holding a session ref is refused by both, rather than recorded here and rejected on read.
|
|
120
|
+
const parsed = FlowPredicateSchema.safeParse(raw);
|
|
121
|
+
return parsed.success ? parsed.data : undefined;
|
|
118
122
|
}
|
|
119
123
|
/**
|
|
120
124
|
* Compile a single reticle_act invocation into a normalized RecordedStep using the action result. Anchor
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Predicate } from '@reticlehq/core';
|
|
2
2
|
export declare class AnnotationStore {
|
|
3
3
|
#private;
|
|
4
4
|
/** Testids whose CONTENT must not be asserted at replay (mark-dynamic). Copy, never the live array. */
|
|
5
5
|
dynamic(name: string): string[];
|
|
6
6
|
/** The flow's golden end-condition (success-state), or undefined. */
|
|
7
|
-
success(name: string):
|
|
7
|
+
success(name: string): Predicate | undefined;
|
|
8
8
|
/** The flow's declared business goal (intent annotation), or undefined. */
|
|
9
9
|
intent(name: string): string | undefined;
|
|
10
10
|
/** Per-step expect predicates compiled from assert-* annotations, keyed by step index. */
|
|
11
|
-
stepExpect(name: string): Map<number,
|
|
11
|
+
stepExpect(name: string): Map<number, Predicate>;
|
|
12
12
|
/** Append a dynamic testid (mark-dynamic). De-duped so repeated marks stay idempotent. */
|
|
13
13
|
addDynamic(name: string, testid: string): void;
|
|
14
14
|
/** Set the flow's success expectation (success-state); a later set overwrites. */
|
|
15
|
-
setSuccess(name: string, expect:
|
|
15
|
+
setSuccess(name: string, expect: Predicate): void;
|
|
16
16
|
/** Set the flow's declared business goal (intent); a later set overwrites. */
|
|
17
17
|
setIntent(name: string, text: string): void;
|
|
18
18
|
/** Set one step's expect (assert-signal / assert-visible); a later set on the same index wins. */
|
|
19
|
-
setStepExpect(name: string, index: number, expect:
|
|
19
|
+
setStepExpect(name: string, index: number, expect: Predicate): void;
|
|
20
20
|
/** Drop a recording's annotations (called after reticle_flow_save folds them onto disk). */
|
|
21
21
|
clear(name: string): void;
|
|
22
22
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname } from 'node:path';
|
|
2
|
+
import { writeFileAtomic } from '../../../memory/project/fs/write-atomic.js';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { reticleDirPaths } from '../../../memory/project/dir/reticle-dir.js';
|
|
4
5
|
import { withFileLock } from '../../../memory/project/file-lock.js';
|
|
@@ -77,9 +78,7 @@ export class AssertionTiersStore {
|
|
|
77
78
|
async save(tiers) {
|
|
78
79
|
await this.#fs.mkdir(dirname(this.#path));
|
|
79
80
|
const body = `${JSON.stringify({ version: ASSERTION_TIERS_VERSION, flows: tiers }, null, 2)}\n`;
|
|
80
|
-
|
|
81
|
-
await this.#fs.writeFile(tmp, body);
|
|
82
|
-
await this.#fs.rename(tmp, this.#path);
|
|
81
|
+
await writeFileAtomic(this.#fs, this.#path, body);
|
|
83
82
|
}
|
|
84
83
|
/** Record one flow's assertion shape + covered sources after it PASSED. Never breaks a replay. */
|
|
85
84
|
async recordPassing(name, steps, sources = []) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname } from 'node:path';
|
|
2
|
+
import { writeFileAtomic } from '../../../memory/project/fs/write-atomic.js';
|
|
2
3
|
import { reticleDirPaths } from '../../../memory/project/dir/reticle-dir.js';
|
|
3
4
|
import { FlakeFileSchema, emptyRecord, isFlaky, recordOutcome, } from '../recording/flake.js';
|
|
4
5
|
const FLAKE_FILE_VERSION = 1;
|
|
@@ -38,9 +39,7 @@ export class FlakeStore {
|
|
|
38
39
|
async save(ledger) {
|
|
39
40
|
await this.#fs.mkdir(dirname(this.#path));
|
|
40
41
|
const body = `${JSON.stringify({ version: FLAKE_FILE_VERSION, flows: ledger }, null, 2)}\n`;
|
|
41
|
-
|
|
42
|
-
await this.#fs.writeFile(tmp, body);
|
|
43
|
-
await this.#fs.rename(tmp, this.#path);
|
|
42
|
+
await writeFileAtomic(this.#fs, this.#path, body);
|
|
44
43
|
}
|
|
45
44
|
/** Accrue one unchanged-code replay outcome for a flow and persist. */
|
|
46
45
|
async record(flow, passed) {
|
|
@@ -24,7 +24,7 @@ interface CoverageDimension {
|
|
|
24
24
|
/** Declared-but-never-exercised members — what "everything" is silently missing. */
|
|
25
25
|
uncovered: string[];
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
interface Coverage {
|
|
28
28
|
testids: CoverageDimension;
|
|
29
29
|
signals: CoverageDimension;
|
|
30
30
|
flows: CoverageDimension;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - one flow throwing never sinks the run — it is captured as that item's outcome, because a suite that
|
|
10
10
|
* aborts on the first crash tells you nothing about the other 199 flows.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
interface Outcome<T> {
|
|
13
13
|
ok: boolean;
|
|
14
14
|
value?: T;
|
|
15
15
|
error?: string;
|
|
@@ -24,3 +24,4 @@ export declare function mapWithConcurrency<I, T>(items: readonly I[], limit: num
|
|
|
24
24
|
* and never below 1. Explicit `requested` (the tool's `parallel` arg) wins when it is a sane number.
|
|
25
25
|
*/
|
|
26
26
|
export declare function resolveConcurrency(flowCount: number, poolCapacity: number, requested?: number): number;
|
|
27
|
+
export {};
|