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
75 changes: 75 additions & 0 deletions .github/scripts/changelog-ai.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
#
# Draft CHANGELOG.md entries for a range of merges from their actual diff.
#
# Usage: changelog-ai.sh <git-range> e.g. changelog-ai.sh abc123..HEAD
#
# Prints Keep-a-Changelog bullets (grouped under ### Added / ### Changed /
# ### Fixed) for the user-visible changes in the range, in this repo's
# changelog voice, to stdout. Prints NOTHING and exits 0 when it cannot or
# should not draft — no API key, the call failed, the range holds nothing
# user-facing, or the response didn't look like changelog markdown — so the
# caller can treat empty output as "leave the changelog alone".
#
# Called from .github/workflows/changelog.yml, which runs it over the merges
# that landed since the last commit that touched CHANGELOG.md and proposes
# the result as a bot PR — a draft for human review, never a direct push.
#
# AI_GATEWAY_API_KEY required to produce output; unset -> warn, print nothing
# CHANGELOG_AI_MODEL optional, default anthropic/claude-sonnet-5
set -euo pipefail

range="${1:?usage: changelog-ai.sh <git-range>}"

if ! command -v jq >/dev/null 2>&1; then
echo "changelog-ai: jq not found; printing nothing." >&2
exit 0
fi
if [ -z "${AI_GATEWAY_API_KEY:-}" ]; then
echo "changelog-ai: AI_GATEWAY_API_KEY not set; printing nothing." >&2
exit 0
fi

model="${CHANGELOG_AI_MODEL:-anthropic/claude-sonnet-5}"

commits="$(git log --no-merges --format='%s' "$range" | head -n 100 || true)"
if [ -z "$commits" ]; then
echo "changelog-ai: no commits in range '${range}'; printing nothing." >&2
exit 0
fi

bodies="$(git log --no-merges --format='--- %s%n%b' "$range" | head -n 400 || true)"
stat="$(git diff --stat "$range" -- ':(exclude)Cargo.lock' ':(exclude)site/pnpm-lock.yaml' | tail -n 100 || true)"
diff="$(git diff "$range" -- ':(exclude)Cargo.lock' ':(exclude)site/pnpm-lock.yaml' 2>/dev/null | head -c 100000 || true)"

# shellcheck disable=SC2016 # the format string's backticks are literal markdown
prompt="$(printf '%s\n\n## Commit subjects\n%s\n\n## Commit bodies (squash-merge PR descriptions)\n%s\n\n## Files changed\n%s\n\n## Diff (truncated)\n```diff\n%s\n```\n' \
'Write CHANGELOG.md bullets for the merges below, from the Context Graph Protocol repository: a protocol specification plus Rust crates (contextgraph-types, contextgraph-host, contextgraph-conformance), TypeScript/Python/Go SDKs, and a docs site. Keep a Changelog format: "### Added" / "### Changed" / "### Fixed" / "### Removed" headings (only the ones that apply, in that order), one bullet per distinct change, in this repository'"'"'s established voice: a bold lead phrase, then the PR reference in parens, then an em-dash and two to four lines of concrete prose wrapped at 80 columns — for example: "- **Frame representations — full/compact/reference** (#41) — a provider can now answer with ...". Cover only what a protocol implementer, SDK user, or spec reader would notice: wire types, host runtime behavior, conformance checks, CLI, SDKs, published schemas, normative spec text, docs pages. Skip repo chores, lockfiles, logos, CI plumbing, and site build internals entirely. Base every claim on the diff below — commit messages describe intent, the diff is what shipped. If NOTHING in the range is user-visible, output exactly the single word NOTHING. Output ONLY the bullet markdown - no version heading, no preamble, no code fences.' \
"$commits" "$bodies" "$stat" "$diff")"

payload="$(jq -n --arg m "$model" --arg c "$prompt" \
'{model:$m, messages:[{role:"user",content:$c}], temperature:0.2}')"

resp="$(curl -sS --max-time 150 \
-H "Authorization: Bearer ${AI_GATEWAY_API_KEY}" \
-H "Content-Type: application/json" \
-d "$payload" \
https://ai-gateway.vercel.sh/v1/chat/completions || true)"

entries="$(printf '%s' "$resp" | jq -r '.choices[0].message.content // empty' 2>/dev/null || true)"

entries="$(printf '%s\n' "$entries" | sed -e '/^```/d')"
entries="$(printf '%s' "$entries" | sed -e 's/[[:space:]]*$//')"
if [ "$entries" = "NOTHING" ]; then
echo "changelog-ai: model judged the range internal-only; printing nothing." >&2
exit 0
fi
case "$entries" in
'### '* | '- '*) ;;
*)
echo "changelog-ai: response did not look like changelog markdown; printing nothing." >&2
exit 0
;;
esac

printf '%s\n' "$entries"
118 changes: 118 additions & 0 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: changelog

# Keep CHANGELOG.md's [Unreleased] section from silently falling behind main.
#
# The convention is that a PR records its own user-visible changes under
# `## [Unreleased]` in the same merge. When a merge lands WITHOUT touching
# CHANGELOG.md, this workflow drafts the missing entries from the actual
# diff (.github/scripts/changelog-ai.sh) and proposes them as a bot PR —
# a draft for human review, never a direct push to main.
#
# The gap is self-resetting: it is measured from the last commit that
# touched CHANGELOG.md to HEAD. Merging the bot PR touches CHANGELOG.md,
# which empties the gap; each run regenerates the WHOLE current gap, so
# force-updating the bot branch never loses coverage, and a bot PR left
# unmerged while more merges land is simply superseded by a fuller one.
#
# Degrade-open: no AI_GATEWAY_API_KEY, a failed call, or an internal-only
# gap produces no PR and a log line — never a red check.

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: changelog
cancel-in-progress: false

permissions:
contents: write # push the bot/changelog branch
pull-requests: write # open/update the bot PR

jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Measure the gap since the changelog was last touched
id: gap
run: |
set -euo pipefail
base="$(git log -1 --format=%H HEAD -- CHANGELOG.md)"
if [ -z "$base" ]; then
echo "no commit has ever touched CHANGELOG.md; nothing to measure."
echo "empty=true" >> "$GITHUB_OUTPUT"
exit 0
fi
count="$(git rev-list --count "${base}..HEAD")"
echo "gap: ${count} commit(s) since ${base} last touched CHANGELOG.md"
if [ "$count" -eq 0 ]; then
echo "empty=true" >> "$GITHUB_OUTPUT"
else
echo "empty=false" >> "$GITHUB_OUTPUT"
echo "base=${base}" >> "$GITHUB_OUTPUT"
fi

- name: Draft entries from the gap's diff
id: draft
if: steps.gap.outputs.empty != 'true'
env:
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}
BASE: ${{ steps.gap.outputs.base }}
run: |
set -euo pipefail
entries="${RUNNER_TEMP}/changelog-entries.md"
./.github/scripts/changelog-ai.sh "${BASE}..HEAD" > "${entries}" || true
if [ -s "${entries}" ]; then
echo "entries=${entries}" >> "$GITHUB_OUTPUT"
echo "----- drafted entries -----"; cat "${entries}"
else
echo "nothing drafted (no key, call failed, or internal-only gap); done."
fi

- name: Propose the entries as a bot PR
if: steps.gap.outputs.empty != 'true' && steps.draft.outputs.entries != ''
env:
GH_TOKEN: ${{ github.token }}
BASE: ${{ steps.gap.outputs.base }}
ENTRIES_FILE: ${{ steps.draft.outputs.entries }}
run: |
set -euo pipefail
branch="bot/changelog"

git checkout -B "${branch}" HEAD

# Insert the drafted bullets directly under `## [Unreleased]`,
# above whatever the section already holds — the gap's commits by
# definition wrote none of the existing entries, so nothing can be
# duplicated.
perl -pi -e '
if (/^## \[Unreleased\]$/) {
open my $fh, "<", $ENV{ENTRIES_FILE} or next;
my $e = do { local $/; <$fh> };
$e =~ s/\s+\z//;
$_ .= "\n" . $e . "\n";
}' CHANGELOG.md
if git diff --quiet -- CHANGELOG.md; then
echo "insertion was a no-op ([Unreleased] heading missing?); done."
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
git commit -m "docs(changelog): record the merges since ${BASE:0:8} touched the changelog"
git push -f origin "HEAD:refs/heads/${branch}"

title="docs(changelog): AI-drafted entries for merges that skipped the changelog"
body="$(printf 'One or more merges landed without touching CHANGELOG.md. These entries were drafted from the actual diff of %s..main by .github/scripts/changelog-ai.sh.\n\nReview like any contribution: edit freely, or close if the gap really was internal-only. Each push to main regenerates this PR to cover the whole current gap, so it is always the newest, fullest draft.' "${BASE:0:8}")"
existing="$(gh pr list --head "${branch}" --state open --json number --jq '.[0].number // empty')"
if [ -n "${existing}" ]; then
gh pr edit "${existing}" --title "${title}" --body "${body}"
else
gh pr create --base main --head "${branch}" --title "${title}" --body "${body}"
fi
119 changes: 85 additions & 34 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ independent axes — see [docs/stability.md](./docs/stability.md). This changelo
records crate releases and spec-repository milestones together, noting which is
which. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

A PR records its own user-visible changes under `## [Unreleased]` in the same
merge. When a merge skips that, [`changelog.yml`](.github/workflows/changelog.yml)
drafts the missing entries from the merge's actual diff
([`changelog-ai.sh`](.github/scripts/changelog-ai.sh)) and proposes them as a
bot PR for review — the file never falls silently behind main, and no drafted
text lands without a human merge.

## [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
Expand Down Expand Up @@ -237,7 +245,83 @@ which. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1
- Recommended relation vocabulary `frame::rel` (#7).
- Embedding fingerprint format and exact-match rule (E1) (#11).

- [`schema/contextgraph-envelope.schema.json`](./schema/contextgraph-envelope.schema.json) — a
machine-readable JSON Schema (Draft 2020-12) for the Context Graph Protocol envelope and all wire
types. Validates in any language (`ajv`, Python `jsonschema`, Rust
`jsonschema`, Go `gojsonschema`). Includes `schema/validate-examples.py` to
check the bundled examples and serve as a validator-usage reference.
- [`examples/`](./examples/) — diffable wire transcripts of a complete Context Graph Protocol
session (NDJSON + pretty-printed reference messages), so an implementer in
any language can diff their output against the exact shapes on the wire.
- `GOVERNANCE.md` — maintainer-led model, normative-change process, and the
concrete criteria for the `contextgraph/1.0-draft` → `contextgraph/1.0` freeze.
- Repository governance files: `SECURITY.md`, `CODE_OF_CONDUCT.md`, and
GitHub issue/PR templates.
- Prominent **License** section in the README clarifying the dual MIT OR
Apache-2.0 licensing of all Context Graph Protocol crates.
- A consolidated **Conformance requirements** section in
`docs/protocol-surface.md`, with RFC 2119 keywords and a formal ABNF grammar
for the protocol version string.

- **Frame identity, deterministic composition, and usage reports** (#32) —
`FrameId`, the (provider, frame, content-digest) triple whose derived order
IS the canonical composition order; `compose_context` renders an unchanged
frame set byte-identically across turns and hosts, so prompt-cache prefixes
stay stable by construction; `ContextFrame.content_digest` joins the type and
schema; and `UsageReport` itemizes a fan-out's per-frame cost so a billed
total re-sums from the exact frames it names (`FanOut::usage_report`).
- **Golden wire fixtures with attested digests** (#35) — a published vector set
under `contextgraph-conformance` (frame and query fixtures, digest-profile
vectors, a manifest of JCS-canonicalized sha256 hashes) that a downstream
implementation copies and drift-gates against, so "our wire matches the
reference" is a diff, not a claim.
- **ADR 0007 — the protocol/product boundary** (#61, #27) — classifies every
delta from the adaptive-context bundle against the spec: the bundle's
task-wide frame is the host-owned `CompiledContextFrame`, not the protocol's
atomic `ContextFrame`; frames stay evidence, never directives; and the CGEP
rename is rejected — the canonical name remains Context Graph Protocol.
`docs/adaptive-context-reconciliation.md` routes each adopted item to a spec
change or a normative-track issue.
- **Composition conformance — a downstream host can now be certified** (#70) —
`run_host_conformance` only ever certified this repo's reference host, and
three individually honest providers can still jointly exceed a query's
budget once composed. A new `composition_conformance` suite drives any
`ComposingHost` through four checks: the cross-provider token bound, the
total partition (every offered frame admitted or reported dropped, never
silently truncated), the audit quarantine, and deterministic render order.
Also exports `refuse_insecure_transport`, so C7's loopback rule has one
implementation rather than one per host.

### Changed

- **Breaking:** `token_cost` MUST now equal the canonical count for its content.
Providers that under-declared cost were previously green (#8).
- Withdrew the incorrect claim that CGP rides JSON-RPC 2.0 (#4).
- Code comments cite `SPEC.md` anchors instead of a private repository (#3).

- `docs/protocol-advantages.md`: corrected "MIT licensed" to the accurate
dual-license statement ("MIT OR Apache-2.0") to match the rest of the repo.
- `docs/protocol-advantages.md`: fixed a misspelling — "BTreive" → "Btrieve".
- `docs/protocol-advantages.md`, `docs/running-conformance.md`: removed leftover
references to the unrelated `stella` project, replacing them with Context Graph Protocol-specific
names (`contextgraph-graph`, `contextgraph-example-docs`).

- **Breaking:** the project is renamed — Open Context Protocol (OCP) →
Context Graph Protocol (#1). Crates `ocp-types`/`ocp-host`/`ocp-conformance`
become `contextgraph-*`, binaries `ocp-inspect`/`ocp-example-docs` become
`contextgraph-*`, the wire protocol version `ocp/1.0-draft` becomes
`contextgraph/1.0-draft`, and the schema file and `$id` move with them.
`MIGRATION.md` carries the rename map and the redirect hazard for
downstreams pinning the old URLs.
- **The downstream canary's scheduled run now fails on a break** (#70) — the
canary caught a real downstream break on 2026-07-29, emitted its warning,
and reported success; nobody saw it. A scheduled run gates no PR, so failing
costs nothing and buys a red run plus GitHub's failure notification. The
`pull_request` path stays advisory — a downstream repo still cannot block a
merge here.

### Removed

- **The `site/` documentation app is retired** (#57,
[ADR 0008](./docs/adr/0008-deploy-topology-and-advertised-urls.md)). It was a
fumadocs/Next app built on every PR as a hard gate and deployed nowhere,
Expand All @@ -257,6 +341,7 @@ 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
Expand Down Expand Up @@ -333,40 +418,6 @@ which. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1
validate serialized frames (not just hand-authored examples) against the
schema. `content` remains governed per-representation by the existing `allOf`.

### Changed
- **Breaking:** `token_cost` MUST now equal the canonical count for its content.
Providers that under-declared cost were previously green (#8).
- Withdrew the incorrect claim that CGP rides JSON-RPC 2.0 (#4).
- Code comments cite `SPEC.md` anchors instead of a private repository (#3).

### Added
- [`schema/contextgraph-envelope.schema.json`](./schema/contextgraph-envelope.schema.json) — a
machine-readable JSON Schema (Draft 2020-12) for the Context Graph Protocol envelope and all wire
types. Validates in any language (`ajv`, Python `jsonschema`, Rust
`jsonschema`, Go `gojsonschema`). Includes `schema/validate-examples.py` to
check the bundled examples and serve as a validator-usage reference.
- [`examples/`](./examples/) — diffable wire transcripts of a complete Context Graph Protocol
session (NDJSON + pretty-printed reference messages), so an implementer in
any language can diff their output against the exact shapes on the wire.
- `GOVERNANCE.md` — maintainer-led model, normative-change process, and the
concrete criteria for the `contextgraph/1.0-draft` → `contextgraph/1.0` freeze.
- Repository governance files: `SECURITY.md`, `CODE_OF_CONDUCT.md`, and
GitHub issue/PR templates.
- Prominent **License** section in the README clarifying the dual MIT OR
Apache-2.0 licensing of all Context Graph Protocol crates.
- A consolidated **Conformance requirements** section in
`docs/protocol-surface.md`, with RFC 2119 keywords and a formal ABNF grammar
for the protocol version string.

### Changed
- `docs/protocol-advantages.md`: corrected "MIT licensed" to the accurate
dual-license statement ("MIT OR Apache-2.0") to match the rest of the repo.
- `docs/protocol-advantages.md`: fixed a misspelling — "BTreive" → "Btrieve".
- `docs/protocol-advantages.md`, `docs/running-conformance.md`: removed leftover
references to the unrelated `stella` project, replacing them with Context Graph Protocol-specific
names (`contextgraph-graph`, `contextgraph-example-docs`).

### Fixed
- `contextgraph-host` and `contextgraph-conformance` did not compile from a
half-applied merge of #37 (egress-scope + consent receipts): `host.rs` used
`ConsentReceipt`/`EgressScope` without importing them and a `DataFlow` literal
Expand Down
Loading