Skip to content

feat(host): prompt ingestion as a local provider (ADR 0006) - #44

Merged
macanderson merged 3 commits into
mainfrom
worktree-prompt-ingest-provider
Jul 23, 2026
Merged

feat(host): prompt ingestion as a local provider (ADR 0006)#44
macanderson merged 3 commits into
mainfrom
worktree-prompt-ingest-provider

Conversation

@macanderson

@macanderson macanderson commented Jul 23, 2026

Copy link
Copy Markdown
Owner

What & why

The prompt's biggest un-disciplined input is the text a user pastes: a realistic turn mixes a log, a table, a directory reference, and the actual ask under one blob, and only the last is intent. Pasted whole it is re-sent verbatim every turn (no cache, no dedup), its cost is never accounted, nothing is content-addressed, and the model is handed material it must itself decide is mostly irrelevant.

This models the paste as a local, egress-free CGP provider — the ingestion-side dual of compose_context. It's host-side reference behavior; no envelope shape or SPEC.md change, using only the representation fields ADR 0005 added.

  • intentquery.goal, verbatim (never rewritten — the one load-bearing UX guarantee)
  • directory referencesquery.anchors (zero tokens; the graph provider resolves them)
  • evidence (logs, tables, code, notes) → content-addressed frames, served compact by default with the full bytes rehydratable via a [full] re-query

The guarantee is not "zero wasted tokens" (relevance is only knowable downstream) but bounded default cost with lossless retrieval.

Honest by construction

  • token_cost and the inline content_digest are recomputed for the exact representation emitted (§B3) — never carried across a representation flip
  • provenance is derivation, not file, so it doesn't trip §F5 (a paste has no re-readable URI); the real hash lives in canonical_content_hash
  • content-addressed + immutable ⇒ verify is exact (valid / stale / gone), and the same paste dedups to one frame
  • local-only (egress: false, LocalOnly scope) ⇒ auto-permitted, no consent friction

resolve: true vs ADR 0004

Serving compact/reference requires resolve: true (representations_consistent). Unlike the upsert/subscribe flags ADR 0004 deleted, resolve has a live consistency rule and a working, tested in-process rehydration path today (a [full]-preference re-query returns the full bytes from the store). The ADR spells this out; the wire context/resolve method remains a later phase.

Wire conformance — and one corrective schema fix

Verifying "frames are wire-conformant" (not just Rust-invariant-conformant) surfaced a real, pre-existing schema/serializer mismatch: the JSON Schema listed provenance and relations as globally required on a ContextFrame, but the reference serializer omits both when empty (skip_serializing_if = Vec::is_empty) and no frame-validity check requires either. Every ingest frame has no graph edges, so it failed schema validation. Prior schema coverage only validated hand-authored example transcripts — never Rust-serialized frames — so the mismatch sat unexercised.

  • schema: ContextFrame.required is now exactly id/kind/title/score/token_cost (what the serializer always emits); content stays governed per-representation by the existing allOf.
  • Empirically confirmed with jsonschema: all three representations validate against schema/contextgraph-envelope.schema.json; existing examples still pass.

Tests

  • contextgraph-host/src/ingest/tests.rs — segmentation, §B3 honesty across all three representations, representation invariants, SHA-256 known-answer vectors (incl. padding boundaries), content-addressed dedup, query budget/frame caps, verify
  • contextgraph-host/tests/prompt_ingest.rs — end-to-end through the real Host: fan-out, budget audit, byte-stable compose, verify_frames
  • contextgraph-conformance/tests/ingest_conformance.rs — real ingested frames (full/compact/reference) through check_frames + check_budget + representation_invariants + the schema's required-key set + NDJSON envelope round-trip (the guard that catches serialized frames drifting from the schema)

cargo test --workspace, cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --all --check, and python3 schema/validate-examples.py all green. New direct dep: sha2 (host crate only; contextgraph-types stays zero-dep beyond serde).

Not in scope (additive follow-ups)

Stack-trace parsing and richer tabular inference behind the existing classify seam; the context/resolve wire method; making provenance non-required is left as-is (ingest frames always carry it).

Treat the user's paste — the prompt's largest un-disciplined input — as a
local, egress-free CGP provider. Intent passes through verbatim as
query.goal; directory references become query.anchors; pasted evidence
(logs, tables, code, notes) is deterministically segmented, content-
addressed, and served as compact frames with the full bytes rehydratable
via a [full] re-query.

Host-side reference behavior (the ingestion-side dual of compose_context),
so no wire, schema, or SPEC.md change — it uses only the representation
fields ADR 0005 added. Honest by construction: token_cost and the inline
content_digest are recomputed per representation served (B3), provenance is
`derivation` (not `file`, F5-exempt), and verify is exact on immutable
content. resolve:true is backed by a real, tested in-process rehydration
path — the ADR reconciles this against ADR 0004's dead-flag removals.

- contextgraph-host/src/ingest.rs (+ inline tests, sha256 KATs)
- contextgraph-host/tests/prompt_ingest.rs — end-to-end via the real Host
- docs/adr/0006-prompt-ingestion-as-a-local-provider.md
- sha2 dependency (host crate only; types stays zero-dep beyond serde)

@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, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Jul 23, 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 23, 2026 7:35pm

…ingest wire-conformance test

Verifying ADR 0006's "frames are wire-conformant" claim surfaced a real
schema/serializer mismatch: the JSON Schema listed `provenance` and
`relations` as globally required on a ContextFrame, but the reference
serializer omits both when empty (skip_serializing_if=Vec::is_empty) and no
frame-validity check requires either. Every ingest frame has no graph edges,
so it failed schema validation. Prior schema coverage only validated
hand-authored example transcripts, never Rust-serialized frames, so the
mismatch sat unexercised.

- schema: ContextFrame.required is now exactly id/kind/title/score/
  token_cost; content stays governed per-representation by the existing allOf
- contextgraph-conformance/tests/ingest_conformance.rs: validates real
  ingested frames (full/compact/reference) through check_frames, check_budget,
  representation_invariants, and the schema's required-key set + NDJSON
  envelope round-trip — the guard that catches serialized frames drifting
  from the schema
- ADR 0006 + CHANGELOG updated to record the corrective schema fix

Empirically confirmed: all three representations validate against
schema/contextgraph-envelope.schema.json via jsonschema; existing examples
still pass.
@macanderson
macanderson marked this pull request as ready for review July 23, 2026 19:35

@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, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@macanderson

Copy link
Copy Markdown
Owner Author

Filed the remaining follow-ups so nothing is left implicit:

context/resolve is already tracked by #50; I added a comment there noting this PR's IngestProvider as a concrete compact/reference consumer with a working in-process rehydration path (and its per-representation B3 handling), rather than opening a duplicate.

Correction to the PR description's "Not in scope" note: this PR removed both provenance and relations from the frame's required list (the reference serializer omits both when empty), not just relations — ingest frames always carry provenance, but the schema no longer requires it, matching the serializer. #54 covers auditing the same pattern across the other wire types.

macanderson added a commit that referenced this pull request Jul 26, 2026
#53, #55)

Follow-ups to #44 / ADR 0006. All of this is *provider policy* — the wire, the
frame shapes, and the conformance surface are untouched; only what the
prompt-ingest provider chooses to inline changes.

**#53 — segmentation and distillation**

- Stack traces are a first-class segment kind with their own distiller, instead
  of folding into the generic log heuristic. Keeping the exception line plus the
  top N frames and eliding the tail beats the log distiller for the single most
  common paste there is.
- Tabular ingestion handles CSV and whitespace-aligned columns, not just pipe
  and tab, and `infer_column_type` recognizes currency, percent, and nullable
  columns — so a distilled table says what its columns *are*.
- Common log timestamps (`2026-07-20 18:00:01,250`, syslog, bracketed) are
  parsed and normalized to the F4 profile, so the temporal window actually
  populates. Guarded: a shape that cannot be normalized yields no timestamp
  rather than a non-F4 string.
- Runs of identical log lines collapse before salient-line selection, so a
  retry storm cannot crowd out the rest of the log.
- Classification-precedence edge cases documented as known limits.

**#55 — docs and a runnable example**

- `docs/prompt-ingestion.md`: paste → intent + anchors + content-addressed
  evidence, compact-by-default with `[full]` on demand, and how it composes
  byte-stably.
- `contextgraph-host/examples/ingest_paste.rs`: a real end-to-end run. On a
  2288-token paste it segments five attachments, serves four frames for 610
  tokens (27% of raw), re-composes byte-identically, and pulls `[full]` on the
  top-ranked frame with a matching content digest.

Verified: fmt, clippy -D warnings, and all 22 test suites green; conformance
12/12; every misbehave mode caught; host-conformance, schema, and the docs-site
witness green. The example was run, not just compiled.
macanderson added a commit that referenced this pull request Jul 26, 2026
…ile closing them (#63)

* fix(schema): make `required` match the reference serializer, and prove it (#54)

`ContextQuery.kinds` and `.anchors` were globally `required` in the schema but
`skip_serializing_if = "Vec::is_empty"` in the reference type. An unfiltered,
unanchored query — the most common query there is, and precisely the one this
suite's own `sample_query()` sends — therefore serialized to JSON the schema
rejected. Same class of bug ADR 0006 hit on `ContextFrame`; fixing that one
frame left the class alive.

It survived because schema validation only ever ran over hand-authored
transcripts. Curated examples prove the schema accepts what a human wrote; they
can never catch the schema disagreeing with the serializer.

The structural fix:

- `schema/reference-vectors.ndjson` — every envelope variant serialized from
  the reference Rust types, in both minimal form (empty vecs, None options,
  where omission bugs live) and maximal form.
- `tests/reference_vectors.rs` generates them, fails on drift naming the
  drifted vector (regenerate: REGENERATE_VECTORS=1), asserts each round-trips
  through the wire decoder, and asserts every frame is *conformant* — B3 costs,
  F5 digests, representation invariants — so the file doubles as an attested
  vector set downstreams can diff against (#52).
- `schema/validate-examples.py` validates every vector, so CI's existing schema
  job covers serialized reference output, not only curated fixtures.

Verified red-then-green: restoring the old `required` list fails vector line 4
(query/minimal-unfiltered) while every hand-authored example still passes —
which is exactly why the bug lived this long.

* docs: canonicalize the domain, make `$id` dereferenceable, finish the rename (#58, #21, #56)

#58 — the schema's `$id` was `https://context-graph-protocol.org/...`, a
hyphenated host that was never registered: it returned a DNS failure, so every
third party that tried to resolve the schema's public identity got nothing.
Repointed at the live `contextgraphprotocol.org`, and `workspace.homepage` now
names the site rather than the repo.

An `$id` that 404s is barely better than one that never resolves, so the site
actually serves the file from `site/public/schema/`. A served copy can drift
from the source of truth — worse than a 404, because a stale schema that still
resolves silently validates the wrong thing — so `validate-examples.py` asserts
the copy is byte-identical and that `$id` matches the URL it is served from.
Verified red-then-green by mutating the copy.

#21 — residue the mechanical OCP→Context Graph Protocol rename left behind:
"An Context Graph Protocol host" (twice) and "Context Graph Protocol protocol
version" (three places, the doubled noun the old acronym hid). MIGRATION.md's
`ocp-*` references are deliberately kept: naming the old crates is the entire
job of a migration guide.

#56 — removed docs/future/context-receipt-impact-trace-plan.md. Verified before
deleting: it is a completed agentic implementation plan (embedded
"REQUIRED SUB-SKILL" directive, `- [ ]` task tracking), it names a stack this
repo does not use (Vinext/Vite, Cloudflare Workers vs. the actual
Next.js/Fumadocs site), the feature it plans already shipped
(site/src/app/_components/ContextReceiptDemo.tsx + site/tests/context-receipt.test.mjs),
and nothing links to it. Its sibling *design* doc is referenced from
docs/sketches/host-trace.md and stays.

* conformance: enforce the guarantees the suite only claimed to check (#51)

Requiring each `--misbehave` mode to be caught by the check it *declares* it
trips — instead of by any check at all — immediately found a hole, which was
the point of tightening it.

**§H4 correlation was unenforced.** `drop-correlation-id` claimed to trip a
`correlation` check that did not exist. It "went red" only because dropping the
id desynchronizes the host's demultiplexer and breaks every other check
downstream. So an external implementation — each of the three SDKs — could
declare `correlation: true`, never echo an id, and pass the suite. Added a
raw-stdio `correlation` probe that asserts the echo verbatim.

**§Q1 `kinds` was a filter nothing honored.** `sample_query()` sends
`kinds: []`, so every provider was only ever asked the unfiltered question — and
not one of the four reference implementations honored the filter at all. They
declared `capabilities.query.kinds` and returned whatever they had. That is
ADR 0004's dead-capability surface in its subtler form: not an unreachable
field, but a reachable one that silently does nothing.

Specified rather than dropped (SPEC §5 Q1 + §5.1), because `unsupported_kind`
already exists to answer "you asked for kinds I don't serve", which presupposes
the filter binds. A host that narrows to `["snippet"]` and silently receives
`doc` frames has had its budget spent on content it explicitly excluded.
Reference provider now filters; new `kinds-filter` check and `ignore-kinds`
misbehave mode. The fixture's second frame became a `snippet` on purpose — with
every frame one kind the check passed vacuously, which is the decorative-check
sin this round is removing, not adding.

**`granted_at` was never stamped.** Every consent decision entered the audit
ledger with no time on it, because the host had no way to spell the current
instant. Added `format_protocol_timestamp` to contextgraph-types —
dependency-free Gregorian arithmetic, since every implementer in every language
ports this crate and "pull in chrono" is a cost paid by the whole ecosystem.
`ConsentStore::record` stamps on insertion; a caller-supplied instant is
preserved (replaying a persisted decision must not rewrite the trail to "now"),
and a non-F4 instant is refused rather than stored.

**The docs-site witness never ran.** It passed locally and executed nowhere,
making it decoration rather than a gate. Now a step in the `site` job.

Verified: suite 11/11 green; every misbehave mode caught by its declared check;
all three SDK example providers (TS, Python, Go) still 11/11 conformant.

* fix(compose): frame content can no longer break out of the fence that quotes it (#15)

R3 says a host MUST treat frame `content` as untrusted data, delimited as
quoted material and never executed as instructions. `compose_context` performed
the concatenation that defeats it: `content`, `id`, and the citation label were
interpolated into the `<frame …>` fence with no escaping.

So a provider could serve content containing `</frame>` and every byte after it
landed *outside* the quoted block, read at the host's own level — retrieved text
promoted to instruction, which is the exact failure R3 exists to prevent.
A `"` in a citation label was the same breakout through a field nobody thinks of
as content. SPEC §11.1 already listed this as a known gap; this closes it.

Escaping rather than an unguessable fence, deliberately. A random
per-composition delimiter is the other standard answer and it is wrong *here*:
this module's entire purpose is a byte-stable prompt prefix, so a nonce that
changed per turn would bust the provider prompt cache it exists to protect —
paying a real, measured cost for a guarantee escaping already provides.

Only the delimiter is touched. Escaping `<`/`>` wholesale would mangle the code
and markup frame content most often *is*, degrading every honest frame to harden
against a rare one. Content is neutralized, never dropped: `<\/frame>` stays
legible to whoever reads the prompt.

Tests cover the closing-fence breakout, a forged sibling frame, an attribute
breakout via a quoted citation label, that ordinary markup and generics survive
untouched, and that escaping stays deterministic.

* graph: make anchoring decidable and witness it (#7, #15 follow-through)

The protocol is named for the graph, and the graph was its least exercised
surface. The reference fixture declared `graph: false` and served every frame
with `relations: vec![]`, so G1 (labelled edges) and G2 (non-empty target URIs)
passed vacuously — there were no edges to validate — and G3's anchor boost was
never witnessed at all.

G3 also never said what an anchor is compared *against*. Two conformant
providers could reasonably match anchors against the frame `uri`, against
`relations[].target_uri`, or against neither, and no test could tell graph
traversal apart from ignoring `anchors` entirely.

- **SPEC §G4 + §8.2**: a frame is *anchored* when its own `uri` equals an anchor
  (zero hops) or any `relations[].target_uri` does (one hop). A graph-declaring
  provider given anchors MUST return an anchored frame when it has one, and
  SHOULD rank anchored first. A floor on what must be found, not a ceiling on
  how hard a provider may look — deeper traversal stays provider-private.
- **Fixture**: now genuinely graph-capable — real `doc.documents` edges, and it
  honors anchors by stable-partitioning anchored frames first (stable, so
  composition stays deterministic).
- **`anchor-relevance` check**: asks an unanchored question first to discover a
  URI the provider actually serves, then re-asks anchored on it. Deriving the
  anchor from the provider's own output is what keeps it fair — the suite never
  invents a URI and demands the provider know it. It prefers a *relation
  target*, so passing requires traversing an edge rather than string-matching a
  frame's own uri. G3's ranking half is a SHOULD, so it is reported, not
  enforced.
- **`ignore-anchors`** misbehave mode, caught by the matching check.
- **R3 host check** now asserts breakout-resistance rather than carrying a note
  that compose_context lacked it — that note went stale when #15 landed.

Suite is 12/12 green; every misbehave mode caught by its declared check. The
three SDK example providers declare `graph: false`, so the check skips for them
and they remain conformant.

* attribution: specify how a frame's effect is evaluated (#31)

The Context Frame spec's sixth question — "why was each item included, and can
its effect be evaluated later?" — had provenance for the first half and nothing
for the second. A host could say a frame cost 42 tokens and came from
retry-policy.md, and nothing about whether including it helped. Not theoretical:
a host in this ecosystem already A/B-suppresses recall to measure whether
retrieval earns its budget, entirely outside the protocol, because the protocol
gave it no vocabulary to say so.

**The id is not new (A1).** `FrameId` is already what composition, dedup, usage
reports, and `verify` key on. A second attribution id would be free to disagree
with the first, and a disagreement between "the frame that was billed" and "the
frame that was cited" is the confusion attribution exists to remove.

**Three booleans, not a score (A2).** `selected` / `rendered` / `cited` — ADR
0007's `context_use` vocabulary — because they are separately observable and
collapse badly. The case that matters is `rendered` but not `cited`: tokens
paid, content read, nothing changed. A used/unused flag cannot express it and a
0–1 usefulness score would invent precision nobody measured. `cited` means the
output referred to the frame — observable — never that the frame *influenced*
it, which is not.

**Reconcilable (A3).** Records must be coherent and name a frame the paired
usage report actually billed; attribution that cannot be walked back to the bill
is not auditable, and is the shape a mis-keyed identity takes.

`AttributionReport` pairs the outcome records with `UsageReport`, so cost and
outcome finally sit together — value-per-token, which #31 and #8 each supply
half of.

**Deliberately not on the wire.** No `context/feedback`, no
`Capabilities.feedback`. Shipping a negotiated method with no provider consuming
it and no check able to witness it would recreate exactly the dead capability
surface ADR 0004 removed and §Q1 had to be written to repair — days before a
freeze, when the asymmetry runs the other way: adding the method later is
family-safe, removing a dead one is not. The vocabulary ships now because it
must be shared for scores to compare; the transport is sketched in
docs/sketches/attribution-feedback.md with its open questions (privacy of
`cited` for an egress provider, batching, what a conformance check could even
assert).

* ingest: deepen segmentation and distillation, and make it demonstrable (#53, #55)

Follow-ups to #44 / ADR 0006. All of this is *provider policy* — the wire, the
frame shapes, and the conformance surface are untouched; only what the
prompt-ingest provider chooses to inline changes.

**#53 — segmentation and distillation**

- Stack traces are a first-class segment kind with their own distiller, instead
  of folding into the generic log heuristic. Keeping the exception line plus the
  top N frames and eliding the tail beats the log distiller for the single most
  common paste there is.
- Tabular ingestion handles CSV and whitespace-aligned columns, not just pipe
  and tab, and `infer_column_type` recognizes currency, percent, and nullable
  columns — so a distilled table says what its columns *are*.
- Common log timestamps (`2026-07-20 18:00:01,250`, syslog, bracketed) are
  parsed and normalized to the F4 profile, so the temporal window actually
  populates. Guarded: a shape that cannot be normalized yields no timestamp
  rather than a non-F4 string.
- Runs of identical log lines collapse before salient-line selection, so a
  retry storm cannot crowd out the rest of the log.
- Classification-precedence edge cases documented as known limits.

**#55 — docs and a runnable example**

- `docs/prompt-ingestion.md`: paste → intent + anchors + content-addressed
  evidence, compact-by-default with `[full]` on demand, and how it composes
  byte-stably.
- `contextgraph-host/examples/ingest_paste.rs`: a real end-to-end run. On a
  2288-token paste it segments five attachments, serves four frames for 610
  tokens (27% of raw), re-composes byte-identically, and pulls `[full]` on the
  top-ranked frame with a matching content digest.

Verified: fmt, clippy -D warnings, and all 22 test suites green; conformance
12/12; every misbehave mode caught; host-conformance, schema, and the docs-site
witness green. The example was run, not just compiled.

* fix(clippy): use slice::contains in the anchor predicate, and link the ingest page

Two follow-ups.

`cargo clippy --workspace --all-targets -- -D warnings` was failing on
`manual_contains` in the §G4 anchor predicate I added in a9a5348. My earlier
"clippy clean" reading was a **cached** result — the crate had not been re-linted
after the edit, so clippy emitted no diagnostics and I misread silence as
success. Re-verified here from a clean build: exit 0.

Also links docs/prompt-ingestion.md from the docs index. It is deliberately not
added to site/content/docs/meta.json: the site mirrors a hand-maintained subset
of docs/ (context-reuse.md is likewise absent), and listing a page with no mdx
copy would fail the docs-site build, which is now a hard gate.

* sdk: make the three example providers graph-capable (#7 follow-through)

Adding the `anchor-relevance` check broke all three SDK CI jobs, and the way it
broke them is worth recording.

`conformance-external.sh` asserts "every check green, **none skipped**" — a
deliberately stricter bar than SPEC §12's "green for your declared capability
set", because it is how the GOVERNANCE freeze criterion "≥2 independent
implementations pass conformance" is machine-verified. A skip there means the
external implementation did not exercise that surface at all.

The new check correctly *skips* for a `graph: false` provider, and all three
examples declared `graph: false` with `relations: []` — exactly the vacuum the
Rust fixture had. So the honest fix is to make them graph-capable rather than to
relax the gate: weakening "none skipped" would let an SDK silently stop
declaring a capability and still pass.

Each example now declares `graph: true`, carries a labelled `doc.documents`
edge, and ranks anchored frames first (§G4) — Go via `sort.SliceStable`, so
equally-anchored frames keep their relative order and composition stays
deterministic.

All three verified 12/12 with no skips: `go vet` clean, `tsc` builds, and each
runs green under conformance-external.sh.
macanderson added a commit that referenced this pull request Jul 26, 2026
…64)

Three defects, all the same shape: a normative claim nothing checked.

1. SPEC.md §9's `verify` example was invalid against the schema SPEC.md
   ships. Both envelopes carried `"id": "v1"`, but §3.2 grants an `id` only
   to `query`/`frames`/`error`, `Envelope::Verify`/`Verified` have no such
   field, and the schema is `additionalProperties: false`. Removed — verify
   correlates by full frame identity, not by envelope id.

   Root cause: validate-examples.py checked `examples/` but never SPEC.md,
   so the one example surface with no machine check was the one that
   drifted. It now validates every fenced jsonc block in SPEC.md (comments
   and documented placeholders normalized; structure checked), so CI's
   existing `schema` job catches this class. Verified by re-introducing the
   `id`: the harness fails both envelopes and exits non-zero.

2. An ordinary ContextQuery was un-representable in conformant JSON.
   `kinds` and `anchors` were `required` while both are
   skip_serializing_if = "Vec::is_empty", so an unfiltered, unanchored
   query failed validation — the bug PR #44 fixed for ContextFrame, one
   type over. A test now asserts an ordinary query satisfies the schema's
   own `required` array (reads the schema, so it cannot drift), and a
   cross-audit of all 16 shared types confirms no other type demands a
   field its serializer elides.

3. §G2 claimed "Verified by frame-validity" while `target_uri` appeared
   nowhere in the conformance or validation code — the self-attestation
   §11.1 exists to reject. Implemented rather than downgraded: check_frames
   now rejects an edge with an empty target_uri.

   Auditing the other ten rules citing frame-validity found §D1 unenforced
   too: the frame's own content_digest was never format-checked, and the
   evidence string claimed "well-formed digests" while accepting
   `sha256:abc` — the very placeholder validate.rs exists to reject. Also
   fixed; the remaining nine were confirmed enforced.

Both new checks are mutation-tested: disabling either makes its test fail.

Also corrects contextgraph-host::wire, which said correlation is
"negotiated by observation, not by a capability flag" — contradicting
§3.2 and the shipped Capabilities::correlation, and inverting a MUST NOT.

cargo test --workspace: 243 passed, 0 failed. fmt, clippy -D warnings,
validate-examples.py, and conformance-{green,red}/host all pass.

Signed-off-by: Mac Anderson <mac@oxagen.sh>
macanderson added a commit that referenced this pull request Jul 30, 2026
Three defects, all the same shape: a normative claim nothing checked.

1. SPEC.md §9's `verify` example was invalid against the schema SPEC.md
   ships. Both envelopes carried `"id": "v1"`, but §3.2 grants an `id` only
   to `query`/`frames`/`error`, `Envelope::Verify`/`Verified` have no such
   field, and the schema is `additionalProperties: false`. Removed — verify
   correlates by full frame identity, not by envelope id.

   Root cause: validate-examples.py checked `examples/` but never SPEC.md,
   so the one example surface with no machine check was the one that
   drifted. It now validates every fenced jsonc block in SPEC.md (comments
   and documented placeholders normalized; structure checked), so CI's
   existing `schema` job catches this class. Verified by re-introducing the
   `id`: the harness fails both envelopes and exits non-zero.

2. An ordinary ContextQuery was un-representable in conformant JSON.
   `kinds` and `anchors` were `required` while both are
   skip_serializing_if = "Vec::is_empty", so an unfiltered, unanchored
   query failed validation — the bug PR #44 fixed for ContextFrame, one
   type over. A test now asserts an ordinary query satisfies the schema's
   own `required` array (reads the schema, so it cannot drift), and a
   cross-audit of all 16 shared types confirms no other type demands a
   field its serializer elides.

3. §G2 claimed "Verified by frame-validity" while `target_uri` appeared
   nowhere in the conformance or validation code — the self-attestation
   §11.1 exists to reject. Implemented rather than downgraded: check_frames
   now rejects an edge with an empty target_uri.

   Auditing the other ten rules citing frame-validity found §D1 unenforced
   too: the frame's own content_digest was never format-checked, and the
   evidence string claimed "well-formed digests" while accepting
   `sha256:abc` — the very placeholder validate.rs exists to reject. Also
   fixed; the remaining nine were confirmed enforced.

Both new checks are mutation-tested: disabling either makes its test fail.

Also corrects contextgraph-host::wire, which said correlation is
"negotiated by observation, not by a capability flag" — contradicting
§3.2 and the shipped Capabilities::correlation, and inverting a MUST NOT.

cargo test --workspace: 243 passed, 0 failed. fmt, clippy -D warnings,
validate-examples.py, and conformance-{green,red}/host all pass.
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