Skip to content

Release: v2.5.0#264

Merged
themightychris merged 24 commits into
mainfrom
develop
Jul 18, 2026
Merged

Release: v2.5.0#264
themightychris merged 24 commits into
mainfrom
develop

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

Schema contracts: sheets can now declare implements on named, versioned, immutable JSON Schema contracts — vendored into the repo as the canonical lock artifact, enforced by construction on every write, and mechanically verifiable by cross-system consumers (openSheet({ contract }), gitsheets contracts). See the new contracts guide.

Improvements

Technical

themightychris and others added 3 commits July 18, 2026 13:32
…ruction

New specs/behaviors/contracts.md: a sheet may declare `implements` on named,
versioned, immutable JSON Schema contracts. Contracts are vendored in canonical
TOML under .gitsheets/contracts/ — the vendored bytes are simultaneously the
lock state and the enforced artifact (composed via allOf into write-time
validation), so producer conformance is by construction and identity is a
content hash, not a recorded claim. Consumers verify via a two-rung ladder:
declared identity (fast, guarantees future writes) falling back to structural
record validation (works against any sheet, guarantees the present).

Amendments: validation.md layer-1 composition; repository.md openSheet
contract option + verification modes; cli.md `git sheet contracts` command
group + exit code 67; errors.md ContractError + codes + ValidationIssue
contract/record fields; concepts.md Contract entry; deferred.md entries for
source shorthand/vanity resolution, registry-as-gitsheet + succession
metadata, $ref closure vendoring, and sheet-level assertions.

Motivating consumer pair: a generic toolkit module reading a meal-bank sheet
from a personal repo via instance configuration — cross-system sheet
consumption as a checked interface instead of a hopeful convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
Three plans bridging specs/behaviors/contracts.md to code: contracts-core
(declaration parsing, vendored store, allOf enforcement, canonical-hash
primitive) unblocking contracts-cli (adopt/verify/test/sync/export/prune +
sources sidecar) and contracts-consumer-verify (openSheet two-rung ladder,
conformance report, advisory drift) in parallel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
specs+plans: schema contracts (implements, vendored canonical-TOML lock, two-rung consumer verification)
@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Author

Changelog

- specs+plans: schema contracts (implements, vendored canonical-TOML lock, two-rung consumer verification) [#263] @themightychris
- feat(core): schema contracts — implements, vendored store, composed enforcement [#265] @themightychris
- feat: consumer-side contract verification — the two-rung ladder in openSheet [#267] @themightychris
- feat(cli): git sheet contracts — adopt, verify, test, sync, export, prune [#268] @themightychris
- docs: schema contracts guide + contract attribution on write-failure output [#269] @themightychris
- chore(napi): sync manifests to published @gitsheets/core-napi 0.4.0 [#270] @themightychris

themightychris and others added 21 commits July 18, 2026 14:49
…osition

Core mechanics of specs/behaviors/contracts.md in gitsheets-core:

- `gitsheet.implements` config key, validated against the name rules
  (host-qualified, path-segment character rules, no `.`/`..`, no trailing
  slash) — a new `contract` module owns name validation, the derived
  `.gitsheets/contracts/<name>.toml` path, loading + document-requirement
  checks (Draft-07 strict compile, self-contained, open, TOML-data-model-only,
  `$id`↔path, canonical bytes), and the `canonical_contract_hash` identity
  primitive (data / JSON text / TOML text → canonical TOML → SHA-256 hex).
- Effective-schema composition: `allOf: [<each declared contract>,
  [gitsheet.schema]]`, compiled once per sheet exactly like a bare
  `[gitsheet.schema]` when `implements` is empty (byte/behavior-identical to
  before contracts existed). `ValidationIssue` gained a `contract` field,
  attributed from the failing `allOf` branch index.
- `ContractError` variants (`contract_missing`, `contract_invalid`, and the
  reserved `contract_unsatisfied` code for the later consumer-verification
  plan) in the core error taxonomy.
- The unknown-keyword strict-mode walker in `validation.rs` is refactored into
  a generic `walk_schema` so the contract document-requirement checks
  (self-containment, openness, null-bearing keywords) reuse its exact
  "never misread a data position as a keyword" descent rules.

Also pins `rust` via asdf (`asdf set rust 1.94.1`) since `rust/.tool-versions`
had no entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
Adds the napi surface for schema contracts, riding the existing generic
structured-error marshalling (no new per-class plumbing needed beyond the
`contract` payload field on both the top-level error and each
`ValidationIssue`):

- `canonicalContractHash(input, format?)` — the identity primitive, accepting
  already-parsed data or a string (`format: 'json' | 'toml'` required for a
  string input).
- `contract` on `JsValidationIssue` and on thrown structured errors.
- `binding.cjs` gains a `ContractError` class and wraps the new function.

test/contracts.mjs proves sheet-open `contract_missing`/`contract_invalid`,
`allOf` composition naming the failing contract on write, and
`canonicalContractHash` agreement across data/JSON/TOML input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
The Python analogue of the napi contracts surface: a `ContractError`
exception class wired into the existing `raise_core_error` mapping (which
already carries `issues`/`conflicting_paths`; now also `contract` on both the
exception and each issue dict), and `canonical_contract_hash(input,
format=None)` — parsed data or a `str` with `format='json'|'toml'`.

tests/test_contracts.py covers sheet-open contract_missing/contract_invalid,
allOf composition naming the contract, and hash agreement across input forms.
test_cross_binding.py gains a Node↔Python canonical_contract_hash parity
proof, extending _node_writer.mjs with a `contract-hash` op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
Extends the `gitsheets` npm package's error taxonomy and public surface for
schema contracts:

- `ContractError` (codes `contract_missing`/`contract_invalid`/
  `contract_unsatisfied`), carrying optional `contract` and `issues` — mapped
  from the core's structured `gitsheetsClass: 'ContractError'` in `core.ts`.
- `ValidationIssue.contract`, threaded straight through from the core (no
  extra mapping needed — `ValidationError.issues` already passes the raw
  per-issue object through unchanged).
- `canonicalContractHash(input, options?)` in a new `contracts.ts` — the one
  new public entry point contracts add; `implements` declaration and enforced
  composition ride the existing `openSheet`/`upsert` path with no new API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
feat(core): schema contracts — implements, vendored store, composed enforcement
Adds `verify_sheet_contract(repo, tree, open_root, sheet, document, mode)`
(rust/gitsheets-core/src/contract.rs), implementing
specs/behaviors/contracts.md "Consumer verification":

- Rung 1 (declared identity): the sheet's `implements` names the document's
  contract (derived from its `$id`) AND the vendored bytes at the derived
  path are canonical-hash-identical to the consumer's document. Pass ⇒
  `Rung::Declared`, zero records read.
- Rung 2 (structural): every record validates against the consumer's
  document ALONE (never composed with the sheet's own schema/contracts —
  pure duck typing). Pass ⇒ `Rung::Structural`.
- Modes: `Verify` (rung 1 → rung 2, default), `Declared` (rung 1 only, fails
  fast, never reads records), `Structural` (rung 2 only, ignores
  declarations).
- Both rungs miss ⇒ `Error::ContractUnsatisfied` carrying the conformance
  report — per-record, per-field `ValidationIssue`s for a rung-2 miss, empty
  `issues` for a `Declared`-mode miss.

`ValidationIssue` gains a `record: Option<String>` field (the record's
sheet-relative path in a multi-record conformance report) — `None` for every
existing single-record write-time issue.

`canonical_contract_hash` is refactored to share its parse step
(`resolve_contract_document`) with the new verification path, so both build
on the identical canonicalize-then-hash primitive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
Exposes `gitsheets_core::verify_sheet_contract` at the napi boundary:

- `verifySheetContract(gitDir, treeRef, sheetName, configPath, openRoot,
  prefix, schema, format?, mode?)` — opens the sheet read-only (config read +
  effective-schema compile, same as any other read) and runs the two-rung
  ladder against `schema` (parsed data, or text with `format` naming which
  form — mirrors `canonicalContractHash`'s no-auto-detection input handling).
  Returns `{ name, rung, conforming, issues }`; a verification failure
  surfaces as the existing generic `ContractError(contract_unsatisfied)`
  marshalling, now carrying `issue.record` alongside `issue.contract`.
- `JsValidationIssue` gains `record`; `throw_structured_error` and the shared
  `issue_to_js` marshal it through.
- `binding.cjs` wraps and re-exports the new function for the boundary suite.

test/contracts.mjs adds direct napi-level coverage: rung-1 pass, declared
mode fails fast without reading records, structural mode duck-types a
contract-unaware sheet, and rung-2 failure issue quality (record + field +
contract).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
Wires the two-rung ladder into the public library surface:

- `openSheet(name, { contract: { schema, format?, mode?, onDrift? } })` —
  verifies the sheet against a contract document the consumer holds before
  the handle is returned; throws `ContractError('contract_unsatisfied')` on
  failure. `schema` accepts parsed data or JSON/TOML text (`format` required
  for text, matching `canonicalContractHash`'s no-auto-detection rule — a
  minimal spec amendment to specs/api/repository.md, which named `schema`
  but not the text-format companion).
- `sheet.contractVerification` — `{ name, rung, tree, conforming, issues }`
  on success; `undefined` when no `contract` option was given.
- Advisory drift: for rung-2 (structural) verified sheets with an `onDrift`
  callback, `Sheet.rebindReadTree` (the freshness rebind path) schedules a
  lazy, fire-and-forget re-verification against the new tree; a regression
  invokes `onDrift` with the regressed report. Reads are never gated on
  this — a non-`ContractError` from the re-check (e.g. a transient read
  issue) is swallowed, not surfaced. No `onDrift` registered ⇒ no
  re-validation work.
- `ValidationIssue.record` — the record path in a multi-record conformance
  report.
- New types in contracts.ts: `ConformanceReport`, `ContractVerificationMode`,
  `OpenSheetContractOptions`.

contract-verification.test.ts covers the plan's validation list: rung-1 pass
with zero record reads (a garbage record file that would blow up parsing if
touched), rung-1 miss falling through to a rung-2 pass (a newer,
data-compatible producer version), rung-2 failure report quality (record +
field + contract), `declared` mode failing fast, `structural` mode
duck-typing a contract-unaware sheet, the drift callback firing after a
non-conforming commit while reads still succeed, and the motivating
end-to-end meal-bank scenario.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
specs/behaviors/contracts.md and the plan's Approach both say the consumer
document input "match[es] canonicalContractHash's input handling" — but that
primitive requires an explicit `format: 'json' | 'toml'` when the input is a
string (no auto-detection), and api/repository.md's `opts.contract` example
never listed it. Adds `format?` alongside `schema`, and expands
`sheet.contractVerification`'s documented shape to the full conformance-report
fields (`conforming`/`issues`) the implementation returns, matching what
`onDrift` receives on a regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
feat: consumer-side contract verification — the two-rung ladder in openSheet
…contract_load, delete_file

The `git sheet contracts` CLI (adopt/verify/test/sync/export/prune) needs a
few thin surfaces the core doesn't expose yet, reusing the exact mechanics
gitsheets-core already implements rather than re-checking anything host-side:

- `check_contract_document` (core, contract.rs): wraps the existing private
  `check_document_requirements` + `reject_unknown_keywords` for a NOT-yet-
  vendored candidate document — `adopt`'s pre-vendor gate.
- napi wrappers: `validateContractName`, `contractPath`, `checkContractDocument`,
  `contractLoad` (the last runs `load_contract` end-to-end: byte-canonical +
  document-requirement + $id-path checks against the committed tree).
- `checkContractDocument` takes `Either<String, JsValue> + format`, mirroring
  `canonicalContractHash` — a JSON-text input is parsed directly via
  `serde_json::from_str` (preserving a literal `null`), deliberately NOT via
  the `JsValue` marshalling boundary, which silently drops null-valued keys
  per the documented type-fidelity rules. Requirement 4 (no null-bearing
  keywords) can only ever be violated by a JSON-sourced document, so routing
  through the null-dropping path would make that check unreachable for
  exactly the input shape it exists to catch.
- `value_to_json` (validation.rs) widened from `pub(crate)` to `pub` so the
  napi crate can do this conversion for TOML-text/already-parsed-data input.
- `CoreTransaction.delete_file` / `Transaction.deleteFile` (napi + JS): the
  inverse of the existing `write_file`, for `contracts prune` to remove
  vendored documents. Deep paths go through `MutableTree::delete_child_deep`.
- `gitsheets-napi` gained a direct `serde_json` dependency (previously only
  transitive via gitsheets-core) for the above.

Regenerated `rust/gitsheets-napi/index.d.ts`/`index.js` via `napi build`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
…rune

Implements the `git sheet contracts <subcommand>` command group per
specs/api/cli.md and specs/behaviors/contracts.md, riding contracts-core's
loader/validation primitives (PR #265) plus the new thin napi surfaces added
in the prior commit:

- `adopt <source> [--sheet <name>]...` — reads a local file, an `https://`
  URL (one-shot fetch, 15s timeout), or `-` (stdin); JSON or TOML. Derives the
  contract name from `$id`, validates it, checks the document requirements,
  canonicalizes through the same encoder records use, and vendors it at the
  derived path. Records provenance in `.gitsheets/contracts/sources.toml`
  (omitted for stdin input — non-load-bearing, no loss of function). With
  `--sheet`, builds the would-be effective schema (existing declared
  contracts + the candidate + the sheet's local schema) and validates every
  existing record against it, streaming per-record issues to stderr and
  refusing to write anything on any failure.
- `verify [<sheet>]...` — the offline CI gate: every declared contract
  resolves + is document-valid + canonical + $id-path-consistent (via
  `contractLoad`/`load_contract`), every record validates against the
  effective schema, and a closed local schema (`additionalProperties: false`)
  warns without failing.
- `test <sheet> --against <file-or-name>` — rung-2 structural check: records
  against the target document ALONE, not the effective schema. Works on
  sheets that declare nothing.
- `sync [<name>]...` — re-fetches each contract's recorded source and reports
  match/drift/unsyncable/missing; never rewrites the vendored file.
- `export <name>` — vendored contract as interchange JSON on stdout.
- `prune [--dry-run] [--yes]` — lists (git ls-tree over `.gitsheets/contracts`)
  vendored documents no sheet declares; removal prompts for confirmation
  unless `--yes` (added for scriptability; not in the original spec text —
  the spec only says "needs confirmation" without dictating the mechanism).
- `ContractError` wired to exit 67 (was missing from `exitCodeForError`);
  `reportError` now also prints a `ContractError`'s attached `issues`.
- `GlobalArgs`/`buildTxOpts` extracted to a new `cli/shared.ts` (used by both
  `index.ts` and `contracts.ts`) to avoid a circular import between them.

The one piece of "logic" assembled host-side rather than in the core is the
effective-schema `allOf` array — a documented, mechanical formula
(specs/behaviors/contracts.md "Composition and enforcement"), validated
through the existing `validateBatch`, not a re-implementation of anything.

Tests (packages/gitsheets/src/cli/cli-contracts.test.ts) cover every
Validation bullet in plans/contracts-cli.md: adopt from a local JSON file and
a local TOML file producing byte-identical vendored bytes, adopt over HTTPS
with `fetch` mocked (no real network in unit tests — see the plan's Risks
note), `--sheet` refusal leaving the tree untouched, `verify` passing +
contract_missing + non-canonical-vendored (contract_invalid) + a record
violation + the closed-local-schema warning, `test` passing and failing
against a contract-unaware sheet, `sync` drift detection without rewriting,
`export | adopt -` byte-identical round-trip, and `prune --dry-run`/`--yes`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
feat(cli): git sheet contracts — adopt, verify, test, sync, export, prune
New docs/contracts.md — the full producer→consumer tour (declare + adopt,
composed enforcement, the two-rung verification ladder, evolution patterns,
housekeeping), with every command output pasted from a live run against the
shipped CLI. Touch-ups: cli.md command group + exit 67, api.md surface/errors/
canonicalContractHash, concepts.md Contract entry, mkdocs nav.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
The core tags a failing allOf branch with its contract name
(specs/behaviors/contracts.md "a failing write says which obligation it
violated") and the ContractError printer already rendered it, but the
ValidationError printer dropped it — an upsert rejected by a contract looked
identical to one rejected by the local schema. Append the [contract-name] tag
when present; verified live against a declaring sheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
docs: schema contracts guide + contract attribution on write-failure output
Post-publish sync per the release rules in CLAUDE.md (napi versions are
tag-stamped at publish, never pre-committed). Generated by:

  npm pkg set version=0.4.0 (+ platform optionalDependencies) in rust/gitsheets-napi
  npm install @gitsheets/core-napi@^0.4.0 -w gitsheets

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJCxogamrSUZ7etFGsnoD1
…sync

chore(napi): sync manifests to published @gitsheets/core-napi 0.4.0
@themightychris themightychris changed the title Release: v2.4.2 Release: v2.5.0 Jul 18, 2026
@themightychris
themightychris merged commit 2bb3f10 into main Jul 18, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant