Skip to main content

caboodle/skill_profile/
query.rs

1//! Bounded native query planning and decoding, without a transport or writes.
2//! The eventual transport must bind a response to this exact request; parsing
3//! alone cannot attest the server, selected dataset or loaded shape authority.
4use super::{select, CatalogueSnapshot, Harness, Pin, Release, Selection, SkillProfile};
5use anyhow::{bail, Context, Result};
6use serde_json::{json, Value};
7use std::collections::{BTreeMap, BTreeSet};
8
9const NS: &str = "urn:quipu:skills:";
10const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
11const STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
12const BOOLEAN: &str = "http://www.w3.org/2001/XMLSchema#boolean";
13const MAX_ROWS: usize = 8192;
14
15/// A single explicit-dataset, transaction-pinned SELECT request.
16#[derive(Debug)]
17pub struct Plan {
18    profile: String,
19    transaction: u64,
20    request: Value,
21}
22
23fn iri(value: &str) -> bool {
24    let Some((scheme, rest)) = value.split_once(':') else {
25        return false;
26    };
27    !rest.is_empty()
28        && value.len() <= 2048
29        && scheme
30            .as_bytes()
31            .first()
32            .is_some_and(u8::is_ascii_alphabetic)
33        && scheme
34            .bytes()
35            .all(|b| b.is_ascii_alphanumeric() || b"+-.".contains(&b))
36        && !value
37            .chars()
38            .any(|c| c.is_whitespace() || c.is_control() || "<>\"{}|^`\\".contains(c))
39}
40
41/// Construct data, never execute it. Inputs cannot inject SPARQL syntax.
42pub fn plan(profile: &str, graph: &str, transaction: u64) -> Result<Plan> {
43    if !iri(profile) || !iri(graph) || transaction == 0 || transaction > i64::MAX as u64 {
44        bail!("invalid explicit catalogue query pin");
45    }
46    // Collect all properties rather than inner-joining required fields: a
47    // malformed selected pin must remain visible and refuse, never disappear.
48    let query = format!(
49        r#"PREFIX s: <{NS}>
50SELECT ?record ?subject ?predicate (STR(?value) AS ?text)
51 (IF(isIRI(?value), "iri", IF(isLiteral(?value), "literal", "blank")) AS ?kind)
52 (IF(isLiteral(?value), STR(DATATYPE(?value)), "") AS ?datatype)
53FROM <{graph}>
54WHERE {{
55 {{ BIND(<{profile}> AS ?subject) ?subject ?predicate ?value . BIND("profile" AS ?record) }}
56 UNION {{ <{profile}> s:pin ?subject . ?subject ?predicate ?value . BIND("pin" AS ?record) }}
57 UNION {{ <{profile}> s:pin ?pin . ?pin s:skill ?identity ; s:shareId ?share ; s:version ?version .
58 ?subject a s:Release ; s:skill ?identity ; s:shareId ?share ; s:version ?version ; ?predicate ?value . BIND("release" AS ?record) }}
59 FILTER(isIRI(?subject))
60}} LIMIT 8193"#
61    );
62    Ok(Plan {
63        profile: profile.into(),
64        transaction,
65        request: json!({"query": query, "tx": transaction, "verbose": true}),
66    })
67}
68
69type Object = (String, String, String); // kind, lexical value, datatype
70#[derive(Default)]
71struct Record(BTreeMap<String, BTreeSet<Object>>);
72impl Record {
73    fn values(&self, property: &str, kind: &str, datatype: &str) -> Result<Vec<String>> {
74        let key = if property == RDF_TYPE {
75            property.to_owned()
76        } else {
77            format!("{NS}{property}")
78        };
79        self.0
80            .get(&key)
81            .context("required catalogue property missing")?
82            .iter()
83            .map(|(k, v, d)| {
84                if k != kind || d != datatype || (kind == "iri" && !iri(v)) {
85                    bail!("wrong catalogue RDF term kind");
86                }
87                Ok(v.clone())
88            })
89            .collect()
90    }
91    fn one(&self, property: &str, kind: &str, datatype: &str) -> Result<String> {
92        let values = self.values(property, kind, datatype)?;
93        if values.len() != 1 {
94            bail!("ambiguous catalogue property");
95        }
96        Ok(values[0].clone())
97    }
98    fn typed(&self, class: &str) -> Result<()> {
99        if !self
100            .values(RDF_TYPE, "iri", "")?
101            .contains(&format!("{NS}{class}"))
102        {
103            bail!("catalogue record has no explicit required type");
104        }
105        Ok(())
106    }
107    fn pin(&self) -> Result<Pin> {
108        Ok(Pin {
109            skill: self.one("skill", "iri", "")?,
110            share_id: self.one("shareId", "literal", STRING)?,
111            version: self.one("version", "literal", STRING)?,
112        })
113    }
114    fn harnesses(&self) -> Result<Vec<Harness>> {
115        self.values("harness", "literal", STRING)?
116            .iter()
117            .map(|s| match s.as_str() {
118                "claude" => Ok(Harness::Claude),
119                "codex" => Ok(Harness::Codex),
120                _ => anyhow::bail!("unsupported catalogue harness"),
121            })
122            .collect()
123    }
124}
125impl Plan {
126    /// Native Quipu query arguments, without credentials or a server endpoint.
127    pub fn request(&self) -> &Value {
128        &self.request
129    }
130
131    /// Decode only a complete, bounded response belonging to this request.
132    /// Transport provenance remains the caller's obligation; no I/O occurs.
133    pub fn decode(&self, bytes: &[u8]) -> Result<Selection> {
134        if bytes.len() > 4 * 1024 * 1024 {
135            bail!("catalogue response exceeds byte budget");
136        }
137        let response: Value = serde_json::from_slice(bytes)?;
138        let rows = response["rows"]
139            .as_array()
140            .context("catalogue response has no rows")?;
141        if response["truncated"] != false
142            || response["count"].as_u64() != Some(rows.len() as u64)
143            || rows.len() > MAX_ROWS
144        {
145            bail!("catalogue response coverage is unknown");
146        }
147        let expected: BTreeSet<_> = ["record", "subject", "predicate", "text", "kind", "datatype"]
148            .into_iter()
149            .collect();
150        let variables: BTreeSet<_> = response["variables"]
151            .as_array()
152            .context("missing catalogue columns")?
153            .iter()
154            .map(|v| v.as_str().context("invalid catalogue column"))
155            .collect::<Result<_>>()?;
156        if variables != expected {
157            bail!("unexpected catalogue query columns");
158        }
159        let mut records: BTreeMap<(String, String), Record> = BTreeMap::new();
160        for row in rows {
161            let field = |key: &str| {
162                row[key]
163                    .as_str()
164                    .context("malformed catalogue row")
165                    .map(str::to_owned)
166            };
167            let record = field("record")?;
168            let subject = field("subject")?;
169            let predicate = field("predicate")?;
170            if !["profile", "pin", "release"].contains(&record.as_str())
171                || !iri(&subject)
172                || !iri(&predicate)
173            {
174                bail!("invalid catalogue record identity");
175            }
176            records
177                .entry((record, subject))
178                .or_default()
179                .0
180                .entry(predicate)
181                .or_default()
182                .insert((field("kind")?, field("text")?, field("datatype")?));
183        }
184        let profile = records
185            .remove(&("profile".into(), self.profile.clone()))
186            .context("selected profile absent")?;
187        profile.typed("Profile")?;
188        let public_only = match profile.one("publicOnly", "literal", BOOLEAN)?.as_str() {
189            "true" | "1" => true,
190            "false" | "0" => false,
191            _ => bail!("invalid public-only policy"),
192        };
193        let mut pins = Vec::new();
194        for id in profile.values("pin", "iri", "")? {
195            let pin = records
196                .remove(&("pin".into(), id))
197                .context("selected pin absent")?;
198            pin.typed("Pin")?;
199            pins.push(pin.pin()?);
200        }
201        let selected_keys: BTreeSet<_> = pins
202            .iter()
203            .map(|p| (&p.skill, &p.share_id, &p.version))
204            .collect();
205        let mut releases = Vec::new();
206        for ((kind, _), record) in records {
207            if kind != "release" {
208                bail!("unexpected or unselected catalogue record");
209            }
210            record.typed("Release")?;
211            let pin = record.pin()?;
212            if !selected_keys.contains(&(&pin.skill, &pin.share_id, &pin.version)) {
213                bail!("unselected catalogue release");
214            }
215            let public = match record.one("scope", "literal", STRING)?.as_str() {
216                "public" => true,
217                "internal" => false,
218                _ => bail!("unsupported catalogue scope"),
219            };
220            releases.push(Release {
221                pin,
222                name: record.one("name", "literal", STRING)?,
223                render_spec: record.one("renderSpec", "iri", "")?,
224                public,
225                harnesses: record.harnesses()?,
226            });
227        }
228        let profile = SkillProfile {
229            id: self.profile.clone(),
230            public_only,
231            harnesses: profile.harnesses()?,
232            pins,
233        };
234        select(
235            &profile,
236            &CatalogueSnapshot {
237                transaction: self.transaction,
238                complete: true,
239                releases,
240            },
241        )
242    }
243}
244
245#[cfg(test)]
246mod tests;