diff --git a/CHANGELOG.md b/CHANGELOG.md index 7666fb3..b07fee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/README.md b/README.md index 118107c..dac7ea9 100644 --- a/README.md +++ b/README.md @@ -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.** diff --git a/sdk/PUBLISHING.md b/sdk/PUBLISHING.md index 5f9fafa..1dbe88e 100644 --- a/sdk/PUBLISHING.md +++ b/sdk/PUBLISHING.md @@ -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. @@ -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 diff --git a/sdk/go/examples/example-docs/fixtures/configuration.md b/sdk/go/examples/example-docs/fixtures/configuration.md new file mode 100644 index 0000000..3248fb5 --- /dev/null +++ b/sdk/go/examples/example-docs/fixtures/configuration.md @@ -0,0 +1,5 @@ +# Configuration example + +Wire a documentation provider into a host: + + let host = Host::new().with_provider("docs", provider); diff --git a/sdk/go/examples/example-docs/fixtures/getting-started.md b/sdk/go/examples/example-docs/fixtures/getting-started.md new file mode 100644 index 0000000..5e79541 --- /dev/null +++ b/sdk/go/examples/example-docs/fixtures/getting-started.md @@ -0,0 +1,4 @@ +# Getting Started + +Install the reference binding with `cargo add contextgraph-types`, then implement +the four required methods. diff --git a/sdk/go/examples/example-docs/main.go b/sdk/go/examples/example-docs/main.go index cade16a..14d5f59 100644 --- a/sdk/go/examples/example-docs/main.go +++ b/sdk/go/examples/example-docs/main.go @@ -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" ) @@ -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) { @@ -51,7 +85,7 @@ 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), @@ -59,7 +93,7 @@ func docFrame(id, title, content, file, rng string, score float64, digest string 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", diff --git a/sdk/python/README.md b/sdk/python/README.md index 8c3e8e6..f31cabe 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -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 ``` diff --git a/sdk/python/examples/example_docs.py b/sdk/python/examples/example_docs.py index b85209c..e31617b 100644 --- a/sdk/python/examples/example_docs.py +++ b/sdk/python/examples/example_docs.py @@ -8,6 +8,7 @@ from __future__ import annotations +import hashlib import os import sys from typing import Any @@ -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) + + +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: @@ -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), @@ -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", diff --git a/sdk/python/examples/fixtures/configuration.md b/sdk/python/examples/fixtures/configuration.md new file mode 100644 index 0000000..3248fb5 --- /dev/null +++ b/sdk/python/examples/fixtures/configuration.md @@ -0,0 +1,5 @@ +# Configuration example + +Wire a documentation provider into a host: + + let host = Host::new().with_provider("docs", provider); diff --git a/sdk/python/examples/fixtures/getting-started.md b/sdk/python/examples/fixtures/getting-started.md new file mode 100644 index 0000000..5e79541 --- /dev/null +++ b/sdk/python/examples/fixtures/getting-started.md @@ -0,0 +1,4 @@ +# Getting Started + +Install the reference binding with `cargo add contextgraph-types`, then implement +the four required methods. diff --git a/sdk/typescript/examples/example-docs.ts b/sdk/typescript/examples/example-docs.ts index 2803d74..f3b1b49 100644 --- a/sdk/typescript/examples/example-docs.ts +++ b/sdk/typescript/examples/example-docs.ts @@ -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 { @@ -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 @@ -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), @@ -78,7 +114,7 @@ function docFrame( provenance: [ { type: "file", - uri: `file:///docs/${file}`, + uri: fixtureUri(file), range, digest, by: "contextgraph-ts-example-docs", diff --git a/sdk/typescript/examples/fixtures/configuration.md b/sdk/typescript/examples/fixtures/configuration.md new file mode 100644 index 0000000..3248fb5 --- /dev/null +++ b/sdk/typescript/examples/fixtures/configuration.md @@ -0,0 +1,5 @@ +# Configuration example + +Wire a documentation provider into a host: + + let host = Host::new().with_provider("docs", provider); diff --git a/sdk/typescript/examples/fixtures/getting-started.md b/sdk/typescript/examples/fixtures/getting-started.md new file mode 100644 index 0000000..5e79541 --- /dev/null +++ b/sdk/typescript/examples/fixtures/getting-started.md @@ -0,0 +1,4 @@ +# Getting Started + +Install the reference binding with `cargo add contextgraph-types`, then implement +the four required methods.