Skip to main content

wenlan_types/
lint_agent.rs

1// SPDX-License-Identifier: Apache-2.0
2use 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}