Pre-freeze normative sweep: SPEC.md, canonical token accounting, conformance that actually catches things#33
Merged
Merged
Conversation
…11) Three ADRs and three design sketches covering the decisions the roadmap (#22) blocks on, plus a rustfmt pass. - ADR 0002 (#4): add an optional envelope `id` rather than adopting JSON-RPC; correct the false JSON-RPC claim in the docs. Keeps the two live downstreams building and leaves a JSON-RPC transport binding open. - ADR 0003 (#8): `token_cost` MUST equal ceil(utf8_len(content)/4), exact equality, so budget honesty checks truth rather than arithmetic. - ADR 0004 (#5, #6, #11): drop `upsert`, `subscribe`, and `filters` from the 1.0 surface; keep `DataFlow.writes` with a corrected definition because it is a consent-surface declaration, not a capability flag. - Sketches for each dropped capability so re-adding them in 1.x is additive rather than a rediscovery. The rustfmt pass fixes drift introduced by the OCP -> contextgraph rename in d6768a8, which lengthened identifiers past the width limit and left `main` failing `cargo fmt --check` — issue #2's CI would have gone red on arrival. Refs #4 #5 #6 #8 #11 #21 #22
Implements ADR 0003 and ADR 0004 in contextgraph-types, plus the validation primitives issues #10 and #12 need. BREAKING (pre-freeze, permitted by docs/stability.md): - remove `Capabilities.upsert` and `Capabilities.subscribe` (#5, #6) - remove `QueryCapability.filters` (#11) - `DataFlow.writes` is kept with a corrected definition: it is a consent-surface declaration, not a capability flag The wire stays compatible — a provider still emitting the removed fields handshakes successfully, since serde ignores unknown fields. There is a test for that claim because two live downstreams depend on it. New: - `token`: `budget_tokens(content) = ceil(utf8_len/4)`, the canonical accounting unit that makes budget honesty checkable rather than merely arithmetically consistent (#8). A frame declaring `token_cost: 1` on a 10k-token body is now caught. - `error_code`: an open `ErrorCode` vocabulary with host-reaction guidance; unknown codes round-trip verbatim and react as `internal` so the vocabulary can grow in a 1.x minor (#9). - `validate`: a dependency-free timestamp profile (`YYYY-MM-DDTHH:MM:SSZ`, a strict UTC-only subset of RFC 3339) and the `sha256:<64 lowercase hex>` digest grammar (#10, #12). - `frame::rel`: the recommended, open relation vocabulary (#7). - Frame/result helpers naming offending fields and frame ids, so a conformance failure is actionable rather than a bare bool. 51 unit tests + 3 doctests pass in contextgraph-types. Refs #5 #6 #7 #8 #9 #10 #11 #12
Implements ADR 0002 (#4) and the wire half of #9. - `Envelope::Query` / `Frames` / `Error` carry an optional `id`. A provider MUST echo the id of the request it answers; an envelope with no id is a notification, which is the shape push invalidation needs. Optional so providers written against an earlier revision stay conformant and the host falls back to lock-step. - `Envelope::Error` carries an optional `code`. Absent reads as `internal` — a host must not infer "safe to retry" from silence. - `Envelope::correlation_id()` and `error_code()` accessors. - Withdrew the false JSON-RPC claim from the wire module docs. The envelope never was JSON-RPC; a JSON-RPC transport binding is left open as a future additive encoding of the same semantic layer. - Propagated the ADR 0004 removals through host, conformance, and the example provider. cargo test --workspace: 91 passed, 0 failed. Refs #4 #9
Turns four documented-but-unenforced contracts into checks with red witnesses, and adds request correlation end to end. Checks (SPEC.md section ids): - B3 token_cost equals the canonical count for the content served. The headline case from #8 — `token_cost: 1` on a 10k-token frame — now fails with "declared total 2, canonical total 55". - B4 frame count respects `max_frames` (#10). A provider returning 64 cheap frames against max_frames=8 used to pass everything. - F4 temporal fields are RFC 3339 UTC, naming the offending field (#10). - F5 file provenance carries a `sha256:<64 lowercase hex>` digest (#12). - G1 graph relations carry a display_name (#7). Correlation (#4): `Capabilities.correlation` negotiates the echo, the host mints and verifies ids on both transports, and a mismatch is a named `HostError::CorrelationMismatch`. ADR 0002 is amended rather than quietly changed: it originally specified negotiation "by observation" with no capability flag. Writing the check proved that unworkable — a reply with no id cannot be distinguished from a provider that does not implement correlation, so the guarantee was uncheckable. That is the failure mode this protocol exists to eliminate, so the ADR records the correction and why. Also: the reference provider now serves honest costs, valid timestamps, and well-formed digests, and replies `bad_request` on a garbage line instead of silently ignoring it (#9). Verified — conformance green on the fixture, and all 11 misbehave modes trip their check: under-report-cost -> budget-honesty (B3) bad-timestamp -> frame-validity (F4) malformed-digest -> frame-validity (F5) flood-frames -> budget-honesty (B4) drop-correlation-id -> correlation mismatch, named cargo test --workspace: 91 passed, 0 failed. Refs #4 #7 #8 #9 #10 #12
The repo's pitch is "conformance is a test you run in CI"; the spec repo had no CI at all. Every normative guarantee is now machine-verified per PR. - ci.yml: fmt, clippy -D warnings, test, MSRV (read from Cargo.toml, not hardcoded), conformance, schema, and an advisory site build. - conformance-green.sh asserts the suite passes on the conformant fixture; conformance-red.sh asserts each --misbehave mode is CAUGHT. Green alone is satisfied by a suite that never fails and red alone by one that never passes — both together make "CGP conformant" checkable. Modes are discovered from the binary's own --help, so adding a misbehaviour without a check that catches it turns CI red. - examples_roundtrip.rs deserializes both transcripts through the Rust wire types, closing the schema-vs-types drift gap: the two were independent descriptions of one wire and nothing tied them together. Schema and examples updated to the new wire (correlation, error codes, RFC 3339 timestamp pattern, sha256 digest pattern, dropped capabilities). Verified the schema now REJECTS the pre-change examples — that rejection is the drift detection issue #2 asked for. Local proof: - conformance-green.sh: all 5 checks pass - conformance-red.sh: all 11 misbehave modes caught - python3 schema/validate-examples.py: all examples validate - cargo clippy --workspace --all-targets -- -D warnings: exit 0 - cargo test --workspace: 95 passed, 0 failed Also fixed three clippy lints and the leap-year helper flagged under -D warnings, which main would have failed on. Refs #2 #4 #9 #10 #12
The normative authority the code cited did not exist in this repository: nearly every source file grounded its contracts in section references to `docs/specs/stella-rust-cli/06-context-protocol.md`, which lives in a private repo. A third party could not resolve a single one — so the spec was not merely unbuildable without the reference code, it was unreadable. - SPEC.md is now the single normative home: transport binding, handshake and correlation, consent, query, frames, budget honesty, graph, errors, robustness, conformance, and the change process. Every requirement has a stable anchor (H1, B3, F5, ...) that will not be renumbered within the contextgraph/1 family. - Normative vs informative is marked explicitly, so reference-host behaviour is not mistaken for protocol. - §10.1 lists the known enforcement gaps by name (R3, host-binding rules, and digest byte-verification). A suite that quietly omitted the rules it cannot check would be the self-attestation this project rejects. - 17 files repointed from the private-repo citations to SPEC.md anchors; `rg "stella-rust-cli|06-context-protocol"` now returns nothing. - docs/protocol-surface.md defers to SPEC.md rather than competing with it. - MIGRATION.md documents the rename map and every breaking change, and leads with the GitHub redirect hazard: downstreams pin the pre-rename URL, which works only via a redirect that a future repo of that name would silently capture. Tag commands are recorded, not executed — cutting a tag needs maintainer authorization. cargo test --workspace: 11 suites green; clippy -D warnings: exit 0. Refs #3 #30
There was a problem hiding this comment.
Sorry @macanderson, your pull request is larger than the review limit of 150000 diff characters
Rounds out two sweeps that were left partial.
1. Dangling section anchors (a regression from the previous commit).
Repointing the code comments off the private-repo spec stripped the
filename but left ~25 orphan `§3.2`-style anchors pointing at
subsections SPEC.md does not have. Each is now mapped to the section
that actually carries the statement:
§3.1 -> §2 (transport) §3.2 -> §3 (handshake)
§3.3 -> §5 (query) §3.4 -> §6 (frames)
§3.6 -> §11 (conformance)
§3.5 covered two unrelated rules in the old document, so it is
disambiguated by context: consent/egress -> §4, crash/malformed ->
§10. Where it referred to reference-host environment scrubbing — which
has no normative section and should not pretend to — the anchor is
dropped rather than pointed somewhere plausible.
Two citations the mechanical pass had mangled are rewritten by hand:
`host.rs`'s header and a stella-internal reference in `stdio.rs` that
had become a SPEC.md anchor for a rule SPEC.md never made.
2. Rename grammar artifacts (#21). "an Context Graph Protocol ..." in 18
places across docs, crate READMEs, rustdoc, and the site content, left
by the mechanical OCP -> Context Graph Protocol replacement in d6768a8.
Now "a CGP ...", matching the abbreviation convention introduced in
protocol-advantages.md.
Verified:
cargo test --workspace 11 suites ok
cargo clippy -- -D warnings exit 0
cargo fmt --all -- --check exit 0
conformance-green.sh 5/5 checks pass
conformance-red.sh 11/11 misbehave modes caught
schema/validate-examples.py all examples validate
rg "§[0-9]+\.[0-9]+" no dangling anchors remain
rg "an Context Graph Protocol" no hits remain
Refs #3 #21
macanderson
marked this pull request as ready for review
July 21, 2026 19:46
There was a problem hiding this comment.
Sorry @macanderson, your pull request is larger than the review limit of 150000 diff characters
Signed-off-by: Mac Anderson <mac@oxagen.sh>
This was referenced Jul 21, 2026
Resolves the 12 conflict hunks across 7 files. Every conflict was additive — main's consent/egress-scope track (#35, #36, #37) versus this branch's normative sweep — so the resolution is a union in each case, with main's newer normative wording kept where both sides described the same rule. Resolutions: - contextgraph-types/src/lib.rs — union of both module sets and re-exports. - contextgraph-conformance/src/lib.rs — keeps this branch's check_budget (§B1/§B3/§B4, strictly stronger than main's inline respects_budget) and adds main's check_consent_scopes; the runner already calls both. - contextgraph-example-docs — union of imports and of the misbehave modes, so scope-lie joins the branch's eleven. - schema — main's egress_scopes + EgressScope $def, rewritten in the expanded JSON style this branch normalized the file to. - host.rs / host consent.rs / reference-messages.json — main's newer text. Also repairs breakage that predates this merge: the previous merge commit (10808ed) silently dropped ContextFrame::identity() while keeping the content_digest field it feeds, so PR 33's head did not compile against its own tree. identity() is restored and the reference provider now declares a content_digest. rustfmt applied to main's frame_representation_witness.rs, which arrived unformatted. Verified: cargo check/clippy -D warnings clean, fmt clean, schema examples validate, conformance green 6/6 and red 12/12 misbehave modes caught, cargo test 136 passing. Known-red, pre-existing on main and not addressed here: frame_representation_witness::reference_frame_without_inline_content_deserializes fails identically on origin/main — ContextFrame.content is a required String, so a content-less reference frame cannot deserialize. Making it optional is a normative decision that interacts with §B3 canonical token accounting, so it is left for the maintainer.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…est` property while keeping `additionalProperties: false`, so any frame carrying a content digest fails schema validation.
This commit fixes the issue reported at schema/contextgraph-envelope.schema.json:372
## Bug
The `$defs.ContextFrame` definition in `schema/contextgraph-envelope.schema.json` lists these properties:
`id, kind, title, content, uri, score, token_cost, valid_from, valid_to, recorded_at, provenance, citation_label, embedding, relations`
but the Rust wire type `ContextFrame` (`contextgraph-types/src/frame.rs:153`) defines a `content_digest` field between `content` and `uri`:
```rust
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_digest: Option<String>,
```
Because the schema retains `"additionalProperties": false`, any frame whose serialized JSON includes the `content_digest` key is **rejected** by schema validation. The reference provider (`contextgraph-conformance/src/bin/contextgraph-example-docs.rs:302`) always emits the field, so its `frames` envelope produces wire JSON that fails schema validation — schema/Rust contract drift.
CI misses it because `schema/validate-examples.py` only checks fixtures that happen to omit `content_digest` (dropped by `skip_serializing_if`), and the `examples_roundtrip` Rust test only checks Rust↔examples, never schema↔Rust.
## Fix
Re-add the `content_digest` property to the `ContextFrame` schema.
**Important:** the digest must be treated as **opaque**, matching the pre-PR schema and the Rust semantics. `content_digest` is *never* validated by `is_well_formed_digest` — only `Provenance.digest` is (`frame.rs:55`). The field is forwarded verbatim to `FrameId::new` (`frame.rs:188`) and the docs (`identity.rs:11-18`) state a provider "picks" the scheme. A strict `^sha256:[0-9a-f]{64}$` pattern would be wrong: it would reject legitimate opaque digests (a different algorithm) *and* even the Rust code's own test value `"sha256:abc"` (`frame.rs:258`), introducing a new false rejection.
Therefore the property is restored with the **original** constraint: `"type": ["string","null"]` with `"minLength": 1` and **no** `pattern`, plus a `$comment` describing it as an opaque provider-declared digest and the third component of `FrameId`. Not listed in `required` (matching `Option<String>`).
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: macanderson <mac@macanderson.com>
This was referenced Jul 22, 2026
test(types): defer the frame-representations witness so the pre-freeze sweep can land `frame_representation_witness::reference_frame_without_inline_content_deserializes` is a red TDD witness for the not-yet-built frame-representations feature (reference frames that omit inline `content` and carry `content_ref` + `canonical_content_hash`). It has been failing on `main` and is the sole red check on #33. Implementing it is a normative wire change (`content` becomes optional; new fields) that needs its own proposal + ADR per GOVERNANCE.md — out of scope for #33's issue-closing sweep. Mark the witness `#[ignore]` (kept, not deleted, as executable documentation of the intended shape) so fmt/clippy/test go green and #33 — which closes #3 and the rest of the pre-freeze track — can merge. The legacy-compat witness stays active; un-ignore when the feature lands.
macanderson
added a commit
that referenced
this pull request
Jul 22, 2026
…phase 2) (#41) Extend ContextFrame so a frame states *how* it carries its content, per the CGEP lifecycle build prompt §"ContextFrame representations". Additive and backward-compatible: `representation` absent ⇒ `full`, and full/legacy frames are byte-identical on the wire. Satisfies frame_representation_witness.rs (both cases). Types (contextgraph-types): - Representation {full|compact|reference}, ContentFidelity, ContentRef, Transform, InlineContentRequirement. - ContextFrame: `content` → Option (absent for references); adds representation, content_fidelity, canonical_content_hash, content_ref, transform, minimum_content_fidelity, inline_content_requirement, canonical_token_cost, tokenizer_ref. `content_digest` kept as the inline-content hash (the spec's content_hash, which feeds FrameId); canonical_content_hash is the full-source hash. `full`/`reference` constructors + representation_invariants(). - Negotiation: ContextQuery.representation_preferences (+ select_representation); Capabilities.representations + resolve (+ representations_consistent). token_cost kept required (u32); canonical_token_cost/tokenizer_ref added additively. Making token_cost optional reopens PR #33's B3 budget-accounting decision on the same field, so it is deferred to that reconciliation. Wire + docs deliverables: - JSON Schema: new $defs + per-representation invariants (allOf if/then); `content` no longer globally required. - examples/reference-messages.json: representation-capable handshake_ack, a query with representation_preferences, and compact + reference frames (all validate). - docs/protocol-surface.md representation section; docs/adr/0005; CHANGELOG. Also unbreaks `main`: contextgraph-host and -conformance did not compile from a half-applied #37 merge (missing ConsentReceipt/EgressScope/FrameId/DropReason imports, a DataFlow literal missing egress_scopes, a CHECK_VERIFY_HONESTY test import, and a stale check-count assertion). Fixed so the workspace builds and the full suite passes. Co-authored-by: macanderson <ops@oxagen.sh>
Reconcile the pre-freeze normative sweep (#33) with frame representations (#41, full/compact/reference) and context/verify (#38), which landed on main after this branch forked. Key reconciliations: - Capabilities: keep #33's ADR-0004 removal of upsert/subscribe/filters and its `correlation` field; add #41's `verify`/`representations`/`resolve`. - ContextFrame: adopt main's optional `content` + representation model (representation/content_ref/canonical_content_hash/…); rename #33's `canonical_token_cost()` method to `expected_inline_token_cost()` to free the name for #41's `canonical_token_cost` field, and make it handle absent content. - wire::Envelope: keep #33's correlation `id` on query/frames and structured `error` (id/code) AND #41's Verify/Verified variants. - Conformance fixture: keep #33's strict frame-building (valid F5 digests, F4 timestamps) so its enhanced checks pass, grafted with main's verify (verify_honestly/current_digest derived from the fixture's own digests). - schema/examples: union of #33's stricter validations (timestamp/embedding patterns) and #41's representation $defs; drop the removed capability fields. Verified green: cargo build/clippy -D warnings/test, schema validate-examples, and conformance-red.sh (all 14 misbehave modes caught, integrating both #33's and #41's). The frame-representations witness (previously #[ignore]) now passes against main's implementation.
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.
Addresses the pre-freeze normative track of the roadmap (#22). The organizing
idea is the roadmap's own: close every gap where the repo claims more than it
enforces, because each one is a place the project's critique of the blob-pipe
applies to itself.
Every issue in the backlog has a disposition below — implemented, decided, or
explicitly deferred with a reason. Nothing was silently skipped.
Landed
-D warnings, test, MSRV (read fromCargo.toml), conformance green and--misbehavered, schema validation, advisory site buildSPEC.md— self-contained normative spec with stable anchors; 17 files repointed off the private-repo citationsid+Capabilities.correlation; the JSON-RPC claim withdrawn (ADR 0002)upsert,subscribe,filtersdropped; design sketches committed (ADR 0004)frame::rel, graph frame payload rule,display_namecheck (G1)token_costMUST equalceil(utf8_len/4)(B3) — ADR 0003ErrorCodevocabulary with host-reaction guidance; provider repliesbad_requestmax_framesaudit (B4), RFC 3339 UTC profile (F4)sha256:<64 lowercase hex>grammar + digested-bytes definition (F5)MIGRATION.mdincl. the GitHub redirect hazardTwo things worth review attention
1. A design in ADR 0002 was wrong, and the test caught it. The ADR originally
specified correlation "negotiated by observation" — no capability flag. Writing
the conformance check proved that unworkable: a reply with no
idis ambiguousbetween doesn't implement correlation and implements it wrong, so the
guarantee was uncheckable.
Capabilities.correlationresolves it. The ADRrecords the amendment and the reasoning rather than quietly changing.
2.
DataFlow.writesis kept, against #5's proposal.Capabilitiessayswhat you may call on me;
DataFlowsays what I do with your data. Those comeapart — a provider with no write method still persists if it indexes queries, and
a consenting user deserves to know. Removing it would have narrowed the consent
surface as a side effect of tidying an unrelated flag.
Breaking changes (pre-freeze, permitted by
docs/stability.md)Capabilities.upsert/.subscribe,QueryCapability.filtersremoved.Wire-compatible — serde ignores unknown fields, and there's a test for that
claim. Rust-API breaking:
stellaneeds threefilters: Vec::new(),linesdeleted.
token_costmust now equal the canonical count. This turns previously-greenunder-declaring providers red, which is the point.
Verification
mainwas not rustfmt-clean before this branch (the rename ind6768a8pushed lines past the width limit), so #2's CI would have gone red on arrival.
Fixed here.
The schema now rejects the pre-change examples — that rejection is the drift
detection #2 asked for.
Not in this PR
feat/composition-usage-consent-verify(identity, compose determinism, usage,consent, verify). Deliberately not duplicated.
SPEC.md§10.1,which names the host-binding rules as known unchecked, but not built.
Downstream canary CI: build stella and the oxagen copy against HEAD to catch breaking changes before the freeze #29, Specify frame-item attribution hooks so hosts can evaluate retrieval quality #31 — not started. Subagent capacity was lost to an account session
limit partway through; these are the ecosystem tier and each is its own body of
work.
real bytes needs host-side re-reading and a sha256 dependency.
$idpoints at an unregistered domain.dig +short context-graph-protocol.orgreturns nothing andcurlcannot resolve it, sohttps://context-graph-protocol.org/schema/…is not a fetchable identifier.The issue asks to confirm the domain is "registered or intended" — I can
prove the first is false, but only you can answer the second, and repointing
it is cheap either way.
.stella/is tracked (~2.5 MB of SQLite + WAL files). Untracking it iswhat the issue suggests, but a concurrent session is actively writing that
directory in a sibling worktree, so I did not unilaterally
git rm --cachedtracked binary state that something else is using.
Pre-existing breakage found (not caused here): the
site/build fails withCannot find module 'mdx/types'. That is why the site CI job iscontinue-on-error: truerather than a hard gate — shipping it red by defaultwould have trained everyone to ignore CI on day one. Worth its own issue.
Issues are referenced, not auto-closed — several are partially addressed and the
maintainer should judge each against its own checkboxes.