Skip to content

feat(schema): name the schemas' identity on a branded, family-versioned URL - #109

Merged
macanderson merged 1 commit into
mainfrom
fix/schema-id-brand-b726a4bc
Aug 30, 2026
Merged

feat(schema): name the schemas' identity on a branded, family-versioned URL#109
macanderson merged 1 commit into
mainfrom
fix/schema-id-brand-b726a4bc

Conversation

@macanderson

@macanderson macanderson commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Pull request

Summary

Both JSON Schemas' $id moves from raw.githubusercontent.com/…/main/schema/…
to https://contextgraphprotocol.org/schema/v1/… — the protocol's own domain,
versioned by major protocol family. $id is what validators resolve and
third parties quote, so this is protocol-visible; nothing is asked of
implementers.

Closes #79, Closes #58

The decision, and why

#79 asks for one call to be made and recorded: is the branded $id versioned,
or does it track main the way the current one does? Versioned, by major
family.
ADR 0013
carries the reasoning:

  • main is a git branch — unbounded change under a stable name. A 1.x
    additive minor already changes what a cached resolver sees. The protocol has
    no concept of main; that segment names where the bytes happen to live.
  • Per-minor paths (/schema/1.2/) are the wrong granularity and are
    rejected. Within contextgraph/1 evolution is additive-only (GOVERNANCE.md,
    SPEC §13 U1–U4), so a consumer holding an older copy is never wrong, only
    less complete — the staleness a per-minor path guards against cannot hurt.
    Against that it would cost a new published path and a fresh $id every
    minor, with every pin going stale by design.
  • Family is the notch where compatibility actually changes, and it is the
    axis the protocol already versions on — the URL now agrees with
    versions_compatible, which compares exactly this prefix.

v1 means contextgraph/1, not the crate version — which is already 2.x
against that same wire (docs/stability.md). The ADR says so explicitly, because
that is the misreading waiting to happen.

contextgraph/2 would be /schema/v2/, and /schema/v1/ would keep answering.

ADR 0008 permits this rather than resisting it

Its rule (1) — advertise only on a host this repo serves — is unchanged. #78
made the apex such a host for /schema/. Only 0008's conclusion for the
schemas ("GitHub-raw is the only host it can honestly advertise", called
permanent) expires, and it is amended in place rather than left to contradict
what ships.

What changed

  • schema/*.schema.json — the two $id values. The files do not move;
    that is what keeps the raw URL resolving.
  • .github/workflows/publish-spec.yml — publishes /schema/v1/, and its
    post-publish step now dereferences each $id and asserts the served body
    reports that same $id
    . The old step accepted any 200 whose body had a
    .title, which would happily accept a stale object at an alias path. Header
    comment rewritten: it said the publisher deliberately does not move identity.
  • schema/validate-examples.py — pins the new $ids via one shared
    constant. Stays offline; see the ordering note below.
  • .github/scripts/check-deploy-hygiene.py/schema/v1/ prefix row,
    longest-prefix matching, and a URL pattern that admits a version segment.
  • Docs — ADR 0013 (new), ADR 0008 (amended), SPEC.md §13.1 (new),
    PUBLISHING.md, MIGRATION.md §6, CHANGELOG.md, CONTRIBUTING.md,
    docs/GUIDE.md, and the lifecycle profile's normative LF2.

Two guard bugs found on the way

  1. check-deploy-hygiene.py was about to go silently blind. Its URL
    pattern required the filename to sit directly under schema/, so it matched
    no versioned URL — the two $ids it exists to police would have produced
    no offender check and no missing-artifact check, and a green run. Proven by
    diffing old and new match sets across every tracked file: 0 lost, +2
    gained
    , the two $ids.
  2. Prefix matching was first-hit, not longest. /schema/ and /schema/v1/
    nest and map to different repo paths, so the old order would have resolved
    the versioned URL against the wrong row and looked for a schema/v1/ that
    deliberately does not exist.

Three docs that stated the opposite of what ships

CONTRIBUTING.md still said a contextgraphprotocol.org/schema/… URL 404s
(stale since #78); validate-examples.py's docstring claimed it checked "a
byte-identical served copy" when the code is a pure offline string compare, no
network anywhere; ADR 0008 cited a SERVED_HOSTS that #78 replaced with a
prefix map. All three fixed here.

The ordering hazard, stated plainly

https://contextgraphprotocol.org/schema/v1/… 404s right now — this PR
creates it. The fix is the publisher, not a weakened validator:

  • The validate job (the only one that runs on a PR) runs
    validate-examples.py, which is offline and passes.
  • The publish job is if: github.event_name != 'pull_request'. On merge it
    syncs /schema/v1/, invalidates the CDN, then dereferences. Order is
    enforced by step order.

So the identity cannot be live before the merge, and cannot be dead after one
without failing the job. The offline validator was deliberately not made to
fetch: it runs on every PR and fork, against commits whose publish has not
happened, so a fetch there trades a real guarantee for a flaky one.

Evidence

$ curl -sS https://contextgraphprotocol.org/schema/contextgraph-envelope.schema.json | jq -e '.["$id"]'
"https://raw.githubusercontent.com/.../main/schema/contextgraph-envelope.schema.json"   # pre-merge: old bytes
$ curl -sS https://contextgraphprotocol.org/schema/contextgraph-lifecycle-record.schema.json | jq -e '.["$id"]'
"https://raw.githubusercontent.com/.../main/schema/contextgraph-lifecycle-record.schema.json"

$ curl -o /dev/null -w '%{http_code}' <both raw URLs>        ->  200, 200
$ python3 schema/validate-examples.py                        ->  rc=0, all PASS
$ python3 .github/scripts/check-deploy-hygiene.py            ->  rc=0, 4/4 PASS

Witness (fails on the old value, passes on the new): reverting only the two
$id strings makes validate-examples.py report
2 failure(s)FAIL $id is https://contextgraphprotocol.org/schema/v1/…
for both schemas; restoring them returns rc=0.

Negative test of the hygiene guard: a probe line carrying
…/schema/v1/does-not-exist.schema.json and
https://cgp.oxagen.sh/schema/v9/… makes both halves fail (rc=1) — so the
guard sees versioned URLs now, rather than passing vacuously.

$ref coherence: jq over both schemas returns only #/$defs/… — every
$ref is a same-document pointer and neither schema references the other,
so $id has no bearing on resolution. Both validate fully offline. No Rust,
Python, TS or Go source in this repo reads $id or the old URL (checked).

#58 sweep: rg -n 'contextgraphprotocol\.io|context-graph-protocol\.org'
returns two hits, both prose citations of the dead host in the records that
explain why it was wrong (validate-examples.py's comment, ADR 0013's Context).
Not zero, and deliberately so — rewriting them would make the history lie about
itself, the same reasoning check-deploy-hygiene.py already encodes in its
EXEMPT list. No live URL remains. Cargo.toml homepage is correct
(https://contextgraphprotocol.org, fixed in #45); SDK metadata carries no
stale host.

ADR numbering

The ADR is 0013, not 0012. PR #106 was opened first and holds 0012
(0012-sdk-version-pins-share-a-major.md). Because the two filenames differ,
git merges both cleanly and the tree ends up with two ADR 0012s — neither PR's
CI can see it. Numbers are being allocated centrally until a guard exists;
that guard, and the GUIDE's ADR index skipping 0009–0011, are #129.

Which issue closes, and why both

Both. They are one problem seen twice, and each contributes something the other
does not, so closing either alone would strand work.

Checklist

  • One logical change per PR (smaller lands faster)
  • Gate is green locally — see below
  • A witness test is included, or a reason there isn't one is stated below
  • Docs updated in the same PR if behavior or flags changed
  • All commits signed off (git commit -s, DCO)
  • CHANGELOG.md updated under [Unreleased]

On fmt/clippy/test: this change touches no Rust — the diff is two
JSON string values, two Python scripts, one workflow and prose. Per SCR-001 the
Rust suite is CI's job and nothing here could move it. The checks that can
judge this change were run and are quoted above.

On the witness: validate-examples.py's $id pin is the witness, and the
fail→pass flip is demonstrated above rather than asserted. The guard change has
its own negative test and a match-set diff.

Registry submission

  • Not applicable — this PR does not add/change a conformance registry entry

Protocol-stability impact

  • Additive (new optional field/check) — safe within contextgraph/1

Nothing on the wire changes: no field, no type, no PROTOCOL_VERSION. What
changes is the schemas' published identity, which GOVERNANCE.md reaches
through the conformance-requirement clause — the lifecycle profile's normative
LF2 pins that identity, and is updated here. GOVERNANCE.md's process for
such a change is followed: CHANGELOG.md under [Unreleased], and a witness.

Old and new URLs both serve identical bytes and $ref resolution is unchanged,
so a contextgraph/1.0-draft provider, a pinned local copy, and a cached
fetch of the old URL all keep working untouched.

License

By submitting this pull request, I agree to dual-license this contribution
under MIT OR Apache-2.0, as certified by my DCO sign-off.

Summary by Sourcery

Adopt branded, major-family-versioned URLs as the canonical identities for both JSON Schemas while preserving backward-compatible access to existing URLs.

Bug Fixes:

  • Ensure deployment hygiene checks recognize versioned schema URLs and resolve nested published prefixes correctly.
  • Verify after publication that all schema endpoints serve documents with the expected canonical $id.

Enhancements:

  • Move both JSON Schemas to branded, major-family-versioned identities under contextgraphprotocol.org/schema/v1/ while retaining the existing aliases and raw GitHub URLs.
  • Pin schema identities in offline validation and document the identity, compatibility, migration, and publishing guarantees.

CI:

  • Update schema publishing to sync the versioned identity path and validate both versioned and unversioned public endpoints.

Deployment:

  • Publish schemas under /schema/v1/ as the canonical path while preserving existing schema locations.

Documentation:

  • Add ADR 0013 and amend ADR 0008 to record branded, family-versioned schema identity.
  • Update protocol, publishing, contribution, migration, changelog, guide, and lifecycle-profile documentation for the new schema identities.

Tests:

  • Update offline schema validation to enforce the new $id values.

@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've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 16 hours and 7 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR gives both JSON Schemas a stable, branded $id under /schema/v1/, versioned by the contextgraph/1 protocol family rather than the crate or git branch, while preserving legacy URLs and source paths. Publishing, offline validation, live post-deploy verification, repository URL hygiene, normative profile requirements, and documentation are updated to enforce and explain the new identity.

Sequence diagram for post-publish schema identity verification

sequenceDiagram
    participant Workflow as publish-spec.yml
    participant CDN as contextgraphprotocol.org
    participant Schema as Published schema JSON

    Workflow->>CDN: sync schema/*.json to /schema/v1/
    Workflow->>CDN: invalidate CDN
    Workflow->>CDN: dereference schema $id URL
    CDN-->>Workflow: served schema body
    Workflow->>Schema: read $id
    alt served $id matches requested identity
        Workflow-->>Workflow: publish succeeds
    else identity mismatch
        Workflow-->>Workflow: publish fails
    end
Loading

Flow diagram for schema identity validation and URL hygiene

flowchart TD
    Start[Schema validation]
    Offline[validate-examples.py]
    Pin[Compare schema $id with PUBLISHED_SCHEMA_BASE]
    Hygiene[check-deploy-hygiene.py]
    Prefix[Match longest served URL prefix]
    Deploy[Published deployment]
    Live[Dereference each $id and compare served body $id]
    Pass[Checks pass]

    Start --> Offline
    Offline --> Pin
    Pin --> Hygiene
    Hygiene --> Prefix
    Prefix --> Deploy
    Deploy --> Live
    Live --> Pass
Loading

File-Level Changes

Change Details Files
Move both schemas to a branded, protocol-family-versioned canonical identity while preserving existing URLs and schema bytes.
  • Replace both schema $id values with https://contextgraphprotocol.org/schema/v1/..., where v1 denotes the contextgraph/1 wire family.
  • Publish the same source schemas under /schema/v1/ while retaining the unversioned branded alias and raw GitHub URLs.
  • Document the identity decision, compatibility rationale, migration impact, and future family-versioning policy.
  • Update the lifecycle profile's normative LF2 requirement and changelog.
schema/contextgraph-envelope.schema.json
schema/contextgraph-lifecycle-record.schema.json
docs/adr/0012-schema-identity-on-a-branded-versioned-url.md
docs/adr/0008-deploy-topology-and-advertised-urls.md
SPEC.md
MIGRATION.md
PUBLISHING.md
CHANGELOG.md
CONTRIBUTING.md
docs/GUIDE.md
docs/profiles/context-exchange-provider.md
Split schema identity validation into an offline string pin and a post-publish live dereference check.
  • Centralize the expected branded base URL in validate-examples.py and keep PR validation network-independent.
  • After publishing and CDN invalidation, dereference both versioned identities and unversioned aliases and assert each served document reports the canonical $id.
  • Retain the raw source-file locations so former GitHub URLs continue resolving.
schema/validate-examples.py
.github/workflows/publish-spec.yml
Make deploy-hygiene checks correctly recognize and resolve nested versioned artifact URLs.
  • Add the /schema/v1/ served-prefix mapping back to the repository's schema/ source directory.
  • Allow URL patterns to contain intermediate path segments such as schema/v1/.
  • Use longest-prefix matching so /schema/v1/ takes precedence over /schema/.
.github/scripts/check-deploy-hygiene.py

Assessment against linked issues

Issue Objective Addressed Explanation
#58 Ensure the workspace homepage is set to https://contextgraphprotocol.org and no active references use the stale .io or hyphenated .org hosts.
#58 Change both schema $id values to resolvable, repository-published URLs on contextgraphprotocol.org, and ensure those paths serve the matching schemas.
#58 Document the schema identity decision and add validation/deployment checks to prevent stale or non-dereferenceable schema URLs from recurring.
#79 Move both schemas' $id values from the raw GitHub URLs to a project-controlled, versioned branded URL, with the versioning decision documented.
#79 Publish the schemas at the new $id paths and add validation that pins the new identities, verifies the deployed responses report the expected $id, and preserves the old raw GitHub URLs and schema file locations.
#79 Update repository safeguards and documentation—including deployment hygiene checks, ADRs, specification and publishing guidance, migration notes, and the changelog—to describe and enforce the new schema identity without requiring implementer action.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

…ed URL

Both schemas' `$id` moves from
`raw.githubusercontent.com/macanderson/context-graph-protocol/main/schema/…`
to `https://contextgraphprotocol.org/schema/v1/…`.

`$id` is the identity a validator resolves and a third party quotes, so this
is protocol-visible rather than a deployment detail. The old URL had three
defects: a code-hosting domain with no stable-content guarantee, not the
project's own name, and a `main` pin — a git branch, so a `1.x` additive
minor silently changed what a cached resolver saw.

ADR 0013 records the decision and why the version segment is the **major
family** rather than the minor. Within `contextgraph/1` evolution is
additive-only, so a consumer holding an older copy is never wrong, only less
complete; per-minor paths would mint a new identity every minor and buy
nothing. `v1` is `contextgraph/1`, not the crate version, which is already
2.x against that same wire.

ADR 0008's rule — advertise only on a host this repo serves — is what permits
the move rather than resisting it. #78 made the apex such a host for
`/schema/`; only 0008's conclusion for the schemas expires, and it is amended
in place rather than left to contradict what ships.

Nothing is asked of implementers. The bytes are identical, the raw URL keeps
returning 200 (guaranteed by never moving `schema/*.schema.json`), and every
`$ref` in both schemas is a same-document pointer with no cross-schema
reference, so resolution is unchanged offline and online.

Identity is checked in two halves, neither weakened for the other:
`validate-examples.py` pins the string and stays offline, because it runs on
every PR and fork against commits whose publish has not happened;
`publish-spec.yml` publishes the identity path, then dereferences it and
fails unless the served body reports that same `$id`. A 200 alone is not
accepted — a static site answers its 404 page with one.

Fixes a live gap in `check-deploy-hygiene.py` found on the way: its URL
pattern required the filename to sit directly under `schema/`, so it matched
no versioned URL at all and would have gone silently blind to the two `$id`s
it exists to police. Its prefix map also needed longest-match, since
`/schema/` and `/schema/v1/` now nest and resolve to different repo paths.

Also repairs three docs that had come to state the opposite of what ships:
CONTRIBUTING.md still said a `contextgraphprotocol.org/schema/…` URL 404s
(stale since #78), `validate-examples.py`'s docstring claimed a byte-identical
served-copy fetch it never performed, and ADR 0008 cited a `SERVED_HOSTS` that

The ADR is numbered 0013 rather than 0012: PR #106 was opened first and holds
0012, and because the two filenames differ git would have merged both cleanly
into a tree with two ADR 0012s and no CI able to see it. Numbers are being
allocated centrally until a guard exists — that guard, and the GUIDE's ADR
index skipping 0009-0011, are #129.

Closes #79, Closes #58

Signed-off-by: macanderson <mac@oxagen.sh>
@macanderson
macanderson force-pushed the fix/schema-id-brand-b726a4bc branch from 85bc531 to 282fa52 Compare August 30, 2026 04:30
@macanderson
macanderson merged commit 3f16d28 into main Aug 30, 2026
26 checks passed
@macanderson
macanderson deleted the fix/schema-id-brand-b726a4bc branch August 30, 2026 04:39
macanderson added a commit that referenced this pull request Aug 30, 2026
Two doc conflicts, both in tables this branch and main each added a row
to.

docs/GUIDE.md — main's #109 added ADR 0013 plus a comment saying 0009,
0010 and 0011 were missing from the table and tracked in #129. This
branch fills that gap, so the merged table carries 0009 through 0013 and
0017, and the comment goes with the gap it described (#129).

docs/profiles/context-exchange-provider.md §9 — LF1 and LF3 are this
branch's, describing the vectors it publishes and what the suite now
checks; LF2 is main's, where #109 moved the schema $id from a GitHub-raw
URL to the protocol's own branded, family-versioned one.

Signed-off-by: macanderson <mac@oxagen.sh>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant