1use 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}