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}