Skip to main content

a3s_code_core/
cognitive_context.rs

1//! Exact-generation cognitive-package context injected by an embedding host.
2//!
3//! A3S Code deliberately does not install packages, resolve Registry state, or
4//! choose a "latest" generation. The host supplies a provider and one complete
5//! immutable binding obtained from A3S Use. Every request and response repeats
6//! that binding, and Code validates cited, bounded Markdown before it can enter
7//! the model context.
8
9use crate::context::{
10    ContextItem, ContextProvider, ContextProviderFailureMode, ContextQuery, ContextResult,
11    ContextType,
12};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::collections::HashSet;
16use std::sync::Arc;
17use thiserror::Error;
18
19pub const COGNITIVE_PACKAGE_BINDING_SCHEMA: &str = "a3s.code.cognitive-package-session-binding.v1";
20pub const COGNITIVE_KNOWLEDGE_BINDING_SCHEMA: &str =
21    "agentic.ontology.r0-knowledge-lease-binding.v1";
22pub const COGNITIVE_CONTEXT_REQUEST_SCHEMA: &str = "a3s.code.cognitive-context-request.v1";
23pub const COGNITIVE_CONTEXT_RESPONSE_SCHEMA: &str = "a3s.code.cognitive-context-response.v1";
24pub const COGNITIVE_CONTEXT_REQUEST_DIGEST_DOMAIN: &str = "a3s.code.cognitive-context-request.v1";
25pub const OKF_KNOWLEDGE_SEARCH_REQUEST_SCHEMA: &str = "a3s.use.okf-knowledge-search-request.v1";
26pub const OKF_KNOWLEDGE_READ_REQUEST_SCHEMA: &str = "a3s.use.okf-knowledge-read-request.v1";
27pub const OKF_KNOWLEDGE_CITATION_SCHEMA: &str = "a3s.use.okf-knowledge-citation.v1";
28pub const COGNITIVE_CITATION_METADATA: &str = "a3s.cognitive.citation";
29pub const COGNITIVE_PACKAGE_BINDING_METADATA: &str = "a3s.cognitive.package_binding";
30
31const CAPABILITY_SNAPSHOT_DIGEST_DOMAIN: &str = "a3s.use.capability-snapshot.v1";
32const CANONICAL_DIGEST_PREFIX: &[u8] = b"agentic-ontology-canonical-v1\0";
33const MAX_QUERY_BYTES: usize = 4 * 1024;
34const MAX_PROVIDER_NAME_BYTES: usize = 128;
35const MAX_PACKAGE_VERSION_BYTES: usize = 128;
36const MAX_SURFACE_ID_BYTES: usize = 256;
37const MAX_FORMAT_VERSION_BYTES: usize = 64;
38const MAX_DOCUMENT_PATH_BYTES: usize = 512;
39const MAX_HEADING_BYTES: usize = 128;
40const MAX_EVIDENCE_IDS: usize = 256;
41const MAX_CONTEXT_RESULTS: usize = 4;
42const MAX_CONTEXT_DOCUMENT_BYTES: usize = 6 * 1024;
43const MAX_CONTEXT_TOTAL_BYTES: usize = 6 * 1024;
44
45/// Validation failures at the Code-owned cognitive context boundary.
46#[derive(Debug, Clone, PartialEq, Eq, Error)]
47pub enum CognitiveContextError {
48    #[error("invalid cognitive package binding: {0}")]
49    InvalidBinding(String),
50    #[error("invalid cognitive context limits: {0}")]
51    InvalidLimits(String),
52    #[error("invalid cognitive context request: {0}")]
53    InvalidRequest(String),
54    #[error("cognitive context provider failed: {0}")]
55    Provider(String),
56    #[error("cognitive context response drifted: {0}")]
57    ResponseDrift(String),
58}
59
60pub type CognitiveContextResult<T> = std::result::Result<T, CognitiveContextError>;
61
62/// Exact A3S Use Knowledge surface visible to this Code session.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase", deny_unknown_fields)]
65pub struct CognitiveKnowledgeBindingV1 {
66    pub schema: String,
67    pub surface_id: String,
68    pub format_version: String,
69    pub content_digest: String,
70    pub search_schema: String,
71    pub read_schema: String,
72    pub citation_schema: String,
73    pub lifecycle_generation: u64,
74    pub generation_digest: String,
75}
76
77impl CognitiveKnowledgeBindingV1 {
78    pub fn new(
79        surface_id: impl Into<String>,
80        format_version: impl Into<String>,
81        content_digest: impl Into<String>,
82        lifecycle_generation: u64,
83        generation_digest: impl Into<String>,
84    ) -> CognitiveContextResult<Self> {
85        let binding = Self {
86            schema: COGNITIVE_KNOWLEDGE_BINDING_SCHEMA.to_string(),
87            surface_id: surface_id.into(),
88            format_version: format_version.into(),
89            content_digest: content_digest.into(),
90            search_schema: OKF_KNOWLEDGE_SEARCH_REQUEST_SCHEMA.to_string(),
91            read_schema: OKF_KNOWLEDGE_READ_REQUEST_SCHEMA.to_string(),
92            citation_schema: OKF_KNOWLEDGE_CITATION_SCHEMA.to_string(),
93            lifecycle_generation,
94            generation_digest: generation_digest.into(),
95        };
96        binding.validate()?;
97        Ok(binding)
98    }
99
100    pub fn validate(&self) -> CognitiveContextResult<()> {
101        if self.schema != COGNITIVE_KNOWLEDGE_BINDING_SCHEMA
102            || self.search_schema != OKF_KNOWLEDGE_SEARCH_REQUEST_SCHEMA
103            || self.read_schema != OKF_KNOWLEDGE_READ_REQUEST_SCHEMA
104            || self.citation_schema != OKF_KNOWLEDGE_CITATION_SCHEMA
105        {
106            return Err(invalid_binding(
107                "Knowledge schema negotiation is not the exact R0 typed search/read/citation contract",
108            ));
109        }
110        if !valid_machine_id(&self.surface_id, MAX_SURFACE_ID_BYTES) {
111            return Err(invalid_binding("Knowledge surface id is invalid"));
112        }
113        if !valid_plain_value(&self.format_version, MAX_FORMAT_VERSION_BYTES) {
114            return Err(invalid_binding("Knowledge format version is invalid"));
115        }
116        if self.lifecycle_generation == 0 {
117            return Err(invalid_binding(
118                "lifecycle generation must be an exact non-zero generation",
119            ));
120        }
121        if !valid_sha256(&self.content_digest) || !valid_sha256(&self.generation_digest) {
122            return Err(invalid_binding(
123                "Knowledge content and generation identities must be SHA-256 digests",
124            ));
125        }
126        Ok(())
127    }
128}
129
130/// Prompt-injection bounds frozen into the durable session binding.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase", deny_unknown_fields)]
133pub struct CognitiveContextLimits {
134    pub max_results: usize,
135    pub max_document_bytes: usize,
136    pub max_total_bytes: usize,
137}
138
139impl CognitiveContextLimits {
140    pub fn new(
141        max_results: usize,
142        max_document_bytes: usize,
143        max_total_bytes: usize,
144    ) -> CognitiveContextResult<Self> {
145        let limits = Self {
146            max_results,
147            max_document_bytes,
148            max_total_bytes,
149        };
150        limits.validate()?;
151        Ok(limits)
152    }
153
154    pub fn validate(&self) -> CognitiveContextResult<()> {
155        if self.max_results == 0 || self.max_results > MAX_CONTEXT_RESULTS {
156            return Err(CognitiveContextError::InvalidLimits(format!(
157                "max_results must be between 1 and {MAX_CONTEXT_RESULTS}"
158            )));
159        }
160        if self.max_document_bytes == 0 || self.max_document_bytes > MAX_CONTEXT_DOCUMENT_BYTES {
161            return Err(CognitiveContextError::InvalidLimits(format!(
162                "max_document_bytes must be between 1 and {MAX_CONTEXT_DOCUMENT_BYTES}"
163            )));
164        }
165        if self.max_total_bytes == 0 || self.max_total_bytes > MAX_CONTEXT_TOTAL_BYTES {
166            return Err(CognitiveContextError::InvalidLimits(format!(
167                "max_total_bytes must be between 1 and {MAX_CONTEXT_TOTAL_BYTES}"
168            )));
169        }
170        Ok(())
171    }
172}
173
174impl Default for CognitiveContextLimits {
175    fn default() -> Self {
176        Self {
177            max_results: MAX_CONTEXT_RESULTS,
178            max_document_bytes: MAX_CONTEXT_DOCUMENT_BYTES,
179            max_total_bytes: MAX_CONTEXT_TOTAL_BYTES,
180        }
181    }
182}
183
184/// Durable exact-generation identity attached to one A3S Code session.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase", deny_unknown_fields)]
187pub struct CognitivePackageBindingV1 {
188    pub schema: String,
189    pub package_id: String,
190    pub package_version: String,
191    pub lifecycle_generation: u64,
192    pub generation_digest: String,
193    pub capability_snapshot_digest: String,
194    pub knowledge: CognitiveKnowledgeBindingV1,
195    pub limits: CognitiveContextLimits,
196}
197
198impl CognitivePackageBindingV1 {
199    #[allow(clippy::too_many_arguments)]
200    pub fn new(
201        package_id: impl Into<String>,
202        package_version: impl Into<String>,
203        lifecycle_generation: u64,
204        generation_digest: impl Into<String>,
205        capability_snapshot_digest: impl Into<String>,
206        knowledge: CognitiveKnowledgeBindingV1,
207        limits: CognitiveContextLimits,
208    ) -> CognitiveContextResult<Self> {
209        let binding = Self {
210            schema: COGNITIVE_PACKAGE_BINDING_SCHEMA.to_string(),
211            package_id: package_id.into(),
212            package_version: package_version.into(),
213            lifecycle_generation,
214            generation_digest: generation_digest.into(),
215            capability_snapshot_digest: capability_snapshot_digest.into(),
216            knowledge,
217            limits,
218        };
219        binding.validate()?;
220        Ok(binding)
221    }
222
223    pub fn validate(&self) -> CognitiveContextResult<()> {
224        if self.schema != COGNITIVE_PACKAGE_BINDING_SCHEMA {
225            return Err(invalid_binding("session binding schema is unsupported"));
226        }
227        if !valid_package_id(&self.package_id) {
228            return Err(invalid_binding(
229                "package id must be a canonical publisher/name identity",
230            ));
231        }
232        if !valid_plain_value(&self.package_version, MAX_PACKAGE_VERSION_BYTES) {
233            return Err(invalid_binding("package version is invalid"));
234        }
235        if self.lifecycle_generation == 0 {
236            return Err(invalid_binding(
237                "lifecycle generation must be an exact non-zero generation",
238            ));
239        }
240        if !valid_sha256(&self.generation_digest) || !valid_sha256(&self.capability_snapshot_digest)
241        {
242            return Err(invalid_binding(
243                "generation and capability snapshot identities must be SHA-256 digests",
244            ));
245        }
246        self.knowledge.validate()?;
247        self.limits.validate()?;
248        if self.knowledge.lifecycle_generation != self.lifecycle_generation
249            || self.knowledge.generation_digest != self.generation_digest
250        {
251            return Err(invalid_binding(
252                "Knowledge surface belongs to a different lifecycle generation",
253            ));
254        }
255        let expected = capability_snapshot_digest(self)?;
256        if self.capability_snapshot_digest != expected {
257            return Err(invalid_binding(
258                "capability snapshot digest does not bind this package, generation, and Knowledge surface",
259            ));
260        }
261        Ok(())
262    }
263}
264
265/// One exact request from Code to the host-injected cognitive provider.
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase", deny_unknown_fields)]
268pub struct CognitiveContextRequestV1 {
269    pub schema: String,
270    pub session_id: String,
271    pub query: String,
272    pub binding: CognitivePackageBindingV1,
273    pub request_digest: String,
274}
275
276impl CognitiveContextRequestV1 {
277    pub fn new(
278        session_id: impl Into<String>,
279        query: impl Into<String>,
280        binding: CognitivePackageBindingV1,
281    ) -> CognitiveContextResult<Self> {
282        let mut request = Self {
283            schema: COGNITIVE_CONTEXT_REQUEST_SCHEMA.to_string(),
284            session_id: session_id.into(),
285            query: query.into(),
286            binding,
287            request_digest: String::new(),
288        };
289        request.request_digest = request.digest()?;
290        request.validate()?;
291        Ok(request)
292    }
293
294    pub fn validate(&self) -> CognitiveContextResult<()> {
295        self.binding.validate()?;
296        if self.schema != COGNITIVE_CONTEXT_REQUEST_SCHEMA
297            || !valid_machine_id(&self.session_id, 256)
298            || self.query.trim() != self.query
299            || self.query.is_empty()
300            || self.query.len() > MAX_QUERY_BYTES
301            || self
302                .query
303                .chars()
304                .any(|character| character.is_control() && !matches!(character, '\n' | '\t'))
305        {
306            return Err(CognitiveContextError::InvalidRequest(
307                "request schema, session, or query is invalid or unbounded".to_string(),
308            ));
309        }
310        if self.request_digest != self.digest()? {
311            return Err(CognitiveContextError::InvalidRequest(
312                "request digest does not bind the exact query and session generation".to_string(),
313            ));
314        }
315        Ok(())
316    }
317
318    fn digest(&self) -> CognitiveContextResult<String> {
319        canonical_digest(
320            COGNITIVE_CONTEXT_REQUEST_DIGEST_DOMAIN,
321            &(
322                self.schema.as_str(),
323                self.session_id.as_str(),
324                self.query.as_str(),
325                &self.binding,
326            ),
327        )
328    }
329}
330
331/// Use-owned citation projected into the cross-project R0 carrier.
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "camelCase", deny_unknown_fields)]
334pub struct CognitiveKnowledgeCitationV1 {
335    pub schema: String,
336    pub package_id: String,
337    pub package_version: String,
338    pub lifecycle_generation: u64,
339    pub generation_digest: String,
340    pub surface_id: String,
341    pub content_digest: String,
342    pub document_path: String,
343    pub heading: String,
344    pub evidence_ids: Vec<String>,
345    pub citation_digest: String,
346}
347
348impl CognitiveKnowledgeCitationV1 {
349    #[allow(clippy::too_many_arguments)]
350    pub fn new(
351        binding: &CognitivePackageBindingV1,
352        document_path: impl Into<String>,
353        heading: impl Into<String>,
354        evidence_ids: Vec<String>,
355    ) -> CognitiveContextResult<Self> {
356        let mut citation = Self {
357            schema: OKF_KNOWLEDGE_CITATION_SCHEMA.to_string(),
358            package_id: binding.package_id.clone(),
359            package_version: binding.package_version.clone(),
360            lifecycle_generation: binding.lifecycle_generation,
361            generation_digest: binding.generation_digest.clone(),
362            surface_id: binding.knowledge.surface_id.clone(),
363            content_digest: binding.knowledge.content_digest.clone(),
364            document_path: document_path.into(),
365            heading: heading.into(),
366            evidence_ids,
367            citation_digest: String::new(),
368        };
369        citation.citation_digest = citation.digest()?;
370        citation.validate_for(binding)?;
371        Ok(citation)
372    }
373
374    pub fn validate_for(&self, binding: &CognitivePackageBindingV1) -> CognitiveContextResult<()> {
375        binding.validate()?;
376        if self.schema != OKF_KNOWLEDGE_CITATION_SCHEMA
377            || self.package_id != binding.package_id
378            || self.package_version != binding.package_version
379            || self.lifecycle_generation != binding.lifecycle_generation
380            || self.generation_digest != binding.generation_digest
381            || self.surface_id != binding.knowledge.surface_id
382            || self.content_digest != binding.knowledge.content_digest
383        {
384            return Err(response_drift(
385                "citation does not belong to the session's exact package generation and Knowledge surface",
386            ));
387        }
388        if !valid_markdown_path(&self.document_path)
389            || self.heading.trim() != self.heading
390            || self.heading.is_empty()
391            || self.heading.len() > MAX_HEADING_BYTES
392            || self.heading.chars().any(char::is_control)
393            || self.evidence_ids.is_empty()
394            || self.evidence_ids.len() > MAX_EVIDENCE_IDS
395            || !self.evidence_ids.iter().all(|value| valid_sha256(value))
396            || !self.evidence_ids.windows(2).all(|pair| pair[0] < pair[1])
397        {
398            return Err(response_drift(
399                "citation path, heading, or canonical evidence identity set is invalid",
400            ));
401        }
402        if self.citation_digest != self.digest()? {
403            return Err(response_drift("citation digest was substituted"));
404        }
405        Ok(())
406    }
407
408    fn digest(&self) -> CognitiveContextResult<String> {
409        canonical_digest(
410            OKF_KNOWLEDGE_CITATION_SCHEMA,
411            &(
412                self.package_id.as_str(),
413                self.package_version.as_str(),
414                self.lifecycle_generation,
415                &self.generation_digest,
416                self.surface_id.as_str(),
417                self.content_digest.as_str(),
418                self.document_path.as_str(),
419                self.heading.as_str(),
420                &self.evidence_ids,
421            ),
422        )
423    }
424}
425
426/// One complete bounded Markdown read and its unchanged citation.
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase", deny_unknown_fields)]
429pub struct CognitiveContextDocumentV1 {
430    pub citation: CognitiveKnowledgeCitationV1,
431    pub source_digest: String,
432    pub content: String,
433    pub byte_count: usize,
434}
435
436impl CognitiveContextDocumentV1 {
437    pub fn new(
438        citation: CognitiveKnowledgeCitationV1,
439        content: impl Into<String>,
440    ) -> CognitiveContextResult<Self> {
441        let content = content.into();
442        let document = Self {
443            citation,
444            source_digest: sha256(content.as_bytes()),
445            byte_count: content.len(),
446            content,
447        };
448        if document.content.is_empty() {
449            return Err(response_drift("cognitive document is empty"));
450        }
451        Ok(document)
452    }
453
454    fn validate_for(&self, request: &CognitiveContextRequestV1) -> CognitiveContextResult<()> {
455        self.citation.validate_for(&request.binding)?;
456        if self.content.is_empty()
457            || self.byte_count != self.content.len()
458            || self.byte_count > request.binding.limits.max_document_bytes
459            || !valid_sha256(&self.source_digest)
460            || self.source_digest != sha256(self.content.as_bytes())
461        {
462            return Err(response_drift(
463                "document bytes do not match the cited bounded source read",
464            ));
465        }
466        Ok(())
467    }
468}
469
470/// Exact response that Code validates before converting it to prompt context.
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase", deny_unknown_fields)]
473pub struct CognitiveContextResponseV1 {
474    pub schema: String,
475    pub request_digest: String,
476    pub binding: CognitivePackageBindingV1,
477    pub documents: Vec<CognitiveContextDocumentV1>,
478    pub truncated: bool,
479}
480
481impl CognitiveContextResponseV1 {
482    pub fn new(
483        request: &CognitiveContextRequestV1,
484        documents: Vec<CognitiveContextDocumentV1>,
485        truncated: bool,
486    ) -> CognitiveContextResult<Self> {
487        let response = Self {
488            schema: COGNITIVE_CONTEXT_RESPONSE_SCHEMA.to_string(),
489            request_digest: request.request_digest.clone(),
490            binding: request.binding.clone(),
491            documents,
492            truncated,
493        };
494        response.validate_for(request)?;
495        Ok(response)
496    }
497
498    pub fn validate_for(&self, request: &CognitiveContextRequestV1) -> CognitiveContextResult<()> {
499        request.validate()?;
500        self.binding.validate()?;
501        if self.schema != COGNITIVE_CONTEXT_RESPONSE_SCHEMA
502            || self.request_digest != request.request_digest
503            || self.binding != request.binding
504            || self.documents.is_empty()
505            || self.documents.len() > request.binding.limits.max_results
506        {
507            return Err(response_drift(
508                "response schema, request, generation, or result count drifted",
509            ));
510        }
511
512        let mut total_bytes = 0usize;
513        let mut citations = HashSet::with_capacity(self.documents.len());
514        for document in &self.documents {
515            document.validate_for(request)?;
516            total_bytes = total_bytes
517                .checked_add(document.byte_count)
518                .ok_or_else(|| {
519                    response_drift("response byte accounting overflowed its bounded integer")
520                })?;
521            if !citations.insert(document.citation.citation_digest.as_str()) {
522                return Err(response_drift("response repeats a cited document"));
523            }
524        }
525        if total_bytes > request.binding.limits.max_total_bytes {
526            return Err(response_drift(
527                "response exceeds the session's total cognitive context byte bound",
528            ));
529        }
530        Ok(())
531    }
532}
533
534/// Host port. Implementations typically hold an A3S Use exact-generation
535/// Knowledge lease; Code sees only bounded cited reads and never the Registry,
536/// package filesystem, ontology graph, or a personal-memory fallback.
537#[async_trait::async_trait]
538pub trait CognitiveContextProvider: Send + Sync {
539    fn name(&self) -> &str;
540
541    async fn query(
542        &self,
543        request: &CognitiveContextRequestV1,
544    ) -> CognitiveContextResult<CognitiveContextResponseV1>;
545}
546
547/// Runtime pairing of one durable binding with one non-serializable host port.
548#[derive(Clone)]
549pub struct CognitiveContextSession {
550    binding: CognitivePackageBindingV1,
551    provider: Arc<dyn CognitiveContextProvider>,
552}
553
554impl std::fmt::Debug for CognitiveContextSession {
555    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
556        formatter
557            .debug_struct("CognitiveContextSession")
558            .field("binding", &self.binding)
559            .field("provider", &self.provider.name())
560            .finish()
561    }
562}
563
564impl CognitiveContextSession {
565    pub fn new(
566        binding: CognitivePackageBindingV1,
567        provider: Arc<dyn CognitiveContextProvider>,
568    ) -> CognitiveContextResult<Self> {
569        binding.validate()?;
570        if !valid_plain_value(provider.name(), MAX_PROVIDER_NAME_BYTES) {
571            return Err(CognitiveContextError::Provider(
572                "provider name is empty, unbounded, or contains control characters".to_string(),
573            ));
574        }
575        Ok(Self { binding, provider })
576    }
577
578    pub fn binding(&self) -> &CognitivePackageBindingV1 {
579        &self.binding
580    }
581
582    pub fn provider_name(&self) -> &str {
583        self.provider.name()
584    }
585}
586
587#[async_trait::async_trait]
588impl ContextProvider for CognitiveContextSession {
589    fn name(&self) -> &str {
590        self.provider.name()
591    }
592
593    fn failure_mode(&self) -> ContextProviderFailureMode {
594        ContextProviderFailureMode::FailClosed
595    }
596
597    fn cognitive_package_binding(&self) -> Option<&CognitivePackageBindingV1> {
598        Some(&self.binding)
599    }
600
601    async fn query(&self, query: &ContextQuery) -> anyhow::Result<ContextResult> {
602        if !query.context_types.is_empty() && !query.context_types.contains(&ContextType::Resource)
603        {
604            return Err(CognitiveContextError::InvalidRequest(
605                "cognitive packages expose cited resources only".to_string(),
606            )
607            .into());
608        }
609        let session_id = query.session_id.as_deref().unwrap_or_default();
610        let request =
611            CognitiveContextRequestV1::new(session_id, query.query.clone(), self.binding.clone())?;
612        let response = self.provider.query(&request).await?;
613        response.validate_for(&request)?;
614
615        let mut result = ContextResult::new(self.provider.name());
616        result.truncated = response.truncated;
617        for (position, document) in response.documents.into_iter().enumerate() {
618            let citation_json = serde_json::to_value(&document.citation)?;
619            let binding_json = serde_json::to_value(&self.binding)?;
620            let source_digest = document.source_digest.clone();
621            let rendered = format!(
622                "[cognitive citation={} document={} heading={}]\n\n{}",
623                document.citation.citation_digest,
624                document.citation.document_path,
625                document.citation.heading,
626                document.content
627            );
628            let token_count = rendered.len().div_ceil(4).max(1);
629            let relevance = (1.0_f32 - (position as f32 * 0.02)).max(0.9);
630            result.add_item(
631                ContextItem::new(
632                    document.citation.citation_digest.clone(),
633                    ContextType::Resource,
634                    rendered,
635                )
636                .with_source(format!(
637                    "a3s-use://citation/{}",
638                    document
639                        .citation
640                        .citation_digest
641                        .strip_prefix("sha256:")
642                        .unwrap_or(&document.citation.citation_digest)
643                ))
644                .with_metadata(COGNITIVE_CITATION_METADATA, citation_json)
645                .with_metadata(COGNITIVE_PACKAGE_BINDING_METADATA, binding_json)
646                .with_metadata("a3s.cognitive.source_digest", source_digest.into())
647                .with_provenance("a3s-use-cognitive-package")
648                .with_priority(1.0)
649                .with_trust(1.0)
650                .with_freshness(1.0)
651                .with_relevance(relevance)
652                .with_token_count(token_count),
653            );
654        }
655        Ok(result)
656    }
657}
658
659fn capability_snapshot_digest(
660    binding: &CognitivePackageBindingV1,
661) -> CognitiveContextResult<String> {
662    canonical_digest(
663        CAPABILITY_SNAPSHOT_DIGEST_DOMAIN,
664        &(
665            binding.package_id.as_str(),
666            binding.package_version.as_str(),
667            binding.lifecycle_generation,
668            &binding.generation_digest,
669            binding.knowledge.surface_id.as_str(),
670            binding.knowledge.format_version.as_str(),
671            binding.knowledge.content_digest.as_str(),
672        ),
673    )
674}
675
676fn canonical_digest<T: Serialize + ?Sized>(
677    domain: &str,
678    value: &T,
679) -> CognitiveContextResult<String> {
680    let encoded = serde_json::to_vec(value).map_err(|error| {
681        CognitiveContextError::InvalidBinding(format!(
682            "canonical identity could not be serialized: {error}"
683        ))
684    })?;
685    let mut hasher = Sha256::new();
686    hasher.update(CANONICAL_DIGEST_PREFIX);
687    hasher.update((domain.len() as u64).to_be_bytes());
688    hasher.update(domain.as_bytes());
689    hasher.update((encoded.len() as u64).to_be_bytes());
690    hasher.update(encoded);
691    Ok(format!("sha256:{:x}", hasher.finalize()))
692}
693
694fn sha256(bytes: &[u8]) -> String {
695    format!("sha256:{:x}", Sha256::digest(bytes))
696}
697
698fn invalid_binding(message: impl Into<String>) -> CognitiveContextError {
699    CognitiveContextError::InvalidBinding(message.into())
700}
701
702fn response_drift(message: impl Into<String>) -> CognitiveContextError {
703    CognitiveContextError::ResponseDrift(message.into())
704}
705
706fn valid_sha256(value: &str) -> bool {
707    value.strip_prefix("sha256:").is_some_and(|digest| {
708        digest.len() == 64
709            && digest
710                .bytes()
711                .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
712    })
713}
714
715fn valid_machine_id(value: &str, max_bytes: usize) -> bool {
716    !value.is_empty()
717        && value.len() <= max_bytes
718        && value.is_ascii()
719        && value
720            .as_bytes()
721            .first()
722            .is_some_and(u8::is_ascii_alphanumeric)
723        && value.bytes().all(|byte| {
724            byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b':' | b'/' | b'@')
725        })
726}
727
728fn valid_plain_value(value: &str, max_bytes: usize) -> bool {
729    !value.is_empty()
730        && value.len() <= max_bytes
731        && value.trim() == value
732        && !value.chars().any(char::is_control)
733}
734
735fn valid_package_id(value: &str) -> bool {
736    value.split_once('/').is_some_and(|(publisher, name)| {
737        !publisher.is_empty()
738            && !name.is_empty()
739            && [publisher, name].into_iter().all(|segment| {
740                segment.bytes().all(|byte| {
741                    byte.is_ascii_lowercase()
742                        || byte.is_ascii_digit()
743                        || matches!(byte, b'-' | b'_')
744                })
745            })
746    })
747}
748
749fn valid_markdown_path(value: &str) -> bool {
750    !value.is_empty()
751        && value.len() <= MAX_DOCUMENT_PATH_BYTES
752        && value.ends_with(".md")
753        && !value.starts_with('/')
754        && !value.contains('\\')
755        && !value.chars().any(char::is_control)
756        && value
757            .split('/')
758            .all(|component| !component.is_empty() && !matches!(component, "." | ".."))
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    fn digest(byte: u8) -> String {
766        format!("sha256:{}", format!("{byte:02x}").repeat(32))
767    }
768
769    fn binding() -> CognitivePackageBindingV1 {
770        let knowledge =
771            CognitiveKnowledgeBindingV1::new("domain-knowledge", "0.2", digest(1), 7, digest(2))
772                .unwrap();
773        let mut binding = CognitivePackageBindingV1 {
774            schema: COGNITIVE_PACKAGE_BINDING_SCHEMA.to_string(),
775            package_id: "contra-sense/handbook".to_string(),
776            package_version: "0.1.0".to_string(),
777            lifecycle_generation: 7,
778            generation_digest: digest(2),
779            capability_snapshot_digest: String::new(),
780            knowledge,
781            limits: CognitiveContextLimits::default(),
782        };
783        binding.capability_snapshot_digest = capability_snapshot_digest(&binding).unwrap();
784        binding.validate().unwrap();
785        binding
786    }
787
788    fn response(request: &CognitiveContextRequestV1) -> CognitiveContextResponseV1 {
789        let citation = CognitiveKnowledgeCitationV1::new(
790            &request.binding,
791            "concepts/retry-policy.md",
792            "Retry policy",
793            vec![digest(3)],
794        )
795        .unwrap();
796        let document = CognitiveContextDocumentV1::new(
797            citation,
798            "Retry only before an observable side effect.",
799        )
800        .unwrap();
801        CognitiveContextResponseV1::new(request, vec![document], false).unwrap()
802    }
803
804    #[test]
805    fn exact_binding_rejects_latest_and_capability_drift() {
806        let mut unpinned = binding();
807        unpinned.lifecycle_generation = 0;
808        unpinned.knowledge.lifecycle_generation = 0;
809        assert!(matches!(
810            unpinned.validate(),
811            Err(CognitiveContextError::InvalidBinding(_))
812        ));
813
814        let mut drifted = binding();
815        drifted.knowledge.content_digest = digest(9);
816        assert!(matches!(
817            drifted.validate(),
818            Err(CognitiveContextError::InvalidBinding(_))
819        ));
820    }
821
822    #[test]
823    fn response_rejects_generation_citation_and_source_drift() {
824        let request = CognitiveContextRequestV1::new("session-1", "retry", binding()).unwrap();
825
826        let mut generation = response(&request);
827        generation.binding.lifecycle_generation += 1;
828        assert!(matches!(
829            generation.validate_for(&request),
830            Err(CognitiveContextError::ResponseDrift(_))
831                | Err(CognitiveContextError::InvalidBinding(_))
832        ));
833
834        let mut citation = response(&request);
835        citation.documents[0].citation.heading = "Substituted".to_string();
836        assert!(matches!(
837            citation.validate_for(&request),
838            Err(CognitiveContextError::ResponseDrift(_))
839        ));
840
841        let mut source = response(&request);
842        source.documents[0].content.push_str(" changed");
843        source.documents[0].byte_count = source.documents[0].content.len();
844        assert!(matches!(
845            source.validate_for(&request),
846            Err(CognitiveContextError::ResponseDrift(_))
847        ));
848    }
849
850    #[test]
851    fn response_rejects_empty_duplicate_and_unbounded_documents() {
852        let request = CognitiveContextRequestV1::new("session-1", "retry", binding()).unwrap();
853        assert!(CognitiveContextResponseV1::new(&request, Vec::new(), false).is_err());
854
855        let valid = response(&request);
856        let duplicate = vec![valid.documents[0].clone(), valid.documents[0].clone()];
857        assert!(CognitiveContextResponseV1::new(&request, duplicate, false).is_err());
858
859        let citation = CognitiveKnowledgeCitationV1::new(
860            &request.binding,
861            "concepts/large.md",
862            "Large",
863            vec![digest(4)],
864        )
865        .unwrap();
866        let large = CognitiveContextDocumentV1::new(
867            citation,
868            "x".repeat(request.binding.limits.max_document_bytes + 1),
869        )
870        .unwrap();
871        assert!(CognitiveContextResponseV1::new(&request, vec![large], false).is_err());
872    }
873}