Skip to content
Merged
9 changes: 9 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ The facade is orchestration glue. It is not the storage engine itself.
bundles build deterministic bounded-fanout trees and support targeted member
traversal without hydrating the complete structure.

- **`StagingWorkspaceRegistry` and `StagingWorkspace`** — own renewable
temporary RootSet generations for multi-step application construction.
`WorkspaceCompoundAdmission` and `WorkspaceCompoundScope` serialize an
explicitly bounded sequence of provisional page and bundle batches through
one operation-owned persistence view, then install the union of prior and new
targets in one exact generation. Existing workspace methods retain each
result independently and remain the boundary when a handle leaves private
construction code before later writes begin.

- **`RetentionService` and `PublicationService`** — validate complete handle
graphs, then either retain them in a RootSet with generation-scoped evidence
or publish them atomically under an allowlisted application ref.
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Compound staging-workspace admission** - `workspace.batch()` runs an
explicitly operation-bounded sequence of dependent page and ordered-bundle
batches through one private persistence scope, then returns the callback
value only after one exact workspace generation retains every staged handle.
Scope calls serialize by invocation order, return frozen handle arrays, stop
queued work after failure, and preserve the prior generation on refusal.

### Performance

- **One retained generation for dependent write waves** - a clean five-sample
SHA-1/SHA-256 witness reduced a 33-operation, 81-handle graph from 200 to 23
Git child processes and from 33 workspace commits and checked ref updates to
one, with identical application-handle digests. Median wall time fell by
80.5% on the measured host. Compound admission changes workspace-ref update
frequency, while object bytes, handle identity, ref layout and namespaces,
readers, and existing independently retained workspace methods remain
compatible.

## [6.5.8] — 2026-08-23

### Added
Expand Down
25 changes: 25 additions & 0 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,31 @@ no partial result array on failure. Higher `maxBatchAssets` values can reduce
protocol round trips when the caller can afford more simultaneously live
source pipelines; the default remains four.

When later bundle waves depend on handles created by earlier waves, keep the
intermediate handles inside one compound staging-workspace operation:

```js
const admitted = await workspace.batch({
maxOperations: 3,
operation: async (scope) => {
const pages = await scope.pages.putBatch({ pages: pageRequests });
const leaves = await scope.bundles.putOrderedBatch({
bundles: leafRequests(pages),
});
return (await scope.bundles.putOrderedBatch({
bundles: [rootRequest(leaves)],
}))[0];
},
});
```

`admitted.value` becomes caller-visible only with `admitted.retention`, after
one exact workspace generation anchors every staged handle. The default
operation ceiling is 64 and the hard ceiling is 1,024; each scope call also
preserves its ordinary page or bundle batch bounds. Use the existing
independently retained workspace methods when intermediate handles must leave
the private callback.

Repeated `pages.get()` calls reuse immutable payload reads within the store's
bounded page cache. The defaults retain at most 128 payloads and 8 MiB; use
`pageCacheEntries` and `pageCacheBytes` to tune that ceiling. Every result is a
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ Unlike traditional LFS which moves files to external servers, `git-cas` treats t
blob reads at or below a fixed 10 MiB ceiling use one bounded session read;
larger payloads retain the genuine one-shot streaming path. Explicitly
bounded page, asset, and ordered-bundle batches pipeline independent Git
writes and retain each successful workspace batch under one exact generation
without changing content identity.
writes. `workspace.batch()` can compose dependent page and bundle waves in
one private persistence scope and retain their union under one exact final
generation without changing content identity.
- **Key Lifecycle**: Envelope encryption separates DEKs from KEKs. Rotate passphrases across an entire vault without re-encrypting data blobs. Privacy mode HMAC-hashes slug names to prevent metadata discovery.
- **Runtime-Adaptive**: A single core supports Node.js 22+, Bun, and Deno through a strict hexagonal port architecture with runtime-specific crypto adapters.

Expand Down Expand Up @@ -157,9 +158,10 @@ Core capabilities:
compare-and-swap refs, and immutable lifecycle evidence.
- **Scoped staging workspaces**: `workspaces.open()` mirrors application writes
behind one renewable temporary RootSet, supports one-generation bounded
asset/page/bundle batches, returns only after each handle is anchored, promotes
destination-first, and exposes bounded age, expiry, logical-content, and
direct-root diagnostics with opaque cleanup pagination.
asset/page/bundle batches plus compound dependent page/bundle admission,
returns only after each public result is anchored, promotes destination-first,
and exposes bounded age, expiry, logical-content, and direct-root diagnostics
with opaque cleanup pagination.
- **Envelope recipients**: multi-recipient key wrapping and recipient rotation
avoid re-encrypting data blobs.
- **Operational diagnostics**: `cas.diagnostics.doctor()` streams repository
Expand Down
39 changes: 39 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,45 @@

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.5.8 To v6.5.9

v6.5.9 adds `workspace.batch()` and requires no application or stored-data
migration. Existing handles, object bytes, workspace descriptors, ref
namespaces, retention witnesses, read paths, and independently retained
workspace methods remain compatible. Existing repositories open in place.

Use the new method only when intermediate page and bundle handles remain
private to one bounded construction:

```js
const admitted = await workspace.batch({
maxOperations: 3,
operation: async (scope) => {
const pages = await scope.pages.putBatch({ pages: pageRequests });
const leaves = await scope.bundles.putOrderedBatch({
bundles: buildLeafRequests(pages),
});
return (await scope.bundles.putOrderedBatch({
bundles: [buildRootRequest(leaves)],
}))[0];
},
});

admitted.value; // retained root BundleHandle
admitted.retention; // exact final workspace generation and witnesses
```

The operation defaults to at most 64 scope calls and cannot exceed 1,024.
Every page or bundle call retains its existing count, object, member, and byte
bounds. A callback or staged-write failure publishes no compound generation;
immutable objects written before failure remain unreachable for Git's normal
reclamation. Use the existing workspace methods when a handle must become
independently retained before arbitrary caller code observes it.

The release changes physical admission cost only. It does not introduce a new
transaction format, migration command, authority cutover, or mixed-version
rewrite.

## v6.5.7 To v6.5.8

v6.5.8 adds bounded application-write batches and requires no application or
Expand Down
49 changes: 49 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,55 @@ await workspace.bundles.putOrdered(options);
await workspace.bundles.putOrderedBatch(options);
```

Dependent page and bundle waves can instead share one bounded compound
admission:

```javascript
const admitted = await workspace.batch({
maxOperations: 3,
operation: async (scope) => {
const pages = await scope.pages.putBatch({ pages: pageRequests });
const leaves = await scope.bundles.putOrderedBatch({
bundles: buildLeafRequests(pages),
});
return (await scope.bundles.putOrderedBatch({
bundles: [buildRootRequest(leaves)],
}))[0];
},
});

admitted.value; // BundleHandle returned by the callback
admitted.retention; // exact WorkspaceCheckpointResult
```

`batch({ operation, maxOperations })` calls `operation` exactly once. Its scope
exposes only `pages.putBatch()` and `bundles.putOrderedBatch()`. Both methods
retain their existing per-call limits and validation, serialize by invocation
order, and return frozen arrays of provisional handles rather than staged
result objects. The operation defaults to at most 64 scope calls and rejects a
limit above the exported hard maximum of 1,024.

Success installs the union of previously retained targets and every compound
target in one exact workspace generation. The returned `value` is the callback
value; it becomes caller-visible only after scoped Git resources close and the
paired `retention` result proves the final generation. The scope closes before
the outer promise settles. Calling an escaped scope later fails with
`WORKSPACE_STATE_INVALID`.

The callback is trusted application code, not a JavaScript capability sandbox.
The API cannot prevent a callback from assigning a provisional handle into
external state as a side effect. Such a handle has no compound retention
witness and must not be used outside the callback. Only the settled outer
result carries the admission guarantee.

An empty operation, invalid bound, callback failure, staged-write failure,
session-close failure, or final checked-ref failure returns no admitted value.
Queued work stops after the first failure, the prior workspace generation does
not move, and distinct callback/write or operation/close failures remain
available through `AggregateError`. Immutable objects written before refusal
may remain unreachable for Git's ordinary reclamation. Compound admission is
not a cross-workspace, cross-ref, or arbitrary application transaction.

Each method returns only after a direct workspace generation reaches the
returned typed handle. The result is otherwise the ordinary staged result plus
a workspace `RetentionWitness`. Calls on one workspace serialize their ref
Expand Down
Loading
Loading