Skip to content

Release: v2.3.0#245

Merged
themightychris merged 23 commits into
mainfrom
develop
Jul 5, 2026
Merged

Release: v2.3.0#245
themightychris merged 23 commits into
mainfrom
develop

Conversation

@github-actions

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

Copy link
Copy Markdown

Consumer-hardening release driven by the first production 2.x migration: read-your-writes freshness, streaming attachment reads, one-call byte attachments, an exposed write lock, cast-free Standard Schema typing, and 1.x-parity null handling.

Improvements

Technical

themightychris and others added 12 commits July 4, 2026 15:12
Make the existing one-liner ('Null values are omitted from the output')
explicit and complete: null/undefined-valued table keys are dropped
recursively (nested tables, and tables inside objects within arrays)
before validation and serialization, in every binding — restoring the
1.x @iarna/toml drop semantics that the Rust-core cutover regressed
(#232). A null array *element* is a typed error naming the index: TOML
arrays cannot hold nulls, and silently dropping an element (as 1.x did)
shifts sibling indices — a silent data mutation the bytes-authority
refuses. Absent key == cleared optional field is the specced
equivalence; required-field nulls fail validation as missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F5fwEknfSxpaGCVEME5D4h
Restores the 1.x @iarna/toml drop semantics the Rust-core cutover
regressed: a null/undefined (JS) or None (Python) valued table key is
dropped at the host->core marshal boundary, recursively — top-level
fields, nested tables, and objects inside arrays — so the standard
.nullable().optional() + '?? null' consumer pattern serializes
byte-identically to never setting the field.

A null array ELEMENT is still rejected, now with a typed error naming
the index: TOML arrays cannot hold nulls, and dropping an element (as
1.x silently did) shifts sibling indices — a silent data mutation, not
a semantics-preserving omission. A null value itself (a whole record,
a scalar position) also stays an error.

The rejection messages are minted in gitsheets-core (null_array_element_msg
/ null_value_msg) so the boundary's diagnostics read identically from
every binding; each binding's marshal recursion applies the same specced
rules (specs/behaviors/normalization.md 'Null / undefined handling').
Merge-patch marshalling, where null is the RFC 7396 delete sentinel, is
deliberately untouched.

Tests: core unit (message contract), napi boundary (recursive drop,
byte-parity with the pre-stripped record, element/scalar rejections,
sparse-array holes), Python mirrors, and a new 'nullish' cross-binding
byte-parity fixture proving Node and Python land on identical bytes.

Closes #232

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F5fwEknfSxpaGCVEME5D4h
Add specs/behaviors/freshness.md: non-transaction Sheet reads resolve
through a rebindable read snapshot — rebound automatically after each
commit-producing repo.transact (read-your-writes) and explicitly via
sheet.refresh() / store.refresh() / repo.refresh(). Spec the streaming
read surface: repo.readBlobStream(ref, path) resolved at call time, and
sheet.getAttachmentStream(recordOrPath, name) resolved through the
snapshot. Align indexing/transactions wording to the rebind model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F5fwEknfSxpaGCVEME5D4h
… reads (#184, #235)

Implement specs/behaviors/freshness.md host-side (the Rust core is
stateless for non-transaction reads, so no core changes):

- Sheet: mutable read snapshot + rebindReadTree (drops the memoized
  config; index builds invalidate via the existing treeHashAtBuild
  comparison); public sheet.refresh(); TypeError when tx-bound.
- Repository: weak sheet registry populated from the Sheet constructor;
  repo.refresh(); transact rebinds every live sheet to the post-commit
  HEAD tree before post-commit hooks (read-your-writes).
- Store: store.refresh() delegating to repo.refresh().
- repo.readBlobStream(ref, path): call-time-resolved streamed blob read
  with typed RefError/NotFoundError; sheet.getAttachmentStream(): the
  snapshot-resolved sibling, null when absent.

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

The real-world shape that surfaced the regression: a Standard Schema
validator normalizing cleared optionals to explicit nulls ('?? null').
Verifies the write succeeds, the read-back has absent keys (not nulls),
the on-disk bytes carry no trace of the cleared fields, re-writing the
null-cleared record over the stripped one is a byte-level no-op (no new
commit), the drop recurses through nested tables and objects inside
arrays, a null-valued required field fails validation as missing, and a
null array element is rejected with the index named and no tree
mutation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F5fwEknfSxpaGCVEME5D4h
…ion (#233)

Enumerates the three breaks that surfaced in the first production 1.x ->
2.x upgrade, each with before/after and the migration step:

1. hologit dropped — BlobObject.write + repo.hologitRepo replaced by
   repo.writeBlob(buf) + Sheet.setAttachments.
2. null/undefined-valued fields — the initial 2.x releases threw on
   marshal; fixed in this release to restore the specced 1.x drop
   semantics (recursive key drop), with the array-element edge case
   (error naming the index; 1.x silently dropped elements) called out.
3. Canonical byte re-baseline per #196 — the three value-preserving
   reformat classes and the one-time re-serialize commit recipe.

Closes #233

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

feat(api): read freshness (refresh + transact auto-refresh) and streaming blob reads
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Author

Changelog

- feat(api): read freshness (refresh + transact auto-refresh) and streaming blob reads [#239] @themightychris
- feat(api): bytes attachments, repo.withLock, and cast-free Standard Schema typing [#242] @themightychris
- fix(marshal): drop null/undefined-valued keys instead of throwing (1.x parity) [#241] @themightychris
- docs(specs): correct attachment handle naming to shipped types [#246] @themightychris

themightychris and others added 11 commits July 5, 2026 02:59
… contract (#234, #236, #237)

- setAttachment(s) values accept raw Buffer/Uint8Array bytes (and UTF-8
  strings) alongside BlobHandle — one-call attachment writes.
- repo.withLock(fn): expose the transact write mutex for non-transact
  git ops; non-reentrant with lock_held deadlock guards (new
  TransactionError code).
- Standard Schema type-level contract: gitsheets' declared
  StandardSchemaV1 mirrors the published interface exactly, so
  compliant Zod v4 / Valibot / ArkType schemas assign without casts,
  regression-guarded by a compile-time test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F5fwEknfSxpaGCVEME5D4h
Generated by: npm install -w gitsheets --save-dev zod

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F5fwEknfSxpaGCVEME5D4h
…a typing (#234, #236, #237)

- Sheet.setAttachment(s) values widen to string | Buffer | Uint8Array |
  BlobHandle — raw bytes are written to the object store during staging,
  making the common attach-these-bytes case one call (#234).
- repo.withLock(fn) exposes the transact write mutex for non-transact
  git ops; FIFO with transactions, released on settle, and explicitly
  non-reentrant — withLock-in-withLock, withLock-in-transact, and
  transact-in-withLock all throw the new TransactionError('lock_held')
  instead of deadlocking, via an AsyncLocalStorage lock context (#236).
- The exported StandardSchemaV1 types now mirror the published Standard
  Schema v1 interface exactly (PropertyKey issue paths, ~standard.types
  carrier), so compliant Zod v4 schemas assign with no as-cast; the
  compile-time contract is regression-guarded by store-zod.test.ts
  against real Zod v4, plus runtime validate/transform/reject coverage
  through the typed Store (#237).

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F5fwEknfSxpaGCVEME5D4h
feat(api): bytes attachments, repo.withLock, and cast-free Standard Schema typing
fix(marshal): drop null/undefined-valued keys instead of throwing (1.x parity)
The writeBlobFromFile example was already superseded by #242's
bytes-accepting setAttachment rewrite; this sweeps the residual
hologit-era BlobObject references to the shipped BlobHandle /
AttachmentBlobHandle types. Deliberately no file-path helper:
setAttachment(bytes) + readFile covers the case.

Closes #244

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…naming

docs(specs): correct attachment handle naming to shipped types
@themightychris themightychris changed the title Release: v2.2.1 Release: v2.3.0 Jul 5, 2026
@themightychris
themightychris merged commit e24fcf7 into main Jul 5, 2026
16 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