Skip to main content

pointlock_ir/
assertion.rs

1//! Assertions: the "questions" of the IR (02 §5.3, §9.1).
2//!
3//! An assertion is a pure predicate over observations / action outputs. It
4//! yields `pass | fail | unknown` at runtime — the answer (Verdict) is a
5//! runtime artifact and deliberately absent from the IR (principle 3).
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use crate::expr::Expr;
11use crate::primitives::{AssertId, OnMissingInput};
12use crate::selector::{ElementSelectorIR, RectIR, TextMatchIR};
13use crate::vocab::{ElementState, VerifyChannel};
14
15/// A single assertion with its explicit verify-chain.
16///
17/// The three baseline `allOf` conditionals are reproduced on the generated
18/// schema via `#[schemars(extend)]`:
19/// 1. `expr` predicates consume no observation channel (`verifyVia: []`);
20///    all other predicates need at least one channel.
21/// 2. `visual` predicates are vision-only (`verifyVia == ["vision"]`).
22/// 3. For `elementState`/`elementText`, `visionPrompt` is required iff the
23///    chain contains `vision`, forbidden otherwise (03 §1.4 rule 5; the
24///    compiler never synthesizes vision prompts, principle 6).
25///
26/// "vision only at the chain tail" is order-sensitive and remains a
27/// bind-phase check (not expressible in JSON Schema).
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30#[schemars(extend("allOf" = [
31    {
32        "if": {
33            "properties": {
34                "predicate": {
35                    "type": "object",
36                    "properties": { "type": { "const": "expr" } },
37                    "required": ["type"]
38                }
39            },
40            "required": ["predicate"]
41        },
42        "then": { "properties": { "verifyVia": { "maxItems": 0 } } },
43        "else": { "properties": { "verifyVia": { "minItems": 1 } } }
44    },
45    {
46        "if": {
47            "properties": {
48                "predicate": {
49                    "type": "object",
50                    "properties": { "type": { "const": "visual" } },
51                    "required": ["type"]
52                }
53            },
54            "required": ["predicate"]
55        },
56        "then": { "properties": { "verifyVia": { "const": ["vision"] } } }
57    },
58    {
59        "if": {
60            "properties": {
61                "predicate": {
62                    "type": "object",
63                    "properties": { "type": { "enum": ["elementState", "elementText"] } },
64                    "required": ["type"]
65                },
66                "verifyVia": { "type": "array", "contains": { "const": "vision" } }
67            },
68            "required": ["predicate", "verifyVia"]
69        },
70        "then": { "required": ["visionPrompt"] },
71        "else": { "not": { "required": ["visionPrompt"] } }
72    }
73]))]
74pub struct AssertionIR {
75    /// Stable assertion id (unique within the step).
76    pub assert_id: AssertId,
77    /// The predicate to evaluate.
78    pub predicate: PredicateIR,
79    /// Explicit, ordered verify-chain — a subsequence of `[dom, uiTree,
80    /// vision]`. Order carries semantics (degradation order) and participates
81    /// verbatim in `judgeHash` (02 §12.1 rule 5). Uniqueness is enforced by
82    /// the schema (`uniqueItems`), not by this type.
83    #[schemars(schema_with = "unique_verify_via_schema")]
84    pub verify_via: Vec<VerifyChannel>,
85    /// Author-written vision prompt, handed verbatim to the VisionVerifier
86    /// when `vision` is the declared degraded tail of an
87    /// `elementState`/`elementText` verify-chain (YAML surface key `visual`).
88    /// Required iff such a chain contains `vision`, forbidden otherwise.
89    /// Part of the assertion, hence inside the `judgeHash` domain (02 §12.3).
90    #[serde(skip_serializing_if = "Option::is_none")]
91    #[schemars(length(min = 1, max = 16384))]
92    pub vision_prompt: Option<String>,
93    /// Const `"unknown"` (principle 4): a channel that cannot complete
94    /// evaluation yields unknown for that channel and the chain advances; an
95    /// exhausted chain yields unknown. A completed negative is final
96    /// (spine R5).
97    pub on_missing_input: OnMissingInput,
98}
99
100fn unique_verify_via_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
101    let mut schema = <Vec<VerifyChannel>>::json_schema(generator);
102    schema
103        .ensure_object()
104        .insert("uniqueItems".to_owned(), serde_json::Value::Bool(true));
105    schema
106}
107
108/// The four predicate types (closed, spine A.4), internally tagged `type`.
109///
110/// Note on closedness: `#[schemars(deny_unknown_fields)]` closes each
111/// variant object in the generated schema (baseline parity); serde's
112/// internally-tagged deserialization is lenient about unknown fields at
113/// runtime (a documented serde limitation) — the schema stays authoritative.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
115#[serde(tag = "type", rename_all = "camelCase")]
116#[schemars(deny_unknown_fields)]
117pub enum PredicateIR {
118    /// Element state check; values equal DeviceRail
119    /// `WaitForElementCondition` verbatim.
120    ElementState {
121        /// The element to check.
122        selector: ElementSelectorIR,
123        /// The expected state.
124        state: ElementState,
125    },
126    /// Element text check.
127    ElementText {
128        /// The element to check.
129        selector: ElementSelectorIR,
130        /// The text matcher.
131        r#match: TextMatchIR,
132    },
133    /// Pure expression assertion over outputs (consumes no observation
134    /// channel; `verifyVia` is empty).
135    Expr {
136        /// The boolean expression to evaluate.
137        expr: Expr,
138    },
139    /// Visual assertion (vision-only; the prompt lives here, not in
140    /// `visionPrompt`).
141    Visual {
142        /// Author-written vision prompt.
143        #[schemars(length(min = 1, max = 16384))]
144        prompt: String,
145        /// Optional region of interest.
146        #[serde(skip_serializing_if = "Option::is_none")]
147        region: Option<RectIR>,
148    },
149}