1use contextgraph_host::{
74 ConsentRecord, ContextProvider, DigestVerification, DropReason, Host, HostError,
75 RawStdioConnection, frame_kind_name, verify_file_provenance,
76};
77use contextgraph_types::capability::fingerprint_dimensions;
78use contextgraph_types::{
79 Capabilities, ConsentReceipt, ContextQuery, ContextQueryResult, ErrorCode, FrameId, FrameKind,
80 Grantor, ProviderInfo,
81};
82
83pub mod composition_conformance;
84pub mod host_conformance;
85mod report;
86
87pub use composition_conformance::{
88 CCHECK_BUDGET_BOUND, CCHECK_DETERMINISM, CCHECK_QUARANTINE, CCHECK_TOTAL_PARTITION,
89 ComposingHost, Composition, ExcludedFrame, ReferenceComposingHost, run_composition_conformance,
90};
91pub use host_conformance::{
92 HCHECK_BUDGET_DROP, HCHECK_COMPOSITION_AUDIT, HCHECK_CONSENT_GATE, HCHECK_CONTENT_QUOTING,
93 HCHECK_CRASH_ISOLATION, HCHECK_FRAME_LIMIT, HCHECK_PROVENANCE_BYTES, HCHECK_SCOPE_RECEIPT,
94 HCHECK_VERSION_REJECT, run_host_conformance,
95};
96pub use report::{CheckResult, CheckStatus, ConformanceReport};
97
98pub const CHECK_HANDSHAKE: &str = "handshake";
100pub const CHECK_CONSENT_SCOPE: &str = "consent-scope";
101pub const CHECK_FRAME_VALIDITY: &str = "frame-validity";
102pub const CHECK_VERIFY_HONESTY: &str = "verify-honesty";
103pub const CHECK_BUDGET_HONESTY: &str = "budget-honesty";
104pub const CHECK_AS_OF: &str = "as-of-temporal";
105pub const CHECK_SHUTDOWN: &str = "shutdown-clean";
106pub const CHECK_MALFORMED: &str = "malformed-input-tolerance";
107pub const CHECK_EMBEDDING_FINGERPRINT: &str = "embedding-fingerprint";
108pub const CHECK_CORRELATION: &str = "correlation";
109pub const CHECK_KINDS_FILTER: &str = "kinds-filter";
110pub const CHECK_ANCHOR_RELEVANCE: &str = "anchor-relevance";
111pub const CHECK_PROVENANCE_FIXTURE_CONSISTENCY: &str = "provenance-fixture-consistency";
112
113pub enum ProviderTarget {
116 Stdio { program: String, args: Vec<String> },
118 Http { url: String },
120 InProcess(Box<dyn ContextProvider>),
122}
123
124impl ProviderTarget {
125 pub fn describe(&self) -> String {
127 match self {
128 ProviderTarget::Stdio { program, args } => {
129 if args.is_empty() {
130 format!("stdio: {program}")
131 } else {
132 format!("stdio: {program} {}", args.join(" "))
133 }
134 }
135 ProviderTarget::Http { url } => format!("http: {url}"),
136 ProviderTarget::InProcess(provider) => format!("in-process: {}", provider.id()),
137 }
138 }
139}
140
141pub async fn run_conformance(target: ProviderTarget) -> ConformanceReport {
145 let description = target.describe();
146
147 let stdio_probe = match &target {
150 ProviderTarget::Stdio { program, args } => Some((program.clone(), args.clone())),
151 _ => None,
152 };
153
154 let mut checks = Vec::new();
155
156 match build_host(target).await {
157 Ok((host, id, info, caps)) => {
158 if info.name.trim().is_empty() || info.version.trim().is_empty() {
159 checks.push(CheckResult::fail(
160 CHECK_HANDSHAKE,
161 format!(
162 "provider identity incomplete: name='{}' version='{}'",
163 info.name, info.version
164 ),
165 ));
166 } else {
167 checks.push(CheckResult::pass(
168 CHECK_HANDSHAKE,
169 describe_handshake(&info, &caps),
170 ));
171 }
172 checks.push(check_consent_scopes(&info));
173 run_query_and_shutdown_checks(host, &id, &caps, &mut checks).await;
174 }
175 Err(error) => {
176 checks.push(CheckResult::fail(
177 CHECK_HANDSHAKE,
178 format!("could not establish provider: {error}"),
179 ));
180 for name in [
181 CHECK_FRAME_VALIDITY,
182 CHECK_VERIFY_HONESTY,
183 CHECK_CONSENT_SCOPE,
184 CHECK_BUDGET_HONESTY,
185 CHECK_AS_OF,
186 CHECK_KINDS_FILTER,
187 CHECK_ANCHOR_RELEVANCE,
188 CHECK_PROVENANCE_FIXTURE_CONSISTENCY,
189 CHECK_SHUTDOWN,
190 ] {
191 checks.push(CheckResult::skip(name, "handshake failed"));
192 }
193 }
194 }
195
196 match stdio_probe {
197 Some((program, args)) => {
198 checks.push(malformed_stdio_probe(&program, &args).await);
199 checks.push(embedding_fingerprint_stdio_probe(&program, &args).await);
200 checks.push(correlation_stdio_probe(&program, &args).await);
201 }
202 None => {
203 checks.push(CheckResult::skip(
204 CHECK_MALFORMED,
205 "wire-level malformed-input probe applies to stdio providers only",
206 ));
207 checks.push(CheckResult::skip(
208 CHECK_EMBEDDING_FINGERPRINT,
209 "wire-level §E1 bad_request probe applies to stdio providers only",
210 ));
211 checks.push(CheckResult::skip(
212 CHECK_CORRELATION,
213 "wire-level §H4 id-echo probe applies to stdio providers only",
214 ));
215 }
216 }
217
218 ConformanceReport {
219 target: description,
220 checks,
221 }
222}
223
224async fn build_host(
229 target: ProviderTarget,
230) -> Result<(Host, String, ProviderInfo, Capabilities), HostError> {
231 let mut host = Host::new();
232 let (id, info, caps) = match target {
233 ProviderTarget::Stdio { program, args } => {
234 let id = "provider-under-test".to_string();
235 host.add_stdio(id.clone(), &program, &args).await?;
236 capture_identity(&host, &id)?
237 }
238 ProviderTarget::Http { url } => {
239 let id = "provider-under-test".to_string();
240 host.add_http(id.clone(), url, None).await?;
241 capture_identity(&host, &id)?
242 }
243 ProviderTarget::InProcess(provider) => {
244 let id = provider.id().to_string();
245 let info = provider.info().clone();
246 let caps = provider.capabilities().clone();
247 host.register(provider);
248 (id, info, caps)
249 }
250 };
251
252 if info.data_flow.egress {
256 host.record_consent(ConsentRecord::new(
257 id.clone(),
258 info.data_flow.clone(),
259 "conformance run under test",
260 ));
261 }
262 for scope in info.data_flow.off_machine_scopes() {
263 host.record_receipt(ConsentReceipt::new(
264 id.clone(),
265 &info,
266 scope.clone(),
267 Grantor::Policy("conformance-suite".into()),
268 "2026-07-21T00:00:00Z",
269 ));
270 }
271
272 Ok((host, id, info, caps))
273}
274
275fn capture_identity(
276 host: &Host,
277 id: &str,
278) -> Result<(String, ProviderInfo, Capabilities), HostError> {
279 let provider = host
280 .provider(id)
281 .ok_or_else(|| HostError::UnknownProvider(id.to_string()))?;
282 Ok((
283 id.to_string(),
284 provider.info().clone(),
285 provider.capabilities().clone(),
286 ))
287}
288
289async fn run_query_and_shutdown_checks(
290 host: Host,
291 id: &str,
292 caps: &Capabilities,
293 checks: &mut Vec<CheckResult>,
294) {
295 let query = sample_query();
296 match host.query_provider(id, &query).await {
297 Ok(result) => {
298 let (ok, evidence) = check_frames(&result);
299 checks.push(CheckResult::from_bool(CHECK_FRAME_VALIDITY, ok, evidence));
300 checks.push(check_verify_honesty(&host, id, caps, &result).await);
301
302 let (budget_ok, budget_evidence) = check_budget(&result, &query);
303 checks.push(CheckResult::from_bool(
304 CHECK_BUDGET_HONESTY,
305 budget_ok,
306 budget_evidence,
307 ));
308 }
309 Err(error) => {
310 let evidence = format!("query failed: {error}");
311 checks.push(CheckResult::fail(CHECK_FRAME_VALIDITY, evidence.clone()));
312 checks.push(CheckResult::fail(CHECK_VERIFY_HONESTY, evidence.clone()));
313 checks.push(CheckResult::fail(CHECK_BUDGET_HONESTY, evidence));
314 }
315 }
316
317 checks.push(check_as_of(&host, id).await);
322 checks.push(check_kinds_filter(&host, id, caps).await);
323 checks.push(check_anchor_relevance(&host, id, caps).await);
324 checks.push(check_provenance_fixture_consistency(&host, id).await);
325
326 let results = host.shutdown().await;
327 match results.iter().find(|(pid, _)| pid == id) {
328 Some((_, Ok(()))) => checks.push(CheckResult::pass(
329 CHECK_SHUTDOWN,
330 "provider acknowledged shutdown and tore down cleanly",
331 )),
332 Some((_, Err(error))) => checks.push(CheckResult::fail(
333 CHECK_SHUTDOWN,
334 format!("shutdown error: {error}"),
335 )),
336 None => checks.push(CheckResult::fail(
337 CHECK_SHUTDOWN,
338 "provider vanished before shutdown could be attempted",
339 )),
340 }
341}
342
343const MUTATED_SUFFIX: &str = "-contextgraph-conformance-mutated";
348
349async fn check_verify_honesty(
369 host: &Host,
370 id: &str,
371 caps: &Capabilities,
372 result: &ContextQueryResult,
373) -> CheckResult {
374 if !caps.verify {
375 return CheckResult::skip(
376 CHECK_VERIFY_HONESTY,
377 "provider does not advertise `verify`; a host falls back to re-querying its frames (§4)",
378 );
379 }
380
381 let held: Vec<FrameId> = result
382 .frames
383 .iter()
384 .filter(|frame| frame.content_digest.is_some())
385 .map(|frame| FrameId::new(id, frame.id.clone(), frame.content_digest.clone()))
386 .collect();
387 if held.is_empty() {
388 return CheckResult::skip(
389 CHECK_VERIFY_HONESTY,
390 "provider served no frame carrying a `content_digest`, so nothing is verifiable (§1 D4)",
391 );
392 }
393
394 let unchanged = host.verify_frames(&held).await;
397 if !unchanged.dropped.is_empty() {
398 let detail: Vec<String> = unchanged
399 .dropped
400 .iter()
401 .map(|dropped| format!("{} => {:?}", dropped.frame.frame_id, dropped.reason))
402 .collect();
403 return CheckResult::fail(
404 CHECK_VERIFY_HONESTY,
405 format!(
406 "provider advertises `verify` but did not answer `valid` for {} of {} frame(s) it had just served with unchanged digests: {}",
407 unchanged.dropped.len(),
408 held.len(),
409 detail.join(", ")
410 ),
411 );
412 }
413
414 let mutated: Vec<FrameId> = held
417 .iter()
418 .map(|frame| {
419 FrameId::new(
420 id,
421 frame.frame_id.clone(),
422 frame
423 .content_digest
424 .as_ref()
425 .map(|digest| format!("{digest}{MUTATED_SUFFIX}")),
426 )
427 })
428 .collect();
429 let changed = host.verify_frames(&mutated).await;
430
431 if !changed.retained.is_empty() {
432 return CheckResult::fail(
433 CHECK_VERIFY_HONESTY,
434 format!(
435 "provider answered `valid` for {} frame(s) whose content digest it never served — a rubber stamp that lets a host cite stale evidence",
436 changed.retained.len()
437 ),
438 );
439 }
440 let not_stale: Vec<String> = changed
441 .dropped
442 .iter()
443 .filter(|dropped| !matches!(dropped.reason, DropReason::Stale { .. }))
444 .map(|dropped| format!("{} => {:?}", dropped.frame.frame_id, dropped.reason))
445 .collect();
446 if !not_stale.is_empty() {
447 return CheckResult::fail(
448 CHECK_VERIFY_HONESTY,
449 format!(
450 "a digest mismatch on a frame the provider still serves MUST verify `stale` (§4 V1); got: {}",
451 not_stale.join(", ")
452 ),
453 );
454 }
455
456 CheckResult::pass(
457 CHECK_VERIFY_HONESTY,
458 format!(
459 "provider verified {n} unchanged frame(s) `valid` and all {n} mutated digest(s) `stale`, carrying no frame bodies",
460 n = held.len()
461 ),
462 )
463}
464
465async fn malformed_stdio_probe(program: &str, args: &[String]) -> CheckResult {
474 let mut conn = match RawStdioConnection::spawn(program, args).await {
475 Ok(conn) => conn,
476 Err(error) => {
477 return CheckResult::fail(
478 CHECK_MALFORMED,
479 format!("could not spawn provider: {error}"),
480 );
481 }
482 };
483 if let Err(error) = conn.handshake().await {
484 return CheckResult::fail(
485 CHECK_MALFORMED,
486 format!("handshake failed before the probe could run: {error}"),
487 );
488 }
489 if let Err(error) = conn.send_raw_line("this is not valid json {{{\n").await {
490 return CheckResult::fail(
491 CHECK_MALFORMED,
492 format!("provider closed its input on a malformed line: {error}"),
493 );
494 }
495 if let Err(error) = conn
496 .send(&contextgraph_host::Envelope::Query {
497 id: None,
498 query: sample_query(),
499 })
500 .await
501 {
502 return CheckResult::fail(
503 CHECK_MALFORMED,
504 format!("provider died after a malformed line (before a valid query): {error}"),
505 );
506 }
507 match conn.recv().await {
508 Ok(contextgraph_host::Envelope::Frames { .. }) => CheckResult::pass(
509 CHECK_MALFORMED,
510 "provider ignored a malformed line and still answered a valid query",
511 ),
512 Ok(contextgraph_host::Envelope::Error {
518 code: Some(ErrorCode::BadRequest),
519 message,
520 ..
521 }) => CheckResult::pass(
522 CHECK_MALFORMED,
523 format!(
524 "provider errored cleanly on malformed input with `bad_request` and stayed alive: {message}"
525 ),
526 ),
527 Ok(contextgraph_host::Envelope::Error { code, message, .. }) => CheckResult::fail(
532 CHECK_MALFORMED,
533 format!(
534 "provider stayed alive but answered malformed input with `{}` rather than the `bad_request` §R1 recommends: {message}",
535 code.map(|c| c.to_string())
536 .unwrap_or_else(|| "no code".to_string())
537 ),
538 ),
539 Ok(other) => CheckResult::fail(
540 CHECK_MALFORMED,
541 format!(
542 "provider replied to a valid query with an unexpected `{}` envelope",
543 contextgraph_host::envelope_kind(&other)
544 ),
545 ),
546 Err(HostError::ProviderCrashed { .. }) => CheckResult::fail(
547 CHECK_MALFORMED,
548 "provider crashed on a malformed line — it must error-or-ignore, not die",
549 ),
550 Err(error) => CheckResult::fail(
551 CHECK_MALFORMED,
552 format!("provider mishandled malformed input: {error}"),
553 ),
554 }
555}
556
557async fn embedding_fingerprint_stdio_probe(program: &str, args: &[String]) -> CheckResult {
573 let mut conn = match RawStdioConnection::spawn(program, args).await {
574 Ok(conn) => conn,
575 Err(error) => {
576 return CheckResult::fail(
577 CHECK_EMBEDDING_FINGERPRINT,
578 format!("could not spawn provider: {error}"),
579 );
580 }
581 };
582 let caps = match conn.handshake().await {
583 Ok((_, caps)) => caps,
584 Err(error) => {
585 return CheckResult::skip(
586 CHECK_EMBEDDING_FINGERPRINT,
587 format!("handshake failed before the §E1 probe could run: {error}"),
588 );
589 }
590 };
591 let Some(fingerprint) = caps.embeddings_fingerprint.clone() else {
592 return CheckResult::skip(
593 CHECK_EMBEDDING_FINGERPRINT,
594 "provider declares no embeddings_fingerprint, so §E1 has no dimension to contradict",
595 );
596 };
597 let Some(dimension) = fingerprint_dimensions(&fingerprint) else {
598 return CheckResult::skip(
599 CHECK_EMBEDDING_FINGERPRINT,
600 format!(
601 "fingerprint `{fingerprint}` declares no parseable dimension, so §E1 cannot be probed"
602 ),
603 );
604 };
605
606 let wrong_len = if dimension == 1 { 2 } else { 1 };
609 let mut query = sample_query();
610 query.embedding = Some(vec![0.0; wrong_len]);
611 if let Err(error) = conn
612 .send(&contextgraph_host::Envelope::Query { id: None, query })
613 .await
614 {
615 return CheckResult::fail(
616 CHECK_EMBEDDING_FINGERPRINT,
617 format!("provider closed its input before the §E1 probe query: {error}"),
618 );
619 }
620 match conn.recv().await {
621 Ok(contextgraph_host::Envelope::Error {
624 code: Some(ErrorCode::BadRequest),
625 ..
626 }) => CheckResult::pass(
627 CHECK_EMBEDDING_FINGERPRINT,
628 format!(
629 "provider declares {fingerprint} ({dimension}-dim) and rejected a {wrong_len}-dim embedding with `bad_request` (§E1)"
630 ),
631 ),
632 Ok(contextgraph_host::Envelope::Error { code, message, .. }) => CheckResult::pass(
635 CHECK_EMBEDDING_FINGERPRINT,
636 format!(
637 "provider rejected a {wrong_len}-dim embedding against {fingerprint} with `{}` rather than the `bad_request` §E1 recommends: {message}",
638 code.unwrap_or(ErrorCode::Internal)
639 ),
640 ),
641 Ok(contextgraph_host::Envelope::Frames { .. }) => CheckResult::fail(
643 CHECK_EMBEDDING_FINGERPRINT,
644 format!(
645 "provider declares {fingerprint} ({dimension}-dim) but scored a {wrong_len}-dim embedding into frames instead of rejecting it — meaningless similarity from a different vector space (§E1)"
646 ),
647 ),
648 Ok(other) => CheckResult::fail(
649 CHECK_EMBEDDING_FINGERPRINT,
650 format!(
651 "provider answered the §E1 probe with an unexpected `{}` envelope",
652 contextgraph_host::envelope_kind(&other)
653 ),
654 ),
655 Err(HostError::ProviderCrashed { .. }) => CheckResult::fail(
656 CHECK_EMBEDDING_FINGERPRINT,
657 "provider crashed on a dimension-mismatched embedding — §E1 asks it to reply `bad_request`, not die",
658 ),
659 Err(error) => CheckResult::fail(
660 CHECK_EMBEDDING_FINGERPRINT,
661 format!("provider mishandled the §E1 probe: {error}"),
662 ),
663 }
664}
665
666const CORRELATION_PROBE_ID: &str = "cgp-conformance-h4-7f3a";
670
671async fn correlation_stdio_probe(program: &str, args: &[String]) -> CheckResult {
688 let mut conn = match RawStdioConnection::spawn(program, args).await {
689 Ok(conn) => conn,
690 Err(error) => {
691 return CheckResult::fail(
692 CHECK_CORRELATION,
693 format!("could not spawn provider: {error}"),
694 );
695 }
696 };
697 let caps = match conn.handshake().await {
698 Ok((_, caps)) => caps,
699 Err(error) => {
700 return CheckResult::skip(
701 CHECK_CORRELATION,
702 format!("handshake failed before the §H4 probe could run: {error}"),
703 );
704 }
705 };
706 if !caps.correlation {
707 return CheckResult::skip(
710 CHECK_CORRELATION,
711 "provider does not declare capabilities.correlation, so §H4 does not bind it",
712 );
713 }
714
715 let query = sample_query();
716 if let Err(error) = conn
717 .send(&contextgraph_host::Envelope::Query {
718 id: Some(CORRELATION_PROBE_ID.to_string()),
719 query,
720 })
721 .await
722 {
723 return CheckResult::fail(
724 CHECK_CORRELATION,
725 format!("provider closed its input before the §H4 probe query: {error}"),
726 );
727 }
728
729 let reply = match conn.recv().await {
730 Ok(reply) => reply,
731 Err(error) => {
732 return CheckResult::fail(
733 CHECK_CORRELATION,
734 format!("provider mishandled the §H4 probe: {error}"),
735 );
736 }
737 };
738
739 let kind = contextgraph_host::envelope_kind(&reply);
740 match reply.correlation_id() {
742 Some(echoed) if echoed == CORRELATION_PROBE_ID => CheckResult::pass(
743 CHECK_CORRELATION,
744 format!(
745 "provider declares correlation and echoed the request id verbatim on its `{kind}` reply (§H4)"
746 ),
747 ),
748 Some(echoed) => CheckResult::fail(
749 CHECK_CORRELATION,
750 format!(
751 "provider declares correlation but echoed `{echoed}` on its `{kind}` reply instead of the request's `{CORRELATION_PROBE_ID}` — a host demultiplexing on the id would match this reply to the wrong request (§H4)"
752 ),
753 ),
754 None if matches!(reply, contextgraph_host::Envelope::Frames { .. })
755 || matches!(reply, contextgraph_host::Envelope::Error { .. }) =>
756 {
757 CheckResult::fail(
758 CHECK_CORRELATION,
759 format!(
760 "provider declares correlation but its `{kind}` reply carried no id — the host cannot match it to the request it answers, so the connection is forced back to lock-step (§H4)"
761 ),
762 )
763 }
764 None => CheckResult::fail(
765 CHECK_CORRELATION,
766 format!("provider answered the §H4 probe with an unexpected `{kind}` envelope"),
767 ),
768 }
769}
770
771const AS_OF_PIN: &str = "2026-07-01T00:00:00Z";
775
776async fn check_as_of(host: &Host, id: &str) -> CheckResult {
790 match host.query_provider(id, &as_of_query()).await {
791 Ok(result) => {
792 let not_yet_valid: Vec<String> = result
793 .frames
794 .iter()
795 .filter_map(|frame| {
796 frame
797 .valid_from
798 .as_deref()
799 .filter(|valid_from| *valid_from > AS_OF_PIN)
800 .map(|valid_from| format!("{} (valid_from={valid_from})", frame.id))
801 })
802 .collect();
803 if not_yet_valid.is_empty() {
804 CheckResult::pass(
805 CHECK_AS_OF,
806 format!(
807 "as_of={AS_OF_PIN}: none of the {} returned frame(s) is dated after the pin",
808 result.frames.len()
809 ),
810 )
811 } else {
812 CheckResult::fail(
813 CHECK_AS_OF,
814 format!(
815 "provider returned {} frame(s) whose valid_from is after as_of={AS_OF_PIN} — content that was not yet true at the pinned instant (§6.1): {}",
816 not_yet_valid.len(),
817 not_yet_valid.join(", ")
818 ),
819 )
820 }
821 }
822 Err(error) => CheckResult::fail(CHECK_AS_OF, format!("as_of query failed: {error}")),
823 }
824}
825
826async fn check_kinds_filter(host: &Host, id: &str, caps: &Capabilities) -> CheckResult {
838 let Some(declared) = caps.query.kinds.first() else {
839 return CheckResult::skip(
840 CHECK_KINDS_FILTER,
841 "provider declares no query kinds, so §Q1 has no kind to narrow to",
842 );
843 };
844 let Some(kind) = frame_kind_from_wire(declared) else {
845 return CheckResult::skip(
846 CHECK_KINDS_FILTER,
847 format!(
848 "provider declares kind `{declared}`, which is outside the closed FrameKind vocabulary, so §Q1 cannot be probed"
849 ),
850 );
851 };
852
853 let query = ContextQuery {
854 kinds: vec![kind],
855 ..sample_query()
856 };
857 match host.query_provider(id, &query).await {
858 Ok(result) => {
859 let off_kind: Vec<String> = result
860 .frames
861 .iter()
862 .filter(|frame| frame.kind != kind)
863 .map(|frame| format!("{} (kind={})", frame.id, frame_kind_name(frame.kind)))
864 .collect();
865 if off_kind.is_empty() {
866 CheckResult::pass(
867 CHECK_KINDS_FILTER,
868 format!(
869 "kinds=[{declared}]: all {} returned frame(s) are of the requested kind (§Q1)",
870 result.frames.len()
871 ),
872 )
873 } else {
874 CheckResult::fail(
875 CHECK_KINDS_FILTER,
876 format!(
877 "provider returned {} frame(s) outside the requested kinds=[{declared}] — content the host explicitly excluded, charged against its budget (§Q1): {}",
878 off_kind.len(),
879 off_kind.join(", ")
880 ),
881 )
882 }
883 }
884 Err(error) => CheckResult::fail(
885 CHECK_KINDS_FILTER,
886 format!("kinds-filtered query failed: {error}"),
887 ),
888 }
889}
890
891fn frame_kind_from_wire(kind: &str) -> Option<FrameKind> {
895 match kind {
896 "snippet" => Some(FrameKind::Snippet),
897 "symbol" => Some(FrameKind::Symbol),
898 "fact" => Some(FrameKind::Fact),
899 "doc" => Some(FrameKind::Doc),
900 "memory" => Some(FrameKind::Memory),
901 "episode" => Some(FrameKind::Episode),
902 "graph" => Some(FrameKind::Graph),
903 _ => None,
904 }
905}
906
907async fn check_anchor_relevance(host: &Host, id: &str, caps: &Capabilities) -> CheckResult {
921 if !caps.graph {
922 return CheckResult::skip(
923 CHECK_ANCHOR_RELEVANCE,
924 "provider does not declare capabilities.graph, so §G3/§G4 do not bind it",
925 );
926 }
927
928 let baseline = match host.query_provider(id, &sample_query()).await {
929 Ok(result) => result,
930 Err(error) => {
931 return CheckResult::fail(
932 CHECK_ANCHOR_RELEVANCE,
933 format!("baseline query failed: {error}"),
934 );
935 }
936 };
937
938 let anchor = baseline
941 .frames
942 .iter()
943 .find_map(|frame| frame.relations.first().map(|r| r.target_uri.clone()))
944 .or_else(|| baseline.frames.iter().find_map(|frame| frame.uri.clone()));
945 let Some(anchor) = anchor else {
946 return CheckResult::skip(
947 CHECK_ANCHOR_RELEVANCE,
948 "provider declares graph but served no frame carrying a uri or a relation target to anchor on",
949 );
950 };
951
952 let anchored_query = ContextQuery {
953 anchors: vec![anchor.clone()],
954 ..sample_query()
955 };
956 match host.query_provider(id, &anchored_query).await {
957 Ok(result) => {
958 let anchored: Vec<&contextgraph_types::ContextFrame> = result
959 .frames
960 .iter()
961 .filter(|frame| frame_is_anchored(frame, &anchor))
962 .collect();
963 if anchored.is_empty() {
964 return CheckResult::fail(
965 CHECK_ANCHOR_RELEVANCE,
966 format!(
967 "provider declares capabilities.graph but returned no frame anchored on `{anchor}` — a URI drawn from its own previous answer (§G4)"
968 ),
969 );
970 }
971 let first_is_anchored = result
976 .frames
977 .first()
978 .is_some_and(|frame| frame_is_anchored(frame, &anchor));
979 let ranking = if first_is_anchored {
980 "and ranked it first"
981 } else {
982 "though it did not rank it first (§G3 is a SHOULD)"
983 };
984 CheckResult::pass(
985 CHECK_ANCHOR_RELEVANCE,
986 format!(
987 "anchored on `{anchor}`: provider returned {} anchored frame(s) {ranking}",
988 anchored.len()
989 ),
990 )
991 }
992 Err(error) => CheckResult::fail(
993 CHECK_ANCHOR_RELEVANCE,
994 format!("anchored query failed: {error}"),
995 ),
996 }
997}
998
999fn frame_is_anchored(frame: &contextgraph_types::ContextFrame, anchor: &str) -> bool {
1002 frame.uri.as_deref() == Some(anchor) || frame.relations.iter().any(|r| r.target_uri == anchor)
1003}
1004
1005async fn check_provenance_fixture_consistency(host: &Host, id: &str) -> CheckResult {
1023 let result = match host.query_provider(id, &sample_query()).await {
1024 Ok(result) => result,
1025 Err(error) => {
1026 return CheckResult::fail(
1027 CHECK_PROVENANCE_FIXTURE_CONSISTENCY,
1028 format!("query failed: {error}"),
1029 );
1030 }
1031 };
1032
1033 let mut verified = 0usize;
1034 let mut unreadable = 0usize;
1035 let mut mismatches = Vec::new();
1036 for frame in &result.frames {
1037 for (index, outcome) in verify_file_provenance(frame) {
1038 match outcome {
1039 DigestVerification::Verified => verified += 1,
1040 DigestVerification::Mismatch { expected, actual } => mismatches.push(format!(
1041 "{} provenance[{index}] declared {expected} but its bytes hash to {actual}",
1042 frame.id
1043 )),
1044 DigestVerification::Unreadable { .. } => unreadable += 1,
1045 DigestVerification::NotFileProvenance => {}
1046 }
1047 }
1048 }
1049
1050 if !mismatches.is_empty() {
1051 return CheckResult::fail(
1052 CHECK_PROVENANCE_FIXTURE_CONSISTENCY,
1053 format!(
1054 "{} file-provenance digest(s) do not match the bytes they name — a stale or forged digest that passes §F5's grammar but not its bytes (§6.2): {}",
1055 mismatches.len(),
1056 mismatches.join("; ")
1057 ),
1058 );
1059 }
1060 if verified == 0 {
1061 return CheckResult::skip(
1062 CHECK_PROVENANCE_FIXTURE_CONSISTENCY,
1063 format!(
1064 "no locally re-readable file provenance to verify ({unreadable} link(s) name files this host cannot see); §6.2 byte-verification is host-local"
1065 ),
1066 );
1067 }
1068 CheckResult::pass(
1069 CHECK_PROVENANCE_FIXTURE_CONSISTENCY,
1070 format!(
1071 "re-read and re-hashed {verified} file-provenance digest(s) against the bytes on disk — all match (§6.2)"
1072 ),
1073 )
1074}
1075
1076fn as_of_query() -> ContextQuery {
1079 ContextQuery {
1080 as_of: Some(AS_OF_PIN.into()),
1081 ..sample_query()
1082 }
1083}
1084
1085pub fn sample_query() -> ContextQuery {
1088 ContextQuery {
1089 goal: "conformance probe: return your most relevant frames".into(),
1090 query_text: Some("conformance probe".into()),
1091 embedding: None,
1092 kinds: vec![],
1093 anchors: vec![],
1094 max_frames: 8,
1095 max_tokens: 4096,
1096 as_of: None,
1097 representation_preferences: vec![],
1098 }
1099}
1100
1101pub fn check_frames(result: &ContextQueryResult) -> (bool, String) {
1105 if result.frames.is_empty() {
1106 return (
1107 true,
1108 "provider returned 0 frames (permitted — nothing relevant to the probe)".into(),
1109 );
1110 }
1111
1112 let mut problems = Vec::new();
1113 for (i, frame) in result.frames.iter().enumerate() {
1114 if !frame.has_valid_score() {
1115 problems.push(format!("frame[{i}] score {} is outside [0,1]", frame.score));
1116 }
1117 if frame.title.trim().is_empty() {
1118 problems.push(format!("frame[{i}] has an empty title"));
1119 }
1120 match &frame.citation_label {
1121 Some(label) if !label.trim().is_empty() => {}
1122 _ => problems.push(format!(
1123 "frame[{i}] is missing a citation_label (§F3 — never a bare id)"
1124 )),
1125 }
1126 if let Err(violation) = frame.representation_invariants() {
1131 problems.push(format!("frame[{i}] {violation} (§P1–P3)"));
1132 }
1133 for field in frame.invalid_temporal_fields() {
1138 problems.push(format!(
1139 "frame[{i}] field `{field}` is not an RFC 3339 UTC timestamp (§F4)"
1140 ));
1141 }
1142 if !frame.has_usable_content_digest() {
1148 problems.push(format!(
1149 "frame[{i}] content_digest is present but not `sha256:<64 lowercase hex>` (§D1)"
1150 ));
1151 }
1152 for index in frame.provenance_with_unusable_digests() {
1155 problems.push(format!(
1156 "frame[{i}] provenance[{index}] addresses a file but its digest is missing or not `sha256:<64 lowercase hex>` (§F5)"
1157 ));
1158 }
1159 for (edge_index, edge) in frame.relations.iter().enumerate() {
1164 if !edge.has_display_name() {
1165 problems.push(format!(
1166 "frame[{i}] relation[{edge_index}] `{}` has no display_name (§G1 — an edge is surfaced by label, never a raw id)",
1167 edge.rel
1168 ));
1169 }
1170 if !edge.has_target_uri() {
1171 problems.push(format!(
1172 "frame[{i}] relation[{edge_index}] `{}` has an empty target_uri (§G2 — an edge to nowhere is not an edge)",
1173 edge.rel
1174 ));
1175 }
1176 }
1177 }
1178
1179 if problems.is_empty() {
1180 (
1181 true,
1182 format!(
1183 "{} frame(s) — scores in [0,1], titles, citation labels, honest representations, RFC 3339 timestamps, well-formed digests, labelled and targeted relations",
1184 result.frames.len()
1185 ),
1186 )
1187 } else {
1188 (false, problems.join("; "))
1189 }
1190}
1191
1192pub fn check_budget(result: &ContextQueryResult, query: &ContextQuery) -> (bool, String) {
1203 let mut problems = Vec::new();
1204
1205 let declared = result.total_token_cost();
1206 if declared > query.max_tokens as u64 {
1207 problems.push(format!(
1208 "declared cost {declared} exceeds the query budget of {} (§B1)",
1209 query.max_tokens
1210 ));
1211 }
1212
1213 let dishonest = result.frames_with_dishonest_cost();
1214 if !dishonest.is_empty() {
1215 let canonical = result.canonical_token_cost();
1216 problems.push(format!(
1217 "{} frame(s) misdeclare token_cost — {} (§B3); declared total {declared}, canonical total {canonical}",
1218 dishonest.len(),
1219 dishonest.join(", ")
1220 ));
1221 }
1222
1223 if !result.respects_frame_limit(query.max_frames) {
1224 problems.push(format!(
1225 "returned {} frames against max_frames={} (§B4)",
1226 result.frames.len(),
1227 query.max_frames
1228 ));
1229 }
1230
1231 if problems.is_empty() {
1232 (
1233 true,
1234 format!(
1235 "{} frame(s), {declared} tokens within the {} budget; every declared cost matches its canonical count",
1236 result.frames.len(),
1237 query.max_tokens
1238 ),
1239 )
1240 } else {
1241 (false, problems.join("; "))
1242 }
1243}
1244
1245fn check_consent_scopes(info: &ProviderInfo) -> CheckResult {
1251 if info.data_flow.scopes_consistent() {
1252 let scopes: Vec<&str> = info
1253 .data_flow
1254 .egress_scopes
1255 .iter()
1256 .map(|scope| scope.as_str())
1257 .collect();
1258 CheckResult::pass(
1259 CHECK_CONSENT_SCOPE,
1260 format!(
1261 "declared egress scopes {scopes:?} are well-formed and consistent with egress={}",
1262 info.data_flow.egress
1263 ),
1264 )
1265 } else {
1266 CheckResult::fail(
1267 CHECK_CONSENT_SCOPE,
1268 format!(
1269 "egress scopes {:?} are inconsistent with egress={}: an off-machine scope alongside egress=false, or a non-namespaced custom scope (§3, C5)",
1270 info.data_flow
1271 .egress_scopes
1272 .iter()
1273 .map(|scope| scope.as_str())
1274 .collect::<Vec<_>>(),
1275 info.data_flow.egress
1276 ),
1277 )
1278 }
1279}
1280
1281fn describe_handshake(info: &ProviderInfo, caps: &Capabilities) -> String {
1282 format!(
1283 "provider '{}' v{} — data-flow reads={} writes={} egress={}; query kinds={:?}, graph={}",
1284 info.name,
1285 info.version,
1286 info.data_flow.reads,
1287 info.data_flow.writes,
1288 info.data_flow.egress,
1289 caps.query.kinds,
1290 caps.graph,
1291 )
1292}