1use super::{LintContractError, LintDigest, LintOpaqueId};
3use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
4use std::collections::{BTreeMap, BTreeSet};
5
6pub const LINT_AGENT_WORK_SCHEMA_VERSION: u16 = 2;
7pub const LINT_AGENT_RECORD_CAP: usize = 96;
8pub const LINT_AGENT_CANDIDATE_CAP: usize = 48;
9pub const LINT_AGENT_EXCERPT_CHAR_CAP: usize = 600;
10const LINT_AGENT_MEMORY_TYPE_CHAR_CAP: usize = 64;
11const LINT_AGENT_REFS_PER_CANDIDATE_CAP: usize = 8;
12const LINT_SEMANTIC_CONFIDENCE_MAX: u16 = 10_000;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15pub enum LintSemanticCheckId {
16 #[serde(rename = "memories.semantic.classification")]
17 MemoryClassification,
18 #[serde(rename = "memories.semantic.contradiction")]
19 MemoryContradiction,
20 #[serde(rename = "memories.semantic.staleness")]
21 MemoryStaleness,
22 #[serde(rename = "kg.semantic.memory_entity_links")]
23 MemoryEntityLinks,
24 #[serde(rename = "kg.semantic.entity_relations")]
25 EntityRelations,
26 #[serde(rename = "pages.semantic.faithfulness")]
27 PageFaithfulness,
28 #[serde(rename = "pages.semantic.provenance_adequacy")]
29 PageProvenanceAdequacy,
30 #[serde(rename = "pages.semantic.evidence_links")]
31 PageEvidenceLinks,
32 #[serde(rename = "serving.semantic.retrieval_quality")]
33 RetrievalQuality,
34}
35
36impl LintSemanticCheckId {
37 pub const ALL: [Self; 9] = [
38 Self::MemoryClassification,
39 Self::MemoryContradiction,
40 Self::MemoryStaleness,
41 Self::MemoryEntityLinks,
42 Self::EntityRelations,
43 Self::PageFaithfulness,
44 Self::PageProvenanceAdequacy,
45 Self::PageEvidenceLinks,
46 Self::RetrievalQuality,
47 ];
48
49 pub const fn as_str(self) -> &'static str {
50 match self {
51 Self::MemoryClassification => "memories.semantic.classification",
52 Self::MemoryContradiction => "memories.semantic.contradiction",
53 Self::MemoryStaleness => "memories.semantic.staleness",
54 Self::MemoryEntityLinks => "kg.semantic.memory_entity_links",
55 Self::EntityRelations => "kg.semantic.entity_relations",
56 Self::PageFaithfulness => "pages.semantic.faithfulness",
57 Self::PageProvenanceAdequacy => "pages.semantic.provenance_adequacy",
58 Self::PageEvidenceLinks => "pages.semantic.evidence_links",
59 Self::RetrievalQuality => "serving.semantic.retrieval_quality",
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum LintAgentRecordKind {
67 Memory,
68 Page,
69 Entity,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum LintSemanticCandidateKind {
75 RecordReview,
76 PairReview,
77 MissingLink,
78 ExistingLink,
79 RetrievalTrace,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum LintSemanticAction {
85 ReclassifyMemory,
86 ReviewContradiction,
87 ReviewStaleness,
88 SupersedeMemory,
89 AddMemoryEntityLink,
90 RemoveMemoryEntityLink,
91 AddEntityRelation,
92 RemoveEntityRelation,
93 ReviewPageClaim,
94 AddPageEvidence,
95 RemovePageEvidence,
96 ReviewRetrieval,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum LintSemanticReasonCode {
102 ClassificationMismatch,
103 PotentialContradiction,
104 PotentialStaleness,
105 MentionWithoutLink,
106 ExistingLinkMismatch,
107 SharedContextWithoutRelation,
108 ExistingRelationMismatch,
109 PotentialUnfaithfulClaim,
110 PotentialInadequateProvenance,
111 ClaimOverlapWithoutEvidence,
112 ExistingEvidenceMismatch,
113 PotentialRetrievalMiss,
114 DanglingOwner,
115 TemporalEvolution,
116 RelatedButNotEvidence,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum LintSemanticDecision {
122 Pass,
123 Finding,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum LintSemanticProviderRoute {
129 OnDevice,
130 ConfiguredExternal,
131 CallingAgent,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
135pub struct LintAgentRecord {
136 reference: u16,
137 kind: LintAgentRecordKind,
138 excerpt: String,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 memory_type: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 evidence_count: Option<u64>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 source_excerpt: Option<String>,
145}
146
147#[derive(Deserialize)]
148#[serde(deny_unknown_fields)]
149struct LintAgentRecordWire {
150 reference: u16,
151 kind: LintAgentRecordKind,
152 excerpt: String,
153 #[serde(default)]
154 memory_type: Option<String>,
155 #[serde(default)]
156 evidence_count: Option<u64>,
157 #[serde(default)]
158 source_excerpt: Option<String>,
159}
160
161impl LintAgentRecord {
162 pub fn try_new(
163 reference: u16,
164 kind: LintAgentRecordKind,
165 excerpt: String,
166 memory_type: Option<String>,
167 evidence_count: Option<u64>,
168 source_excerpt: Option<String>,
169 ) -> Result<Self, LintContractError> {
170 let source_excerpt_valid = source_excerpt.as_ref().is_none_or(|value| {
171 !value.trim().is_empty() && value.chars().count() <= LINT_AGENT_EXCERPT_CHAR_CAP
172 });
173 let memory_type_valid = memory_type
174 .as_ref()
175 .is_none_or(|value| value.chars().count() <= LINT_AGENT_MEMORY_TYPE_CHAR_CAP);
176 let shape_valid = match kind {
177 LintAgentRecordKind::Memory => evidence_count.is_none() && source_excerpt.is_none(),
178 LintAgentRecordKind::Page => memory_type.is_none(),
179 LintAgentRecordKind::Entity => {
180 memory_type.is_none() && evidence_count.is_none() && source_excerpt.is_none()
181 }
182 };
183 if reference == 0
184 || excerpt.trim().is_empty()
185 || excerpt.chars().count() > LINT_AGENT_EXCERPT_CHAR_CAP
186 || !source_excerpt_valid
187 || !memory_type_valid
188 || !shape_valid
189 {
190 return Err(LintContractError::InvalidAgentRecord);
191 }
192 Ok(Self {
193 reference,
194 kind,
195 excerpt,
196 memory_type,
197 evidence_count,
198 source_excerpt,
199 })
200 }
201
202 pub const fn reference(&self) -> u16 {
203 self.reference
204 }
205 pub const fn kind(&self) -> LintAgentRecordKind {
206 self.kind
207 }
208 pub fn excerpt(&self) -> &str {
209 &self.excerpt
210 }
211 pub fn memory_type(&self) -> Option<&str> {
212 self.memory_type.as_deref()
213 }
214 pub const fn evidence_count(&self) -> Option<u64> {
215 self.evidence_count
216 }
217 pub fn source_excerpt(&self) -> Option<&str> {
218 self.source_excerpt.as_deref()
219 }
220}
221
222impl<'de> Deserialize<'de> for LintAgentRecord {
223 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224 where
225 D: Deserializer<'de>,
226 {
227 let wire = LintAgentRecordWire::deserialize(deserializer)?;
228 Self::try_new(
229 wire.reference,
230 wire.kind,
231 wire.excerpt,
232 wire.memory_type,
233 wire.evidence_count,
234 wire.source_excerpt,
235 )
236 .map_err(D::Error::custom)
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
241pub struct LintSemanticPopulation {
242 check_id: LintSemanticCheckId,
243 eligible: u64,
244 candidates: u64,
245 packet_candidates: u64,
246 truncated: bool,
247}
248
249#[derive(Deserialize)]
250#[serde(deny_unknown_fields)]
251struct LintSemanticPopulationWire {
252 check_id: LintSemanticCheckId,
253 eligible: u64,
254 candidates: u64,
255 packet_candidates: u64,
256 truncated: bool,
257}
258
259impl LintSemanticPopulation {
260 pub fn try_new(
261 check_id: LintSemanticCheckId,
262 eligible: u64,
263 candidates: u64,
264 packet_candidates: u64,
265 truncated: bool,
266 ) -> Result<Self, LintContractError> {
267 if packet_candidates > candidates || truncated != (packet_candidates < candidates) {
268 return Err(LintContractError::InvalidAgentWork);
269 }
270 Ok(Self {
271 check_id,
272 eligible,
273 candidates,
274 packet_candidates,
275 truncated,
276 })
277 }
278
279 pub const fn check_id(&self) -> LintSemanticCheckId {
280 self.check_id
281 }
282 pub const fn eligible(&self) -> u64 {
283 self.eligible
284 }
285 pub const fn candidates(&self) -> u64 {
286 self.candidates
287 }
288 pub const fn packet_candidates(&self) -> u64 {
289 self.packet_candidates
290 }
291 pub const fn truncated(&self) -> bool {
292 self.truncated
293 }
294}
295
296impl<'de> Deserialize<'de> for LintSemanticPopulation {
297 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
298 where
299 D: Deserializer<'de>,
300 {
301 let wire = LintSemanticPopulationWire::deserialize(deserializer)?;
302 Self::try_new(
303 wire.check_id,
304 wire.eligible,
305 wire.candidates,
306 wire.packet_candidates,
307 wire.truncated,
308 )
309 .map_err(D::Error::custom)
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
314pub struct LintAgentCandidate {
315 reference: u16,
316 check_id: LintSemanticCheckId,
317 kind: LintSemanticCandidateKind,
318 evidence_refs: Vec<u16>,
319 counterevidence_refs: Vec<u16>,
320 proposed_action: LintSemanticAction,
321 reason_code: LintSemanticReasonCode,
322}
323
324#[derive(Deserialize)]
325#[serde(deny_unknown_fields)]
326struct LintAgentCandidateWire {
327 reference: u16,
328 check_id: LintSemanticCheckId,
329 kind: LintSemanticCandidateKind,
330 evidence_refs: Vec<u16>,
331 counterevidence_refs: Vec<u16>,
332 proposed_action: LintSemanticAction,
333 reason_code: LintSemanticReasonCode,
334}
335
336impl LintAgentCandidate {
337 pub fn try_new(
338 reference: u16,
339 check_id: LintSemanticCheckId,
340 kind: LintSemanticCandidateKind,
341 evidence_refs: Vec<u16>,
342 counterevidence_refs: Vec<u16>,
343 proposed_action: LintSemanticAction,
344 reason_code: LintSemanticReasonCode,
345 ) -> Result<Self, LintContractError> {
346 if reference == 0
347 || !valid_refs(&evidence_refs, false)
348 || !valid_refs(&counterevidence_refs, true)
349 || evidence_refs
350 .iter()
351 .any(|reference| counterevidence_refs.contains(reference))
352 || !action_matches_check(check_id, proposed_action)
353 {
354 return Err(LintContractError::InvalidAgentWork);
355 }
356 Ok(Self {
357 reference,
358 check_id,
359 kind,
360 evidence_refs,
361 counterevidence_refs,
362 proposed_action,
363 reason_code,
364 })
365 }
366
367 pub const fn reference(&self) -> u16 {
368 self.reference
369 }
370 pub const fn check_id(&self) -> LintSemanticCheckId {
371 self.check_id
372 }
373 pub const fn kind(&self) -> LintSemanticCandidateKind {
374 self.kind
375 }
376 pub fn evidence_refs(&self) -> &[u16] {
377 &self.evidence_refs
378 }
379 pub fn counterevidence_refs(&self) -> &[u16] {
380 &self.counterevidence_refs
381 }
382 pub const fn proposed_action(&self) -> LintSemanticAction {
383 self.proposed_action
384 }
385 pub const fn reason_code(&self) -> LintSemanticReasonCode {
386 self.reason_code
387 }
388}
389
390impl<'de> Deserialize<'de> for LintAgentCandidate {
391 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
392 where
393 D: Deserializer<'de>,
394 {
395 let wire = LintAgentCandidateWire::deserialize(deserializer)?;
396 Self::try_new(
397 wire.reference,
398 wire.check_id,
399 wire.kind,
400 wire.evidence_refs,
401 wire.counterevidence_refs,
402 wire.proposed_action,
403 wire.reason_code,
404 )
405 .map_err(D::Error::custom)
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
410pub struct LintAgentWork {
411 work_schema_version: u16,
412 work_digest: LintDigest,
413 populations: Vec<LintSemanticPopulation>,
414 records: Vec<LintAgentRecord>,
415 candidates: Vec<LintAgentCandidate>,
416}
417
418#[derive(Deserialize)]
419#[serde(deny_unknown_fields)]
420struct LintAgentWorkWire {
421 work_schema_version: u16,
422 work_digest: LintDigest,
423 populations: Vec<LintSemanticPopulation>,
424 records: Vec<LintAgentRecord>,
425 candidates: Vec<LintAgentCandidate>,
426}
427
428impl LintAgentWork {
429 pub fn try_new(
430 work_digest: LintDigest,
431 populations: Vec<LintSemanticPopulation>,
432 records: Vec<LintAgentRecord>,
433 candidates: Vec<LintAgentCandidate>,
434 ) -> Result<Self, LintContractError> {
435 let population_ids = populations
436 .iter()
437 .map(LintSemanticPopulation::check_id)
438 .collect::<BTreeSet<_>>();
439 let packet_counts = candidates
440 .iter()
441 .fold(BTreeMap::new(), |mut counts, candidate| {
442 *counts.entry(candidate.check_id()).or_insert(0_u64) += 1;
443 counts
444 });
445 let populations_valid = populations.len() == LintSemanticCheckId::ALL.len()
446 && population_ids == LintSemanticCheckId::ALL.into_iter().collect()
447 && populations.iter().all(|population| {
448 packet_counts
449 .get(&population.check_id())
450 .copied()
451 .unwrap_or(0)
452 == population.packet_candidates()
453 });
454 let records_valid = records.len() <= LINT_AGENT_RECORD_CAP
455 && records
456 .iter()
457 .enumerate()
458 .all(|(index, record)| record.reference() == u16::try_from(index + 1).unwrap_or(0));
459 let candidates_valid = candidates.len() <= LINT_AGENT_CANDIDATE_CAP
460 && candidates.iter().enumerate().all(|(index, candidate)| {
461 candidate.reference() == u16::try_from(index + 1).unwrap_or(0)
462 && candidate
463 .evidence_refs()
464 .iter()
465 .chain(candidate.counterevidence_refs())
466 .all(|reference| usize::from(*reference) <= records.len())
467 });
468 if !populations_valid || !records_valid || !candidates_valid {
469 return Err(LintContractError::InvalidAgentWork);
470 }
471 Ok(Self {
472 work_schema_version: LINT_AGENT_WORK_SCHEMA_VERSION,
473 work_digest,
474 populations,
475 records,
476 candidates,
477 })
478 }
479
480 pub fn work_digest(&self) -> &LintDigest {
481 &self.work_digest
482 }
483 pub fn populations(&self) -> &[LintSemanticPopulation] {
484 &self.populations
485 }
486 pub fn records(&self) -> &[LintAgentRecord] {
487 &self.records
488 }
489 pub fn candidates(&self) -> &[LintAgentCandidate] {
490 &self.candidates
491 }
492}
493
494impl<'de> Deserialize<'de> for LintAgentWork {
495 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
496 where
497 D: Deserializer<'de>,
498 {
499 let wire = LintAgentWorkWire::deserialize(deserializer)?;
500 if wire.work_schema_version != LINT_AGENT_WORK_SCHEMA_VERSION {
501 return Err(D::Error::custom(LintContractError::InvalidAgentWork));
502 }
503 Self::try_new(
504 wire.work_digest,
505 wire.populations,
506 wire.records,
507 wire.candidates,
508 )
509 .map_err(D::Error::custom)
510 }
511}
512
513#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
514pub struct LintAgentVerdict {
515 candidate_ref: u16,
516 decision: LintSemanticDecision,
517 #[serde(skip_serializing_if = "Option::is_none")]
518 second_decision: Option<LintSemanticDecision>,
519 reason_code: LintSemanticReasonCode,
520 confidence_basis_points: u16,
521 counterevidence_refs: Vec<u16>,
522}
523
524#[derive(Deserialize)]
525#[serde(deny_unknown_fields)]
526struct LintAgentVerdictWire {
527 candidate_ref: u16,
528 decision: LintSemanticDecision,
529 #[serde(default)]
530 second_decision: Option<LintSemanticDecision>,
531 reason_code: LintSemanticReasonCode,
532 confidence_basis_points: u16,
533 #[serde(default)]
534 counterevidence_refs: Vec<u16>,
535}
536
537impl LintAgentVerdict {
538 pub fn try_new(
539 candidate_ref: u16,
540 decision: LintSemanticDecision,
541 second_decision: Option<LintSemanticDecision>,
542 reason_code: LintSemanticReasonCode,
543 confidence_basis_points: u16,
544 counterevidence_refs: Vec<u16>,
545 ) -> Result<Self, LintContractError> {
546 if candidate_ref == 0
547 || confidence_basis_points > LINT_SEMANTIC_CONFIDENCE_MAX
548 || !valid_refs(&counterevidence_refs, true)
549 {
550 return Err(LintContractError::InvalidAgentSubmission);
551 }
552 Ok(Self {
553 candidate_ref,
554 decision,
555 second_decision,
556 reason_code,
557 confidence_basis_points,
558 counterevidence_refs,
559 })
560 }
561
562 pub const fn candidate_ref(&self) -> u16 {
563 self.candidate_ref
564 }
565 pub const fn decision(&self) -> LintSemanticDecision {
566 self.decision
567 }
568 pub const fn second_decision(&self) -> Option<LintSemanticDecision> {
569 self.second_decision
570 }
571 pub const fn reason_code(&self) -> LintSemanticReasonCode {
572 self.reason_code
573 }
574 pub const fn confidence_basis_points(&self) -> u16 {
575 self.confidence_basis_points
576 }
577 pub fn counterevidence_refs(&self) -> &[u16] {
578 &self.counterevidence_refs
579 }
580 pub fn has_unresolved_disagreement(&self) -> bool {
581 self.second_decision
582 .is_some_and(|second| second != self.decision)
583 }
584}
585
586impl<'de> Deserialize<'de> for LintAgentVerdict {
587 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
588 where
589 D: Deserializer<'de>,
590 {
591 let wire = LintAgentVerdictWire::deserialize(deserializer)?;
592 Self::try_new(
593 wire.candidate_ref,
594 wire.decision,
595 wire.second_decision,
596 wire.reason_code,
597 wire.confidence_basis_points,
598 wire.counterevidence_refs,
599 )
600 .map_err(D::Error::custom)
601 }
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
605pub struct LintAgentSubmission {
606 work_digest: LintDigest,
607 verdicts: Vec<LintAgentVerdict>,
608}
609
610#[derive(Deserialize)]
611#[serde(deny_unknown_fields)]
612struct LintAgentSubmissionWire {
613 work_digest: LintDigest,
614 verdicts: Vec<LintAgentVerdict>,
615}
616
617impl LintAgentSubmission {
618 pub fn try_new(
619 work_digest: LintDigest,
620 verdicts: Vec<LintAgentVerdict>,
621 ) -> Result<Self, LintContractError> {
622 if verdicts.len() > LINT_AGENT_CANDIDATE_CAP
623 || !verdicts
624 .windows(2)
625 .all(|pair| pair[0].candidate_ref() < pair[1].candidate_ref())
626 {
627 return Err(LintContractError::InvalidAgentSubmission);
628 }
629 Ok(Self {
630 work_digest,
631 verdicts,
632 })
633 }
634
635 pub fn work_digest(&self) -> &LintDigest {
636 &self.work_digest
637 }
638 pub fn verdicts(&self) -> &[LintAgentVerdict] {
639 &self.verdicts
640 }
641}
642
643impl<'de> Deserialize<'de> for LintAgentSubmission {
644 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
645 where
646 D: Deserializer<'de>,
647 {
648 let wire = LintAgentSubmissionWire::deserialize(deserializer)?;
649 Self::try_new(wire.work_digest, wire.verdicts).map_err(D::Error::custom)
650 }
651}
652
653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654pub struct LintSemanticFinding {
655 candidate_id: LintOpaqueId,
656 proposed_action: LintSemanticAction,
657 reason_code: LintSemanticReasonCode,
658 confidence_basis_points: u16,
659 provider_route: LintSemanticProviderRoute,
660 evidence_ids: Vec<LintDigest>,
661 counterevidence_ids: Vec<LintDigest>,
662 unresolved_disagreement: bool,
663}
664
665impl LintSemanticFinding {
666 pub fn try_new(
667 candidate_id: LintOpaqueId,
668 proposed_action: LintSemanticAction,
669 reason_code: LintSemanticReasonCode,
670 confidence_basis_points: u16,
671 provider_route: LintSemanticProviderRoute,
672 evidence_ids: Vec<LintDigest>,
673 counterevidence_ids: Vec<LintDigest>,
674 ) -> Result<Self, LintContractError> {
675 Self::try_new_with_disagreement(
676 candidate_id,
677 proposed_action,
678 reason_code,
679 confidence_basis_points,
680 provider_route,
681 evidence_ids,
682 counterevidence_ids,
683 false,
684 )
685 }
686
687 #[allow(clippy::too_many_arguments)]
688 pub fn try_new_with_disagreement(
689 candidate_id: LintOpaqueId,
690 proposed_action: LintSemanticAction,
691 reason_code: LintSemanticReasonCode,
692 confidence_basis_points: u16,
693 provider_route: LintSemanticProviderRoute,
694 evidence_ids: Vec<LintDigest>,
695 counterevidence_ids: Vec<LintDigest>,
696 unresolved_disagreement: bool,
697 ) -> Result<Self, LintContractError> {
698 if confidence_basis_points > LINT_SEMANTIC_CONFIDENCE_MAX
699 || evidence_ids.len() > LINT_AGENT_REFS_PER_CANDIDATE_CAP
700 || counterevidence_ids.len() > LINT_AGENT_REFS_PER_CANDIDATE_CAP
701 {
702 return Err(LintContractError::InvalidAgentSubmission);
703 }
704 Ok(Self {
705 candidate_id,
706 proposed_action,
707 reason_code,
708 confidence_basis_points,
709 provider_route,
710 evidence_ids,
711 counterevidence_ids,
712 unresolved_disagreement,
713 })
714 }
715
716 pub const fn candidate_id(&self) -> LintOpaqueId {
717 self.candidate_id
718 }
719 pub const fn proposed_action(&self) -> LintSemanticAction {
720 self.proposed_action
721 }
722 pub const fn reason_code(&self) -> LintSemanticReasonCode {
723 self.reason_code
724 }
725 pub const fn confidence_basis_points(&self) -> u16 {
726 self.confidence_basis_points
727 }
728 pub const fn provider_route(&self) -> LintSemanticProviderRoute {
729 self.provider_route
730 }
731 pub fn evidence_ids(&self) -> &[LintDigest] {
732 &self.evidence_ids
733 }
734 pub fn counterevidence_ids(&self) -> &[LintDigest] {
735 &self.counterevidence_ids
736 }
737 pub const fn unresolved_disagreement(&self) -> bool {
738 self.unresolved_disagreement
739 }
740}
741
742fn valid_refs(refs: &[u16], empty_allowed: bool) -> bool {
743 (empty_allowed || !refs.is_empty())
744 && refs.len() <= LINT_AGENT_REFS_PER_CANDIDATE_CAP
745 && !refs.contains(&0)
746 && refs.windows(2).all(|pair| pair[0] < pair[1])
747}
748
749fn action_matches_check(check_id: LintSemanticCheckId, action: LintSemanticAction) -> bool {
750 match check_id {
751 LintSemanticCheckId::MemoryClassification => action == LintSemanticAction::ReclassifyMemory,
752 LintSemanticCheckId::MemoryContradiction => matches!(
753 action,
754 LintSemanticAction::ReviewContradiction | LintSemanticAction::SupersedeMemory
755 ),
756 LintSemanticCheckId::MemoryStaleness => matches!(
757 action,
758 LintSemanticAction::ReviewStaleness | LintSemanticAction::SupersedeMemory
759 ),
760 LintSemanticCheckId::MemoryEntityLinks => matches!(
761 action,
762 LintSemanticAction::AddMemoryEntityLink | LintSemanticAction::RemoveMemoryEntityLink
763 ),
764 LintSemanticCheckId::EntityRelations => matches!(
765 action,
766 LintSemanticAction::AddEntityRelation | LintSemanticAction::RemoveEntityRelation
767 ),
768 LintSemanticCheckId::PageFaithfulness | LintSemanticCheckId::PageProvenanceAdequacy => {
769 action == LintSemanticAction::ReviewPageClaim
770 }
771 LintSemanticCheckId::PageEvidenceLinks => matches!(
772 action,
773 LintSemanticAction::AddPageEvidence | LintSemanticAction::RemovePageEvidence
774 ),
775 LintSemanticCheckId::RetrievalQuality => action == LintSemanticAction::ReviewRetrieval,
776 }
777}