use std::io::{BufRead, Write};
use clap::{Parser, ValueEnum};
use sha2::{Digest, Sha256};
use contextgraph_host::wire::Envelope;
use contextgraph_types::capability::{QueryCapability, fingerprint_dimensions};
use contextgraph_types::frame::rel;
use contextgraph_types::{
Capabilities, ContextFrame, ContextQuery, ContextQueryResult, DataFlow, EgressScope, ErrorCode,
FrameKind, FrameVerdict, PROTOCOL_VERSION, Provenance, ProviderInfo, Relation, Representation,
Verdict, VerifyRequest, VerifyResponse, budget_tokens,
};
const EMBEDDING_FINGERPRINT: &str = "bge-small-en-v1.5/384/l2";
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
enum Misbehave {
LyingCosts,
BadScore,
EmptyCitation,
BadVersion,
CrashOnQuery,
CrashOnGarbage,
MislabelMalformed,
UnderReportCost,
BadTimestamp,
MalformedDigest,
StaleDigest,
FloodFrames,
DropCorrelationId,
LyingRepresentation,
IgnoreAsOf,
AcceptBadEmbedding,
RubberStampVerify,
HollowVerify,
ScopeLie,
IgnoreKinds,
IgnoreAnchors,
}
#[derive(Parser)]
#[command(
name = "contextgraph-example-docs",
about = "A tiny reference Context Graph Protocol provider serving canned documentation frames over stdio."
)]
struct Args {
#[arg(long, value_enum)]
misbehave: Option<Misbehave>,
}
fn main() {
let args = Args::parse();
let stdin = std::io::stdin();
let mut input = stdin.lock();
let mut stdout = std::io::stdout();
let mut line = String::new();
loop {
line.clear();
match input.read_line(&mut line) {
Ok(0) | Err(_) => break, Ok(_) => {}
}
let envelope = match serde_json::from_str::<Envelope>(line.trim_end()) {
Ok(envelope) => envelope,
Err(_) => {
if args.misbehave == Some(Misbehave::CrashOnGarbage) {
std::process::exit(1);
}
let code = if args.misbehave == Some(Misbehave::MislabelMalformed) {
ErrorCode::Internal
} else {
ErrorCode::BadRequest
};
write_envelope(
&mut stdout,
&Envelope::Error {
id: None,
code: Some(code),
message: "line was not a valid CGP envelope".into(),
},
);
continue;
}
};
match envelope {
Envelope::Handshake { .. } => {
let protocol_version = if args.misbehave == Some(Misbehave::BadVersion) {
"contextgraph/2.0".to_string()
} else {
PROTOCOL_VERSION.to_string()
};
write_envelope(
&mut stdout,
&Envelope::HandshakeAck {
protocol_version,
provider: provider_info(args.misbehave),
capabilities: capabilities(),
},
);
}
Envelope::Query { id, query } => {
if args.misbehave == Some(Misbehave::CrashOnQuery) {
std::process::exit(1);
}
let echoed = if args.misbehave == Some(Misbehave::DropCorrelationId) {
None
} else {
id
};
if args.misbehave != Some(Misbehave::AcceptBadEmbedding)
&& let Some(error) = embedding_dimension_error(&query, echoed.clone())
{
write_envelope(&mut stdout, &error);
continue;
}
let mut frames = canned_frames(args.misbehave);
if args.misbehave != Some(Misbehave::IgnoreKinds) && !query.kinds.is_empty() {
frames.retain(|f| query.kinds.contains(&f.kind));
}
if !query.anchors.is_empty() {
if args.misbehave == Some(Misbehave::IgnoreAnchors) {
frames.retain(|f| !is_anchored(f, &query.anchors));
} else {
frames.sort_by_key(|f| !is_anchored(f, &query.anchors));
}
}
if args.misbehave != Some(Misbehave::IgnoreAsOf)
&& let Some(as_of) = query.as_of.as_deref()
{
frames.retain(|f| !f.valid_from.as_deref().is_some_and(|vf| vf > as_of));
}
write_envelope(
&mut stdout,
&Envelope::Frames {
id: echoed,
result: ContextQueryResult {
frames,
truncated: false,
dropped_estimate: None,
},
},
);
}
Envelope::Verify { request } => {
let response = match args.misbehave {
Some(Misbehave::RubberStampVerify) => {
VerifyResponse::uniform(&request, Verdict::Valid)
}
Some(Misbehave::HollowVerify) => {
VerifyResponse::uniform(&request, Verdict::Unknown)
}
_ => verify_honestly(&request, args.misbehave),
};
write_envelope(&mut stdout, &Envelope::Verified { response });
}
Envelope::Shutdown => std::process::exit(0),
_ => {}
}
}
}
fn write_envelope(stdout: &mut std::io::Stdout, envelope: &Envelope) {
if let Ok(line) = serde_json::to_string(envelope) {
let _ = writeln!(stdout, "{line}");
let _ = stdout.flush();
}
}
fn provider_info(misbehave: Option<Misbehave>) -> ProviderInfo {
let (egress, egress_scopes) = if misbehave == Some(Misbehave::ScopeLie) {
(false, vec![EgressScope::ThirdPartyIndex])
} else {
(false, vec![EgressScope::LocalOnly])
};
ProviderInfo {
name: "contextgraph-example-docs".into(),
version: env!("CARGO_PKG_VERSION").into(),
data_flow: DataFlow {
reads: true,
writes: false,
egress,
egress_scopes,
},
}
}
fn capabilities() -> Capabilities {
Capabilities {
query: QueryCapability {
kinds: vec!["doc".into(), "snippet".into()],
},
correlation: true,
graph: true,
embeddings_fingerprint: Some(EMBEDDING_FINGERPRINT.into()),
verify: true,
representations: vec![],
resolve: false,
}
}
fn embedding_dimension_error(query: &ContextQuery, id: Option<String>) -> Option<Envelope> {
let embedding = query.embedding.as_ref()?;
let expected = fingerprint_dimensions(EMBEDDING_FINGERPRINT)?;
if embedding.len() == expected {
return None;
}
Some(Envelope::Error {
id,
code: Some(ErrorCode::BadRequest),
message: format!(
"query embedding has {} dimensions; this provider indexes {} ({EMBEDDING_FINGERPRINT}) (§E1)",
embedding.len(),
expected
),
})
}
const FIXTURE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/example-docs");
fn fixture_uri(file: &str) -> String {
format!("file://{FIXTURE_DIR}/{file}")
}
fn fixture_digest(file: &str) -> String {
let bytes = std::fs::read(format!("{FIXTURE_DIR}/{file}")).unwrap_or_default();
let hex: String = Sha256::digest(&bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
format!("sha256:{hex}")
}
fn stale_digest(real: &str) -> String {
let mut digest = real.to_string();
if let Some(last) = digest.pop() {
digest.push(if last == '0' { '1' } else { '0' });
}
digest
}
fn declared_digest(file: &str, misbehave: Option<Misbehave>) -> String {
match misbehave {
Some(Misbehave::MalformedDigest) => "sha256:abc".to_string(),
Some(Misbehave::StaleDigest) => stale_digest(&fixture_digest(file)),
_ => fixture_digest(file),
}
}
fn current_digest(frame_id: &str, misbehave: Option<Misbehave>) -> Option<String> {
match frame_id {
"frm_getting_started" => Some(declared_digest("getting-started.md", misbehave)),
"frm_configuration" => Some(declared_digest("configuration.md", misbehave)),
_ => None,
}
}
fn verify_honestly(request: &VerifyRequest, misbehave: Option<Misbehave>) -> VerifyResponse {
VerifyResponse::new(
request
.frames
.iter()
.map(|frame| {
let verdict = match current_digest(&frame.frame_id, misbehave) {
None => Verdict::Gone,
Some(current) => match frame.content_digest.as_deref() {
None => Verdict::Unknown,
Some(presented) if current.as_str() == presented => Verdict::Valid,
Some(_) => Verdict::Stale {
replacement_digest: Some(current),
},
},
};
FrameVerdict::new(frame.clone(), verdict)
})
.collect(),
)
}
fn is_anchored(frame: &ContextFrame, anchors: &[String]) -> bool {
let zero_hop = frame
.uri
.as_deref()
.is_some_and(|u| anchors.iter().any(|a| a == u));
zero_hop
|| frame
.relations
.iter()
.any(|r| anchors.contains(&r.target_uri))
}
fn canned_frames(misbehave: Option<Misbehave>) -> Vec<ContextFrame> {
let bad_score = misbehave == Some(Misbehave::BadScore);
let empty_citation = misbehave == Some(Misbehave::EmptyCitation);
if misbehave == Some(Misbehave::FloodFrames) {
return (0..64)
.map(|i| {
let mut frame = base_frame(bad_score, empty_citation, misbehave);
frame.id = format!("frm_flood_{i}");
frame.content = Some("x".into());
frame.token_cost = frame.expected_inline_token_cost();
frame
})
.collect();
}
vec![
doc_frame(
"frm_getting_started",
"Getting Started",
"Install the reference binding with `cargo add contextgraph-types`, then implement \
the four required methods.",
"getting-started.md",
"L1-40",
"2026-01-01T00:00:00Z",
0.82,
misbehave,
),
{
let mut frame = doc_frame(
"frm_configuration",
"Configuration example",
"let host = Host::new().with_provider(\"docs\", provider);",
"configuration.md",
"L1-25",
"2026-09-01T00:00:00Z",
0.61,
misbehave,
);
frame.kind = FrameKind::Snippet;
frame
},
]
.into_iter()
.enumerate()
.map(|(index, mut frame)| {
if index == 0 {
if bad_score {
frame.score = 1.5;
}
if empty_citation {
frame.citation_label = Some(String::new());
}
if misbehave == Some(Misbehave::LyingRepresentation) {
frame.representation = Representation::Reference;
}
}
frame
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn doc_frame(
id: &str,
title: &str,
content: &str,
file: &str,
range: &str,
valid_from: &str,
score: f32,
misbehave: Option<Misbehave>,
) -> ContextFrame {
let honest_cost = budget_tokens(content);
ContextFrame {
id: id.into(),
kind: FrameKind::Doc,
title: title.into(),
content: Some(content.into()),
content_digest: Some(declared_digest(file, misbehave)),
uri: Some(fixture_uri(file)),
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: match misbehave {
Some(Misbehave::LyingCosts) => 99_999,
Some(Misbehave::UnderReportCost) => 1,
_ => honest_cost,
},
canonical_token_cost: None,
tokenizer_ref: None,
valid_from: Some(match misbehave {
Some(Misbehave::BadTimestamp) => "last tuesday".into(),
_ => valid_from.to_string(),
}),
valid_to: None,
recorded_at: Some("2026-07-20T18:00:00Z".into()),
provenance: vec![Provenance {
kind: "file".into(),
uri: Some(fixture_uri(file)),
range: Some(range.into()),
digest: Some(declared_digest(file, misbehave)),
method: None,
by: Some("contextgraph-example-docs".into()),
}],
citation_label: Some(format!("{file} {range}")),
embedding: None,
relations: vec![Relation {
rel: rel::DOC_DOCUMENTS.into(),
target_uri: format!("symbol:///docs/{file}#overview"),
display_name: Some(format!("{title} overview")),
}],
}
}
fn base_frame(
_bad_score: bool,
_empty_citation: bool,
misbehave: Option<Misbehave>,
) -> ContextFrame {
doc_frame(
"frm_flood",
"Flood",
"x",
"flood.md",
"L1",
"2026-01-01T00:00:00Z",
0.5,
misbehave.filter(|m| !matches!(m, Misbehave::FloodFrames)),
)
}