Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ which. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1
## [Unreleased]

### Added
- **Frame representations** on `ContextFrame` — `full` | `compact` | `reference`
(CGEP lifecycle phase 2). A frame now states *how* it carries its content:
`reference` frames carry no inline content, only a `content_ref` resolver
handle and a `canonical_content_hash`; `compact` frames inline a transformed
rendering alongside both. Additive and backward-compatible — `representation`
absent ⇒ `full`, and full/legacy frames are unchanged on the wire. Adds
`content_ref`, `canonical_content_hash`, `content_fidelity`, `transform`,
`minimum_content_fidelity`, `inline_content_requirement`, `canonical_token_cost`,
and `tokenizer_ref`; `content` becomes optional (absent for references).
Negotiated via `ContextQuery.representation_preferences` and
`Capabilities.representations` + `Capabilities.resolve`. Enforced in Rust
(`ContextFrame::representation_invariants`), the JSON Schema, and conformance
tests. See
[docs/adr/0005-frame-representations.md](./docs/adr/0005-frame-representations.md).
- [`schema/contextgraph-envelope.schema.json`](./schema/contextgraph-envelope.schema.json) — a
machine-readable JSON Schema (Draft 2020-12) for the Context Graph Protocol envelope and all wire
types. Validates in any language (`ajv`, Python `jsonschema`, Rust
Expand Down Expand Up @@ -39,6 +53,14 @@ which. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1
names (`contextgraph-graph`, `contextgraph-example-docs`).

### Fixed
- `contextgraph-host` and `contextgraph-conformance` did not compile from a
half-applied merge of #37 (egress-scope + consent receipts): `host.rs` used
`ConsentReceipt`/`EgressScope` without importing them and a `DataFlow` literal
omitted `egress_scopes`; the conformance crate used `FrameId`/`DropReason`
without importing them, a test omitted a `CHECK_VERIFY_HONESTY` import, and a
check-count assertion was stale (6, now 7). Restored so the workspace builds
and the full test suite passes. (Pre-existing on `main`; unrelated to frame
representations but required to build the branch.)
- `docs/index.md`: removed dangling references to `PUBLISHING.md` and
`RELEASING.md`, which do not exist in this repository.
- `CONTRIBUTING.md`: commit-message examples and issue-tracker links no longer
Expand Down
76 changes: 36 additions & 40 deletions contextgraph-conformance/src/bin/contextgraph-example-docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use clap::{Parser, ValueEnum};
use contextgraph_host::wire::Envelope;
use contextgraph_types::capability::QueryCapability;
use contextgraph_types::{
Capabilities, ContextFrame, ContextQueryResult, DataFlow, FrameKind, FrameVerdict,
Capabilities, ContextFrame, ContextQueryResult, DataFlow, EgressScope, FrameKind, FrameVerdict,
PROTOCOL_VERSION, Provenance, ProviderInfo, Verdict, VerifyRequest, VerifyResponse,
};

Expand Down Expand Up @@ -197,6 +197,9 @@ fn capabilities() -> Capabilities {
// cannot watch its sources, so it does not advertise `subscribe` — the
// exact provider shape verify exists for.
verify: true,
// Serves inline full frames only; it does not resolve references.
representations: vec![],
resolve: false,
}
}

Expand Down Expand Up @@ -255,62 +258,55 @@ fn canned_frames(misbehave: Option<Misbehave>) -> Vec<ContextFrame> {
};

vec![
ContextFrame {
id: "frm_getting_started".into(),
kind: FrameKind::Doc,
title: "Getting Started".into(),
content:
{
let mut frame = ContextFrame::full(
"frm_getting_started",
FrameKind::Doc,
"Getting Started",
"Install the reference binding with `cargo add contextgraph-types`, then implement \
the four required methods."
.into(),
content_digest: Some(GETTING_STARTED_DIGEST.into()),
uri: Some("file:///docs/getting-started.md".into()),
score: if bad_score { 1.5 } else { 0.82 },
token_cost,
valid_from: None,
valid_to: None,
recorded_at: None,
provenance: vec![Provenance {
the four required methods.",
if bad_score { 1.5 } else { 0.82 },
token_cost,
);
frame.content_digest = Some(GETTING_STARTED_DIGEST.into());
frame.uri = Some("file:///docs/getting-started.md".into());
frame.provenance = vec![Provenance {
kind: "file".into(),
uri: Some("file:///docs/getting-started.md".into()),
range: Some("L1-40".into()),
digest: None,
method: None,
by: Some("contextgraph-example-docs".into()),
}],
citation_label: Some(if empty_citation {
}];
frame.citation_label = Some(if empty_citation {
String::new()
} else {
"getting-started.md L1-40".into()
}),
embedding: None,
relations: vec![],
});
frame
},
ContextFrame {
id: "frm_configuration".into(),
kind: FrameKind::Doc,
title: "Configuration".into(),
content: "Providers declare their data-flow direction at the handshake so hosts can \
gate consent before sending any query."
.into(),
content_digest: Some(CONFIGURATION_DIGEST.into()),
uri: Some("file:///docs/configuration.md".into()),
score: 0.61,
token_cost,
valid_from: None,
valid_to: None,
recorded_at: None,
provenance: vec![Provenance {
{
let mut frame = ContextFrame::full(
"frm_configuration",
FrameKind::Doc,
"Configuration",
"Providers declare their data-flow direction at the handshake so hosts can \
gate consent before sending any query.",
0.61,
token_cost,
);
frame.content_digest = Some(CONFIGURATION_DIGEST.into());
frame.uri = Some("file:///docs/configuration.md".into());
frame.provenance = vec![Provenance {
kind: "file".into(),
uri: Some("file:///docs/configuration.md".into()),
range: Some("L1-25".into()),
digest: None,
method: None,
by: Some("contextgraph-example-docs".into()),
}],
citation_label: Some("configuration.md L1-25".into()),
embedding: None,
relations: vec![],
}];
frame.citation_label = Some("configuration.md L1-25".into());
frame
},
]
}
1 change: 1 addition & 0 deletions contextgraph-conformance/src/bin/contextgraph-inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ async fn fire_query(host: &Host, id: &str, goal: &str) {
max_frames: 8,
max_tokens: 4096,
as_of: None,
representation_preferences: vec![],
};
match host.query_provider(id, &query).await {
Ok(result) => {
Expand Down
7 changes: 5 additions & 2 deletions contextgraph-conformance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
//! fixture has `--misbehave` flags that trip each one, proving the suite
//! catches a broken provider (task deliverable).

use contextgraph_host::{ConsentRecord, ContextProvider, Host, HostError, RawStdioConnection};
use contextgraph_host::{
ConsentRecord, ContextProvider, DropReason, Host, HostError, RawStdioConnection,
};
use contextgraph_types::{
Capabilities, ConsentReceipt, ContextQuery, ContextQueryResult, Grantor, ProviderInfo,
Capabilities, ConsentReceipt, ContextQuery, ContextQueryResult, FrameId, Grantor, ProviderInfo,
};

mod report;
Expand Down Expand Up @@ -468,6 +470,7 @@ pub fn sample_query() -> ContextQuery {
max_frames: 8,
max_tokens: 4096,
as_of: None,
representation_preferences: vec![],
}
}

Expand Down
7 changes: 4 additions & 3 deletions contextgraph-conformance/tests/conformance_suite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

use contextgraph_conformance::{
CHECK_BUDGET_HONESTY, CHECK_CONSENT_SCOPE, CHECK_FRAME_VALIDITY, CHECK_HANDSHAKE,
CHECK_MALFORMED, CHECK_SHUTDOWN, CheckStatus, ProviderTarget, run_conformance,
CHECK_MALFORMED, CHECK_SHUTDOWN, CHECK_VERIFY_HONESTY, CheckStatus, ProviderTarget,
run_conformance,
};

/// Path to the fixture binary, built automatically for integration tests.
Expand Down Expand Up @@ -37,8 +38,8 @@ async fn a_well_behaved_provider_is_fully_conformant() {
"expected conformant; failures: {:?}",
report.failures().collect::<Vec<_>>()
);
// All six checks ran and passed (none skipped for a stdio provider).
assert_eq!(report.checks.len(), 6);
// All seven checks ran and passed (none skipped for a stdio provider).
assert_eq!(report.checks.len(), 7);
for name in [
CHECK_HANDSHAKE,
CHECK_CONSENT_SCOPE,
Expand Down
14 changes: 13 additions & 1 deletion contextgraph-conformance/tests/golden_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,26 @@ const FIXTURE_FILES: [&str; 5] = [
"normalization-vectors.json",
"strict-validation.invalid.json",
];
const FRAME_FIELDS: [&str; 14] = [
// The pinned `contextgraph-1.0-draft` strict frame profile. `content_digest` is
// intentionally excluded here as it was before the representation work; the nine
// representation/cost fields below are additive and default-absent.
const FRAME_FIELDS: [&str; 23] = [
"id",
"kind",
"title",
"content",
"uri",
"representation",
"content_fidelity",
"canonical_content_hash",
"content_ref",
"transform",
"minimum_content_fidelity",
"inline_content_requirement",
"score",
"token_cost",
"canonical_token_cost",
"tokenizer_ref",
"valid_from",
"valid_to",
"recorded_at",
Expand Down
28 changes: 12 additions & 16 deletions contextgraph-conformance/tests/stdio_roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fn query(goal: &str) -> ContextQuery {
max_frames: 8,
max_tokens: 4096,
as_of: None,
representation_preferences: vec![],
}
}

Expand Down Expand Up @@ -133,22 +134,17 @@ impl ContextProvider for HealthyProvider {
}
async fn query(&self, _query: &ContextQuery) -> Result<ContextQueryResult, HostError> {
Ok(ContextQueryResult {
frames: vec![ContextFrame {
id: "frm_healthy".into(),
kind: FrameKind::Doc,
title: "in-process frame".into(),
content: "still here".into(),
content_digest: None,
uri: None,
score: 0.5,
token_cost: 8,
valid_from: None,
valid_to: None,
recorded_at: None,
provenance: vec![],
citation_label: Some("in-process frame".into()),
embedding: None,
relations: vec![],
frames: vec![{
let mut frame = ContextFrame::full(
"frm_healthy",
FrameKind::Doc,
"in-process frame",
"still here",
0.5,
8,
);
frame.citation_label = Some("in-process frame".into());
frame
}],
truncated: false,
dropped_estimate: None,
Expand Down
1 change: 1 addition & 0 deletions contextgraph-conformance/tests/usage_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fn query() -> ContextQuery {
max_frames: 8,
max_tokens: 4096,
as_of: None,
representation_preferences: vec![],
}
}

Expand Down
16 changes: 14 additions & 2 deletions contextgraph-host/src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ fn render_frame(provider_id: &str, frame: &ContextFrame) -> String {
provider = provider_id,
id = frame.id,
kind = frame_kind_name(frame.kind),
content = frame.content,
// A `reference` frame carries no inline content — it must be resolved
// (`context/resolve`, a later phase) before composition; here it renders
// as empty rather than fabricating bytes.
content = frame.content.as_deref().unwrap_or_default(),
)
}

Expand All @@ -95,11 +98,20 @@ mod tests {
id: id.into(),
kind: FrameKind::Doc,
title: id.into(),
content: content.into(),
content: Some(content.into()),
content_digest: digest.map(Into::into),
uri: None,
representation: Default::default(),
content_fidelity: None,
canonical_content_hash: None,
content_ref: None,
transform: None,
minimum_content_fidelity: None,
inline_content_requirement: None,
score: 0.5,
token_cost: 10,
canonical_token_cost: None,
tokenizer_ref: None,
valid_from: None,
valid_to: None,
recorded_at: None,
Expand Down
17 changes: 14 additions & 3 deletions contextgraph-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use std::collections::HashMap;
use std::time::Duration;

use contextgraph_types::{
ContextFrame, ContextQuery, ContextQueryResult, DataFlow, FrameId, ProviderUsage, ServedFrame,
UsageReport, Verdict, VerifyRequest,
ConsentReceipt, ContextFrame, ContextQuery, ContextQueryResult, DataFlow, EgressScope, FrameId,
ProviderUsage, ServedFrame, UsageReport, Verdict, VerifyRequest,
};

use crate::consent::{ConsentDecision, ConsentRecord, ConsentStore};
Expand Down Expand Up @@ -775,11 +775,20 @@ mod tests {
id: id.into(),
kind: FrameKind::Doc,
title: id.into(),
content: "c".into(),
content: Some("c".into()),
content_digest: None,
uri: None,
representation: Default::default(),
content_fidelity: None,
canonical_content_hash: None,
content_ref: None,
transform: None,
minimum_content_fidelity: None,
inline_content_requirement: None,
score: 0.5,
token_cost: cost,
canonical_token_cost: None,
tokenizer_ref: None,
valid_from: None,
valid_to: None,
recorded_at: None,
Expand All @@ -800,6 +809,7 @@ mod tests {
max_frames: 10,
max_tokens: 1000,
as_of: None,
representation_preferences: vec![],
}
}

Expand Down Expand Up @@ -1184,6 +1194,7 @@ mod tests {
reads: true,
writes: false,
egress: false,
egress_scopes: vec![],
},
})
}
Expand Down
12 changes: 11 additions & 1 deletion contextgraph-host/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,20 @@ mod tests {
id: "frm_h".into(),
kind: FrameKind::Doc,
title: "remote doc".into(),
content: "remote content".into(),
content: Some("remote content".into()),
content_digest: None,
uri: Some("https://example.test/doc".into()),
representation: Default::default(),
content_fidelity: None,
canonical_content_hash: None,
content_ref: None,
transform: None,
minimum_content_fidelity: None,
inline_content_requirement: None,
score: 0.6,
token_cost: 20,
canonical_token_cost: None,
tokenizer_ref: None,
valid_from: None,
valid_to: None,
recorded_at: None,
Expand All @@ -271,6 +280,7 @@ mod tests {
max_frames: 5,
max_tokens: 4000,
as_of: None,
representation_preferences: vec![],
}
}

Expand Down
1 change: 1 addition & 0 deletions contextgraph-host/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ mod tests {
max_frames: 5,
max_tokens: 1000,
as_of: None,
representation_preferences: vec![],
}
}

Expand Down
Loading