feat: context-reuse guarantees — deterministic composition, usage reports, consent scopes, context/verify (#23–#26) - #32
Merged
Conversation
… frame identity + canonical ordering (#23) Introduces the frame-identity spine the context-reuse guarantees share and the reference host's deterministic composer, so an unchanged context set renders byte-identically across turns and hosts — keeping prompt-cache prefixes stable by construction instead of forfeiting them to arrival-order composition. contextgraph-types: - new `identity` module: `FrameId { provider_id, frame_id, content_digest }`, the (provider, frame, digest) triple; derived `Ord` IS the canonical composition order (provider, then frame, then digest). Zero new deps. - `ContextFrame.content_digest: Option<String>` — provider-declared, opaque (never host-computed, to keep non-Rust providers interoperable). Absent ⇒ the frame is unverifiable and a host re-queries it. - `ContextFrame::identity(provider_id)`, `canonical_order()`. contextgraph-host: - new `compose` module: `compose_context()` renders a frame set in canonical order, excluding score/token_cost (relevance/cost must not bust the prefix), de-duping identical identities, fencing content as quoted material. - `FanOut::accepted_with_provider()` + `FanOut::compose()`; a host-side round-trip test proves two fan-outs over the same set compose to identical bytes. schema: `content_digest` added to the `ContextFrame` definition. docs: new `docs/context-reuse.md` §1 (normative identity + ordering, cache- economics rationale appendix); protocol-surface conformance rows D1–D2, index + stability notes on the additive, no-flag-day family growth. Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb Signed-off-by: macanderson <mac@oxagen.sh>
…ost roll-up for metering (#24) A host that meters context into a billing system (usage events → ClickHouse → Stripe) needs an aggregate the protocol didn't give it: which providers served how many frames, at what token cost, against which budget. Without a standard shape, every host invents one and context cost stays unauditable one level up from the wire. This adds that shape as a host-side artifact — no new envelope variant, no new required field. contextgraph-types: - new `usage` module: `UsageReport { budget_requested, budget_consumed, as_of, providers }`, `ProviderUsage { provider_id, frames_served, frames_rejected, token_cost, served_frames }`, and `ServedFrame { frame: FrameId, token_cost }`. Zero new deps (serde only). Each served frame is itemized by its #23 stable identity AND declared cost, so the report is self-verifying: totals re-sum from the frames they itemize (`is_consistent()`), and an auditor can walk from a billed total back to the exact frames. contextgraph-host: - `FanOut::usage_report(&query, as_of)` rolls a fan-out up into a report: accepted frames itemized, a budget lie's dropped frames counted as rejected, a consent-gated/failed provider contributing nothing. `budget_requested` is the query's `max_tokens`; `as_of` is the host-supplied accounting snapshot (the report's own clock, not the query's bi-temporal `as_of`). conformance: new `usage_report` integration test drives the real `contextgraph-example-docs` fixture, then re-sums the accepted frames INDEPENDENTLY and asserts equality with the host's report — the arithmetic identity (U1) proven against a live provider, not the builder. docs: context-reuse.md §2 (report shape, arithmetic identity, worked metering-pipeline example mapping into ClickHouse + Stripe, U1 conformance row). Note: `frames_rejected` folds in verification-driven rejections once #26 lands; `token_cost` semantics pin in #8. Closes #24 https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb
There was a problem hiding this comment.
Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
macanderson
marked this pull request as ready for review
July 21, 2026 22:00
There was a problem hiding this comment.
Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
This was referenced Jul 21, 2026
macanderson
added a commit
that referenced
this pull request
Jul 22, 2026
…#37) * feat(consent): egress-scope vocabulary + append-only consent receipts Completes the receipt semantics noted missing in #25. PR #32 landed the boolean `ConsentRecord`/`ConsentStore` gate; this adds the normative scope vocabulary and the durable audit artifact the acceptance criteria called for. - `contextgraph-types/src/scope.rs`: `EgressScope` — the four normative base classes (local-only, org-tenant, third-party-index, third-party-model) plus namespaced `vendor:scope` extensions. Flat-string wire form; an unrecognized scope is conservatively off-machine so a host never under-gates. - `DataFlow.egress_scopes` (optional, omitted when empty, so the pre-scope wire form is unchanged) + `scopes_consistent()` — an off-machine scope alongside `egress: false` is a lie a host rejects at the handshake (C5). - `ConsentReceipt` with `grantor` (Human | Policy), `granted_at`, optional `expires_at`, and provider identity pinned at grant time. Stored in an append-only ledger: a new grant never edits or erases an old one, and expiry never prunes the audit trail. - `ConsentStore::evaluate` -> `ConsentDecision` scope gate: a provider is permitted only when every off-machine scope has a receipt. A boolean `ConsentRecord` does not satisfy a scope gate. Refusal is the typed `HostError::ConsentScopeRequired` naming exactly what would have leaked; the payload is never transmitted (C6). - `consent-scope` conformance check + a `--misbehave scope-lie` fixture mode proving the suite catches a provider lying about its egress posture. - Schema, reference messages, and docs kept in sync with the types. Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb * refactor(consent): move ConsentReceipt + Grantor into contextgraph-types Issue #25's acceptance criterion says "scope vocabulary + receipt types in contextgraph-types". They were in contextgraph-host on the rationale that a receipt is a host-side artifact — but `UsageReport` (#24) is *equally* host-side (its own module doc says "a host-side artifact, not a wire message") and still lives in contextgraph-types, because it is a protocol-defined shape. A receipt is the same category: any host in any language claiming the consent guarantee must produce it, and an auditor reading a persisted ledger must be able to parse it without depending on one host implementation. `ConsentStore` / `ConsentDecision` — the ledger and gate that *consume* receipts — stay in contextgraph-host. No dependency cycle: the receipt touches only `EgressScope` and `ProviderInfo`, both already in types. The receipt's own serde round-trip tests move with the type, and gain cases for policy-vs-human grantor distinctness, omitted-expiry, and the expiry boundary instant. The store keeps a test that its receipt ledger survives the persistence round-trip. Claude-Session: https://claude.ai/code/session_01BvcuRRNHvpFSYLVhow4HGb
macanderson
added a commit
that referenced
this pull request
Aug 1, 2026
…og, and backfill the gaps (#73) CHANGELOG.md relied on every PR remembering to write its own [Unreleased] entry. An audit of the 22 merges since v0.1.0 whose numbers never appear in the file found 9 user-visible changes with no entry at all — including the OCP -> CGP rename itself, the repo's biggest breaking change. The Unreleased section had also accumulated duplicate category headings (two '### Added', two '### Changed', two '### Fixed') from PRs appending their own blocks. Go-forward: .github/workflows/changelog.yml watches pushes to main. When a merge lands without touching CHANGELOG.md, it drafts the missing entries from the merge's actual diff (.github/scripts/changelog-ai.sh, Vercel AI Gateway, degrade-open: no key or a failed call is a log line, never a red check) and proposes them as a bot/changelog PR — a draft for human review, never a direct push. The gap is measured from the last commit that touched CHANGELOG.md, so merging the bot PR resets it, and each run regenerates the whole current gap, so a superseded bot PR loses nothing. Backfill: entries for the rename (#1), frame identity/composition/usage reports (#32), golden wire fixtures (#35), ADR 0007 (#61, #27), composition conformance + the canary scheduled-run gate (#70) — written from each PR's diff, in the file's existing voice. Three further audit hits (#68, #69, #71) turned out to be already covered by newer entries and were left alone. Duplicate category headings merged: one heading per category, Keep-a-Changelog order.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the four interlocking context-reuse guarantees that make reusing retrieved context across a session's turns cache-friendly, auditable, and safe from serving stale evidence. They share one spine — the stable frame identity from #23 — and grow the
contextgraph/1family purely additively (new optional fields, a capability-gated method, host-side artifacts). No flag day.Status: draft, in progress. #23 and #24 are complete and green; #25 and #26 are being built on this branch in dependency order.
#23 — Deterministic composition (already landed on branch)
Stable
FrameIdidentity(provider id, frame id, content digest)+ canonical ordering, so an unchanged context set renders byte-identically across turns and hosts, keeping prompt-cache prefixes stable. Reference hostcompose_context+ round-trip test.#24 — Usage report envelope ✅
Host-side
UsageReport/ProviderUsage/ServedFrameincontextgraph-types(zero new deps): a per-request roll-up of frame costs, itemized by #23 identity so an auditor can walk from a billed total to the exact frames.FanOut::usage_report(&query, as_of)produces it; a conformance case drives the real fixture and re-sums independently to prove the arithmetic identity (U1). Docs §2 maps it into a metering → ClickHouse → Stripe pipeline.#25 — Consent scopes + receipts (in progress)
Closed egress-scope vocabulary + append-only consent receipts; normative host rejection of unconsented egress.
#26 — context/verify (in progress)
Pull-based per-frame revalidation (
valid | stale | gone | unknown), no frame bodies on the wire; host eviction + re-query fallback.Gates run per issue:
cargo fmt --all --check,cargo clippy --workspace --all-targets -D warnings,cargo test --workspace.Closes #23
Closes #24
Closes #25
Closes #26