Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ which. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1
## [Unreleased]

### Added
- **First real crates.io publish** (2026-07-31) — `contextgraph-types`,
`contextgraph-host`, and `contextgraph-conformance` 0.1.0 are live, published
manually in dependency order per [PUBLISHING.md](./PUBLISHING.md) (one-shot
`cargo publish -p … -p … -p …`, rehearsed with `--dry-run` first). Verified
end to end: an external scratch crate resolves both `contextgraph-types` and
`contextgraph-conformance` from the real registry with no path override and
compiles. The README's crates.io/docs.rs badges now resolve (#16's
acceptance signal).
- **First PyPI publish of the Python SDK** (2026-07-31) —
[`contextgraph-sdk` 0.1.0](https://pypi.org/project/contextgraph-sdk/) is
live, published per [sdk/PUBLISHING.md](./sdk/PUBLISHING.md) (`python -m
build`, `twine check`, `twine upload`). Verified against the *published*
package, not the checkout: `pip install contextgraph-sdk` into a scratch
venv, then the example provider run from that venv passes all 13 checks in
`conformance-external.sh` — the #59 acceptance bar. The Go module publish
(a `sdk/go/v0.1.0` tag) remains the one still-unpublished SDK.
- **Conformance registry + provider badge** (`docs/registry.md`,
`assets/badges/conformant.svg`, #20) — a page listing providers that are green on
`contextgraph-conformance`'s suite, each backed by a reproducible
Expand Down Expand Up @@ -233,6 +249,20 @@ which. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1
host. Wire-compatible; Rust API breaking (#5, #6, #11).

### Fixed
- **The three SDK example providers now serve verifiable file provenance.**
The Python, TypeScript, and Go `example-docs` fixtures cited
`file:///docs/…` paths that exist on no machine, with placeholder digests
(`sha256:1111…`) that hash nothing. That was fine until
`provenance-fixture-consistency` began re-reading the bytes every `file`
provenance names: a provider serving no locally-readable provenance is
skipped, and `conformance-external.sh` requires every check green, none
skipped — so all three "sdk is a conformant implementation" CI jobs went
red. Each example now ships the same two fixture files the Rust reference
provider uses, resolves them to absolute `file://` URIs from its own
location, and computes the real sha256 over the on-disk bytes at startup —
the digest a host re-derives when it re-reads the file, so the check passes
end to end (§6.2, §F5) and cannot drift: the digest is computed from the
same bytes the URI names.
- **The conformance badge this repo hands to providers now resolves** (#57,
[ADR 0008](./docs/adr/0008-deploy-topology-and-advertised-urls.md)).
`docs/registry.md` and `docs/implementing-a-provider.md` told every
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
[![contextgraph-host on crates.io](https://img.shields.io/crates/v/contextgraph-host.svg)](https://crates.io/crates/contextgraph-host) [![contextgraph-host docs](https://img.shields.io/docsrs/contextgraph-host)](https://docs.rs/contextgraph-host)
[![contextgraph-conformance on crates.io](https://img.shields.io/crates/v/contextgraph-conformance.svg)](https://crates.io/crates/contextgraph-conformance) [![contextgraph-conformance docs](https://img.shields.io/docsrs/contextgraph-conformance)](https://docs.rs/contextgraph-conformance)

> These badges read "not found" until the crates are actually published
> (tracked by [#16](https://github.com/macanderson/context-graph-protocol/issues/16)) —
> expected today, and the acceptance signal once a real release ships.

https://contextgraphprotocol.org

**The canonical architecture for building context graphs that agents use to reason over.**
Expand Down
9 changes: 6 additions & 3 deletions sdk/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ green on `.github/scripts/conformance-external.sh` — before anything goes out.
| SDK | Registry | Status |
| --- | --- | --- |
| TypeScript | npm, `@contextgraphprotocol/typescript-sdk` | ✅ published (PR #46) |
| Python | PyPI, `contextgraph-sdk` | ⬜ not yet published |
| Python | PyPI, [`contextgraph-sdk`](https://pypi.org/project/contextgraph-sdk/) | ✅ published 0.1.0 (2026-07-31) |
| Go | Go module proxy, `.../sdk/go/contextgraph` | ⬜ not yet published (tag-gated, see below) |

**Nobody has run the PyPI or Go publish steps yet.** This file exists so the
**Nobody has run the Go publish steps yet.** This file exists so the
*first* real publish of each is a checklist, not an improvisation — exactly
the role [`../PUBLISHING.md`](../PUBLISHING.md) plays for the crates.

Expand Down Expand Up @@ -102,7 +102,10 @@ the in-tree copy — still passes conformance by pointing the example provider's
shebang at the scratch venv's interpreter, or simpler, copy
`sdk/python/examples/example_docs.py` into the scratch dir and run it with the
scratch venv's `python3` (the example only imports `contextgraph_sdk`, so it
is agnostic to where that package physically resolves from):
is agnostic to where that package physically resolves from). Copy the
example's sibling `fixtures/` directory along with it — the provider serves
file provenance whose digests are computed from those files, resolved
relative to the example's own location:

```bash
./.github/scripts/conformance-external.sh -- /tmp/cgp-sdk-smoke/bin/python3 /tmp/example_docs.py
Expand Down
5 changes: 5 additions & 0 deletions sdk/go/examples/example-docs/fixtures/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Configuration example

Wire a documentation provider into a host:

let host = Host::new().with_provider("docs", provider);
4 changes: 4 additions & 0 deletions sdk/go/examples/example-docs/fixtures/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Getting Started

Install the reference binding with `cargo add contextgraph-types`, then implement
the four required methods.
52 changes: 43 additions & 9 deletions sdk/go/examples/example-docs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
package main

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strings"

cg "github.com/macanderson/context-graph-protocol/sdk/go/contextgraph"
)
Expand All @@ -24,13 +28,43 @@ const (
embeddingDimensions = 384
)

// Stable, syntactically valid sha256:<64 hex> digests (SPEC.md F5). Not real
// hashes of anything — this fixture serves string literals, not on-disk bytes —
// but well-formed, and the same value verify answers with, so served frames and
// verify verdicts can never drift apart.
// fixtureDir is the directory holding this provider's on-disk backing files,
// resolved from this source file's own recorded path so a digest is computed
// over the same bytes no matter where the provider is spawned from (SPEC.md
// §6.2). runtime.Caller records the path at build time; the conformance run
// builds and probes on the same machine, so the path resolves.
func fixtureDir() string {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "fixtures"
}
return filepath.Join(filepath.Dir(file), "fixtures")
}

// fixtureURI is the absolute file:// URI a host re-reads to verify a frame's
// provenance digest (provenance-fixture-consistency). Absolute and
// cwd-independent, so verification never depends on the host's working
// directory.
func fixtureURI(file string) string {
return "file://" + filepath.ToSlash(filepath.Join(fixtureDir(), file))
}

// fixtureDigest is the real sha256:<64 lowercase hex> digest over a backing
// file's exact on-disk bytes — byte-for-byte what a host recomputes when it
// re-reads the file, so an unmutated frame verifies end to end (SPEC.md §6.2,
// §F5).
func fixtureDigest(file string) string {
bytes, err := os.ReadFile(filepath.Join(fixtureDir(), file))
if err != nil {
bytes = nil
}
sum := sha256.Sum256(bytes)
return "sha256:" + hex.EncodeToString(sum[:])
}

var (
gettingStartedDigest = "sha256:" + strings.Repeat("11", 32)
configurationDigest = "sha256:" + strings.Repeat("22", 32)
gettingStartedDigest = fixtureDigest("getting-started.md")
configurationDigest = fixtureDigest("configuration.md")
)

func currentDigest(frameID string) (string, bool) {
Expand All @@ -51,15 +85,15 @@ func docFrame(id, title, content, file, rng string, score float64, digest string
Title: title,
Content: content,
ContentDigest: digest,
URI: "file:///docs/" + file,
URI: fixtureURI(file),
Score: score,
// Honest cost: ceil(utf8_len(content)/4) (B3).
TokenCost: cg.BudgetTokens(content),
ValidFrom: "2026-01-01T00:00:00Z",
RecordedAt: "2026-07-20T18:00:00Z",
Provenance: []cg.Provenance{{
Type: "file",
URI: "file:///docs/" + file,
URI: fixtureURI(file),
Range: rng,
Digest: digest,
By: "contextgraph-go-example-docs",
Expand Down
5 changes: 0 additions & 5 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ passes the same conformance suite that judges the Rust reference provider.

## Install

> **Not yet published to PyPI.** The command below does not resolve yet — see
> [`sdk/PUBLISHING.md`](../PUBLISHING.md) for the publish checklist and
> current status. Until then, install from a checkout: `pip install -e
> sdk/python` from the repository root.

```sh
pip install contextgraph-sdk
```
Expand Down
39 changes: 31 additions & 8 deletions sdk/python/examples/example_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

import hashlib
import os
import sys
from typing import Any
Expand All @@ -28,12 +29,34 @@
EMBEDDING_FINGERPRINT = "bge-small-en-v1.5/384/l2"
EMBEDDING_DIMENSIONS = int(EMBEDDING_FINGERPRINT.split("/")[1])

# Stable, syntactically valid sha256:<64 hex> digests (SPEC.md F5). Not real
# hashes of anything -- this fixture serves string literals, not on-disk bytes --
# but well-formed, and the same value verify answers with, so served frames and
# verify verdicts can never drift apart.
GETTING_STARTED_DIGEST = "sha256:" + ("11" * 32)
CONFIGURATION_DIGEST = "sha256:" + ("22" * 32)
# The directory holding this provider's on-disk backing files, resolved
# relative to this file so a digest is computed over the same bytes no matter
# where the provider is spawned from (SPEC.md §6.2).
FIXTURE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures")


def _fixture_uri(file: str) -> str:
"""The absolute ``file://`` URI a host re-reads to verify a frame's
provenance digest (``provenance-fixture-consistency``). Absolute and
cwd-independent, so verification never depends on the host's working
directory."""
return "file://" + os.path.join(FIXTURE_DIR, file)
Comment thread
macanderson marked this conversation as resolved.


def _fixture_digest(file: str) -> str:
"""The real ``sha256:<64 lowercase hex>`` digest over a backing file's exact
on-disk bytes -- byte-for-byte what a host recomputes when it re-reads the
file, so an unmutated frame verifies end to end (SPEC.md §6.2, §F5)."""
try:
with open(os.path.join(FIXTURE_DIR, file), "rb") as handle:
data = handle.read()
except OSError:
data = b""
return "sha256:" + hashlib.sha256(data).hexdigest()


GETTING_STARTED_DIGEST = _fixture_digest("getting-started.md")
CONFIGURATION_DIGEST = _fixture_digest("configuration.md")


def _current_digest(frame_id: str) -> str | None:
Expand Down Expand Up @@ -70,7 +93,7 @@ def _doc_frame(
"title": title,
"content": content,
"content_digest": digest,
"uri": f"file:///docs/{file}",
"uri": _fixture_uri(file),
"score": score,
# Honest cost: ceil(utf8_len(content)/4) (B3).
"token_cost": budget_tokens(content),
Expand All @@ -79,7 +102,7 @@ def _doc_frame(
"provenance": [
{
"type": "file",
"uri": f"file:///docs/{file}",
"uri": _fixture_uri(file),
"range": rng,
"digest": digest,
"by": "contextgraph-py-example-docs",
Expand Down
5 changes: 5 additions & 0 deletions sdk/python/examples/fixtures/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Configuration example

Wire a documentation provider into a host:

let host = Host::new().with_provider("docs", provider);
4 changes: 4 additions & 0 deletions sdk/python/examples/fixtures/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Getting Started

Install the reference binding with `cargo add contextgraph-types`, then implement
the four required methods.
52 changes: 44 additions & 8 deletions sdk/typescript/examples/example-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
* contextgraph-inspect stdio --json -- node dist/examples/example-docs.js
* ```
*/
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

import { budgetTokens } from "../src/budget.js";
import { ProviderError, runStdioProvider, type Provider } from "../src/provider.js";
import type {
Expand All @@ -19,12 +24,43 @@ import type {
VerifyResponse,
} from "../src/types.js";

// Stable, syntactically valid `sha256:<64 hex>` digests (SPEC.md §F5). Not real
// hashes of anything — this fixture serves string literals, not on-disk bytes —
// but well-formed, and the same value verify answers with, so the frames it
// serves and its verify verdicts can never drift apart.
const GETTING_STARTED_DIGEST = `sha256:${"11".repeat(32)}`;
const CONFIGURATION_DIGEST = `sha256:${"22".repeat(32)}`;
// The directory holding this provider's on-disk backing files. Resolved from
// this module's own location so a digest is computed over the same bytes no
// matter where the provider is spawned from (SPEC.md §6.2). The compiled
// module runs from `dist/examples/`, where `fixtures/` is not copied — the
// second candidate reaches the source tree's copy.
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
const FIXTURE_DIR = [
join(MODULE_DIR, "fixtures"),
join(MODULE_DIR, "..", "..", "examples", "fixtures"),
].find((dir) => existsSync(dir)) ?? join(MODULE_DIR, "fixtures");

/**
* The absolute `file://` URI a host re-reads to verify a frame's provenance
* digest (`provenance-fixture-consistency`). Absolute and cwd-independent, so
* verification never depends on the host's working directory.
*/
function fixtureUri(file: string): string {
return pathToFileURL(join(FIXTURE_DIR, file)).href;
}

/**
* The real `sha256:<64 lowercase hex>` digest over a backing file's exact
* on-disk bytes — byte-for-byte what a host recomputes when it re-reads the
* file, so an unmutated frame verifies end to end (SPEC.md §6.2, §F5).
*/
function fixtureDigest(file: string): string {
let bytes: Buffer;
try {
bytes = readFileSync(join(FIXTURE_DIR, file));
} catch {
bytes = Buffer.alloc(0);
}
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
}

const GETTING_STARTED_DIGEST = fixtureDigest("getting-started.md");
const CONFIGURATION_DIGEST = fixtureDigest("configuration.md");

// The embedding space this fixture declares it indexes (SPEC.md §E1). Its
// dimension — the 2nd `/`-separated segment (384) — is the length a query
Expand Down Expand Up @@ -69,7 +105,7 @@ function docFrame(
title,
content,
content_digest: digest,
uri: `file:///docs/${file}`,
uri: fixtureUri(file),
score,
// Honest cost: ceil(utf8_len(content)/4) (B3).
token_cost: budgetTokens(content),
Expand All @@ -78,7 +114,7 @@ function docFrame(
provenance: [
{
type: "file",
uri: `file:///docs/${file}`,
uri: fixtureUri(file),
range,
digest,
by: "contextgraph-ts-example-docs",
Expand Down
5 changes: 5 additions & 0 deletions sdk/typescript/examples/fixtures/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Configuration example

Wire a documentation provider into a host:

let host = Host::new().with_provider("docs", provider);
4 changes: 4 additions & 0 deletions sdk/typescript/examples/fixtures/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Getting Started

Install the reference binding with `cargo add contextgraph-types`, then implement
the four required methods.
Loading