/** * The predicate: what a caller can declare, as a shape that crosses the wire. * * This is the CONTRACT half of the predicate language, and it is here for the reason everything * else in this package is: a message that crosses browser ↔ bridge ↔ agent is defined in core, and * a predicate is now one. `FlowStep.expect` is a `Predicate`, which means a saved flow file carries * one, which means core has to be able to parse what it reads off disk. * * It was in `@reticlehq/engine` and it was already half here: `PredicateKind` has always lived in * `verdict/consequence.ts`, so the engine's schema imported its own vocabulary back from its parent. * This finishes that migration rather than starting one. * * WHAT DID NOT COME WITH IT, and the line is deliberate. Evaluation stays in the engine: * `satisfiesProperty`, the residual-query checks, the grammar and nested-key introspection. Core is * the contract, not the reasoning. A reader looking for "what may I write" is in the right file; a * reader looking for "what does it decide" is one package out. */ import { z } from 'zod'; import { type ElementQuery } from '../wire/types.js'; import { ElementState } from '../wire/constants/constants.js'; import { CompareAs, type CompareSource } from './compare-source.js'; import { PredicateKind } from './consequence.js'; import { type PropertyAssertion } from './property-assertion.js'; export type Predicate = { kind: typeof PredicateKind.ELEMENT; query: ElementQuery; state?: ElementState; absent?: boolean; } | { kind: typeof PredicateKind.TEXT; contains?: string; visible?: boolean; absent?: boolean; /** * Restrict the match to a subtree, as a CSS selector or a ref — the same field, and the same * meaning, as `scope` on an element query. * * Without it the match is page-wide, and a word that appears both in a background tab label * and in the dialog that just opened satisfies the predicate BEFORE the action runs, so * `act_and_wait` reports `already_true` for an action that did exactly the right thing. */ scope?: string; /** Match the scope root itself and check its combined subtree text. Requires `scope`. */ self?: boolean; /** * Assert a PROPERTY of the rendered text rather than its exact bytes. * * The same reasoning `state` carries, applied where the user actually reads the value: an * app whose output IS a model's output is different on every run and correct on every one of * them, so `contains` is the one assertion it cannot satisfy twice. Both may be supplied and * then both must hold — `satisfies` narrows, it never excuses. */ satisfies?: PropertyAssertion; /** Match `contains` regardless of case. Off by default: a check compares case exactly. */ ignoreCase?: boolean; } | { kind: typeof PredicateKind.NET; method?: string; urlContains?: string; status?: number; /** Did the call succeed? The honest field for IPC, which has no status code. */ ok?: boolean; since?: number; count?: number; /** A substring the RESPONSE body must contain — what the server answered, not what was sent. */ bodyContains?: string; /** JSON field match over the RESPONSE body: this FIELD holds this value (#987). A key may be a dotted path. */ bodyMatches?: Record; /** A substring the REQUEST body must contain — what the UI sent, not what came back. */ requestBodyContains?: string; /** JSON field match over the REQUEST body, in the style of `signal.dataMatches`. */ requestBodyMatches?: Record; /** Repeats of this call are expected (a read over POST), so they are not a double submit. */ repeatable?: boolean; } | { kind: typeof PredicateKind.ROUTE; pathname?: string; contains?: string; since?: number; } | { kind: typeof PredicateKind.CONSOLE; level?: string; /** * A substring the captured message must contain. With `absent: true` this flips the meaning * from "no console entries at all" to "THIS message did not appear" — the assertion people * actually write regression checks for (deprecation warnings, no-op handler notices). */ contains?: string; absent?: boolean; since?: number; } | { kind: typeof PredicateKind.ANIMATION; name?: string; target?: string; completed?: boolean; since?: number; } | { kind: typeof PredicateKind.SIGNAL; name?: string; dataMatches?: Record; /** * Exact number of matching signals in the window — the same cardinality assertion `net` * carries, on the channel the app itself speaks. Omit for presence (≥1). */ count?: number; since?: number; } | { kind: typeof PredicateKind.STATE; store?: string; path: string; equals?: unknown; satisfies?: PropertyAssertion; } | { kind: typeof PredicateKind.SETTLED; quietMs?: number; } | { kind: typeof PredicateKind.COMPARE; left: CompareSource; right: CompareSource; /** `value` (default): strict equality. `number`: one number read from each side. */ as?: CompareAs; /** How far apart two numbers may be and still agree. Only with `as: "number"`. */ tolerance?: number; } | { kind: typeof PredicateKind.ALL_OF; predicates: Predicate[]; } | { kind: typeof PredicateKind.ANY_OF; predicates: Predicate[]; } | { kind: typeof PredicateKind.NOT; predicate: Predicate; }; /** * Element-query fields an agent writes FLAT on an `element` predicate instead of nested under * `query`. `reticle_query` takes exactly these at the top level, so an agent that has just located * something writes the same words again when it asserts on it — and got `query: Required` plus an * `unknown field` list for its trouble. Lifting them is unambiguous: they have no other meaning on * this kind. * * Derived from the schema rather than listed beside it. The hand-written list this replaces had * already fallen four fields behind `ElementQuerySchema` — `scope`, `self`, `attrs` and `source` — * so a flat `scope` was refused as an unknown key on the predicate while `reticle_look` accepts it * at the top level, and the agent that had just scoped a search wrote the word it had been taught. * A list beside a schema is a list that drifts; a list read FROM the schema cannot. */ export declare const ELEMENT_QUERY_FIELDS: readonly (keyof ElementQuery)[]; /** * Which aliases a predicate as WRITTEN would have renamed, as `kind.from → to` (and `type → kind`). * * The parse itself is silent on purpose — it is a preprocess step — so this reads the same table * to tell the caller its spelling was normalised: a forgiving parse that never says so teaches a * spelling that only works here. Walks `allOf`/`anyOf`/`not` the way the schema does. Unknown * fields are never guessed at: they are not in the table, so they are never reported here, and the * schema still refuses them. */ export declare function predicateAliasesUsed(input: unknown, depth?: number): string[]; export declare const PredicateSchema: z.ZodType; /** * The fields one predicate kind accepts, as the schema itself declares them. * * Exported because the ENGINE's introspection needs it and the union it reads is built here. A * second copy of the union next door is how a rejection message comes to name a stale field set, * which is worse than naming none: the agent trusts it and retries into the same wall. */ export declare function predicateShapeFor(kind: string): z.ZodRawShape | null;