Skip to main content

ethos_core/
evidence_anchor.rs

1/*
2 * Copyright 2026 The Ethos maintainers
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Evidence-anchor request/report schema types.
18//!
19//! Evidence anchoring is a deterministic source-tracing primitive: caller-provided
20//! evidence refs are checked against a [`crate::grounding::GroundingSource`].
21//! It does not perform semantic answer verification.
22
23use serde::{Deserialize, Serialize};
24
25use crate::grounding::{Capabilities, ParserIdentity};
26use crate::verify_types::CapabilityLimit;
27
28/// Request artifact type for evidence anchoring.
29pub const EVIDENCE_ANCHOR_REQUEST_ARTIFACT_TYPE: &str = "ethos.evidence_anchor_request.v1";
30/// Report artifact type for evidence anchoring.
31pub const EVIDENCE_ANCHOR_REPORT_ARTIFACT_TYPE: &str = "ethos.evidence_anchor_report.v1";
32
33/// Evidence-anchor request envelope.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct EvidenceAnchorRequest {
37    /// Artifact type identity.
38    pub artifact_type: String,
39    /// Schema version.
40    pub schema_version: String,
41    /// Optional source fingerprint the evidence refs were produced against.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub source_fingerprint: Option<String>,
44    /// Caller-provided evidence refs in deterministic input order.
45    pub evidence_refs: Vec<EvidenceRef>,
46}
47
48/// One caller-provided evidence reference.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct EvidenceRef {
52    /// Caller correlation key. Unique within one request.
53    pub evidence_id: String,
54    /// Evidence kind.
55    pub evidence_kind: EvidenceKind,
56    /// Minimum anchor level required by the caller.
57    pub required_anchor_level: AnchorLevel,
58    /// Source locator.
59    pub locator: EvidenceLocator,
60    /// Expected text, when text matching is required.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub expected_text: Option<String>,
63    /// SHA-256 of normalized expected text, when supplied by the caller.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub expected_text_sha256: Option<String>,
66    /// Text normalization profile for expected text.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub text_normalization_profile: Option<TextNormalizationProfile>,
69}
70
71/// Supported and accepted evidence kinds.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum EvidenceKind {
75    /// Page existence.
76    Page,
77    /// Text evidence.
78    Text,
79    /// Text and/or region evidence.
80    TextRegion,
81    /// Table cell evidence.
82    TableCell,
83    /// Accepted but unsupported in v1.
84    Region,
85    /// Accepted but unsupported in v1.
86    Other,
87}
88
89/// Required or achieved anchor level.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum AnchorLevel {
93    /// No anchor.
94    None,
95    /// Page anchor.
96    Page,
97    /// Text anchor.
98    Text,
99    /// Bounding-box anchor.
100    Bbox,
101    /// Text plus bounding-box anchor.
102    TextBbox,
103    /// Table-cell anchor.
104    TableCell,
105}
106
107/// Source locator for an evidence ref.
108#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct EvidenceLocator {
111    /// 1-based parser-neutral page index.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub page_index: Option<u32>,
114    /// Parser-specific page id.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub page_id: Option<String>,
117    /// Parser-specific element id.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub element_id: Option<String>,
120    /// Parser-specific span id.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub span_id: Option<String>,
123    /// Source bbox `[x0, y0, x1, y1]` in integer quanta.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub bbox: Option<[i64; 4]>,
126    /// Parser-specific table id.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub table_id: Option<String>,
129    /// Table cell address.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub cell: Option<AnchorCellRef>,
132    /// Coordinate profile for bbox locators.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub coordinate_profile: Option<CoordinateProfile>,
135}
136
137/// 0-based table cell address.
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(deny_unknown_fields)]
140pub struct AnchorCellRef {
141    /// Row index.
142    pub row: u32,
143    /// Column index.
144    pub col: u32,
145}
146
147/// Supported text normalization profiles.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub enum TextNormalizationProfile {
151    /// Collapse ASCII whitespace, matching the existing verifier normalization.
152    EthosCollapseWhitespaceV1,
153}
154
155/// Supported coordinate profiles.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum CoordinateProfile {
159    /// Ethos integer quanta with top-left origin.
160    EthosQuantizedTopLeftV1,
161}
162
163/// Evidence-anchor report envelope.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(deny_unknown_fields)]
166pub struct EvidenceAnchorReport {
167    /// Artifact type identity.
168    pub artifact_type: String,
169    /// Schema version.
170    pub schema_version: String,
171    /// Source fingerprint, when declared by the grounding source.
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub source_fingerprint: Option<String>,
174    /// Grounding metadata reused from existing verification reports.
175    pub grounding: EvidenceAnchorGrounding,
176    /// Per-ref anchor outcomes.
177    pub anchors: Vec<EvidenceAnchor>,
178}
179
180/// Grounding metadata embedded in evidence-anchor reports.
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(deny_unknown_fields)]
183pub struct EvidenceAnchorGrounding {
184    /// Producing parser identity.
185    pub parser: ParserIdentity,
186    /// Declared source capabilities.
187    pub capabilities: Capabilities,
188}
189
190/// One evidence anchor outcome.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(deny_unknown_fields)]
193pub struct EvidenceAnchor {
194    /// Caller correlation key.
195    pub evidence_id: String,
196    /// Evidence kind.
197    pub evidence_kind: EvidenceKind,
198    /// Rollup status.
199    pub anchor_status: AnchorStatus,
200    /// Required level from the request.
201    pub required_anchor_level: AnchorLevel,
202    /// Best deterministic level achieved.
203    pub achieved_anchor_level: AnchorLevel,
204    /// Per-axis checks.
205    pub checks: AnchorChecks,
206    /// Capability limits that affected this anchor.
207    pub capability_limits: Vec<CapabilityLimit>,
208}
209
210/// Rollup status for one evidence anchor.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
212#[serde(rename_all = "snake_case")]
213pub enum AnchorStatus {
214    /// Required evidence bound to source evidence.
215    Bound,
216    /// A located target failed the expected content/location check.
217    Mismatch,
218    /// Required source target was not found.
219    NotFound,
220    /// Request/source fingerprints differ.
221    StaleFingerprint,
222    /// The source lacks a capability needed to decide the required anchor.
223    CapabilityLimited,
224    /// The evidence kind is accepted but unsupported in v1.
225    UnsupportedEvidenceKind,
226}
227
228/// Per-axis evidence-anchor checks.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(deny_unknown_fields)]
231pub struct AnchorChecks {
232    /// Fingerprint check.
233    pub fingerprint: FingerprintCheck,
234    /// Page check.
235    pub page: PageCheck,
236    /// Text check.
237    pub text: TextCheck,
238    /// Bbox check.
239    pub bbox: BboxCheck,
240    /// Table-cell check.
241    pub table_cell: TableCellCheck,
242}
243
244impl Default for AnchorChecks {
245    fn default() -> Self {
246        AnchorChecks {
247            fingerprint: FingerprintCheck::NotChecked,
248            page: PageCheck::NotChecked,
249            text: TextCheck::NotChecked,
250            bbox: BboxCheck::NotChecked,
251            table_cell: TableCellCheck::NotChecked,
252        }
253    }
254}
255
256/// Fingerprint axis result.
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
258#[serde(rename_all = "snake_case")]
259pub enum FingerprintCheck {
260    /// Fingerprints match.
261    Matched,
262    /// Fingerprints differ.
263    Stale,
264    /// Not checked.
265    NotChecked,
266    /// Source cannot declare a fingerprint.
267    CapabilityLimited,
268}
269
270/// Page axis result.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
272#[serde(rename_all = "snake_case")]
273pub enum PageCheck {
274    /// Page was found.
275    Found,
276    /// Page was not found.
277    NotFound,
278    /// Not checked.
279    NotChecked,
280}
281
282/// Text axis result.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
284#[serde(rename_all = "snake_case")]
285pub enum TextCheck {
286    /// Text matched.
287    Matched,
288    /// Located text mismatched.
289    Mismatch,
290    /// Text target was not found.
291    NotFound,
292    /// Not checked.
293    NotChecked,
294    /// Source lacks required text capability.
295    CapabilityLimited,
296}
297
298/// Bbox axis result.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
300#[serde(rename_all = "snake_case")]
301pub enum BboxCheck {
302    /// Bbox is valid.
303    Valid,
304    /// Located bbox mismatched.
305    Invalid,
306    /// Bbox target was not found.
307    NotFound,
308    /// Not checked.
309    NotChecked,
310    /// Source lacks required coordinate capability.
311    CapabilityLimited,
312}
313
314/// Table-cell axis result.
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
316#[serde(rename_all = "snake_case")]
317pub enum TableCellCheck {
318    /// Table cell matched.
319    Matched,
320    /// Located table cell mismatched.
321    Mismatch,
322    /// Table cell was not found.
323    NotFound,
324    /// Not checked.
325    NotChecked,
326    /// Source lacks table capability.
327    CapabilityLimited,
328}