diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df3976..ac6b9c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [6.5.0] — 2026-07-18 + ### Added - **Direct bundle references** - `bundles.getMemberReference()` and @@ -24,6 +26,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 mutable records are cloned for callers, and no application payloads or ref state enter either cache. +### Fixed + +- **Observed RootSet compare-and-swap races** - exact failed + `update-ref --no-deref` commands with an independently observed head advance + now normalize to `ROOT_SET_CONFLICT` even when Git emits no diagnostic text. + OIDs are compared canonically so case-only differences do not create false + conflicts. +- **Bounded cache settlement** - rejected work is removed before its returned + promise settles, and a resolved value larger than its weight budget is + discarded before eviction runs, preserving retryability and unrelated + residents. + ## [6.4.0] — 2026-07-17 ### Added diff --git a/README.md b/README.md index dd5292b..6c32b04 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Unlike traditional LFS which moves files to external servers, `git-cas` treats t Existing v5 users should read [UPGRADING.md](./UPGRADING.md) and run `npm run upgrade` in dry-run mode before restoring old encrypted vault entries. For the release overview, see the -[v6.4.0 Release Notes](./docs/releases/v6.4.0.md). +[v6.5.0 Release Notes](./docs/releases/v6.5.0.md). ### 1. CLI Usage @@ -94,21 +94,21 @@ the asset reachable. The README is the front door. Detailed mechanics live in the guide set: -| Need | Start Here | -| ------------------------------------------------------- | ------------------------------------------------------------ | -| Productive library and CLI workflows | [Developer Guide](./GUIDE.md) | -| Restore memory behavior | [Streaming and restore matrix](./GUIDE.md#streaming-surface) | -| Encryption scheme selection | [Encryption Modes](./docs/ENCRYPTION_MODES.md) | -| CDC internals, Merkle manifests, KDF policy, and tuning | [Advanced Guide](./ADVANCED_GUIDE.md) | -| Ports, adapters, and collaborator boundaries | [Architecture](./ARCHITECTURE.md) | -| Assets, pages, bundles, retention, and publication | [Application storage](./docs/API.md#application-storage) | -| Temporary retention during multi-step composition | [Scoped staging workspaces](./docs/API.md#scoped-staging-workspaces) | -| GC retention for caches and derived state | [Root Sets](./docs/API.md#root-sets) | -| Managed TTL and capacity caches | [Cache Sets](./docs/API.md#cache-sets) | -| Scoped protection while consuming a cache hit | [Cache acquisitions](./docs/API.md#acquire-and-release) | -| Durable replay markers with expiry-only release | [Expiring Sets](./docs/API.md#expiring-sets) | -| Repository reachability and git-cas usage evidence | [Repository Diagnostics](./docs/API.md#repository-diagnostics) | -| v5 to v6 migration | [Upgrading](./UPGRADING.md) | +| Need | Start Here | +| ------------------------------------------------------- | -------------------------------------------------------------------- | +| Productive library and CLI workflows | [Developer Guide](./GUIDE.md) | +| Restore memory behavior | [Streaming and restore matrix](./GUIDE.md#streaming-surface) | +| Encryption scheme selection | [Encryption Modes](./docs/ENCRYPTION_MODES.md) | +| CDC internals, Merkle manifests, KDF policy, and tuning | [Advanced Guide](./ADVANCED_GUIDE.md) | +| Ports, adapters, and collaborator boundaries | [Architecture](./ARCHITECTURE.md) | +| Assets, pages, bundles, retention, and publication | [Application storage](./docs/API.md#application-storage) | +| Temporary retention during multi-step composition | [Scoped staging workspaces](./docs/API.md#scoped-staging-workspaces) | +| GC retention for caches and derived state | [Root Sets](./docs/API.md#root-sets) | +| Managed TTL and capacity caches | [Cache Sets](./docs/API.md#cache-sets) | +| Scoped protection while consuming a cache hit | [Cache acquisitions](./docs/API.md#acquire-and-release) | +| Durable replay markers with expiry-only release | [Expiring Sets](./docs/API.md#expiring-sets) | +| Repository reachability and git-cas usage evidence | [Repository Diagnostics](./docs/API.md#repository-diagnostics) | +| v5 to v6 migration | [Upgrading](./UPGRADING.md) | Core capabilities: @@ -202,6 +202,8 @@ All three runtimes are tested in CI on every push. The hexagonal architecture is reference-only cache acquisitions with explicit release and doctor evidence. - **[v6.4.0 Release Notes](./docs/releases/v6.4.0.md)**: Scoped staging workspaces for temporary multi-object reachability, promotion, and cleanup. +- **[v6.5.0 Release Notes](./docs/releases/v6.5.0.md)**: Bounded direct bundle + references, immutable metadata reuse, and corrected concurrency settlement. - **[Upgrading](./UPGRADING.md)**: Migration guide for v5 → v6. - **[Changelog](./CHANGELOG.md)**: Version history and migration notes. diff --git a/ROADMAP.md b/ROADMAP.md index 3d11a92..71cfa5d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -29,26 +29,27 @@ Repo docs are supporting records: The milestones below should exist in GitHub. If this table and GitHub disagree, GitHub wins and this file should be corrected. -| Milestone | Theme | Tracker | -| ------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -| [`v6.0.1`](https://github.com/git-stunts/git-cas/milestone/1) | Patch closeout and planning truth | [#37](https://github.com/git-stunts/git-cas/issues/37) | -| [`v6.1.0`](https://github.com/git-stunts/git-cas/milestone/2) | Bounded residency and scale hardening | [#43](https://github.com/git-stunts/git-cas/issues/43), [#44](https://github.com/git-stunts/git-cas/issues/44), [#45](https://github.com/git-stunts/git-cas/issues/45), [#48](https://github.com/git-stunts/git-cas/issues/48) | -| [`v6.2.0`](https://github.com/git-stunts/git-cas/milestone/3) | Emergency application storage and cache ownership boundary | [#50](https://github.com/git-stunts/git-cas/issues/50) | -| [`v6.3.0`](https://github.com/git-stunts/git-cas/milestone/7) | Bounded scoped cache acquisitions | [#69](https://github.com/git-stunts/git-cas/issues/69), [#70](https://github.com/git-stunts/git-cas/issues/70) | -| [`v6.4.0`](https://github.com/git-stunts/git-cas/milestone/4) | Scoped staging workspaces | [#75](https://github.com/git-stunts/git-cas/issues/75), [#77](https://github.com/git-stunts/git-cas/issues/77) | -| [`v6.4.1`](https://github.com/git-stunts/git-cas/milestone/8) | Historical bounded-residency proof closeout | [#38](https://github.com/git-stunts/git-cas/issues/38), [#46](https://github.com/git-stunts/git-cas/issues/46) | -| [`v6.5.0`](https://github.com/git-stunts/git-cas/milestone/5) | Operator TUI and agent automation follow-through | [#39](https://github.com/git-stunts/git-cas/issues/39), [#40](https://github.com/git-stunts/git-cas/issues/40) | -| [`v6.6.0`](https://github.com/git-stunts/git-cas/milestone/9) | Browser and edge read-path exploration | [#41](https://github.com/git-stunts/git-cas/issues/41) | -| [`v7.0.0`](https://github.com/git-stunts/git-cas/milestone/6) | Protocol break only if audit requires it | [#42](https://github.com/git-stunts/git-cas/issues/42), only when justified | +| Milestone | Theme | Tracker | +| -------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [`v6.0.1`](https://github.com/git-stunts/git-cas/milestone/1) | Patch closeout and planning truth | [#37](https://github.com/git-stunts/git-cas/issues/37) | +| [`v6.1.0`](https://github.com/git-stunts/git-cas/milestone/2) | Bounded residency and scale hardening | [#43](https://github.com/git-stunts/git-cas/issues/43), [#44](https://github.com/git-stunts/git-cas/issues/44), [#45](https://github.com/git-stunts/git-cas/issues/45), [#48](https://github.com/git-stunts/git-cas/issues/48) | +| [`v6.2.0`](https://github.com/git-stunts/git-cas/milestone/3) | Emergency application storage and cache ownership boundary | [#50](https://github.com/git-stunts/git-cas/issues/50) | +| [`v6.3.0`](https://github.com/git-stunts/git-cas/milestone/7) | Bounded scoped cache acquisitions | [#69](https://github.com/git-stunts/git-cas/issues/69), [#70](https://github.com/git-stunts/git-cas/issues/70) | +| [`v6.4.0`](https://github.com/git-stunts/git-cas/milestone/4) | Scoped staging workspaces | [#75](https://github.com/git-stunts/git-cas/issues/75), [#77](https://github.com/git-stunts/git-cas/issues/77) | +| [`v6.4.1`](https://github.com/git-stunts/git-cas/milestone/8) | Historical bounded-residency proof closeout | [#38](https://github.com/git-stunts/git-cas/issues/38), [#46](https://github.com/git-stunts/git-cas/issues/46) | +| [`v6.5.0`](https://github.com/git-stunts/git-cas/milestone/5) | Bounded lazy bundle references and immutable metadata reads | [#81](https://github.com/git-stunts/git-cas/issues/81) | +| [`v6.6.0`](https://github.com/git-stunts/git-cas/milestone/9) | Operator TUI and agent automation follow-through | [#39](https://github.com/git-stunts/git-cas/issues/39), [#40](https://github.com/git-stunts/git-cas/issues/40) | +| [`v6.7.0`](https://github.com/git-stunts/git-cas/milestone/10) | Browser and edge read-path exploration | [#41](https://github.com/git-stunts/git-cas/issues/41) | +| [`v7.0.0`](https://github.com/git-stunts/git-cas/milestone/6) | Protocol break only if audit requires it | [#42](https://github.com/git-stunts/git-cas/issues/42), only when justified | ## Latest Landed Design The latest landed design record is: -- [0049-scoped-staging-workspaces](./docs/design/0049-scoped-staging-workspaces/scoped-staging-workspaces.md) +- [0050-lazy-bundle-reference-reads](./docs/design/0050-lazy-bundle-reference-reads/lazy-bundle-reference-reads.md) Its GitHub goalpost issue, -[#75](https://github.com/git-stunts/git-cas/issues/75), owns the release +[#81](https://github.com/git-stunts/git-cas/issues/81), owns the release evidence. The design doc is the durable contract; GitHub records completion. The `v6.2.0` slice record is: diff --git a/STATUS.md b/STATUS.md index d7d82b4..277c0e2 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,8 +1,8 @@ # STATUS **Last tagged release:** `v6.4.0` (`2026-07-18`) -**Current release state:** `v6.4.0` is published to npm and GitHub Releases from signed tag `v6.4.0` at reviewed merge `d47af74a`. -**Latest verification:** Release run `29627828620` passed validation, unit and Node/Bun/Deno real-Git integration, OIDC trusted npm publication, and final GitHub Release creation; npm reports `6.4.0` as `latest` with SLSA provenance. +**Current release state:** `v6.5.0` release candidate; tag, npm publication, and GitHub Release remain pending the reviewed tag workflow. +**Latest verification:** the final v6.5.0 candidate passed 14/14 release-verifier steps with 6,625 observed tests across Node.js, Bun, and Deno; that verifier run's npm dry-run contained 249 files totaling 769,785 packed bytes and 2,149,364 unpacked bytes. **Playback truth:** `main` **Runtimes:** Node.js 22.x, Bun, Deno **Current planning method:** [WORKFLOW.md](./WORKFLOW.md) @@ -18,8 +18,13 @@ - The machine-facing `git cas agent` surface exists and now supports OS-keychain passphrase sources for vault-derived key flows, but parity and portability are still partial. +- **v6.5.0 candidate posture** — bounded direct bundle references, immutable + metadata reuse, and the RootSet/cache concurrency repairs are merged through + reviewed commit `d5f770fb`; npm, JSR, and runtime version metadata identify + `6.5.0`, while the tag and registry artifacts remain deliberately absent + until release-candidate review completes. - **v6.4.0 artifact posture** — signed tag `v6.4.0` resolves to reviewed merge - `d47af74`; npm reports `@git-stunts/git-cas@6.4.0` as `latest` with SLSA + `d47af74a`; npm reports `@git-stunts/git-cas@6.4.0` as `latest` with SLSA provenance, and the final GitHub Release is published. Scoped staging workspaces and the corrected structured RootSet lock retry are shipped. JSR dry-run validation is healthy, but JSR publication is not part of the release @@ -105,11 +110,11 @@ - GitHub Issues are canonical. If this section and GitHub disagree, GitHub wins and this section should be corrected. - Current release goalpost: - [#38 Bounded Residency](https://github.com/git-stunts/git-cas/issues/38) + [#81 Bounded lazy bundle references and immutable metadata reads](https://github.com/git-stunts/git-cas/issues/81) under the - [`v6.4.1` milestone](https://github.com/git-stunts/git-cas/milestone/8). + [`v6.5.0` milestone](https://github.com/git-stunts/git-cas/milestone/5). - The latest landed design record is - [0049-scoped-staging-workspaces](./docs/design/0049-scoped-staging-workspaces/scoped-staging-workspaces.md). + [0050-lazy-bundle-reference-reads](./docs/design/0050-lazy-bundle-reference-reads/lazy-bundle-reference-reads.md). ## Read Next diff --git a/UPGRADING.md b/UPGRADING.md index 9e72caf..114bba0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,51 @@ v6.0.0 is a major release that simplifies the encryption model, hardens security defaults, and cleans up the architecture. This guide covers every breaking change and what you need to do. +## v6.4.0 To v6.5.0 + +v6.5.0 is API-additive and does not require stored-data migration. It adds +bounded reference reads for callers that need a member handle without +recursively resolving that member's complete support graph. + +Use `getMemberReference()` for one exact path: + +```javascript +const reference = await cas.bundles.getMemberReference({ + handle: materializationBundle, + path: 'nodes/user-alice.cbor', +}); + +if (reference) { + await consumeRetainedHandle(reference.handle); +} +``` + +Use `iterateMemberReferences()` for a streaming structural scan: + +```javascript +for await (const reference of cas.bundles.iterateMemberReferences({ + handle: materializationBundle, +})) { + await indexReference(reference); +} +``` + +These APIs validate the bundle root, every traversed descriptor summary, the +selected or enumerated Git tree edges, and each direct target object type. They +do not recursively validate nested page, asset, or bundle support graphs. Keep +using `getMember()` or `iterateMembers()` when the operation requires complete +recursive validation and computed `logicalBytes`. + +Reference results are observations, not retention claims. Keep the containing +bundle reachable through a workspace, cache acquisition, RootSet, or other +documented lifetime while consuming them. The immutable metadata and +descriptor caches introduced in v6.5.0 are bounded internal implementation +details; callers do not manage or invalidate them. + +See [v6.5.0 Release Notes](./docs/releases/v6.5.0.md) and +[Application Storage](./docs/API.md#application-storage) for the +complete integrity and lifetime contract. + ## v6.3.0 To v6.4.0 v6.4.0 is API-additive and does not require stored-data migration. It adds @@ -198,13 +243,13 @@ for example a job workspace or tenant-scoped artifact directory. v5 had 5 encryption scheme identifiers. v6 has 3: -| v5 Scheme | v6 Scheme | Migration | -|---|---|---| -| `whole-v1` | `whole` | **Re-encryption required** (v1 had no AAD) | -| `whole-v2` | `whole` | Rename only (already had AAD) | -| `framed-v1` | `framed` | **Re-encryption required** (v1 had no AAD) | -| `framed-v2` | `framed` | Rename only (already had AAD) | -| `convergent-v1` | `convergent` | Rename only | +| v5 Scheme | v6 Scheme | Migration | +| --------------- | ------------ | ------------------------------------------ | +| `whole-v1` | `whole` | **Re-encryption required** (v1 had no AAD) | +| `whole-v2` | `whole` | Rename only (already had AAD) | +| `framed-v1` | `framed` | **Re-encryption required** (v1 had no AAD) | +| `framed-v2` | `framed` | Rename only (already had AAD) | +| `convergent-v1` | `convergent` | Rename only | ### Why @@ -285,11 +330,11 @@ verifier and the next keyed vault write will add it. ### What Changed -| Scenario | v5 Default | v6 Default | -|---|---|---| -| CDC chunking + encryption | `framed-v1` | `convergent` | -| Fixed chunking + encryption | `framed-v1` | `framed` | -| Explicit `whole` | `whole-v1` | `whole` | +| Scenario | v5 Default | v6 Default | +| --------------------------- | ----------- | ------------ | +| CDC chunking + encryption | `framed-v1` | `convergent` | +| Fixed chunking + encryption | `framed-v1` | `framed` | +| Explicit `whole` | `whole-v1` | `whole` | ### What This Means @@ -382,14 +427,14 @@ import { SCHEME_WHOLE, SCHEME_FRAMED, SCHEME_CONVERGENT } from '@git-stunts/git- ### Behavioral Changes -| Change | Impact | -|---|---| -| Plaintext + gzip restore now streams | Lower memory usage. Should be transparent. | -| AAD always on for `whole` and `framed` | Cannot opt out. v1-style no-AAD is gone. | +| Change | Impact | +| ---------------------------------------- | ------------------------------------------------------------------------------------- | +| Plaintext + gzip restore now streams | This lowers memory usage and should be transparent. | +| AAD always on for `whole` and `framed` | Cannot opt out. v1-style no-AAD is gone. | | Manifest integrity hash verified on read | Corrupted manifests that previously loaded will now throw `MANIFEST_INTEGRITY_ERROR`. | -| KDF policy enforced in `deriveKey()` | Dangerously weak params (e.g., 1 PBKDF2 iteration) now throw `KDF_POLICY_VIOLATION`. | -| Concurrency capped at 64 | Was unbounded. Unlikely to affect real usage. | -| frameBytes capped at 64 MiB | Was unbounded. Unlikely to affect real usage. | +| KDF policy enforced in `deriveKey()` | Dangerously weak params (e.g., 1 PBKDF2 iteration) now throw `KDF_POLICY_VIOLATION`. | +| Concurrency capped at 64 | Was unbounded. Unlikely to affect real usage. | +| frameBytes capped at 64 MiB | Was unbounded. Unlikely to affect real usage. | --- diff --git a/docs/design/0050-lazy-bundle-reference-reads/witness/release-candidate.md b/docs/design/0050-lazy-bundle-reference-reads/witness/release-candidate.md new file mode 100644 index 0000000..fad9c6a --- /dev/null +++ b/docs/design/0050-lazy-bundle-reference-reads/witness/release-candidate.md @@ -0,0 +1,109 @@ +# PERF-0050 v6.5.0 Release Candidate Witness + +Date: 2026-07-18 + +Issue: #81 + +Review: #82 + +## Scope + +This witness records the pre-publication release candidate for bounded lazy +bundle references and immutable metadata reads. It does not claim that a +`v6.5.0` tag, npm artifact, or GitHub Release exists. This is an explicitly +unpublished candidate. + +The candidate: + +- sets npm, JSR, and the runtime `PACKAGE_VERSION` export to `6.5.0`; +- moves the direct-reference API and bounded immutable reuse from `Unreleased` + to `6.5.0`; +- packages and links `docs/releases/v6.5.0.md`; +- adds v6.4.0-to-v6.5.0 adoption guidance to `UPGRADING.md`; +- records an honest release-candidate posture in `STATUS.md`; +- preserves the v6.4.0 publication witness as immutable history; and +- records the emergency milestone resequencing without treating unfinished + TUI, agent, or browser work as shipped. + +## Implementation Provenance + +| Capability | Review anchor | Commit | +| ----------------------------------------------------- | ------------- | ------------------------------------------ | +| Direct bundle references and immutable metadata reuse | #82 | `d5f770fb4589cd33d2244c83232ee7ede1df9ec1` | +| Empty-diagnostic RootSet conflict recovery | #82 | `d7841acbaffbc4c5b14c78d31d3dc65ac4618cce` | +| Canonical OID and oversized-cache review repairs | #82 | `b7695abc5d87038d9c79f42fc3c77c3580008a24` | +| Rejection settlement ordering | #82 | `f1d219973d40bfa8a728093d39f793c3486037ad` | + +The public facade exposes direct lookup and reference iteration while preserving +the existing complete-validation APIs. + +[cite: `index.js#216-224@d5f770fb4589cd33d2244c83232ee7ede1df9ec1`] +[cite: `index.d.ts#1460-1501@d5f770fb4589cd33d2244c83232ee7ede1df9ec1`] + +Selected traversal validates descriptor summaries, bounded descriptor bytes, +traversed tree edges, and the direct target type without recursively resolving +the selected member's support graph. + +[cite: `src/domain/services/BundleService.js#111-152@d5f770fb4589cd33d2244c83232ee7ede1df9ec1`] +[cite: `src/domain/services/BundleService.js#463-492@d5f770fb4589cd33d2244c83232ee7ede1df9ec1`] + +The bounded cache removes rejected and individually oversized work before +settlement can evict valid residents. RootSet recovery compares canonical OIDs +and requires the exact failed CAS command plus an observed head advance. + +[cite: `src/helpers/boundedPromiseCache.js#51-78@d5f770fb4589cd33d2244c83232ee7ede1df9ec1`] +[cite: `src/domain/services/RootSetPersistence.js#49-65@d5f770fb4589cd33d2244c83232ee7ede1df9ec1`] + +## Verification + +The final versioned candidate passed the complete verifier under the supported +Node.js 22 runtime: + +```bash +mise exec node@22.23.0 -- pnpm run release:verify +``` + +| Step | Result | Tests | +| --------------------------- | ------ | ----: | +| Lint | PASS | - | +| Unit Tests (Node) | PASS | 2,020 | +| Example: store-and-restore | PASS | - | +| Example: encrypted-workflow | PASS | - | +| Example: progress-tracking | PASS | - | +| Unit Tests (Bun) | PASS | 2,019 | +| Unit Tests (Deno) | PASS | 2,010 | +| Public type compatibility | PASS | - | +| Integration Tests (Node) | PASS | 192 | +| Integration Tests (Bun) | PASS | 192 | +| Integration Tests (Deno) | PASS | 192 | +| Build metadata stamp | PASS | - | +| npm pack dry-run | PASS | - | +| JSR publish dry-run | PASS | - | + +The verifier passed 14/14 steps with 6,625 observed tests. That verifier run's +npm dry-run contained 249 files totaling 769,785 packed bytes and 2,149,364 +unpacked bytes. Supporting evidence also includes changed unit coverage at +87/87 tests, the guarded RootSet replacement stress at 500/500 two-writer +races, and GitHub CI plus CodeRabbit review with zero unresolved threads. + +The command-count integration test requires an identical warm direct read to +issue zero additional Git metadata commands and requires reference iteration to +beat complete recursive validation. + +[cite: `test/integration/bundle-reference-performance.test.js#111-151@d5f770fb4589cd33d2244c83232ee7ede1df9ec1`] +[cite: `scripts/diagnostics/stress-root-set-replacement.js#1-54@d5f770fb4589cd33d2244c83232ee7ede1df9ec1`] + +## Publication Gate + +Publication remains blocked until all of the following are true: + +1. the release PR passes GitHub CI, self-review, Code Lawyer review, and the + agreed CodeRabbit posture; +2. the release PR is merged without unresolved findings; +3. a signed annotated `v6.5.0` tag points at the reviewed merge commit; +4. the release workflow passes version validation and runtime tests; +5. npm reports `@git-stunts/git-cas@6.5.0` with provenance; and +6. GitHub reports the final non-draft `v6.5.0` Release. + +Downstream git-warp adoption, runtime-scoped acquisition reuse, and its CPU and +bounded-memory performance gates remain separate post-publication evidence. diff --git a/docs/releases/v6.5.0.md b/docs/releases/v6.5.0.md new file mode 100644 index 0000000..e02274b --- /dev/null +++ b/docs/releases/v6.5.0.md @@ -0,0 +1,140 @@ +# git-cas v6.5.0 Release Notes + +v6.5.0 adds bounded direct bundle-reference reads for applications that need to +locate or enumerate retained materialization handles without recursively +hydrating every referenced support graph. It also adds bounded reuse for +immutable Git metadata and bundle descriptors, and repairs two concurrency +edge cases found while proving the new path under stress. + +This release is API-additive. It does not change persisted bundle formats and +does not require stored-data migration. + +## Direct Bundle References + +Read one exact member descriptor without opening its payload or recursively +resolving the member's complete support graph: + +```javascript +const reference = await cas.bundles.getMemberReference({ + handle: materializationBundle, + path: 'nodes/user-alice.cbor', +}); + +if (reference) { + console.log(reference.handle); + console.log(reference.type); + console.log(reference.size); +} +``` + +Stream canonical member descriptors without collecting the whole bundle: + +```javascript +for await (const reference of cas.bundles.iterateMemberReferences({ + handle: materializationBundle, +})) { + await indexReference(reference); +} +``` + +Each `BundleMemberReference` contains: + +- `version: 1`; +- the canonical member `path`; +- the typed application `handle`; +- the direct Git object `type`; and +- the declared byte `size`, or `null` for structured tree members. + +The existing `getMember()` and `iterateMembers()` APIs retain complete recursive +validation semantics and include computed `logicalBytes`. + +## Integrity Boundary + +The direct path validates: + +- the bundle handle codec and root tree type; +- the root descriptor and persisted read limits; +- every descriptor and summary along the selected path, or across the streamed + structural traversal; +- every traversed fanout tree edge; +- the selected or enumerated leaf edge; and +- each direct target Git object type. + +It does not recursively validate a selected page, asset, or nested bundle's +complete support graph. Callers that require that stronger proof must continue +to use `getMember()` or `iterateMembers()`. + +A reference is also not a retention witness. The containing bundle must remain +reachable through a workspace, RootSet, cache acquisition, publication, or +other documented lifetime while the reference is consumed. + +## Bounded Immutable Reuse + +Repeated exact tree-entry and object-info reads now share successful in-flight +and completed work inside each Git persistence adapter. The default metadata +cache retains at most 2,048 immutable entries and can be lowered with the +`metadataCacheEntries` constructor option. + +Bundle descriptors use a separate cache bounded by both 1,024 entries and 16 +MiB. Returned byte arrays and mutable metadata records are copied for callers. +Rejected work removes itself so a later read can retry. Values larger than the +descriptor byte budget are returned to the current caller but are not retained +and do not evict unrelated residents. + +These caches contain immutable Git metadata and structural descriptors only. +They do not retain application payload bytes, mutable refs, cache indexes, or +root-set state. + +## Concurrency Repairs + +The RootSet compare-and-swap classifier now recovers exact failed +`git update-ref --no-deref` races when Git exits fatally without diagnostic +text, provided an independent ref read proves that the managed head advanced. +OID comparisons use canonical lowercase values, avoiding false conflicts from +case-only differences. A checked-in real-Git diagnostic passed 500/500 +two-writer replacement races after the repair. + +The bounded promise cache now removes rejected work before the returned promise +settles, preserving immediate retry semantics. Individually oversized resolved +values are discarded before weight-based eviction runs, preserving unrelated +residents. + +## Performance Evidence + +The stable regression contract counts real Git commands rather than asserting +portable wall-clock latency: + +- a cold direct read must perform metadata work; +- an identical warm direct read must perform zero additional Git metadata + commands; and +- direct reference iteration must issue fewer total and object-inspection + commands than complete recursive validation. + +On the recorded four-node git-warp fixture, the final direct-reference and +descriptor-cache path reduced one selected read from 192 Git commands to 69, +approximately 64 percent. The same host sample reduced the observed wall time +from approximately 3-5 seconds to 1.291 seconds. These timings are diagnostic, +not cross-machine promises; command-count integration is the merge gate. + +## Verification + +The final release candidate passed 14/14 verifier steps with 6,625 observed +tests: + +- Node.js: 2,020 unit tests and 192 real-Git integration tests; +- Bun: 2,019 unit tests and 192 real-Git integration tests; and +- Deno: 2,010 unit tests and 192 real-Git integration tests. + +The verifier also passed lint, examples, public type compatibility, build +stamping, npm package inspection, and the JSR publish dry-run. The checked-in +RootSet race diagnostic passed 500/500 two-writer replacement races. + +## Upgrade Notes + +No stored-data migration is required. Existing complete-validation calls keep +their behavior. Applications may adopt the new reference APIs selectively where +their own retained lifetime already guarantees that recursive support remains +available. + +See [UPGRADING.md](../../UPGRADING.md) for the adoption decision and +[Application Storage](../API.md#application-storage) for the full API contract. diff --git a/jsr.json b/jsr.json index cc05137..d690e1e 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@git-stunts/git-cas", - "version": "6.4.0", + "version": "6.5.0", "exports": { ".": "./index.js", "./service": "./src/domain/services/CasService.js", diff --git a/package.json b/package.json index e31f5e1..57d74b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@git-stunts/git-cas", - "version": "6.4.0", + "version": "6.5.0", "description": "Content-addressed storage backed by Git's object database, with optional encryption and pluggable codecs", "type": "module", "main": "index.js", @@ -36,6 +36,7 @@ "docs/releases/v6.2.0.md", "docs/releases/v6.3.0.md", "docs/releases/v6.4.0.md", + "docs/releases/v6.5.0.md", "docs/THREAT_MODEL.md", "docs/VAULT_INTERNALS.md", "docs/WALKTHROUGH.md", diff --git a/src/package-version.js b/src/package-version.js index 738548e..e2d056e 100644 --- a/src/package-version.js +++ b/src/package-version.js @@ -1 +1 @@ -export const PACKAGE_VERSION = '6.4.0'; +export const PACKAGE_VERSION = '6.5.0'; diff --git a/test/unit/docs/package-docs.test.js b/test/unit/docs/package-docs.test.js index 6ecf545..c2f8e4c 100644 --- a/test/unit/docs/package-docs.test.js +++ b/test/unit/docs/package-docs.test.js @@ -14,16 +14,10 @@ const requiredStandardDocs = [ 'docs/releases/v6.2.0.md', 'docs/releases/v6.3.0.md', 'docs/releases/v6.4.0.md', + 'docs/releases/v6.5.0.md', ]; -const forbiddenPackagePrefixes = [ - 'docs/audit/', - 'docs/archive/', - 'docs/method/', -]; -const forbiddenPackageFiles = [ - 'docs/cli.gif', - 'docs/vault.gif', -]; +const forbiddenPackagePrefixes = ['docs/audit/', 'docs/archive/', 'docs/method/']; +const forbiddenPackageFiles = ['docs/cli.gif', 'docs/vault.gif']; function read(relPath) { return readFileSync(path.join(repoRoot, relPath), 'utf8'); @@ -91,6 +85,7 @@ function publicPackagedMarkdownFiles(files) { 'docs/releases/v6.2.0.md', 'docs/releases/v6.3.0.md', 'docs/releases/v6.4.0.md', + 'docs/releases/v6.5.0.md', 'docs/THREAT_MODEL.md', 'docs/WALKTHROUGH.md', ].filter((file) => files.has(file)); @@ -138,8 +133,9 @@ describe('package documentation surface', () => { it('keeps local documentation links closed across public packaged Markdown files', () => { const files = packedFiles(); - const missing = publicPackagedMarkdownFiles(files) - .flatMap((file) => unpackagedLinkedDocs(file, files)); + const missing = publicPackagedMarkdownFiles(files).flatMap((file) => + unpackagedLinkedDocs(file, files) + ); expect(missing).toEqual([]); }); diff --git a/test/unit/docs/release-state.test.js b/test/unit/docs/release-state.test.js index cff5f03..6c6b126 100644 --- a/test/unit/docs/release-state.test.js +++ b/test/unit/docs/release-state.test.js @@ -3,14 +3,14 @@ import { existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; const repoRoot = process.cwd(); -const v640CandidateMarker = '**Current release state:** `v6.4.0` release candidate'; -const v640PublishedMarker = '**Current release state:** `v6.4.0` is published'; -const v640CandidatePath = - 'docs/design/0049-scoped-staging-workspaces/witness/release-candidate.md'; +const v650CandidateMarker = '**Current release state:** `v6.5.0` release candidate'; +const v650PublishedMarker = '**Current release state:** `v6.5.0` is published'; +const v650CandidatePath = + 'docs/design/0050-lazy-bundle-reference-reads/witness/release-candidate.md'; +const v650PublicationPath = + 'docs/design/0050-lazy-bundle-reference-reads/witness/release-publication.md'; const v640PublicationPath = 'docs/design/0049-scoped-staging-workspaces/witness/release-publication.md'; -const v630PublicationPath = - 'docs/design/0048-scoped-cache-acquisitions/witness/release-publication.md'; function read(relPath) { return readFileSync(path.join(repoRoot, relPath), 'utf8'); @@ -24,16 +24,16 @@ function v6Heading(changelog) { return changelog.match(/^## \[6\.0\.0\] — (.+)$/m)?.[1]; } -function expectNoV640PublicationEvidence(...documents) { +function expectNoV650PublicationEvidence(...documents) { const forbiddenMarkers = [ - '**Last tagged release:** `v6.4.0`', - '**Current release state:** `v6.4.0` is published', - '- Signed annotated tag: `v6.4.0`', - 'https://github.com/git-stunts/git-cas/releases/tag/v6.4.0', + '**Last tagged release:** `v6.5.0`', + '**Current release state:** `v6.5.0` is published', + '- Signed annotated tag: `v6.5.0`', + 'https://github.com/git-stunts/git-cas/releases/tag/v6.5.0', '## npm Registry Evidence', - '| Package | `@git-stunts/git-cas@6.4.0` |', - '| Dist-tag | `latest` -> `6.4.0` |', - 'attestations/@git-stunts%2fgit-cas@6.4.0' + '| Package | `@git-stunts/git-cas@6.5.0` |', + '| Dist-tag | `latest` -> `6.5.0` |', + 'attestations/@git-stunts%2fgit-cas@6.5.0', ]; for (const document of documents) { @@ -43,39 +43,36 @@ function expectNoV640PublicationEvidence(...documents) { } } -function expectV640CandidateState(status, candidate, publication) { - expect(status).toContain('**Last tagged release:** `v6.3.0` (`2026-07-17`)'); - expect(status).toContain(v640CandidateMarker); +function expectV650CandidateState(status, candidate, publication) { + expect(status).toContain('**Last tagged release:** `v6.4.0` (`2026-07-18`)'); + expect(status).toContain(v650CandidateMarker); expect(status).toContain('remain pending the reviewed tag workflow'); - expect(status).toContain('#75 Scoped staging workspaces for multi-object promotion'); - expect(candidate).toContain('# PERF-0049 v6.4.0 Release Candidate Witness'); - expect(candidate).toContain('1ac2fc85be857ca769c459b89c29bf4483b3f304'); - expect(candidate).toContain('8185dfb9819909d9fbe0f0394de6ae31fc0a94a3'); - expect(candidate).toContain('passed 14/14 steps with 6,538 observed tests'); - expect(candidate).toContain('247 files, 763,925 packed bytes'); - expect(candidate).toContain('2,129,717 unpacked bytes'); - expect(candidate).toContain('explicitly unpublished candidate'); + expect(status).toContain('#81 Bounded lazy bundle references and immutable metadata reads'); + expect(candidate).toContain('# PERF-0050 v6.5.0 Release Candidate Witness'); + expect(candidate).toContain('d5f770fb4589cd33d2244c83232ee7ede1df9ec1'); + expect(candidate).toMatch(/passed 14\/14 steps with 6,625 observed tests/); + expect(candidate).toContain('249 files totaling 769,785 packed bytes'); + expect(candidate).toMatch(/2,149,364\s+unpacked bytes/); + expect(candidate).toMatch(/500\/500 two-writer\s+races/); + expect(candidate).toMatch(/explicitly\s+unpublished candidate/); expect(publication).toBeNull(); - expectNoV640PublicationEvidence(status, candidate); + expectNoV650PublicationEvidence(status, candidate); } -function expectV640PublishedState(status, publication) { - expect(status).toMatch(/\*\*Last tagged release:\*\* `v6\.4\.0` \(`\d{4}-\d{2}-\d{2}`\)/); - expect(status).toContain(v640PublishedMarker); - expect(status).toContain('#38 Bounded Residency'); - expect(status).toContain('[`v6.4.1` milestone]'); - expect(publication).toContain('# PERF-0049 v6.4.0 Publication Witness'); - expect(publication).toContain('- Signed annotated tag: `v6.4.0`'); - expect(publication).toContain('https://github.com/git-stunts/git-cas/releases/tag/v6.4.0'); - expect(publication).toContain('| Package | `@git-stunts/git-cas@6.4.0` |'); - expect(publication).toContain('| Dist-tag | `latest` -> `6.4.0` |'); - expect(publication).toContain('attestations/@git-stunts%2fgit-cas@6.4.0'); +function expectV650PublishedState(status, publication) { + expect(status).toMatch(/\*\*Last tagged release:\*\* `v6\.5\.0` \(`\d{4}-\d{2}-\d{2}`\)/); + expect(status).toContain(v650PublishedMarker); + expect(status).toContain('#39 v6.6.0: Operator TUI'); + expect(status).toContain('[`v6.6.0` milestone]'); + expect(publication).toContain('# PERF-0050 v6.5.0 Publication Witness'); + expect(publication).toContain('- Signed annotated tag: `v6.5.0`'); + expect(publication).toContain('https://github.com/git-stunts/git-cas/releases/tag/v6.5.0'); + expect(publication).toContain('| Package | `@git-stunts/git-cas@6.5.0` |'); + expect(publication).toContain('| Dist-tag | `latest` -> `6.5.0` |'); + expect(publication).toContain('attestations/@git-stunts%2fgit-cas@6.5.0'); } -function expectCurrentV640PublicationEvidence(status, publication) { - expect(status).toContain('**Last tagged release:** `v6.4.0` (`2026-07-18`)'); - expect(status).toContain('d47af74a'); - expect(status).toContain('Release run `29627828620`'); +function expectCurrentV640PublicationEvidence(publication) { expect(publication).toContain('d47af74a288ef362dba684536cff11c063cfdcc3'); expect(publication).toContain('3545b8aa81e395e832f112214b301dbe53d9576f'); expect(publication).toContain('01A63D8E9DBEEDE32918AF9C39560E0406CA9135'); @@ -87,55 +84,52 @@ function expectCurrentV640PublicationEvidence(status, publication) { expect(publication).toContain('2,129,716'); } -function expectV640Lifecycle(status, candidate, publication) { - const isCandidate = status.includes(v640CandidateMarker); - const isPublished = status.includes(v640PublishedMarker); +function expectV650Lifecycle(status, candidate, publication) { + const isCandidate = status.includes(v650CandidateMarker); + const isPublished = status.includes(v650PublishedMarker); expect(Number(isCandidate) + Number(isPublished)).toBe(1); if (isCandidate) { - expectV640CandidateState(status, candidate, publication); + expectV650CandidateState(status, candidate, publication); return; } expect(publication).not.toBeNull(); - expectV640PublishedState(status, publication); + expectV650PublishedState(status, publication); } -function expectFutureV640PublicationState() { +function expectFutureV650PublicationState() { const status = [ - '**Last tagged release:** `v6.4.0` (`2026-07-17`)', - `${v640PublishedMarker} to npm with provenance and to GitHub Releases.`, - '#38 Bounded Residency', - '[`v6.4.1` milestone]' + '**Last tagged release:** `v6.5.0` (`2026-07-18`)', + `${v650PublishedMarker} to npm with provenance and to GitHub Releases.`, + '#39 v6.6.0: Operator TUI', + '[`v6.6.0` milestone]', ].join('\n'); const publication = [ - '# PERF-0049 v6.4.0 Publication Witness', - '- Signed annotated tag: `v6.4.0`', - 'https://github.com/git-stunts/git-cas/releases/tag/v6.4.0', - '| Package | `@git-stunts/git-cas@6.4.0` |', - '| Dist-tag | `latest` -> `6.4.0` |', - 'attestations/@git-stunts%2fgit-cas@6.4.0' + '# PERF-0050 v6.5.0 Publication Witness', + '- Signed annotated tag: `v6.5.0`', + 'https://github.com/git-stunts/git-cas/releases/tag/v6.5.0', + '| Package | `@git-stunts/git-cas@6.5.0` |', + '| Dist-tag | `latest` -> `6.5.0` |', + 'attestations/@git-stunts%2fgit-cas@6.5.0', ].join('\n'); - expectV640Lifecycle(status, '', publication); + expectV650Lifecycle(status, '', publication); } describe('release state docs', () => { - it('enforces the v6.4.0 candidate-to-publication lifecycle', () => { + it('enforces the v6.5.0 candidate-to-publication lifecycle', () => { const status = read('STATUS.md'); - const candidate = read(v640CandidatePath); - const publication = readOptional(v640PublicationPath); - const v630Publication = read(v630PublicationPath); + const candidate = read(v650CandidatePath); + const publication = readOptional(v650PublicationPath); + const v640Publication = read(v640PublicationPath); - expectV640Lifecycle(status, candidate, publication); - if (publication !== null) { - expectCurrentV640PublicationEvidence(status, publication); - } - expectFutureV640PublicationState(); + expectV650Lifecycle(status, candidate, publication); + expectFutureV650PublicationState(); + expectCurrentV640PublicationEvidence(v640Publication); expect(status).toContain('Current release goalpost:'); - expect(v630Publication).toContain('33f4171f6b69d75110de834f9a75d64e2d14e1a3'); - expect(v630Publication).toContain('sha512-Cl/WPjj60LvjXl3BqSb1M3a0tx2xpx6KxGEC1TXKekNzgn5so/t43LG7Qz2XuXle+YmXWoCi8H94cJYvfgI8Yw=='); - expect(v630Publication).toContain('https://slsa.dev/provenance/v1'); - expect(v630Publication).toContain('https://github.com/git-stunts/git-cas/releases/tag/v6.3.0'); + expect(status).toContain('d47af74a'); + expect(v640Publication).toContain('https://slsa.dev/provenance/v1'); + expect(v640Publication).toContain('https://github.com/git-stunts/git-cas/releases/tag/v6.4.0'); }); }); diff --git a/test/unit/docs/release-truth.test.js b/test/unit/docs/release-truth.test.js index 8bde995..6c29253 100644 --- a/test/unit/docs/release-truth.test.js +++ b/test/unit/docs/release-truth.test.js @@ -168,7 +168,9 @@ describe('root-set release documentation', () => { expect(api).toContain('| Root set | anchored while present | current generation only |'); expect(api).toContain('`pinned` does not create a\npack `.keep` file'); expect(api).toContain('await rootSet.repair({ entries: authoritativeLiveEntries });'); - expect(upgrading).toContain('adopt every\nstill-live OID before running destructive Git cleanup'); + expect(upgrading).toContain( + 'adopt every\nstill-live OID before running destructive Git cleanup' + ); }); }); @@ -215,6 +217,22 @@ describe('scoped staging workspace release documentation', () => { }); }); +describe('lazy bundle reference release documentation', () => { + it('ships and links the v6.5.0 bounded-read contract', () => { + const readme = read('README.md'); + const upgrading = read('UPGRADING.md'); + const releaseNotes = read('docs/releases/v6.5.0.md'); + + expect(readme).toContain('[v6.5.0 Release Notes](./docs/releases/v6.5.0.md)'); + expect(upgrading).toContain('bundles.getMemberReference'); + expect(upgrading).toContain('observations, not retention claims'); + expect(releaseNotes).toContain('# git-cas v6.5.0 Release Notes'); + expect(releaseNotes).toContain('iterateMemberReferences'); + expect(releaseNotes).toContain('does not recursively validate'); + expect(releaseNotes).toContain('500/500'); + }); +}); + describe('advanced guide rendering', () => { it('keeps the table of contents rendered as Markdown links', () => { const advancedGuide = read('ADVANCED_GUIDE.md');