Skip to content

Pre-freeze normative sweep: SPEC.md, canonical token accounting, conformance that actually catches things#33

Merged
macanderson merged 13 commits into
mainfrom
feat/protocol-backlog-sweep
Jul 22, 2026
Merged

Pre-freeze normative sweep: SPEC.md, canonical token accounting, conformance that actually catches things#33
macanderson merged 13 commits into
mainfrom
feat/protocol-backlog-sweep

Conversation

@macanderson

@macanderson macanderson commented Jul 21, 2026

Copy link
Copy Markdown
Owner

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

Issue What changed
#2 CI: fmt, clippy -D warnings, test, MSRV (read from Cargo.toml), conformance green and --misbehave red, schema validation, advisory site build
#3 SPEC.md — self-contained normative spec with stable anchors; 17 files repointed off the private-repo citations
#4 Envelope id + Capabilities.correlation; the JSON-RPC claim withdrawn (ADR 0002)
#5, #6, #11 upsert, subscribe, filters dropped; design sketches committed (ADR 0004)
#7 Relation vocabulary frame::rel, graph frame payload rule, display_name check (G1)
#8 Canonical token accounting: token_cost MUST equal ceil(utf8_len/4) (B3) — ADR 0003
#9 Open ErrorCode vocabulary with host-reaction guidance; provider replies bad_request
#10 max_frames audit (B4), RFC 3339 UTC profile (F4)
#12 sha256:<64 lowercase hex> grammar + digested-bytes definition (F5)
#21 Grammar/expansion artifacts fixed repo-wide (18 sites); CGP abbreviation convention introduced
#30 MIGRATION.md incl. the GitHub redirect hazard

Two 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 id is ambiguous
between doesn't implement correlation and implements it wrong, so the
guarantee was uncheckable. Capabilities.correlation resolves it. The ADR
records the amendment and the reasoning rather than quietly changing.

2. DataFlow.writes is kept, against #5's proposal. Capabilities says
what you may call on me; DataFlow says what I do with your data. Those come
apart — 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.filters removed.
    Wire-compatible — serde ignores unknown fields, and there's a test for that
    claim. Rust-API breaking: stella needs three filters: Vec::new(), lines
    deleted.
  • token_cost must now equal the canonical count. This turns previously-green
    under-declaring providers red, which is the point.

Verification

cargo test --workspace                              11 suites, all ok
cargo clippy --workspace --all-targets -- -D warnings   exit 0
cargo fmt --all -- --check                          clean
python3 schema/validate-examples.py                 all examples validate
.github/scripts/conformance-green.sh                5/5 checks pass
.github/scripts/conformance-red.sh                  11/11 misbehave modes caught

main was not rustfmt-clean before this branch (the rename in d6768a8
pushed 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

Pre-existing breakage found (not caused here): the site/ build fails with
Cannot find module 'mdx/types'. That is why the site CI job is
continue-on-error: true rather than a hard gate — shipping it red by default
would 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.

…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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
macanderson marked this pull request as ready for review July 21, 2026 19:46

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, your pull request is larger than the review limit of 150000 diff characters

Signed-off-by: Mac Anderson <mac@oxagen.sh>
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.
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
context-graph-protocol Ready Ready Preview, Comment Jul 22, 2026 10:57pm

Comment thread schema/contextgraph-envelope.schema.json
vercel Bot and others added 2 commits July 22, 2026 19:32
…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>
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.
@macanderson
macanderson merged commit 9fb559a into main Jul 22, 2026
11 checks passed
@macanderson
macanderson deleted the feat/protocol-backlog-sweep branch July 22, 2026 23:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant