Skip to main content

a3s_code_core/capability/
knowledge_surface_binding.rs

1use std::fmt;
2
3use sha2::{Digest, Sha256};
4use thiserror::Error;
5
6use super::{CapabilitySetError, Sha256Digest};
7
8pub const KNOWLEDGE_SURFACE_BINDING_SCHEMA: &str = "a3s.code.knowledge-surface-binding.v1";
9pub const MAX_KNOWLEDGE_SURFACE_PROJECTIONS: usize = 256;
10
11const MAX_KNOWLEDGE_PUBLIC_NAME_BYTES: usize = 256;
12const MAX_KNOWLEDGE_FORMAT_VERSION_BYTES: usize = 64;
13const KNOWLEDGE_SURFACE_DIGEST_PREFIX: &[u8] = b"a3s-code-knowledge-surface\0";
14
15/// Host-reviewed, path-free readiness evidence for one Knowledge surface.
16///
17/// This value is deliberately not a cognitive context provider. Multiple
18/// package Knowledge surfaces may coexist in one catalog and satisfy exact
19/// readiness edges for host capabilities such as Flow. A separately selected
20/// [`crate::CognitiveContextSession`] remains the singular Run-visible
21/// cognitive authority.
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct KnowledgeSurfaceBindingSpec {
24    pub public_name: String,
25    pub format_version: String,
26    pub content_digest: Sha256Digest,
27    pub projection_digests: Vec<Sha256Digest>,
28}
29
30/// Immutable multi-instance Knowledge readiness value accepted by the Code
31/// capability projection kernel.
32///
33/// Projection digests are opaque host evidence. Code canonicalizes and binds
34/// them but does not interpret scopes, open package files, query an index, or
35/// select a cognitive package.
36#[derive(Clone, Eq, PartialEq)]
37pub struct KnowledgeSurfaceBinding {
38    public_name: Box<str>,
39    format_version: Box<str>,
40    content_digest: Sha256Digest,
41    projection_digests: Box<[Sha256Digest]>,
42    surface_digest: Sha256Digest,
43}
44
45impl KnowledgeSurfaceBinding {
46    pub fn new(
47        mut spec: KnowledgeSurfaceBindingSpec,
48    ) -> Result<Self, KnowledgeSurfaceBindingError> {
49        validate_required_text(
50            "public_name",
51            &spec.public_name,
52            MAX_KNOWLEDGE_PUBLIC_NAME_BYTES,
53        )?;
54        validate_required_text(
55            "format_version",
56            &spec.format_version,
57            MAX_KNOWLEDGE_FORMAT_VERSION_BYTES,
58        )?;
59        if spec.projection_digests.is_empty() {
60            return Err(KnowledgeSurfaceBindingError::MissingProjectionEvidence);
61        }
62        if spec.projection_digests.len() > MAX_KNOWLEDGE_SURFACE_PROJECTIONS {
63            return Err(KnowledgeSurfaceBindingError::ProjectionCountExceeded {
64                max: MAX_KNOWLEDGE_SURFACE_PROJECTIONS,
65            });
66        }
67        spec.projection_digests.sort();
68        if spec
69            .projection_digests
70            .windows(2)
71            .any(|pair| pair[0] == pair[1])
72        {
73            return Err(KnowledgeSurfaceBindingError::DuplicateProjectionDigest);
74        }
75        let surface_digest = binding_digest(&spec)?;
76        Ok(Self {
77            public_name: spec.public_name.into_boxed_str(),
78            format_version: spec.format_version.into_boxed_str(),
79            content_digest: spec.content_digest,
80            projection_digests: spec.projection_digests.into_boxed_slice(),
81            surface_digest,
82        })
83    }
84
85    pub fn public_name(&self) -> &str {
86        &self.public_name
87    }
88
89    pub fn format_version(&self) -> &str {
90        &self.format_version
91    }
92
93    pub fn content_digest(&self) -> &Sha256Digest {
94        &self.content_digest
95    }
96
97    pub fn projection_digests(&self) -> &[Sha256Digest] {
98        &self.projection_digests
99    }
100
101    pub fn surface_digest(&self) -> &Sha256Digest {
102        &self.surface_digest
103    }
104}
105
106impl fmt::Debug for KnowledgeSurfaceBinding {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        formatter
109            .debug_struct("KnowledgeSurfaceBinding")
110            .field("public_name", &self.public_name)
111            .field("format_version", &self.format_version)
112            .field("content_digest", &self.content_digest)
113            .field("projection_count", &self.projection_digests.len())
114            .field("surface_digest", &self.surface_digest)
115            .finish()
116    }
117}
118
119#[derive(Clone, Debug, Eq, Error, PartialEq)]
120pub enum KnowledgeSurfaceBindingError {
121    #[error("Knowledge surface field '{field}' is invalid: {reason}")]
122    InvalidText {
123        field: &'static str,
124        reason: &'static str,
125    },
126    #[error("Knowledge surface field '{field}' exceeds its byte bound of {max}")]
127    TextTooLarge { field: &'static str, max: usize },
128    #[error("Knowledge surface readiness requires at least one exact projection digest")]
129    MissingProjectionEvidence,
130    #[error("Knowledge surface readiness contains more than {max} projection digests")]
131    ProjectionCountExceeded { max: usize },
132    #[error("Knowledge surface readiness repeats one exact projection digest")]
133    DuplicateProjectionDigest,
134    #[error("Knowledge surface digest construction violated the canonical SHA-256 invariant")]
135    DigestInvariant,
136}
137
138fn validate_required_text(
139    field: &'static str,
140    value: &str,
141    max: usize,
142) -> Result<(), KnowledgeSurfaceBindingError> {
143    if value.is_empty() || value.trim() != value || value.chars().any(char::is_control) {
144        return Err(KnowledgeSurfaceBindingError::InvalidText {
145            field,
146            reason: "it is empty, padded, or contains control characters",
147        });
148    }
149    if value.len() > max {
150        return Err(KnowledgeSurfaceBindingError::TextTooLarge { field, max });
151    }
152    Ok(())
153}
154
155fn binding_digest(
156    spec: &KnowledgeSurfaceBindingSpec,
157) -> Result<Sha256Digest, KnowledgeSurfaceBindingError> {
158    let mut hasher = Sha256::new();
159    hasher.update(KNOWLEDGE_SURFACE_DIGEST_PREFIX);
160    hash_field(&mut hasher, KNOWLEDGE_SURFACE_BINDING_SCHEMA.as_bytes());
161    hash_field(&mut hasher, spec.public_name.as_bytes());
162    hash_field(&mut hasher, spec.format_version.as_bytes());
163    hash_field(&mut hasher, spec.content_digest.as_str().as_bytes());
164    hash_field(
165        &mut hasher,
166        &(spec.projection_digests.len() as u64).to_be_bytes(),
167    );
168    for digest in &spec.projection_digests {
169        hash_field(&mut hasher, digest.as_str().as_bytes());
170    }
171    Sha256Digest::new(format!("sha256:{:x}", hasher.finalize())).map_err(map_digest_error)
172}
173
174fn hash_field(hasher: &mut Sha256, value: &[u8]) {
175    hasher.update((value.len() as u64).to_be_bytes());
176    hasher.update(value);
177}
178
179fn map_digest_error(_error: CapabilitySetError) -> KnowledgeSurfaceBindingError {
180    KnowledgeSurfaceBindingError::DigestInvariant
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    fn digest(byte: char) -> Sha256Digest {
188        Sha256Digest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap()
189    }
190
191    #[test]
192    fn projection_evidence_is_canonical_and_bounded() {
193        let binding = KnowledgeSurfaceBinding::new(KnowledgeSurfaceBindingSpec {
194            public_name: "research:domain".to_owned(),
195            format_version: "0.2".to_owned(),
196            content_digest: digest('a'),
197            projection_digests: vec![digest('c'), digest('b')],
198        })
199        .unwrap();
200        assert_eq!(binding.projection_digests(), &[digest('b'), digest('c')]);
201
202        assert!(matches!(
203            KnowledgeSurfaceBinding::new(KnowledgeSurfaceBindingSpec {
204                public_name: "research:domain".to_owned(),
205                format_version: "0.2".to_owned(),
206                content_digest: digest('a'),
207                projection_digests: vec![digest('b'), digest('b')],
208            }),
209            Err(KnowledgeSurfaceBindingError::DuplicateProjectionDigest)
210        ));
211        assert!(matches!(
212            KnowledgeSurfaceBinding::new(KnowledgeSurfaceBindingSpec {
213                public_name: "research:domain".to_owned(),
214                format_version: "0.2".to_owned(),
215                content_digest: digest('a'),
216                projection_digests: Vec::new(),
217            }),
218            Err(KnowledgeSurfaceBindingError::MissingProjectionEvidence)
219        ));
220    }
221}