ci(sdk): typecheck both typed SDKs, and catch a pin that drifted from its manifest - #106
Conversation
… its manifest Two things the SDK directory shipped on a claim rather than a run. The TypeScript type changes in PR #87 were never typechecked — no local toolchain, and `npx tsc` failed in that environment. `sdk (typescript) is a conformant implementation` does run `tsc` as a side effect of `npm run build`, so the coverage was not zero, but it arrives only after a full `cargo build --workspace --bins` and lasts only as long as the build script stays `tsc`. `sdk (typescript) typechecks` asks the question directly. The Python SDK ships `py.typed`, which promises downstream typecheckers that its annotations are meant to be believed, and nothing checked them; `sdk (python) typechecks` runs `mypy --strict`. Both are clean on this tree. The scaffolder's `DEFAULT_SDK` table names versions of two packages it does not own, and it sat at `^0.1.0` against a shipped `1.0.0` for an unknown length of time. The scaffold job could not have caught it: it overrides both published pins with local paths so CI never depends on a publish, which is the right call for that job. `check-sdk-version-pins.py` is the guard that belongs elsewhere — offline, stdlib only, no registry call. The rule is same major, not equality, because the pins are deliberately ranges: a patch release should not need an edit here, and a major is what a scaffold cannot survive (ADR 0011 widened `FrameKind` in 2.0.0, so a `1.x` pin generates code against a retired vocabulary). ADR 0012 carries the reasoning, including why `sdk/go` and `schema/reference-vectors.ndjson` are out of scope. Closes #94, Closes #98 Signed-off-by: macanderson <mac@oxagen.sh>
There was a problem hiding this comment.
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 12 minutes by commenting @sourcery-ai review. Upgrade to get a review now.
Reviewer's GuideThe PR adds standalone CI jobs to typecheck the TypeScript and Python SDKs, plus an offline standard-library guard that verifies scaffolder pins, template indirection, SDK manifest compatibility, and SDK/crate major-version lockstep; accompanying ADR, changelog, and package-name documentation are updated. Flow diagram for offline SDK version-pin validationflowchart TD
Start[Run check-sdk-version-pins.py] --> Parse[Read DEFAULT_SDK and SDK manifests]
Parse --> Templates[Verify templates use SDK_SPEC]
Templates --> Pins[Parse TypeScript and Python dependency floors]
Pins --> Compatibility{Same major and not ahead?}
Compatibility -->|No| Fail[Print FAIL and exit 1]
Compatibility -->|Yes| Lockstep{SDK and crate majors match?}
Lockstep -->|No| Fail
Lockstep -->|Yes| Pass[Print OK and exit 0]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
`npm ci` rather than `npm install`, so the job runs typescript 5.9.3 as `package-lock.json` pins it instead of whatever `^5.9.0` resolves to today. The mypy job beside it is pinned for the same reason and this one was not, which Sourcery caught against #94's definition of done. The conformance jobs keep `npm install`: they ask whether the SDK still behaves, not whether one compiler release still accepts it. Refs #94 Signed-off-by: macanderson <mac@oxagen.sh>
Settling the one ❌ — "pin or explain every version the new jobs depend on" (#94)Sourcery is half right, and the half it is right about is now fixed. Fixed — the compiler was floating, and it should not have been. The The conformance jobs keep Answered — the action tags and runtime versions are the repository's The runtime version has a second, separate hole that this PR did not open and |
|
@sourcery-ai review |
|
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 15 hours and 57 minutes by commenting |
|
Sourcery's assessment table cannot be refreshed on this head: the re-review request came back So the standing ❌ row is now stale in the part it is most specific about — it names |
…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 #78 replaced with a prefix map. 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>
…the ADR to 0017 The import is used only by the signing and verifying code, so a default build — every CI job except the new feature matrix — failed `-D warnings` on `unused_imports`. Gated to `record-attestation`, with the one ungated doc link that named the type rewritten as an explicit path so it still resolves with the import absent. The local check that reported this clean was `rg -c '^(error|warning)'` over cargo's output. Cargo colourises when it thinks it is talking to a terminal, so the escape sits before the word and `^error` matches nothing — a filter that cannot see the errors, reported as silence. The loop also mis-quoted `--features X` as one argument, so three of the five combinations never ran at all. Re-verified by exit code. ADR 0012 renumbered to 0017: PR #106 adds a differently-named docs/adr/0012-*.md, and two files with different names merge cleanly into a tree holding two ADR 0012s with nothing to catch it. Numbers are now allocated centrally. docs/GUIDE.md's decision log gains the entry, along with 0009, 0010 and 0011, which had been missing since they landed — adding a row to an index while leaving it knowingly incomplete is not a fix. Refs #96 Signed-off-by: macanderson <mac@oxagen.sh>
#106 landed `mypy --strict` over the Python SDK while this branch was open, and a bare `dict` in a signature fails `--strict`: the package ships `py.typed`, so an unparameterized mapping leaks `Any` across the exact boundary that promise covers. `LinkLike` and `FrameLike` name the two shapes the attestation surface accepts — the typed one and any decoded JSON mapping — so a caller reads what is allowed instead of inferring it, and the aliases carry the absent-vs-empty rule the encoding depends on. Refs #93 Signed-off-by: Mac Anderson <ops@oxagen.sh>
…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>
… Go SDKs (#131) * test(contextgraph-types): publish the attestation vectors an SDK port can actually fail The published set could not separate a correct port from an incorrect one. Every string in it was ASCII, so a length prefix counting UTF-16 code units or code points computed the same bytes; the only multi-leaf Merkle vector had four leaves, where RFC 6962's split and the duplicate-the-last-leaf shortcut agree; and there was no signature and no inclusion proof at all, so the two halves of section 6.5.4 had no oracle. Adds a link whose fields are multi-byte UTF-8 ending in an astral-plane character (24 UTF-8 bytes reading as 17 either other way), one-, three- and seven-leaf roots, a seven-leaf inclusion proof, a fixed Ed25519 key with the signature it produces, and the verdict vocabulary. No existing value changes. Mirrors every value into tests/vectors/attestation-vectors.json, which the TypeScript, Python and Go suites read, and asserts here that the mirror agrees — a digest transcribed into four languages is four things that can drift. Refs #93 Signed-off-by: Mac Anderson <ops@oxagen.sh> * feat(sdk-typescript): port provenance attestation, reconciled against the published vectors The SPEC.md 6.5 constructions in TypeScript: the length-prefixed link encoding, the source-first chain fold, the frame commitment, the RFC 6962 root and inclusion proofs, and strict Ed25519 verification with the seven named verdicts. Every length prefix is measured off the bytes TextEncoder produced, never off String.prototype.length. With the published unicode vector wired in, swapping the two turns the suite red on a 0x11 where the vector says 0x18 — which is exactly the divergence an ASCII-only vector set could not see. Node's Ed25519 (OpenSSL) accepts a small-order public key, which 6.5.4 says a strict verifier should not, so verifyCommitment rejects the eight small-order encodings and any key whose y is not reduced before OpenSSL ever sees it. The new script compiles test/attest.test.ts and runs it against tests/vectors/attestation-vectors.json under node --test. Refs #93 Signed-off-by: Mac Anderson <ops@oxagen.sh> * feat(sdk-python): port provenance attestation, reconciled against the published vectors The same constructions in Python, with the length prefix taken off s.encode("utf-8") rather than len(s) — len counts code points, so the published unicode vector separates the two. Ed25519 verification needed a decision: the standard library has none, and the SDK promises zero dependencies. contextgraph_sdk._ed25519 is a self-contained RFC 8032 verifier — verification only, never signing — matching dalek's verify_strict on all four counts: the cofactorless equation, a reduced S, canonical encodings, and small-order rejection. It is checked against RFC 8032 7.1's own vectors, against this repository's dalek-produced signature, and differentially against the cryptography package wherever that is installed. Because it has real field arithmetic, the Python suite is also what proves the small-order table the TypeScript and Go ports carry: it recomputes 8P = identity for every entry rather than trusting the list. Runs on a bare interpreter with python3 -m unittest discover -s tests. Refs #93 Signed-off-by: Mac Anderson <ops@oxagen.sh> * test(vectors): publish the small-order key set a strict verifier declines SPEC.md 6.5.4 says a verifier should reject small-order public keys and non-canonical encodings, and named neither set. The Rust reference gets both from ed25519_dalek::verify_strict; a port on Node's OpenSSL or Go's crypto/ed25519 gets neither, because both accept a small-order key. So the set is published rather than left to each port to rediscover: the eight canonical encodings of a point P with 8P = identity, plus the two non-canonical y values a verifier that reduces mod p would misread as y = 0 and y = 1. Labelled as verifier guidance rather than a wire vector, because it constrains what a verifier accepts and not what anything encodes. The Python suite recomputes 8P = identity for every entry from its own field arithmetic, so the table is checked rather than trusted. Refs #93 Signed-off-by: Mac Anderson <ops@oxagen.sh> * feat(sdk-go): port provenance attestation, and run all four suites in CI Go is the one language of the three where the native string length is already a UTF-8 byte count, so the port is short — but it has its own trap the others do not: contextgraph.Provenance carries its optional fields as string with omitempty and cannot tell an absent URI from a present empty one, which is exactly the distinction the SPEC.md 6.5.1 presence byte makes normative. attest.Link takes pointers, and LinkFromProvenance states the collapse it performs rather than hiding it. Go's crypto/ed25519 accepts a small-order public key, as Node's OpenSSL does, so VerifyCommitment declines the published small-order set and any key whose y is not reduced before the standard library sees them. CI gains one step per SDK job plus, on the Rust side, the one that turned out to matter most: the attestation feature is off by default and no workspace member enables it, so the existing workspace-wide run had never compiled contextgraph_types::attest or its vectors. The oracle three ports now reconcile against was itself unrun. Closes #93 Signed-off-by: Mac Anderson <ops@oxagen.sh> * fix(sdk): accept hex in exactly one spelling across all three ports SPEC.md's digest grammar is 64 lowercase hex characters, and contextgraph_types::is_well_formed_digest enforces it. The ports were inconsistent with each other: TypeScript rejected uppercase, Python's bytes.fromhex accepted it and also skipped whitespace between byte pairs, and Go's encoding/hex accepted it. Three implementations now agree, and each has a named test. The reference itself is the remaining outlier — attest::from_hex accepts A-F, so an uppercase signed_commitment verifies in Rust and is malformed_commitment in all three SDKs. Filed as #145 rather than changed here, because it is a behaviour change to a published Rust function and belongs in its own diff. Refs #93 Signed-off-by: Mac Anderson <ops@oxagen.sh> * fix(sdk-python): type the attestation surface for the strict typechecker #106 landed `mypy --strict` over the Python SDK while this branch was open, and a bare `dict` in a signature fails `--strict`: the package ships `py.typed`, so an unparameterized mapping leaks `Any` across the exact boundary that promise covers. `LinkLike` and `FrameLike` name the two shapes the attestation surface accepts — the typed one and any decoded JSON mapping — so a caller reads what is allowed instead of inferring it, and the aliases carry the absent-vs-empty rule the encoding depends on. Refs #93 Signed-off-by: Mac Anderson <ops@oxagen.sh> --------- Signed-off-by: Mac Anderson <ops@oxagen.sh> Co-authored-by: Mac Anderson <ops@oxagen.sh>
…ed URL (#109) 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>
…#114) * feat(contextgraph-types): implement record_hash and RecordAttestation The lifecycle profile has always defined `record_hash` as the sha256 over the RFC 8785 (JCS) canonicalization of a record with its own `record_hash` removed (LH1), and `RecordAttestation` as a detached Ed25519 signature over it (LC3). Both were prose and a struct. The only hashing code in the workspace was a private helper inside the conformance suite, so the suite proved the fixtures agreed with the suite; and the attestation fixture carried 49 bytes of DER-shaped filler where a signature belongs, with no key published, so no implementation could reproduce or refute it. `contextgraph_types::record_attest` makes the rule callable, behind two new off-by-default features. `record-hash` adds RFC 8785 canonicalization (delegated to serde_json_canonicalizer, whose numbers route through ryu-js — JCS number serialization is ECMAScript Number::toString, and its exponent thresholds are where reimplementations diverge in silence). `record-attestation` adds Ed25519 on top. A frame-only consumer pays for neither, and the crate's zero-dependency default is unchanged. The signed message is domain-separated: "contextgraph/attest/1/record" followed by the digest's 32 raw bytes. A frame commitment is domain-bound by construction; a record_hash is a plain SHA-256 over a JSON document that any number of unrelated systems also compute, so signing it raw would let one signature mean whatever the presenter says it means. Verification recomputes the record's hash rather than reading the stored member, so editing a record and rewriting its hash to match a stolen signature is caught as a mismatch instead of passing. Evidence: the canonicalizer is checked against RFC 8785's own vectors — §3.2.4's byte listing, §3.2.3's sorting data, and Appendix B's IEEE 754 number table. The twelve fixtures' hashes are unchanged, which is what shows this reproduces the existing rule rather than redefining it. tests/fixtures/ now publishes the canonical preimage text of every fixture, a real signature, and the test key that produced it; the conformance suite recomputes all of it through the library. schema/validate-examples.py checks the vectors from Python, without a JCS library, so an implementer who has neither Rust nor a canonicalizer can still rely on them. A CI job builds and runs each feature combination — until now every feature this crate has was off by default and no job turned any of them on, so the attestation code added in #87 compiled nowhere in CI. Also corrects LH2, which said JCS sorts members by code point. RFC 8785 §3.2.3 sorts by UTF-16 code unit, and the two orders differ for a supplementary character. Closes #96 Signed-off-by: macanderson <mac@oxagen.sh> * fix(contextgraph-types): gate the RecordAttestation import, renumber the ADR to 0017 The import is used only by the signing and verifying code, so a default build — every CI job except the new feature matrix — failed `-D warnings` on `unused_imports`. Gated to `record-attestation`, with the one ungated doc link that named the type rewritten as an explicit path so it still resolves with the import absent. The local check that reported this clean was `rg -c '^(error|warning)'` over cargo's output. Cargo colourises when it thinks it is talking to a terminal, so the escape sits before the word and `^error` matches nothing — a filter that cannot see the errors, reported as silence. The loop also mis-quoted `--features X` as one argument, so three of the five combinations never ran at all. Re-verified by exit code. ADR 0012 renumbered to 0017: PR #106 adds a differently-named docs/adr/0012-*.md, and two files with different names merge cleanly into a tree holding two ADR 0012s with nothing to catch it. Numbers are now allocated centrally. docs/GUIDE.md's decision log gains the entry, along with 0009, 0010 and 0011, which had been missing since they landed — adding a row to an index while leaving it knowingly incomplete is not a fix. Refs #96 Signed-off-by: macanderson <mac@oxagen.sh> * docs(guide): complete the ADR decision log The index stopped at 0008; 0009, 0010, 0011 and 0012 had each landed without a row. Adding only 0017 would have left a table that jumps from 0008 to 0017 and still misses four decisions, so all five are in. Refs #96 Signed-off-by: macanderson <mac@oxagen.sh> * fix(contextgraph-types): use as_chunks for the fixed-width hex pairs Rust 1.98's clippy adds `chunks_exact_to_as_chunks`, warn-by-default, so `-D warnings` turned red on every hex decoder here — including `attest.rs`'s `from_hex`, which predates this branch. CI pins `dtolnay/rust-toolchain@stable`, so the toolchain moved under a tree nobody had changed; the pre-existing site is fixed here because the job cannot go green while it stands. `as_chunks::<2>()` is also the better shape: the length check above each loop already rules out a remainder, and a fixed-size chunk lets the compiler see both indexes are in bounds. The two copies of the hex decode in the conformance suite collapse into one `hex32` helper that also checks the length it assumes. A note on how the earlier local run missed this: cargo replays a cached clippy result for an unchanged crate, so a `clippy` that had passed before the lint existed kept reporting success. /tmp/verify.sh now touches every source first. Refs #96 Signed-off-by: macanderson <mac@oxagen.sh> --------- Signed-off-by: macanderson <mac@oxagen.sh>
Pull request
Summary
Two guards over
sdk/: CI now typechecks the two SDKs that carry typeannotations, and a new offline check catches a version pin that has drifted
from the manifest it names.
Closes #94, Closes #98
What changed
Typechecking (#94)
sdk (typescript) typechecks—npm ci && npx tsc --noEmitinsdk/typescript/, in a job that pulls no Rust toolchain.npm ciso thecompiler comes from
package-lock.json(typescript 5.9.3) rather than fromwhatever
^5.9.0resolves to today.sdk (python) typechecks—mypy --strict --python-version 3.10overcontextgraph_sdkandexamples, with mypy pinned for the same reason.The Python half was not skipped, and here is the reasoning. The Python SDK
ships
py.typed. That file is a promise to every downstream typechecker thatthe package's annotations are meant to be believed, and nothing in this repo
had ever tested it.
--strictbecause apy.typedpackage that leaksAnyacross its own boundary keeps the promise to its callers and not to itself. It
is clean on this tree as written — no code changes were needed.
One correction to #94's premise, stated because the issue's framing is what
sized the work. "Nothing in CI appears to run
tsc --noEmit" is right about--noEmitand not quite right about typechecking:sdk (typescript) is a conformant implementationrunsnpm run build, which istsc, andtscexits non-zero on a type error, so that job would have failed. The coverage was
incidental rather than absent. It is still worth its own job: it arrives only
after a full
cargo build --workspace --binsthat has nothing to do with thequestion, and it holds only while the build script stays
tsc. The new jobasks the question directly and in about a minute.
Version-pin drift (#98)
.github/scripts/check-sdk-version-pins.py— stdlib only, offline, noregistry call. Same shape as
check-deploy-hygiene.py: acheck(label, ok)ledger,
PASS/FAILper line, a remedy under each failure.sdk version pins name a version their manifest ships— the CI job that runsit, separate from the scaffold job for the reason the issue gives.
docs/adr/0012-sdk-version-pins-share-a-major.md— the rule and why.The decision the issue asked for: same major, not equality. The pins are
deliberately ranges and differently shaped ones — a caret for npm, a
>=floorfor pip. Equality would make every SDK patch release an edit in the scaffolder
that nobody would remember to make, which defeats the range's whole purpose. A
major is the thing a scaffold cannot survive: majors are where the API changes,
and ADR 0011 is the worked example —
FrameKindwidened in2.0.0, so aproject scaffolded against a
1.xpin generates code against the vocabularythat release retired. Same major is the weakest rule that still catches every
break, which is what a guard should be; a stricter one fails on changes that
are correct.
Two things ride along with it, both because the pin rule leans on them:
^2.1.0against a shipped2.0.0resolves to nothing at all — drift in the other direction, and themanifest version is the newest release that can exist, because these
manifests are what
publish-sdks.ymlpublishes.MIGRATION.md§5.4 both assert this, and^2.0.0protects a scaffold fromADR 0011 only while it holds. Its practical effect: a major bump is one
commit across four files rather than four commits with a drift window in
between.
The other manifests the issue named.
sdk/gocarries no package version —a Go module is versioned by its git tag, not by a field in
go.mod, and thescaffolder emits no Go template, so there is no in-tree pin to compare against;
its
ProtocolVersionis a protocol version, a different axis. The"version": "1.0.0"strings inschema/reference-vectors.ndjsonare providerversions in fixtures and are untouched — rewriting them would change the bytes
the reference vectors pin. Both verdicts are recorded in the script's header
and in the ADR, so the next reader does not re-derive them.
Two drive-by fixes, both one line, both about a name a manifest does not
own:
MIGRATION.md§5.4 called the TypeScript package@contextgraph/sdk. Thepublished name is
@contextgraphprotocol/typescript-sdk.CHANGELOG.mdgains the two[Unreleased]entries.Evidence
Every command below was run in this branch's worktree.
tsc --noEmit— clean, then deliberately broken, then reverted. The breakpushes a string that is not a
KnownFrameKindintoKNOWN_FRAME_KINDS, theexact shape of type the PR #87 changes introduced:
mypy --strict --python-version 3.10— same three states, the break beinga function annotated
-> intthat returns itsstrargument:The version-pin guard on this tree —
python3 .github/scripts/check-sdk-version-pins.py,exit=0:The guard against the drift #98 actually recorded —
DEFAULT_SDK.typescriptput back to
^0.1.0,exit=1:Reverted, and green again — the last block above is the tree as pushed.
Six more drift shapes, each introduced, run, and reverted. Every one exits
1; the failing line is quoted:
>=1.0.0against a2.0.0manifestpython pin shares the major sdk/python/pyproject.toml ships^2.1.0when only2.0.0existstypescript pin does not name a version sdk/typescript/package.json has never shipped^2.0.0instead of{{SDK_SPEC}}the TypeScript template takes its SDK dependency from DEFAULT_SDKsdk/pythonalone bumped to3.0.0python pin shares the major …andthe SDKs and the crates share one major<2.0.0— a ceiling, not a floortypescript pin is a floor this check can readcontextgraph>=2.0.0the Python pin names the package sdk/python publishesgit status --porcelainafter the last revert showed only the new files, andthe guard was green again.
Checklist
fmt,clippy -D warnings,test— not run, and deliberately. This PR touches no Rust: the diff is one
workflow, one Python script, and three markdown files. SCR-001 says build
only what the change touches, and nothing here compiles. CI runs the Rust
gate on the push.
— the Evidence section is the witness for both guards: each is shown
failing on the drift it exists for and passing once it is reverted. There
is no
#[test]because neither guard is Rust; the equivalent proof is thebefore/after run.
README.md,docs/, doc comments,--helptext) — ADR 0012, and the guard's ownheader carries the rationale the way
check-deploy-hygiene.pydoes.git commit -s, DCO)CHANGELOG.mdupdated under[Unreleased]if user-visibleRegistry submission (only if adding a row to
docs/registry.md)Protocol-stability impact (if a spec/wire change)
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.