@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
|
@@ -32,7 +32,22 @@ const PREDICATE_ALIASES = {
|
|
|
32
32
|
// things to prove at once, so failing it is expensive at the worst moment.
|
|
33
33
|
[PredicateKind.ALL_OF]: { of: 'predicates' },
|
|
34
34
|
[PredicateKind.ANY_OF]: { of: 'predicates' },
|
|
35
|
+
// `of` on `not` for the same reason it is accepted on the composites above — an agent that learned
|
|
36
|
+
// the spelling one line earlier applies it to the third composite, and `not` was the one that
|
|
37
|
+
// rejected it. The refusal did name `predicate`, but naming a field still costs the round trip
|
|
38
|
+
// that produced no verdict, and the guess is unambiguous: `not` has exactly one child.
|
|
39
|
+
[PredicateKind.NOT]: { of: 'predicate' },
|
|
35
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* The word half the assertion world spells `kind`.
|
|
43
|
+
*
|
|
44
|
+
* A discriminated union rejects `{ type: "text", ... }` with "Invalid discriminator value" on a field
|
|
45
|
+
* the caller never wrote, so the reply reads as being about `kind` — which is absent — and never
|
|
46
|
+
* says the word `type`. Observed on a live drive as the first rung of a ladder: one field name
|
|
47
|
+
* learned per rejected call, each costing a verdict. No predicate kind declares a `type` field, so
|
|
48
|
+
* lifting it is unambiguous, and an explicit `kind` still wins.
|
|
49
|
+
*/
|
|
50
|
+
const KIND_SPELLING = 'type';
|
|
36
51
|
/**
|
|
37
52
|
* Element-query fields an agent writes FLAT on an `element` predicate instead of nested under
|
|
38
53
|
* `query`. `reticle_query` takes exactly these at the top level, so an agent that has just located
|
|
@@ -144,11 +159,20 @@ export function describeResidual(element, field) {
|
|
|
144
159
|
: element.name;
|
|
145
160
|
return `${element.role} "${element.name}" ${field}=${JSON.stringify(reading)}`;
|
|
146
161
|
}
|
|
162
|
+
/** `type` read as the discriminator when — and only when — `kind` is absent. */
|
|
163
|
+
function renameKindSpelling(obj) {
|
|
164
|
+
if (obj['kind'] !== undefined || 'string' !== typeof obj[KIND_SPELLING])
|
|
165
|
+
return obj;
|
|
166
|
+
const out = { ...obj, kind: obj[KIND_SPELLING] };
|
|
167
|
+
delete out[KIND_SPELLING];
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
147
170
|
/** Rename known aliases before parse; an explicit canonical key always wins. */
|
|
148
171
|
function applyPredicateAliases(input) {
|
|
149
172
|
if (typeof input !== 'object' || null === input || Array.isArray(input))
|
|
150
173
|
return input;
|
|
151
|
-
const
|
|
174
|
+
const given = input;
|
|
175
|
+
const obj = renameKindSpelling(given);
|
|
152
176
|
const kind = 'string' === typeof obj['kind'] ? obj['kind'] : '';
|
|
153
177
|
const aliases = PREDICATE_ALIASES[kind];
|
|
154
178
|
let out = obj;
|
|
@@ -269,6 +293,17 @@ function predicateUnion() {
|
|
|
269
293
|
kind: z.literal(PredicateKind.SIGNAL),
|
|
270
294
|
name: z.string().optional(),
|
|
271
295
|
dataMatches: z.record(z.unknown()).optional(),
|
|
296
|
+
/**
|
|
297
|
+
* Exact number of matching signals since the action — presence becomes cardinality.
|
|
298
|
+
*
|
|
299
|
+
* The double-fire is the defect no state-only oracle can see: a handler wired twice fires
|
|
300
|
+
* the signal twice, the store ends up in the right shape either way, and a presence check
|
|
301
|
+
* is green on both. The wrong-name variant is the same blind spot from the other side — the
|
|
302
|
+
* intended signal fires once while a mistyped sibling fires alongside it, and only a count
|
|
303
|
+
* scoped to the matched name tells the two apart. Omit = presence (≥1); `0` asserts the
|
|
304
|
+
* signal never fired, which is a claim in its own right and NOT the same as omitting it.
|
|
305
|
+
*/
|
|
306
|
+
count: z.number().int().nonnegative().optional(),
|
|
272
307
|
since: z.number().optional(),
|
|
273
308
|
})
|
|
274
309
|
.strict(),
|
|
@@ -304,13 +339,89 @@ export const PredicateSchema = z.lazy(() => z.preprocess(applyPredicateAliases,
|
|
|
304
339
|
* wall. Empty for a kind that is not in the union.
|
|
305
340
|
*/
|
|
306
341
|
export function predicateFieldsFor(kind) {
|
|
342
|
+
const shape = shapeForKind(kind);
|
|
343
|
+
if (null === shape)
|
|
344
|
+
return [];
|
|
345
|
+
return Object.keys(shape).filter((field) => 'kind' !== field);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The union option for `kind`, or null when the kind is not one.
|
|
349
|
+
*
|
|
350
|
+
* One place that walks the union, so the field list and the nested lookup cannot disagree about
|
|
351
|
+
* which option a kind resolves to — and so adding a third reader is a call rather than a fourth
|
|
352
|
+
* copy of the same loop.
|
|
353
|
+
*/
|
|
354
|
+
function shapeForKind(kind) {
|
|
307
355
|
for (const option of predicateUnion().options) {
|
|
308
356
|
const literal = option.shape['kind'];
|
|
309
|
-
if (literal instanceof z.ZodLiteral && literal.value === kind)
|
|
310
|
-
return
|
|
311
|
-
|
|
357
|
+
if (literal instanceof z.ZodLiteral && literal.value === kind)
|
|
358
|
+
return option.shape;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Is this tool parameter ACTUALLY the predicate union?
|
|
364
|
+
*
|
|
365
|
+
* Not a name check. `until` is overloaded on this surface — the act/assert family means a predicate
|
|
366
|
+
* by it, while reticle_observe / _network / _console mean a NUMBER, an upper cursor bound — so any
|
|
367
|
+
* caller keying on the word would treat a numeric parameter as a predicate and describe it to the
|
|
368
|
+
* agent as an object. Keying on the schema cannot make that mistake.
|
|
369
|
+
*
|
|
370
|
+
* Lives beside the schema because two callers now ask the question: the lean tool surface, which
|
|
371
|
+
* compacts these parameters, and `reticle_tools`, which spells their grammar out on request. Two
|
|
372
|
+
* spellings of "is this a predicate" is one chance for the surface and the grammar to disagree.
|
|
373
|
+
*/
|
|
374
|
+
export function isPredicateParam(schema) {
|
|
375
|
+
const inner = schema instanceof z.ZodOptional ? schema.unwrap() : schema;
|
|
376
|
+
return inner === PredicateSchema || inner === PredicateSchema.optional();
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Every kind's fields, in one block an agent can write a predicate from.
|
|
380
|
+
*
|
|
381
|
+
* The tool surface advertises the KIND list and points at `reticle_tools` for the fields, because
|
|
382
|
+
* inlining the 12-variant recursive union in the declared JSON Schema costs thousands of characters
|
|
383
|
+
* per predicate parameter, re-sent every turn, to describe a grammar most calls use one variant of.
|
|
384
|
+
* That trade is right — but the pointer has to land somewhere, and it landed on a parameter
|
|
385
|
+
* description reading "same shape as reticle_assert". So the grammar was reachable from nothing, and
|
|
386
|
+
* an agent that could not find `route`'s fields fell back to a `text` check where a route check was
|
|
387
|
+
* meant: the weaker oracle, which is the expensive half of an undiscoverable grammar.
|
|
388
|
+
*
|
|
389
|
+
* Derived from the schema, like `predicateFieldsFor` and for the same reason: a hand-written grammar
|
|
390
|
+
* that drifts is worse than none, because the agent trusts it and retries into the same wall.
|
|
391
|
+
*/
|
|
392
|
+
export function predicateGrammar() {
|
|
393
|
+
const grammar = {};
|
|
394
|
+
for (const kind of Object.values(PredicateKind)) {
|
|
395
|
+
const nested = predicateNestedFieldsFor(kind);
|
|
396
|
+
grammar[kind] = predicateFieldsFor(kind)
|
|
397
|
+
.map((field) => {
|
|
398
|
+
const keys = nested[field];
|
|
399
|
+
return undefined === keys ? field : `${field} { ${keys.join(', ')} }`;
|
|
400
|
+
})
|
|
401
|
+
.join(', ');
|
|
312
402
|
}
|
|
313
|
-
return
|
|
403
|
+
return grammar;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The keys of a field that is an object, once its wrappers are peeled. Empty for anything else.
|
|
407
|
+
*
|
|
408
|
+
* Exported so the wrapper handling is asserted against THIS function rather than through the
|
|
409
|
+
* rendered sentence. Reached only that way, a wrapper it fails to peel returns `[]`, the old
|
|
410
|
+
* message prints unchanged, and nothing reddens — a regression with no symptom.
|
|
411
|
+
*
|
|
412
|
+
* No top-level predicate field is currently declared behind a wrapper, so the real schema exercises
|
|
413
|
+
* none of these paths; that is exactly why they are worth a case each.
|
|
414
|
+
*/
|
|
415
|
+
export function nestedKeysOf(schema) {
|
|
416
|
+
if (undefined === schema)
|
|
417
|
+
return [];
|
|
418
|
+
const inner = unwrapSchema(schema);
|
|
419
|
+
// `instanceof z.ZodObject` narrows to ZodObject<any>, whose `.shape` is `any`. Name the shape
|
|
420
|
+
// type so the keys are read off something typed rather than laundering an `any` through
|
|
421
|
+
// Object.keys.
|
|
422
|
+
if (!(inner instanceof z.ZodObject))
|
|
423
|
+
return [];
|
|
424
|
+
return Object.keys(inner.shape);
|
|
314
425
|
}
|
|
315
426
|
/** Peel optional/nullable/default/effects wrappers off a field to reach the schema underneath. */
|
|
316
427
|
function unwrapSchema(schema) {
|
|
@@ -341,24 +452,16 @@ function unwrapSchema(schema) {
|
|
|
341
452
|
* list is worse than none, because the agent trusts it and retries into the same wall.
|
|
342
453
|
*/
|
|
343
454
|
export function predicateNestedFieldsFor(kind) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
455
|
+
const shape = shapeForKind(kind);
|
|
456
|
+
if (null === shape)
|
|
457
|
+
return {};
|
|
458
|
+
const nested = {};
|
|
459
|
+
for (const [field, schema] of Object.entries(shape)) {
|
|
460
|
+
if ('kind' === field)
|
|
347
461
|
continue;
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
continue;
|
|
352
|
-
const inner = unwrapSchema(schema);
|
|
353
|
-
// `instanceof z.ZodObject` narrows to ZodObject<any>, whose `.shape` is `any`. Name the shape
|
|
354
|
-
// type so the keys are read off something typed rather than laundering an `any` through
|
|
355
|
-
// Object.keys.
|
|
356
|
-
if (inner instanceof z.ZodObject) {
|
|
357
|
-
const shape = inner.shape;
|
|
358
|
-
nested[field] = Object.keys(shape);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return nested;
|
|
462
|
+
const keys = nestedKeysOf(schema);
|
|
463
|
+
if (0 < keys.length)
|
|
464
|
+
nested[field] = keys;
|
|
362
465
|
}
|
|
363
|
-
return
|
|
466
|
+
return nested;
|
|
364
467
|
}
|
|
@@ -19,6 +19,21 @@ export interface PredicateSession {
|
|
|
19
19
|
* navigates simply omits it and route falls back to change-only, as before.
|
|
20
20
|
*/
|
|
21
21
|
url?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The document currently under observation, as the session derived it from its own event stream.
|
|
24
|
+
*
|
|
25
|
+
* Every oracle below that reads the window asks "what happened here", and a window is scoped by
|
|
26
|
+
* time and by ring-buffer capacity and by nothing else — so it can still hold the traffic, console
|
|
27
|
+
* output and signals of a page a full navigation or a reload has already thrown away. Answering a
|
|
28
|
+
* predicate with one of those is true about the bytes and false about the world, in whichever
|
|
29
|
+
* direction it lands: a stale event that satisfies the assertion is a false green, and one that
|
|
30
|
+
* refutes it is a false red.
|
|
31
|
+
*
|
|
32
|
+
* Optional, and undefined means nobody could say which document is current — `isSameDocument`
|
|
33
|
+
* treats absence as current on both sides, so the engine then behaves exactly as it did before this
|
|
34
|
+
* existed. Named to match `Session.currentDocumentId`, which is what supplies it in production.
|
|
35
|
+
*/
|
|
36
|
+
currentDocumentId?: string | undefined;
|
|
22
37
|
/**
|
|
23
38
|
* Learned per-ref ambient-churn counts (real-time regions that churn with no action driving them).
|
|
24
39
|
* The settle oracle drops events on learned-ambient refs so a chat/ticker page can still go quiet.
|
package/dist/events/predicate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ElementState, PredicateKind, ReticleCommand, } from '@reticlehq/core';
|
|
1
|
+
import { ElementState, PredicateKind, ReticleCommand, isSameDocument, } from '@reticlehq/core';
|
|
2
2
|
import { log } from '../log.js';
|
|
3
3
|
import { bindSpanContext } from '../trace.js';
|
|
4
4
|
import { selectPath, capDepth } from '../session/state-select.js';
|
|
@@ -6,6 +6,7 @@ import { describeTestidMiss } from './testid-near-miss.js';
|
|
|
6
6
|
import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
|
|
7
7
|
import { isAmbient, ambientKeyOf } from '../journal/ambient.js';
|
|
8
8
|
import { evalRoute } from './predicate-route.js';
|
|
9
|
+
import { describeSuperseded } from './observed-in-window.js';
|
|
9
10
|
import { PredicateSchema, matchValue, evalNet, evalConsole, evalAnimation, evalSignal, evalSettled, residualQueryChecks, satisfiesResiduals, describeResidual, } from './predicate-eval.js';
|
|
10
11
|
export { PredicateSchema, PredicateKind };
|
|
11
12
|
/**
|
|
@@ -397,6 +398,23 @@ async function evalState(session, p) {
|
|
|
397
398
|
evidence: { store: storeName, path: p.path, value: capDepth(selection.value, 1) },
|
|
398
399
|
};
|
|
399
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Oracles whose ONLY source of truth is the event window.
|
|
403
|
+
*
|
|
404
|
+
* Element, text and state read the page as it is right now, so supersession cannot reach them. Route
|
|
405
|
+
* is the deliberate omission: it has a second source — where the app is at this moment — and a
|
|
406
|
+
* reload is precisely the case it was given that fallback for, so emptying its window must not take
|
|
407
|
+
* the answer away.
|
|
408
|
+
*/
|
|
409
|
+
const WINDOW_ONLY_KINDS = new Set([
|
|
410
|
+
PredicateKind.NET,
|
|
411
|
+
PredicateKind.CONSOLE,
|
|
412
|
+
PredicateKind.ANIMATION,
|
|
413
|
+
PredicateKind.SIGNAL,
|
|
414
|
+
PredicateKind.SETTLED,
|
|
415
|
+
]);
|
|
416
|
+
/** Names the oracle that answered, in the same shape every other `assertion` here uses. */
|
|
417
|
+
const SUPERSEDED_ASSERTION = 'window.evidence-superseded';
|
|
400
418
|
/** The predicate's own event-time floor, if its kind carries one. */
|
|
401
419
|
function predicateSince(predicate) {
|
|
402
420
|
return 'since' in predicate && 'number' === typeof predicate.since ? predicate.since : 0;
|
|
@@ -411,7 +429,28 @@ diagnose = true) {
|
|
|
411
429
|
// `since` from the act it just performed is scoping the assertion to that action's aftermath. It is
|
|
412
430
|
// applied here, once, rather than in each eval: the floor means the same thing for every kind that
|
|
413
431
|
// reads the event stream, and net/console re-applying it is a no-op.
|
|
414
|
-
const
|
|
432
|
+
const raw = session.eventsSince(Math.max(since, predicateSince(predicate)));
|
|
433
|
+
// Scoped ONCE, here, for the same reason the contradiction pass scopes at its own choke point:
|
|
434
|
+
// reasoning about a dead page's evidence is a defect in every oracle below rather than in whichever
|
|
435
|
+
// one happened to read it, and this is the single place they all take their window from.
|
|
436
|
+
const events = raw.filter((e) => isSameDocument(e.documentId, session.currentDocumentId));
|
|
437
|
+
const superseded = raw.length - events.length;
|
|
438
|
+
// An empty window was always allowed to mean "it did not happen"; that reading is only unsafe once
|
|
439
|
+
// supersession is what emptied it. Without this, dropping stale evidence would trade a wrong pass
|
|
440
|
+
// for a wrong FAILURE — and a failure names a component and sends an agent to fix working code.
|
|
441
|
+
// `inconclusive` is the established way to say "nothing was proven and nobody could have proven
|
|
442
|
+
// it", and `decideVerified` already reads it as UNKNOWN ahead of the failure clause.
|
|
443
|
+
if (superseded > 0 && 0 === events.length && WINDOW_ONLY_KINDS.has(predicate.kind)) {
|
|
444
|
+
const reason = describeSuperseded('observations', superseded);
|
|
445
|
+
return {
|
|
446
|
+
pass: false,
|
|
447
|
+
failureReason: reason,
|
|
448
|
+
inconclusive: reason,
|
|
449
|
+
observed: 'every observation in this window belongs to a document since replaced',
|
|
450
|
+
expected: `evidence recorded under the document now on screen for ${predicate.kind}`,
|
|
451
|
+
assertion: SUPERSEDED_ASSERTION,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
415
454
|
switch (predicate.kind) {
|
|
416
455
|
case PredicateKind.ELEMENT:
|
|
417
456
|
return evalElement(session, predicate.query, predicate.state, predicate.absent ?? false, diagnose);
|
package/dist/flows/coverage.d.ts
CHANGED
|
@@ -33,14 +33,6 @@ export interface Coverage {
|
|
|
33
33
|
}
|
|
34
34
|
/** The gate's outcome when the project has recorded no flows at all — not a pass, and not a failure. */
|
|
35
35
|
export declare const NO_FLOWS = "no_flows";
|
|
36
|
-
/**
|
|
37
|
-
* What to do about it, since the empty state is otherwise indistinguishable from success.
|
|
38
|
-
*
|
|
39
|
-
* Names the TOOLS, not a `reticle record` CLI verb: this CLI does not dispatch one, and a suggestion
|
|
40
|
-
* that errors is a second dead end on top of the first (see suggested-commands-exist.test.ts). Same
|
|
41
|
-
* wording as the empty-suite verdict, which is the same situation reached from `reticle verify`.
|
|
42
|
-
*/
|
|
43
|
-
export declare const NO_FLOWS_NOTE = "no flows recorded \u2014 nothing was checked. Record one with reticle_record { action: \"start\" }, then reticle_flow_save.";
|
|
44
36
|
/** The gate's flow-coverage line. `pct` and `outcome` are mutually exclusive by construction. */
|
|
45
37
|
export interface FlowCoverageReport {
|
|
46
38
|
covered: number;
|
package/dist/flows/coverage.js
CHANGED
|
@@ -13,7 +13,7 @@ export const NO_FLOWS = 'no_flows';
|
|
|
13
13
|
* that errors is a second dead end on top of the first (see suggested-commands-exist.test.ts). Same
|
|
14
14
|
* wording as the empty-suite verdict, which is the same situation reached from `reticle verify`.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
const NO_FLOWS_NOTE = 'no flows recorded — nothing was checked. Record one with reticle_record { action: "start" }, then reticle_flow_save.';
|
|
17
17
|
/**
|
|
18
18
|
* Report flow coverage so that an EMPTY suite cannot be read as a passing one.
|
|
19
19
|
*
|
package/dist/flows/decision.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type FlowFile, type FlowReplayResult, type ReplayDecision, type SuiteVerdict } from '@reticlehq/core';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @param intentStatement the ledger's current wording, when the flow is bound to an intent row.
|
|
4
|
+
* Falls back to the copy on the flow file, which is what a flow saved before the link carries.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildDecision(result: FlowReplayResult, flow?: FlowFile, intentStatement?: string): ReplayDecision;
|
|
3
7
|
/**
|
|
4
8
|
* Aggregate per-flow replays into one suite verdict — the autonomous loop's consolidated answer
|
|
5
9
|
* after a build: did anything break, and what's the prioritized fix list. Pure: pass the already
|
package/dist/flows/decision.js
CHANGED
|
@@ -29,19 +29,45 @@ function whereInSource(step, flow) {
|
|
|
29
29
|
: undefined;
|
|
30
30
|
return source === undefined ? undefined : `${source.file}:${String(source.line)}`;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The business outcome that stopped being true, said before the mechanism that broke it.
|
|
34
|
+
*
|
|
35
|
+
* Order is the point. The intent is what makes a failure legible — a human or an agent reading
|
|
36
|
+
* "step 3 assertion failed" has to reconstruct the stakes — and the mechanical detail underneath is
|
|
37
|
+
* what makes it actionable. Dropping either one costs something, so the summary carries both.
|
|
38
|
+
*/
|
|
39
|
+
const INTENT_BROKEN_PREFIX = 'NO LONGER TRUE: ';
|
|
40
|
+
/**
|
|
41
|
+
* What a report says when nothing declared what the flow is for.
|
|
42
|
+
*
|
|
43
|
+
* Never an intent derived from step names: a guessed goal reads as the product owner's words and an
|
|
44
|
+
* agent will act on it. The same rule as the source pointer — absence stays honest.
|
|
45
|
+
*/
|
|
46
|
+
const NO_INTENT_ON_PASS = 'no intent declared, so this proves the steps ran, not what they were for — declare one with reticle_intent.';
|
|
47
|
+
const NO_INTENT_ON_FAIL = 'no intent declared, so this names the mechanism, not the stakes.';
|
|
48
|
+
/** `<intent> — <mechanical>`, or the mechanical sentence plus the honest absence. */
|
|
49
|
+
function withIntent(mechanical, statement) {
|
|
50
|
+
return statement === undefined
|
|
51
|
+
? `${mechanical} — ${NO_INTENT_ON_FAIL}`
|
|
52
|
+
: `${INTENT_BROKEN_PREFIX}"${statement}" — ${mechanical}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @param intentStatement the ledger's current wording, when the flow is bound to an intent row.
|
|
56
|
+
* Falls back to the copy on the flow file, which is what a flow saved before the link carries.
|
|
57
|
+
*/
|
|
58
|
+
export function buildDecision(result, flow, intentStatement) {
|
|
33
59
|
const { name, status, steps } = result;
|
|
60
|
+
const intentSaid = intentStatement ?? flow?.intent;
|
|
34
61
|
if (status === ReplayStatus.OK) {
|
|
35
62
|
// Green — but is it green-for-the-right-reason? A flow that asserts no consequence can pass while
|
|
36
63
|
// broken, so the honest next action is to add one.
|
|
37
64
|
const grade = flow !== undefined ? classifyFlowAssertions(flow) : undefined;
|
|
38
65
|
const verifiesOutcome = true === grade?.hasConsequenceAssertion;
|
|
39
|
-
const intent = flow?.intent;
|
|
40
66
|
return {
|
|
41
67
|
verdict: 'pass',
|
|
42
|
-
summary:
|
|
43
|
-
? `"${name}" passed — intent "${
|
|
44
|
-
: `"${name}" passed (${steps.length} steps)
|
|
68
|
+
summary: intentSaid !== undefined
|
|
69
|
+
? `"${name}" passed — intent "${intentSaid}" ${verifiesOutcome ? 'verified' : 'NOT asserted'}.`
|
|
70
|
+
: `"${name}" passed (${steps.length} steps) — ${NO_INTENT_ON_PASS}`,
|
|
45
71
|
nextAction: verifiesOutcome
|
|
46
72
|
? 'none — the flow held and its consequence was observed.'
|
|
47
73
|
: 'add a consequence assertion (assert-signal / success-state) so this flow can fail when the feature breaks.',
|
|
@@ -58,7 +84,7 @@ export function buildDecision(result, flow) {
|
|
|
58
84
|
: undefined;
|
|
59
85
|
return {
|
|
60
86
|
verdict: 'drift',
|
|
61
|
-
summary: `"${name}" drifted at step ${step.step} (${step.anchor}).`,
|
|
87
|
+
summary: withIntent(`"${name}" drifted at step ${step.step} (${step.anchor}).`, intentSaid),
|
|
62
88
|
whatChanged: drift.reason,
|
|
63
89
|
...(where !== undefined ? { whereInSource: where } : {}),
|
|
64
90
|
...(fix !== undefined ? { suggestedFix: fix } : {}),
|
|
@@ -72,7 +98,7 @@ export function buildDecision(result, flow) {
|
|
|
72
98
|
const isSuccessOracle = step?.tool === SUCCESS_STEP_TOOL;
|
|
73
99
|
return {
|
|
74
100
|
verdict: 'fail',
|
|
75
|
-
summary: `"${name}" failed${step !== undefined ? ` at step ${step.step} (${step.anchor})` : ''}.`,
|
|
101
|
+
summary: withIntent(`"${name}" failed${step !== undefined ? ` at step ${step.step} (${step.anchor})` : ''}.`, intentSaid),
|
|
76
102
|
whatChanged: message,
|
|
77
103
|
...(where !== undefined ? { whereInSource: where } : {}),
|
|
78
104
|
nextAction: isSuccessOracle
|
package/dist/flows/flake.d.ts
CHANGED
|
@@ -43,8 +43,6 @@ export declare const FlakeFileSchema: z.ZodObject<{
|
|
|
43
43
|
runs: number;
|
|
44
44
|
}>;
|
|
45
45
|
}>;
|
|
46
|
-
/** Minimum unchanged-code replays before flakiness can be judged (too few = noise). */
|
|
47
|
-
export declare const DEFAULT_MIN_FLAKE_RUNS = 5;
|
|
48
46
|
export declare function emptyRecord(): FlakeRecord;
|
|
49
47
|
/** Accrue one unchanged-code replay outcome. */
|
|
50
48
|
export declare function recordOutcome(record: FlakeRecord, passed: boolean): FlakeRecord;
|
package/dist/flows/flake.js
CHANGED
|
@@ -14,7 +14,7 @@ export const FlakeFileSchema = z.object({
|
|
|
14
14
|
flows: z.record(FlakeRecordSchema),
|
|
15
15
|
});
|
|
16
16
|
/** Minimum unchanged-code replays before flakiness can be judged (too few = noise). */
|
|
17
|
-
|
|
17
|
+
const DEFAULT_MIN_FLAKE_RUNS = 5;
|
|
18
18
|
export function emptyRecord() {
|
|
19
19
|
return { runs: 0, fails: 0 };
|
|
20
20
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type FlowFile, type InstrumentationGap } from '@reticlehq/core';
|
|
2
|
+
import { IntentStore } from '../intent/intent-store.js';
|
|
3
|
+
/** The ledger id a flow's own prose goal is declared under. */
|
|
4
|
+
export declare function flowIntentId(flowName: string): string;
|
|
5
|
+
/** The verdict that a replay of this flow, at this time, constitutes — what `provenBy` records. */
|
|
6
|
+
export declare function flowReplayVerdictId(flowName: string, at: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* Promote a flow's prose goal into the ledger and stamp the id back onto the flow.
|
|
9
|
+
*
|
|
10
|
+
* The binding is attached only when the flow asserts an observable consequence, because that is the
|
|
11
|
+
* only case where replaying it green proves anything. A flow that asserts nothing leaves its intent
|
|
12
|
+
* `declared` — and `dischargeIntent` refuses to prove an unbound intent, so an assertion-free flow
|
|
13
|
+
* can never discharge one. That is the whole guard, and it is the ledger's own rule, not a new one.
|
|
14
|
+
*
|
|
15
|
+
* Returns the flow unchanged (and writes nothing) when there is no intent, so a flow file saved
|
|
16
|
+
* before any of this existed keeps its exact bytes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function linkFlowIntent(store: IntentStore, flow: FlowFile): Promise<FlowFile>;
|
|
19
|
+
/**
|
|
20
|
+
* The nudge a flow saved without an intent earns, or nothing.
|
|
21
|
+
*
|
|
22
|
+
* Every save path routes through here, so the three of them cannot drift into three different ways
|
|
23
|
+
* of saying the same thing. It says it in the vocabulary the instrumentation gaps already use — what
|
|
24
|
+
* is missing, what it costs, and the one change that closes it — because "nobody said what this is
|
|
25
|
+
* for" is the same finding `undeclared-change` makes about an edit, one artifact later.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately generic. It names no step, no assertion and not even the flow, for the reason this
|
|
28
|
+
* file already gives twice: anything specific enough to identify the goal is specific enough to be
|
|
29
|
+
* read AS the goal. It also never fails the save — a flow with no intent is still far better than no
|
|
30
|
+
* flow, and a verification tool that refuses to record work is one people route around.
|
|
31
|
+
*/
|
|
32
|
+
export declare function flowIntentGap(flow: FlowFile): InstrumentationGap | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* What the ledger currently says this flow is for, or undefined when nothing declared it.
|
|
35
|
+
*
|
|
36
|
+
* Read from the ledger rather than from the flow file's own copy: the ledger is where an amendment
|
|
37
|
+
* lands, and a report that quotes the stale copy would be the two-vocabularies problem all over
|
|
38
|
+
* again — one sentence in review, a different one in the failure message.
|
|
39
|
+
*/
|
|
40
|
+
export declare function flowIntentStatement(store: IntentStore, flow: FlowFile): Promise<string | undefined>;
|
|
41
|
+
/**
|
|
42
|
+
* Record that this replay proved the flow's intent. False when there was nothing to prove.
|
|
43
|
+
*
|
|
44
|
+
* Best-effort by construction: `IntentStore.discharge` returns false rather than throwing on an
|
|
45
|
+
* unknown or unbound id, because discharge runs off the back of a verdict and must never be the
|
|
46
|
+
* reason one fails to return.
|
|
47
|
+
*/
|
|
48
|
+
export declare function dischargeFlowIntent(store: IntentStore, flow: FlowFile, proof: {
|
|
49
|
+
verdictId: string;
|
|
50
|
+
grade: string;
|
|
51
|
+
at: number;
|
|
52
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { InstrumentationGapKind, instrumentationGap, } from '@reticlehq/core';
|
|
2
|
+
import { IntentStore } from '../intent/intent-store.js';
|
|
3
|
+
import { classifyFlowAssertions } from './flow-classify.js';
|
|
4
|
+
/**
|
|
5
|
+
* The link between a saved flow and the intent ledger — what the flow is FOR.
|
|
6
|
+
*
|
|
7
|
+
* A replay used to report only in the language of the DOM: step 3 drifted, this assertion failed.
|
|
8
|
+
* True, actionable, and illegible — the reader still had to reconstruct what stopped working. The
|
|
9
|
+
* ledger in `.reticle/intent.json` already holds the other half (prose statements, declared → bound
|
|
10
|
+
* → proved, and the verdict that discharged each one), so a flow does not need its own notion of
|
|
11
|
+
* intent. It needs an id.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here derives an intent from step names. A flow that declares none reports that it declares
|
|
14
|
+
* none — a guessed intent puts words in the product owner's mouth and an agent will act on them,
|
|
15
|
+
* which is strictly worse than the honest absence.
|
|
16
|
+
*/
|
|
17
|
+
/** Namespaced so a flow-declared intent never collides with one an agent declared by hand. */
|
|
18
|
+
const FLOW_INTENT_ID_PREFIX = 'flow:';
|
|
19
|
+
const FLOW_VERDICT_ID_PREFIX = 'flow_replay:';
|
|
20
|
+
const ID_SEPARATOR = ':';
|
|
21
|
+
/** The ledger id a flow's own prose goal is declared under. */
|
|
22
|
+
export function flowIntentId(flowName) {
|
|
23
|
+
return `${FLOW_INTENT_ID_PREFIX}${flowName}`;
|
|
24
|
+
}
|
|
25
|
+
/** The verdict that a replay of this flow, at this time, constitutes — what `provenBy` records. */
|
|
26
|
+
export function flowReplayVerdictId(flowName, at) {
|
|
27
|
+
return `${FLOW_VERDICT_ID_PREFIX}${flowName}${ID_SEPARATOR}${String(at)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Which ledger row this flow answers to, or nothing.
|
|
31
|
+
*
|
|
32
|
+
* An explicit `intentId` wins, so a flow can discharge an intent that was declared before it existed.
|
|
33
|
+
* Otherwise the flow's own prose implies one. A flow with neither has no intent, and says so.
|
|
34
|
+
*/
|
|
35
|
+
function intentIdOf(flow) {
|
|
36
|
+
if (flow.intentId !== undefined)
|
|
37
|
+
return flow.intentId;
|
|
38
|
+
// TRIMMED, not merely defined. `intent: " "` satisfied `!== undefined` and silenced the gap, so
|
|
39
|
+
// a flow whose stated goal is whitespace looked identical on disk to one somebody thought about.
|
|
40
|
+
//
|
|
41
|
+
// The worse direction of the two: this gap is the ONLY thing that would ever tell anyone the flow
|
|
42
|
+
// has no goal, so suppressing it wrongly means the flow replays for months and the day it goes red
|
|
43
|
+
// the report names the broken step and nothing else — the precise failure it was written to
|
|
44
|
+
// prevent. Found by probing the seam rather than reading it.
|
|
45
|
+
return undefined === flow.intent || '' === flow.intent.trim()
|
|
46
|
+
? undefined
|
|
47
|
+
: flowIntentId(flow.name);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Promote a flow's prose goal into the ledger and stamp the id back onto the flow.
|
|
51
|
+
*
|
|
52
|
+
* The binding is attached only when the flow asserts an observable consequence, because that is the
|
|
53
|
+
* only case where replaying it green proves anything. A flow that asserts nothing leaves its intent
|
|
54
|
+
* `declared` — and `dischargeIntent` refuses to prove an unbound intent, so an assertion-free flow
|
|
55
|
+
* can never discharge one. That is the whole guard, and it is the ledger's own rule, not a new one.
|
|
56
|
+
*
|
|
57
|
+
* Returns the flow unchanged (and writes nothing) when there is no intent, so a flow file saved
|
|
58
|
+
* before any of this existed keeps its exact bytes.
|
|
59
|
+
*/
|
|
60
|
+
export async function linkFlowIntent(store, flow) {
|
|
61
|
+
const id = intentIdOf(flow);
|
|
62
|
+
if (id === undefined)
|
|
63
|
+
return flow;
|
|
64
|
+
if (flow.intent !== undefined) {
|
|
65
|
+
await store.declare([{ id, statement: flow.intent, surface: { flow: flow.name } }]);
|
|
66
|
+
}
|
|
67
|
+
if (classifyFlowAssertions(flow).hasConsequenceAssertion) {
|
|
68
|
+
await store.bind(id, { flow: flow.name });
|
|
69
|
+
}
|
|
70
|
+
return { ...flow, intentId: id };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The nudge a flow saved without an intent earns, or nothing.
|
|
74
|
+
*
|
|
75
|
+
* Every save path routes through here, so the three of them cannot drift into three different ways
|
|
76
|
+
* of saying the same thing. It says it in the vocabulary the instrumentation gaps already use — what
|
|
77
|
+
* is missing, what it costs, and the one change that closes it — because "nobody said what this is
|
|
78
|
+
* for" is the same finding `undeclared-change` makes about an edit, one artifact later.
|
|
79
|
+
*
|
|
80
|
+
* Deliberately generic. It names no step, no assertion and not even the flow, for the reason this
|
|
81
|
+
* file already gives twice: anything specific enough to identify the goal is specific enough to be
|
|
82
|
+
* read AS the goal. It also never fails the save — a flow with no intent is still far better than no
|
|
83
|
+
* flow, and a verification tool that refuses to record work is one people route around.
|
|
84
|
+
*/
|
|
85
|
+
export function flowIntentGap(flow) {
|
|
86
|
+
if (intentIdOf(flow) !== undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
return instrumentationGap(InstrumentationGapKind.NO_FLOW_INTENT, 'this flow declares no intent, so nothing on disk says what it is meant to prove', 'it will replay for months, and the day it goes red the report can only name the step that broke — not the thing that stopped being true — so whoever reads it has to reconstruct the goal from the steps before they can tell a real regression from a moved button');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* What the ledger currently says this flow is for, or undefined when nothing declared it.
|
|
92
|
+
*
|
|
93
|
+
* Read from the ledger rather than from the flow file's own copy: the ledger is where an amendment
|
|
94
|
+
* lands, and a report that quotes the stale copy would be the two-vocabularies problem all over
|
|
95
|
+
* again — one sentence in review, a different one in the failure message.
|
|
96
|
+
*/
|
|
97
|
+
export async function flowIntentStatement(store, flow) {
|
|
98
|
+
const id = intentIdOf(flow);
|
|
99
|
+
if (id === undefined)
|
|
100
|
+
return undefined;
|
|
101
|
+
const found = (await store.read()).find((intent) => intent.id === id);
|
|
102
|
+
return found?.statement ?? flow.intent;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Record that this replay proved the flow's intent. False when there was nothing to prove.
|
|
106
|
+
*
|
|
107
|
+
* Best-effort by construction: `IntentStore.discharge` returns false rather than throwing on an
|
|
108
|
+
* unknown or unbound id, because discharge runs off the back of a verdict and must never be the
|
|
109
|
+
* reason one fails to return.
|
|
110
|
+
*/
|
|
111
|
+
export async function dischargeFlowIntent(store, flow, proof) {
|
|
112
|
+
const id = intentIdOf(flow);
|
|
113
|
+
if (id === undefined)
|
|
114
|
+
return false;
|
|
115
|
+
try {
|
|
116
|
+
return await store.discharge(id, proof);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// A ledger that cannot be written is a small problem; a verdict that never returns because of
|
|
120
|
+
// one is a large problem. The replay already succeeded — the proof is simply not recorded.
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|