Skip to main content

wenlan_types/
lint_contract.rs

1// SPDX-License-Identifier: Apache-2.0
2use super::LintGateEffect;
3use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
4use std::{fmt, num::NonZeroU64};
5
6pub const LINT_REPORT_SCHEMA_VERSION: u16 = 5;
7pub const LINT_CHECK_CATALOG_VERSION: u16 = 3;
8pub const LINT_MAX_EVIDENCE_PER_CHECK: u16 = 100;
9pub const LINT_GENERAL_CHECK_COUNT: usize = 56;
10pub const LINT_DEEP_CHECK_COUNT: usize = 74;
11
12pub(crate) const LINT_CANONICAL_CHECK_IDS: [&str; LINT_DEEP_CHECK_COUNT] = [
13    "entities.alias_integrity",
14    "entities.partition_inventory",
15    "entities.structural_integrity",
16    "identity.cache_inventory",
17    "identity.memory_state_integrity",
18    "identity.registry_integrity",
19    "identity.session_structure",
20    "identity.tag_integrity",
21    "kg.advisory_inventory",
22    "kg.aggregate_inventory",
23    "kg.semantic.entity_relations",
24    "kg.semantic.memory_entity_links",
25    "kg.substrate_liveness",
26    "memories.derived.episode",
27    "memories.derived.fact",
28    "memories.derived.page_links",
29    "memories.derived.summary",
30    "memories.derived.temporal",
31    "memories.duplicate_inventory",
32    "memories.embedding_integrity",
33    "memories.enrichment_failures",
34    "memories.lifecycle_integrity",
35    "memories.partition_inventory",
36    "memories.retrieval_substrate_inventory",
37    "memories.semantic.classification",
38    "memories.semantic.contradiction",
39    "memories.semantic.staleness",
40    "memories.structured_conflict_inventory",
41    "memories.supersession_integrity",
42    "memory_entities.integrity",
43    "observations.duplicate_inventory",
44    "observations.integrity",
45    "operations.document_queue",
46    "operations.import_checkpoints",
47    "operations.maintenance_backlogs",
48    "operations.refinement_inventory",
49    "operations.rejection_inventory",
50    "operations.source_configuration",
51    "operations.source_lifecycle_residue",
52    "pages.archive_inventory",
53    "pages.citations.partitions",
54    "pages.db.partitions",
55    "pages.duplicate_active_titles",
56    "pages.duplicate_body_inventory",
57    "pages.links.orphan_labels",
58    "pages.project.artifact_inventory",
59    "pages.projection.body_alignment",
60    "pages.projection.identity",
61    "pages.projection.manifest_inventory",
62    "pages.projection.state_contract",
63    "pages.projection.version_alignment",
64    "pages.provenance.source_evidence_coverage",
65    "pages.review_status_inventory",
66    "pages.semantic.evidence_links",
67    "pages.semantic.faithfulness",
68    "pages.semantic.provenance_adequacy",
69    "pages.source_page_integrity",
70    "relations.integrity",
71    "relations.vocabulary_integrity",
72    "runtime.ingest_worker_liveness",
73    "runtime.provider_inventory",
74    "runtime.schema_contract",
75    "runtime.search_index_contract",
76    "runtime.status_parity",
77    "serving.channel.episode",
78    "serving.channel.fact",
79    "serving.channel.graph",
80    "serving.channel.page",
81    "serving.channel.summary",
82    "serving.fact_scope_starvation",
83    "serving.observability_inventory",
84    "serving.reranker_fallback_inventory",
85    "serving.route_scope_contracts",
86    "serving.semantic.retrieval_quality",
87];
88
89const LINT_DEEP_ONLY_CHECK_IDS: [&str; LINT_DEEP_CHECK_COUNT - LINT_GENERAL_CHECK_COUNT] = [
90    "entities.alias_integrity",
91    "kg.semantic.entity_relations",
92    "kg.semantic.memory_entity_links",
93    "memories.duplicate_inventory",
94    "memories.retrieval_substrate_inventory",
95    "memories.semantic.classification",
96    "memories.semantic.contradiction",
97    "memories.semantic.staleness",
98    "memories.structured_conflict_inventory",
99    "observations.duplicate_inventory",
100    "operations.source_lifecycle_residue",
101    "pages.duplicate_body_inventory",
102    "pages.projection.body_alignment",
103    "pages.semantic.evidence_links",
104    "pages.semantic.faithfulness",
105    "pages.semantic.provenance_adequacy",
106    "relations.vocabulary_integrity",
107    "serving.semantic.retrieval_quality",
108];
109
110const LINT_ADVISORY_CHECK_IDS: [&str; 14] = [
111    "kg.semantic.entity_relations",
112    "kg.semantic.memory_entity_links",
113    "memories.duplicate_inventory",
114    "memories.retrieval_substrate_inventory",
115    "memories.semantic.classification",
116    "memories.semantic.contradiction",
117    "memories.semantic.staleness",
118    "memories.structured_conflict_inventory",
119    "observations.duplicate_inventory",
120    "pages.duplicate_body_inventory",
121    "pages.semantic.evidence_links",
122    "pages.semantic.faithfulness",
123    "pages.semantic.provenance_adequacy",
124    "serving.semantic.retrieval_quality",
125];
126
127#[doc(hidden)]
128pub fn canonical_check_ids(profile: LintProfile) -> impl Iterator<Item = &'static str> {
129    LINT_CANONICAL_CHECK_IDS
130        .into_iter()
131        .filter(move |check_id| {
132            profile == LintProfile::Deep
133                || LINT_DEEP_ONLY_CHECK_IDS.binary_search(check_id).is_err()
134        })
135}
136
137#[doc(hidden)]
138pub fn canonical_gate_effect(profile: LintProfile, check_id: &str) -> Option<LintGateEffect> {
139    LINT_CANONICAL_CHECK_IDS.binary_search(&check_id).ok()?;
140    if profile == LintProfile::General && LINT_DEEP_ONLY_CHECK_IDS.binary_search(&check_id).is_ok()
141    {
142        return None;
143    }
144    Some(
145        if LINT_ADVISORY_CHECK_IDS.binary_search(&check_id).is_ok() {
146            LintGateEffect::Advisory
147        } else {
148            LintGateEffect::Actionable
149        },
150    )
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum LintContractError {
155    InvalidDigest,
156    InvalidCommit,
157    InvalidScope,
158    InvalidOutcomeSeverity,
159    InvalidGateEffect,
160    InvalidApplicabilityPrecondition,
161    InvalidCoverage,
162    EvidenceLimitExceeded,
163    EvidenceOutsideAuthorizedDenominator,
164    UnsupportedReportSchema,
165    UnsupportedCheckCatalog,
166    InvalidTotals,
167    InvalidCompleteness,
168    InvalidCatalogShape,
169    InvalidAgentRecord,
170    InvalidAgentWork,
171    InvalidAgentSubmission,
172    TooManyChecks,
173}
174
175impl LintContractError {
176    const fn code(self) -> &'static str {
177        match self {
178            Self::InvalidDigest => "invalid_lint_digest",
179            Self::InvalidCommit => "invalid_lint_commit",
180            Self::InvalidScope => "invalid_lint_scope",
181            Self::InvalidOutcomeSeverity => "invalid_lint_outcome_severity",
182            Self::InvalidGateEffect => "invalid_lint_gate_effect",
183            Self::InvalidApplicabilityPrecondition => "invalid_lint_applicability_precondition",
184            Self::InvalidCoverage => "invalid_lint_coverage",
185            Self::EvidenceLimitExceeded => "lint_evidence_limit_exceeded",
186            Self::EvidenceOutsideAuthorizedDenominator => "lint_evidence_outside_denominator",
187            Self::UnsupportedReportSchema => "unsupported_lint_report_schema",
188            Self::UnsupportedCheckCatalog => "unsupported_lint_check_catalog",
189            Self::InvalidTotals => "invalid_lint_totals",
190            Self::InvalidCompleteness => "invalid_lint_completeness",
191            Self::InvalidCatalogShape => "invalid_lint_catalog_shape",
192            Self::InvalidAgentRecord => "invalid_lint_agent_record",
193            Self::InvalidAgentWork => "invalid_lint_agent_work",
194            Self::InvalidAgentSubmission => "invalid_lint_agent_submission",
195            Self::TooManyChecks => "too_many_lint_checks",
196        }
197    }
198}
199impl fmt::Display for LintContractError {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.write_str(self.code())
202    }
203}
204impl std::error::Error for LintContractError {}
205
206fn is_lower_hex(value: &str, length: usize) -> bool {
207    value.len() == length
208        && value.bytes().all(|byte| {
209            byte.is_ascii_digit() || (byte.is_ascii_lowercase() && byte.is_ascii_hexdigit())
210        })
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
214#[serde(transparent)]
215pub struct LintDigest(String);
216impl LintDigest {
217    pub fn from_u64(value: u64) -> Self {
218        Self(format!("{value:016x}"))
219    }
220    pub fn from_hex(value: &str) -> Result<Self, LintContractError> {
221        if is_lower_hex(value, 16) {
222            Ok(Self(value.to_string()))
223        } else {
224            Err(LintContractError::InvalidDigest)
225        }
226    }
227    pub fn as_str(&self) -> &str {
228        &self.0
229    }
230}
231impl<'de> Deserialize<'de> for LintDigest {
232    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233    where
234        D: Deserializer<'de>,
235    {
236        Self::from_hex(&String::deserialize(deserializer)?).map_err(D::Error::custom)
237    }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
241#[serde(transparent)]
242pub struct LintOpaqueDigest(String);
243impl LintOpaqueDigest {
244    pub fn from_hex(value: &str) -> Result<Self, LintContractError> {
245        if is_lower_hex(value, 64) {
246            Ok(Self(value.to_string()))
247        } else {
248            Err(LintContractError::InvalidDigest)
249        }
250    }
251    pub fn as_str(&self) -> &str {
252        &self.0
253    }
254}
255impl<'de> Deserialize<'de> for LintOpaqueDigest {
256    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257    where
258        D: Deserializer<'de>,
259    {
260        Self::from_hex(&String::deserialize(deserializer)?).map_err(D::Error::custom)
261    }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
265#[serde(transparent)]
266pub struct LintCommitReceipt(String);
267impl LintCommitReceipt {
268    pub fn new(value: &str) -> Result<Self, LintContractError> {
269        if is_lower_hex(value, 40) {
270            Ok(Self(value.to_string()))
271        } else {
272            Err(LintContractError::InvalidCommit)
273        }
274    }
275    pub fn as_str(&self) -> &str {
276        &self.0
277    }
278}
279impl<'de> Deserialize<'de> for LintCommitReceipt {
280    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
281    where
282        D: Deserializer<'de>,
283    {
284        Self::new(&String::deserialize(deserializer)?).map_err(D::Error::custom)
285    }
286}
287
288#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum LintProfile {
291    #[default]
292    General,
293    Deep,
294}
295impl fmt::Display for LintProfile {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        f.write_str(match self {
298            Self::General => "general",
299            Self::Deep => "deep",
300        })
301    }
302}
303impl std::str::FromStr for LintProfile {
304    type Err = &'static str;
305
306    fn from_str(value: &str) -> Result<Self, Self::Err> {
307        match value {
308            "general" => Ok(Self::General),
309            "deep" => Ok(Self::Deep),
310            _ => Err("expected `general` or `deep`"),
311        }
312    }
313}
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "snake_case")]
316pub enum LintScopeKind {
317    Global,
318    Registered,
319    Uncategorized,
320}
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(transparent)]
323pub struct LintOpaqueId(NonZeroU64);
324impl LintOpaqueId {
325    pub fn from_sorted_position(position: usize) -> Option<Self> {
326        position
327            .checked_add(1)
328            .and_then(|ordinal| u64::try_from(ordinal).ok())
329            .and_then(NonZeroU64::new)
330            .map(Self)
331    }
332    pub const fn ordinal(self) -> u64 {
333        self.0.get()
334    }
335}
336#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
337pub struct LintScope {
338    kind: LintScopeKind,
339    #[serde(skip_serializing_if = "Option::is_none")]
340    opaque_scope_ref: Option<LintOpaqueId>,
341}
342#[derive(Deserialize)]
343struct LintScopeWire {
344    kind: LintScopeKind,
345    opaque_scope_ref: Option<LintOpaqueId>,
346}
347impl LintScope {
348    pub const fn global() -> Self {
349        Self {
350            kind: LintScopeKind::Global,
351            opaque_scope_ref: None,
352        }
353    }
354    pub const fn registered(opaque_scope_ref: LintOpaqueId) -> Self {
355        Self {
356            kind: LintScopeKind::Registered,
357            opaque_scope_ref: Some(opaque_scope_ref),
358        }
359    }
360    pub const fn uncategorized() -> Self {
361        Self {
362            kind: LintScopeKind::Uncategorized,
363            opaque_scope_ref: None,
364        }
365    }
366    pub const fn kind(&self) -> LintScopeKind {
367        self.kind
368    }
369    pub const fn opaque_scope_ref(&self) -> Option<LintOpaqueId> {
370        self.opaque_scope_ref
371    }
372    const fn is_valid(&self) -> bool {
373        match (self.kind, self.opaque_scope_ref) {
374            (LintScopeKind::Global, None)
375            | (LintScopeKind::Registered, Some(_))
376            | (LintScopeKind::Uncategorized, None) => true,
377            (LintScopeKind::Global, Some(_))
378            | (LintScopeKind::Registered, None)
379            | (LintScopeKind::Uncategorized, Some(_)) => false,
380        }
381    }
382}
383impl<'de> Deserialize<'de> for LintScope {
384    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
385    where
386        D: Deserializer<'de>,
387    {
388        let wire = LintScopeWire::deserialize(deserializer)?;
389        let scope = Self {
390            kind: wire.kind,
391            opaque_scope_ref: wire.opaque_scope_ref,
392        };
393        if scope.is_valid() {
394            Ok(scope)
395        } else {
396            Err(D::Error::custom(LintContractError::InvalidScope))
397        }
398    }
399}