use serde::{Deserialize, Serialize};
use crate::identity::FrameId;
use crate::token::budget_tokens;
use crate::validate::{is_protocol_timestamp, is_well_formed_digest};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrameKind {
Snippet,
Symbol,
Fact,
Doc,
Memory,
Episode,
Graph,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Representation {
#[default]
Full,
Compact,
Reference,
}
impl Representation {
pub fn is_full(&self) -> bool {
matches!(self, Representation::Full)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentFidelity {
Exact,
Normalized,
Summarized,
Omitted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InlineContentRequirement {
Required,
ResolvableReferenceAllowed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContentRef {
pub provider_id: String,
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Transform {
pub method: String,
pub implementation: String,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Provenance {
#[serde(rename = "type")]
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub by: Option<String>,
}
impl Provenance {
pub fn is_file_provenance(&self) -> bool {
self.kind == "file"
}
pub fn has_well_formed_digest(&self) -> bool {
self.digest.as_deref().is_some_and(is_well_formed_digest)
}
}
pub mod rel {
pub const CODE_CALLS: &str = "code.calls";
pub const CODE_IMPORTS: &str = "code.imports";
pub const CODE_DEFINES: &str = "code.defines";
pub const CODE_REFERENCES: &str = "code.references";
pub const DOC_DOCUMENTS: &str = "doc.documents";
pub const EPISODE_FOLLOWS: &str = "episode.follows";
pub const RECOMMENDED: &[&str] = &[
CODE_CALLS,
CODE_IMPORTS,
CODE_DEFINES,
CODE_REFERENCES,
DOC_DOCUMENTS,
EPISODE_FOLLOWS,
];
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Relation {
pub rel: String,
pub target_uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
}
impl Relation {
pub fn has_display_name(&self) -> bool {
self.display_name
.as_deref()
.is_some_and(|name| !name.trim().is_empty())
}
pub fn has_target_uri(&self) -> bool {
!self.target_uri.trim().is_empty()
}
pub fn uses_recommended_vocabulary(&self) -> bool {
rel::RECOMMENDED.contains(&self.rel.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrameEmbedding {
pub fingerprint: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vector: Option<Vec<f32>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextFrame {
pub id: String,
pub kind: FrameKind,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(default, skip_serializing_if = "Representation::is_full")]
pub representation: Representation,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_fidelity: Option<ContentFidelity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canonical_content_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_ref: Option<ContentRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transform: Option<Transform>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minimum_content_fidelity: Option<ContentFidelity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inline_content_requirement: Option<InlineContentRequirement>,
pub score: f32,
pub token_cost: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canonical_token_cost: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tokenizer_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub valid_from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub valid_to: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recorded_at: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub provenance: Vec<Provenance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub citation_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedding: Option<FrameEmbedding>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub relations: Vec<Relation>,
}
impl ContextFrame {
pub fn full(
id: impl Into<String>,
kind: FrameKind,
title: impl Into<String>,
content: impl Into<String>,
score: f32,
token_cost: u32,
) -> Self {
Self {
id: id.into(),
kind,
title: title.into(),
content: Some(content.into()),
content_digest: None,
uri: None,
representation: Representation::Full,
content_fidelity: None,
canonical_content_hash: None,
content_ref: None,
transform: None,
minimum_content_fidelity: None,
inline_content_requirement: None,
score,
token_cost,
canonical_token_cost: None,
tokenizer_ref: None,
valid_from: None,
valid_to: None,
recorded_at: None,
provenance: Vec::new(),
citation_label: None,
embedding: None,
relations: Vec::new(),
}
}
pub fn reference(
id: impl Into<String>,
kind: FrameKind,
title: impl Into<String>,
content_ref: ContentRef,
canonical_content_hash: impl Into<String>,
score: f32,
) -> Self {
Self {
representation: Representation::Reference,
content: None,
content_ref: Some(content_ref),
canonical_content_hash: Some(canonical_content_hash.into()),
..Self::full(id, kind, title, String::new(), score, 0)
}
}
pub fn has_valid_score(&self) -> bool {
(0.0..=1.0).contains(&self.score)
}
pub fn identity(&self, provider_id: impl Into<String>) -> FrameId {
FrameId::new(provider_id, self.id.clone(), self.content_digest.clone())
}
pub fn expected_inline_token_cost(&self) -> u32 {
budget_tokens(self.content.as_deref().unwrap_or(""))
}
pub fn declares_honest_token_cost(&self) -> bool {
self.token_cost == self.expected_inline_token_cost()
}
pub fn invalid_temporal_fields(&self) -> Vec<&'static str> {
[
("valid_from", self.valid_from.as_deref()),
("valid_to", self.valid_to.as_deref()),
("recorded_at", self.recorded_at.as_deref()),
]
.into_iter()
.filter(|(_, value)| value.is_some_and(|v| !is_protocol_timestamp(v)))
.map(|(name, _)| name)
.collect()
}
pub fn has_valid_temporal_fields(&self) -> bool {
self.invalid_temporal_fields().is_empty()
}
pub fn provenance_with_unusable_digests(&self) -> Vec<usize> {
self.provenance
.iter()
.enumerate()
.filter(|(_, p)| p.is_file_provenance() && !p.has_well_formed_digest())
.map(|(index, _)| index)
.collect()
}
pub fn has_usable_content_digest(&self) -> bool {
self.content_digest
.as_deref()
.is_none_or(is_well_formed_digest)
}
pub fn representation_invariants(&self) -> Result<(), String> {
match self.representation {
Representation::Full => {
if self.content.is_none() {
return Err("full frame requires inline content".into());
}
}
Representation::Compact => {
if self.content.is_none() {
return Err("compact frame requires inline content".into());
}
if self.content_digest.is_none() {
return Err(
"compact frame requires an inline content hash (content_digest)".into(),
);
}
if self.canonical_content_hash.is_none() {
return Err("compact frame requires canonical_content_hash".into());
}
if self.transform.is_none() {
return Err("compact frame requires a transform identity".into());
}
if self.content_ref.is_none() {
return Err("compact frame requires content_ref".into());
}
}
Representation::Reference => {
if self.content.is_some() {
return Err("reference frame must not carry inline content".into());
}
if self.content_ref.is_none() {
return Err("reference frame requires content_ref".into());
}
if self.canonical_content_hash.is_none() {
return Err("reference frame requires canonical_content_hash".into());
}
if self.content_digest.is_some() {
return Err(
"reference frame must omit the inline content hash (content_digest)".into(),
);
}
if self.transform.is_some() {
return Err("reference frame must omit transform".into());
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_frame() -> ContextFrame {
let mut frame = ContextFrame::full(
"frm_1",
FrameKind::Snippet,
"workspace.ts L120-160",
"export interface Workspace { ... }",
0.83,
412,
);
frame.content_digest = Some("sha256:abc".into());
frame.uri = Some("file:///repo/workspace.ts".into());
frame.recorded_at = Some("2026-07-10T00:00:00Z".into());
frame.provenance = vec![Provenance {
kind: "file".into(),
uri: Some("file:///repo/workspace.ts".into()),
range: Some("L120-160".into()),
digest: Some("sha256:abc".into()),
method: None,
by: None,
}];
frame.citation_label = Some("workspace.ts L120-160".into());
frame
}
#[test]
fn context_frame_roundtrips_through_json() {
let frame = sample_frame();
let json = serde_json::to_string(&frame).unwrap();
let back: ContextFrame = serde_json::from_str(&json).unwrap();
assert_eq!(back, frame);
}
#[test]
fn score_out_of_range_fails_the_conformance_check() {
let mut frame = sample_frame();
assert!(frame.has_valid_score());
frame.score = 1.5;
assert!(!frame.has_valid_score());
}
#[test]
fn an_honest_frame_declares_the_canonical_cost_of_its_content() {
let mut frame = sample_frame();
frame.content = Some("abcd".repeat(10)); frame.token_cost = 10;
assert!(frame.declares_honest_token_cost());
assert_eq!(frame.expected_inline_token_cost(), 10);
}
#[test]
fn the_budget_lie_that_used_to_pass_every_check_is_now_caught() {
let mut frame = sample_frame();
frame.content = Some("x".repeat(10_000));
frame.token_cost = 1;
assert!(!frame.declares_honest_token_cost());
assert_eq!(frame.expected_inline_token_cost(), 2_500);
}
#[test]
fn over_reporting_cost_is_a_lie_too_even_though_it_is_self_harming() {
let mut frame = sample_frame();
frame.content = Some("abcd".into());
frame.token_cost = 500;
assert!(!frame.declares_honest_token_cost());
}
#[test]
fn malformed_temporal_fields_are_reported_by_name() {
let mut frame = sample_frame();
frame.valid_from = Some("last tuesday".into());
frame.valid_to = Some("2026-08-01T00:00:00Z".into());
frame.recorded_at = Some("2026-07-10".into());
assert_eq!(
frame.invalid_temporal_fields(),
vec!["valid_from", "recorded_at"]
);
assert!(!frame.has_valid_temporal_fields());
}
#[test]
fn absent_temporal_fields_are_valid_because_they_are_optional() {
let mut frame = sample_frame();
frame.valid_from = None;
frame.valid_to = None;
frame.recorded_at = None;
assert!(frame.has_valid_temporal_fields());
}
#[test]
fn file_provenance_without_a_usable_digest_is_flagged_by_index() {
let mut frame = sample_frame();
assert_eq!(frame.provenance_with_unusable_digests(), vec![0]);
frame.provenance[0].digest = Some(format!("sha256:{}", "a".repeat(64)));
assert!(frame.provenance_with_unusable_digests().is_empty());
}
#[test]
fn non_file_provenance_is_not_required_to_carry_a_digest() {
let mut frame = sample_frame();
frame.provenance = vec![Provenance {
kind: "derivation".into(),
uri: None,
range: None,
digest: None,
method: Some("summarized".into()),
by: Some("contextgraph-docs".into()),
}];
assert!(frame.provenance_with_unusable_digests().is_empty());
}
#[test]
fn a_graph_edge_must_be_citable_by_a_human_label() {
let edge = Relation {
rel: rel::CODE_CALLS.into(),
target_uri: "file:///repo/src/net.rs#retry".into(),
display_name: Some("net::retry".into()),
};
assert!(edge.has_display_name());
assert!(edge.uses_recommended_vocabulary());
let unlabeled = Relation {
rel: "myindex.owns".into(),
target_uri: "file:///repo/src/net.rs".into(),
display_name: None,
};
assert!(!unlabeled.has_display_name());
assert!(!unlabeled.uses_recommended_vocabulary());
}
#[test]
fn a_whitespace_only_display_name_does_not_count_as_a_label() {
let edge = Relation {
rel: rel::DOC_DOCUMENTS.into(),
target_uri: "file:///docs/net.md".into(),
display_name: Some(" ".into()),
};
assert!(!edge.has_display_name());
}
#[test]
fn a_present_content_digest_must_be_usable_but_an_absent_one_is_fine() {
let mut frame = sample_frame();
frame.content_digest = None;
assert!(frame.has_usable_content_digest(), "absent is permitted");
frame.content_digest = Some(format!("sha256:{}", "a".repeat(64)));
assert!(frame.has_usable_content_digest());
for malformed in ["sha256:abc", &format!("sha256:{}", "A".repeat(64))] {
frame.content_digest = Some(malformed.to_string());
assert!(
!frame.has_usable_content_digest(),
"{malformed} is not a comparable digest"
);
}
}
#[test]
fn an_edge_pointing_nowhere_does_not_satisfy_g2() {
let labelled_but_dangling = Relation {
rel: rel::DOC_DOCUMENTS.into(),
target_uri: String::new(),
display_name: Some("Net docs".into()),
};
assert!(labelled_but_dangling.has_display_name(), "§G1 is satisfied");
assert!(!labelled_but_dangling.has_target_uri(), "but §G2 is not");
let whitespace = Relation {
target_uri: " ".into(),
..labelled_but_dangling.clone()
};
assert!(!whitespace.has_target_uri());
let real = Relation {
target_uri: "file:///docs/net.md".into(),
..labelled_but_dangling
};
assert!(real.has_target_uri());
}
#[test]
fn optional_fields_are_omitted_when_absent() {
let frame = sample_frame();
let mut minimal = frame.clone();
minimal.uri = None;
minimal.valid_from = None;
minimal.content_digest = None;
minimal.provenance.clear();
let json = serde_json::to_string(&minimal).unwrap();
assert!(!json.contains("\"uri\""));
assert!(!json.contains("\"provenance\""));
assert!(!json.contains("\"content_digest\""));
}
#[test]
fn full_frame_omits_representation_on_the_wire() {
let frame = sample_frame();
assert_eq!(frame.representation, Representation::Full);
let json = serde_json::to_string(&frame).unwrap();
assert!(
!json.contains("representation"),
"full frames must omit the representation field: {json}"
);
assert!(frame.representation_invariants().is_ok());
}
#[test]
fn reference_frame_omits_content_and_round_trips_its_handle() {
let frame = ContextFrame::reference(
"frm_ref_1",
FrameKind::Doc,
"Deployment runbook",
ContentRef {
provider_id: "provider_example".into(),
uri: "context://provider_example/records/doc_runbook_v1".into(),
expires_at: None,
},
"sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
0.9,
);
frame
.representation_invariants()
.expect("constructed reference frame must be structurally honest");
let json = serde_json::to_string(&frame).unwrap();
assert!(
!json.contains("\"content\""),
"a reference frame must not carry inline content: {json}"
);
assert!(json.contains("\"representation\":\"reference\""));
let back: ContextFrame = serde_json::from_str(&json).unwrap();
assert_eq!(back, frame);
assert_eq!(back.representation, Representation::Reference);
assert_eq!(
back.content_ref.as_ref().unwrap().provider_id,
"provider_example"
);
}
#[test]
fn a_reference_with_inline_content_violates_its_invariants() {
let mut frame = ContextFrame::reference(
"frm_ref_2",
FrameKind::Doc,
"Runbook",
ContentRef {
provider_id: "p".into(),
uri: "context://p/r".into(),
expires_at: None,
},
"sha256:aa",
0.5,
);
frame.content = Some(String::new());
assert!(frame.representation_invariants().is_err());
}
#[test]
fn compact_frame_requires_its_full_metadata_set() {
let mut frame = sample_frame();
frame.representation = Representation::Compact;
assert!(frame.representation_invariants().is_err());
frame.content_digest = Some("sha256:inline".into());
frame.canonical_content_hash = Some("sha256:canonical".into());
frame.transform = Some(Transform {
method: "extractive_summary".into(),
implementation: "provider_default".into(),
version: "1".into(),
});
frame.content_ref = Some(ContentRef {
provider_id: "provider_example".into(),
uri: "context://provider_example/records/x".into(),
expires_at: None,
});
frame.content = Some("summary…".into());
assert!(frame.representation_invariants().is_ok());
}
}