1use serde::{Deserialize, Serialize};
7
8use crate::attest::{FrameAttestation, ProvenanceAttestation};
9use crate::frame::{ContextFrame, FrameKind, Representation};
10use crate::identity::FrameId;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct ContextQuery {
15 pub goal: String,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub query_text: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub embedding: Option<Vec<f32>>,
21 #[serde(default, skip_serializing_if = "Vec::is_empty")]
22 pub kinds: Vec<FrameKind>,
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 pub anchors: Vec<String>,
27 pub max_frames: u32,
28 pub max_tokens: u32,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub as_of: Option<String>,
32 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub representation_preferences: Vec<Representation>,
37}
38
39impl ContextQuery {
40 pub fn preferred_representations(&self) -> Vec<Representation> {
43 if self.representation_preferences.is_empty() {
44 vec![Representation::Full]
45 } else {
46 self.representation_preferences.clone()
47 }
48 }
49
50 pub fn select_representation(&self, supported: &[Representation]) -> Option<Representation> {
55 self.preferred_representations()
56 .into_iter()
57 .find(|wanted| supported.contains(wanted))
58 }
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
77pub struct ContextQueryResult {
78 pub frames: Vec<ContextFrame>,
79 pub truncated: bool,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub dropped_estimate: Option<u32>,
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
87 pub frame_attestations: Vec<FrameAttestation>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub result_attestation: Option<ProvenanceAttestation>,
93}
94
95impl ContextQueryResult {
96 pub fn unattested(
106 frames: Vec<ContextFrame>,
107 truncated: bool,
108 dropped_estimate: Option<u32>,
109 ) -> Self {
110 Self {
111 frames,
112 truncated,
113 dropped_estimate,
114 frame_attestations: Vec::new(),
115 result_attestation: None,
116 }
117 }
118
119 pub fn total_token_cost(&self) -> u64 {
124 self.frames.iter().map(|f| f.token_cost as u64).sum()
125 }
126
127 pub fn respects_budget(&self, max_tokens: u32) -> bool {
128 self.total_token_cost() <= max_tokens as u64
129 }
130
131 pub fn respects_frame_limit(&self, max_frames: u32) -> bool {
140 self.frames.len() as u64 <= max_frames as u64
141 }
142
143 pub fn frames_with_dishonest_cost(&self) -> Vec<&str> {
149 self.frames
150 .iter()
151 .filter(|f| !f.declares_honest_token_cost())
152 .map(|f| f.id.as_str())
153 .collect()
154 }
155
156 pub fn canonical_token_cost(&self) -> u64 {
159 self.frames
160 .iter()
161 .map(|f| f.expected_inline_token_cost() as u64)
162 .sum()
163 }
164
165 pub fn is_attested(&self) -> bool {
168 self.result_attestation.is_some()
169 || self.frame_attestations.iter().any(|a| a.carries_evidence())
170 }
171
172 pub fn attestation_for(&self, frame: &FrameId) -> Option<&FrameAttestation> {
181 self.frame_attestations.iter().find(|a| &a.frame == frame)
182 }
183
184 pub fn orphaned_attestations(&self, provider_id: &str) -> Vec<&FrameId> {
192 let present: Vec<FrameId> = self
193 .frames
194 .iter()
195 .map(|frame| frame.identity(provider_id))
196 .collect();
197 self.frame_attestations
198 .iter()
199 .map(|entry| &entry.frame)
200 .filter(|id| !present.contains(id))
201 .collect()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::attest::{InclusionProof, InclusionStep};
209 use crate::frame::ContextFrame;
210
211 fn frame_with_cost(id: &str, cost: u32) -> ContextFrame {
212 ContextFrame::full(id, FrameKind::Snippet, id, String::new(), 0.5, cost)
213 }
214
215 #[test]
216 fn context_query_roundtrips() {
217 let query = ContextQuery {
218 goal: "fix the failing test".into(),
219 query_text: Some("failing test".into()),
220 embedding: None,
221 kinds: vec![FrameKind::Symbol, FrameKind::Doc],
222 anchors: vec!["file:///repo/src/lib.rs".into()],
223 max_frames: 20,
224 max_tokens: 4000,
225 as_of: None,
226 representation_preferences: vec![],
227 };
228 let json = serde_json::to_string(&query).unwrap();
229 let back: ContextQuery = serde_json::from_str(&json).unwrap();
230 assert_eq!(back, query);
231 }
232
233 #[test]
234 fn representation_preferences_default_to_full_and_select_first_supported() {
235 let mut query = ContextQuery {
238 goal: "g".into(),
239 query_text: None,
240 embedding: None,
241 kinds: vec![],
242 anchors: vec![],
243 max_frames: 1,
244 max_tokens: 10,
245 as_of: None,
246 representation_preferences: vec![],
247 };
248 assert_eq!(
249 query.preferred_representations(),
250 vec![Representation::Full]
251 );
252 assert!(
253 !serde_json::to_string(&query)
254 .unwrap()
255 .contains("representation_preferences")
256 );
257 assert_eq!(
258 query.select_representation(&[Representation::Full]),
259 Some(Representation::Full)
260 );
261
262 query.representation_preferences = vec![Representation::Reference, Representation::Full];
265 assert_eq!(
266 query.select_representation(&[Representation::Full]),
267 Some(Representation::Full),
268 );
269 assert_eq!(
270 query.select_representation(&[Representation::Reference, Representation::Full]),
271 Some(Representation::Reference),
272 );
273 assert_eq!(
274 query.select_representation(&[Representation::Compact]),
275 None
276 );
277 }
278
279 #[test]
280 fn respects_budget_true_when_under_or_at_limit() {
281 let result = ContextQueryResult {
282 frames: vec![frame_with_cost("a", 100), frame_with_cost("b", 200)],
283 truncated: false,
284 dropped_estimate: None,
285 ..Default::default()
286 };
287 assert_eq!(result.total_token_cost(), 300);
288 assert!(result.respects_budget(300));
289 assert!(result.respects_budget(500));
290 }
291
292 #[test]
293 fn respects_budget_false_when_provider_lies_about_cost() {
294 let result = ContextQueryResult {
295 frames: vec![frame_with_cost("a", 400)],
296 truncated: false,
297 dropped_estimate: None,
298 ..Default::default()
299 };
300 assert!(!result.respects_budget(300));
301 }
302
303 fn honest_frame(id: &str, content: &str) -> ContextFrame {
305 let mut frame = frame_with_cost(id, 0);
306 frame.content = Some(content.to_string());
307 frame.token_cost = frame.expected_inline_token_cost();
308 frame
309 }
310
311 #[test]
312 fn frame_limit_catches_the_provider_that_floods_with_cheap_frames() {
313 let flood = ContextQueryResult {
317 frames: (0..50)
318 .map(|i| honest_frame(&format!("f{i}"), "x"))
319 .collect(),
320 truncated: false,
321 dropped_estimate: None,
322 ..Default::default()
323 };
324 assert!(flood.respects_budget(10_000), "the token budget is fine");
325 assert!(!flood.respects_frame_limit(8), "but the frame cap is not");
326 assert!(flood.respects_frame_limit(50), "boundary is inclusive");
327 }
328
329 #[test]
330 fn an_honest_result_reports_no_dishonest_frames() {
331 let result = ContextQueryResult {
332 frames: vec![honest_frame("a", "abcd"), honest_frame("b", "abcdefgh")],
333 truncated: false,
334 dropped_estimate: None,
335 ..Default::default()
336 };
337 assert!(result.frames_with_dishonest_cost().is_empty());
338 assert_eq!(result.total_token_cost(), result.canonical_token_cost());
339 }
340
341 #[test]
342 fn dishonest_frames_are_named_and_the_true_cost_is_recoverable() {
343 let mut liar = honest_frame("liar", &"x".repeat(4_000));
344 liar.token_cost = 1; let result = ContextQueryResult {
346 frames: vec![honest_frame("honest", "abcd"), liar],
347 truncated: false,
348 dropped_estimate: None,
349 ..Default::default()
350 };
351
352 assert_eq!(result.frames_with_dishonest_cost(), vec!["liar"]);
353 assert_eq!(result.total_token_cost(), 2);
355 assert_eq!(result.canonical_token_cost(), 1_001);
356 assert!(result.respects_budget(100));
357 }
358
359 fn sample_attestation(commitment: &str) -> ProvenanceAttestation {
364 ProvenanceAttestation::new(
365 commitment,
366 "key-1",
367 crate::ALGORITHM_ED25519,
368 "example-provider",
369 "ab".repeat(64),
370 "2026-08-29T00:00:00Z",
371 )
372 }
373
374 fn attested_result() -> ContextQueryResult {
375 let mut frame = frame_with_cost("frame:a", 4);
376 frame.content_digest = Some(format!("sha256:{}", "11".repeat(32)));
377 let identity = frame.identity("example-provider");
378 ContextQueryResult {
379 frames: vec![frame],
380 truncated: false,
381 dropped_estimate: None,
382 frame_attestations: vec![
383 FrameAttestation::signed(
384 identity,
385 sample_attestation(&format!("sha256:{}", "22".repeat(32))),
386 )
387 .with_inclusion_proof(InclusionProof {
388 leaf_index: 0,
389 leaf_count: 1,
390 path: vec![],
391 }),
392 ],
393 result_attestation: Some(sample_attestation(&format!("sha256:{}", "33".repeat(32)))),
394 }
395 }
396
397 #[test]
398 fn an_attested_result_round_trips_byte_for_byte() {
399 let result = attested_result();
403 let json = serde_json::to_string(&result).unwrap();
404 let back: ContextQueryResult = serde_json::from_str(&json).unwrap();
405 assert_eq!(back, result);
406 assert_eq!(serde_json::to_string(&back).unwrap(), json);
407 assert!(result.is_attested());
408 }
409
410 #[test]
411 fn the_attestation_never_travels_inside_the_frame_it_covers() {
412 let result = attested_result();
416 let value = serde_json::to_value(&result).unwrap();
417 let frame = &value["frames"][0];
418 for member in ["attestation", "frame_attestations", "result_attestation"] {
419 assert!(
420 frame.get(member).is_none(),
421 "a frame must carry no attestation member, found `{member}`: {frame}"
422 );
423 }
424 assert!(value.get("frame_attestations").is_some());
425 assert!(value.get("result_attestation").is_some());
426 }
427
428 #[test]
429 fn an_unsigned_answer_is_byte_identical_to_one_from_a_provider_that_predates_this() {
430 let result = ContextQueryResult {
434 frames: vec![frame_with_cost("a", 1)],
435 truncated: false,
436 dropped_estimate: None,
437 ..Default::default()
438 };
439 let json = serde_json::to_string(&result).unwrap();
440 assert!(!json.contains("frame_attestations"), "{json}");
441 assert!(!json.contains("result_attestation"), "{json}");
442 assert!(!result.is_attested());
443 }
444
445 #[test]
446 fn an_old_consumer_ignoring_the_new_members_still_parses_the_envelope() {
447 let attested = serde_json::to_value(attested_result()).unwrap();
451 let mut stripped = attested.as_object().unwrap().clone();
452 stripped.remove("frame_attestations");
453 stripped.remove("result_attestation");
454 let back: ContextQueryResult =
455 serde_json::from_value(serde_json::Value::Object(stripped)).unwrap();
456 assert!(!back.is_attested());
457 assert_eq!(back.frames, attested_result().frames);
458 }
459
460 #[test]
461 fn an_entry_is_matched_on_the_whole_identity_triple_not_the_frame_id() {
462 let result = attested_result();
466 let served = result.frames[0].identity("example-provider");
467 assert!(result.attestation_for(&served).is_some());
468
469 let same_id_other_bytes = FrameId::new(
470 "example-provider",
471 "frame:a",
472 Some(format!("sha256:{}", "99".repeat(32))),
473 );
474 assert!(
475 result.attestation_for(&same_id_other_bytes).is_none(),
476 "different bytes must not inherit another frame's attestation"
477 );
478 let other_provider = FrameId::new(
479 "impostor",
480 "frame:a",
481 result.frames[0].content_digest.clone(),
482 );
483 assert!(result.attestation_for(&other_provider).is_none());
484 }
485
486 #[test]
487 fn an_attestation_for_a_frame_the_host_never_received_is_reported_as_orphaned() {
488 let mut result = attested_result();
489 assert!(result.orphaned_attestations("example-provider").is_empty());
490
491 let ghost = FrameId::new("example-provider", "frame:never-sent", None);
492 result.frame_attestations.push(FrameAttestation::signed(
493 ghost.clone(),
494 sample_attestation("sha256:00"),
495 ));
496 assert_eq!(
497 result.orphaned_attestations("example-provider"),
498 vec![&ghost],
499 "evidence for a frame nobody was shown is not evidence"
500 );
501 }
502
503 #[test]
504 fn an_entry_carrying_neither_a_signature_nor_a_proof_asserts_nothing() {
505 let entry = FrameAttestation {
506 frame: FrameId::new("example-provider", "frame:a", None),
507 attestation: None,
508 inclusion_proof: None,
509 };
510 assert!(!entry.carries_evidence());
511 let result = ContextQueryResult {
512 frames: vec![frame_with_cost("frame:a", 1)],
513 truncated: false,
514 dropped_estimate: None,
515 frame_attestations: vec![entry],
516 result_attestation: None,
517 };
518 assert!(
519 !result.is_attested(),
520 "a bare identity must not read as an attested answer"
521 );
522 }
523
524 #[test]
525 fn a_root_signed_set_needs_no_per_frame_signature() {
526 let entry = FrameAttestation::proven(
531 FrameId::new("example-provider", "frame:a", None),
532 InclusionProof {
533 leaf_index: 0,
534 leaf_count: 2,
535 path: vec![InclusionStep {
536 sibling: format!("sha256:{}", "44".repeat(32)),
537 sibling_is_left: false,
538 }],
539 },
540 );
541 assert!(entry.carries_evidence());
542 let json = serde_json::to_string(&entry).unwrap();
543 assert!(
544 !json.contains("\"attestation\""),
545 "an absent per-frame signature must be omitted, not null: {json}"
546 );
547 let back: FrameAttestation = serde_json::from_str(&json).unwrap();
548 assert_eq!(back, entry);
549 }
550}