Skip to content

Commit a4d9dda

Browse files
authored
Merge pull request #72 from git-stunts/slice/og-014-streaming-cas-blob-storage
feat!: mandatory CAS blob storage with streaming I/O (OG-014)
2 parents 3d185b5 + 5677907 commit a4d9dda

48 files changed

Lines changed: 2291 additions & 499 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ node_modules/
55
coverage/
66
CLAUDE.md
77
TASKS-DO-NOT-CHECK-IN.md
8+
EDITORS-REPORT.md
89
git-stunts-git-warp-*.tgz

BACKLOG/OG-010-public-api-design-thinking.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OG-010 — IBM Design Thinking Pass Over Public APIs And README
22

3-
Status: ACTIVE
3+
Status: DONE
44

55
## Problem
66

Lines changed: 84 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,114 @@
1-
# OG-014 — Stream content attachments through git-cas
1+
# OG-014 — Mandatory CAS blob storage with streaming I/O
22

3-
Status: QUEUED
3+
Status: DONE
44

55
Legend: Observer Geometry
66

7-
## Problem
8-
9-
`getContent()` and `getEdgeContent()` currently return full `Uint8Array`
10-
buffers. That means attachment reads materialize the entire payload in memory
11-
before user code can process it.
12-
13-
This is fine for small text blobs, but it is the wrong default shape for large
14-
attachments:
7+
Design doc: `docs/design/streaming-cas-blob-storage.md`
158

16-
- the attachment may not fit comfortably in memory
17-
- the caller cannot decide between buffered read and stream processing
18-
- builder-facing docs risk teaching attachment reads as eager byte loads
19-
- the current blob-storage abstraction still forces `retrieve()` to return a
20-
full buffer rather than a stream-capable interface
9+
## Problem
2110

22-
`git-warp` already contains a `CasBlobAdapter` that stores attachments in
23-
`git-cas` with CDC chunking, but the public attachment path still terminates in
24-
buffered reads. That leaves the most scalable backend present but not fully
25-
expressed through the public API.
11+
Content blob attachments in `git-warp` have two structural problems:
2612

27-
## Why this matters
13+
### 1. CAS blob storage is opt-in
2814

29-
WARP graphs can legitimately carry attached documents, artifacts, and other
30-
payloads that are larger than normal graph properties.
15+
`attachContent()` and `attachEdgeContent()` accept an optional `blobStorage`
16+
injection. When callers do not provide it, blobs fall through to raw
17+
`persistence.writeBlob()` — a single unchunked Git object with no CDC
18+
deduplication, no encryption support, and no streaming restore path.
3119

32-
The API should make the memory tradeoff explicit:
20+
This means the substrate's chunking, deduplication, and encryption capabilities
21+
are present but silently bypassed by default. There is no good reason for a
22+
content blob to skip CAS. Every blob should be chunked.
3323

34-
- buffered reads when you actually want all bytes in memory
35-
- streaming reads when you want to process incrementally
24+
### 2. Neither write nor read paths support streaming
3625

37-
That decision should belong to the caller, not be forced by the default
38-
attachment API shape.
26+
**Write path**: `attachContent(nodeId, content)` accepts `Uint8Array | string`.
27+
The caller must buffer the entire payload in memory before handing it to the
28+
patch builder. `CasBlobAdapter.store()` then wraps that buffer in
29+
`Readable.from([buf])` — a synthetic stream from an already-buffered payload.
3930

40-
## Current state
31+
**Read path**: `getContent(nodeId)` returns `Promise<Uint8Array | null>`. The
32+
full blob is materialized into memory before the caller can process it.
33+
`CasBlobAdapter.retrieve()` calls `cas.restore()` which buffers internally.
4134

42-
Today the attachment read path is eager:
35+
`git-cas` already supports streaming on both sides:
36+
- `cas.store({ source })` accepts any readable/iterable source
37+
- `cas.restoreStream()` returns `AsyncIterable<Buffer>`
4338

44-
- `getContent()` -> `Promise<Uint8Array|null>`
45-
- `getEdgeContent()` -> `Promise<Uint8Array|null>`
46-
- `BlobStoragePort.retrieve()` -> `Promise<Uint8Array>`
47-
- default Git blob reads go through `readBlob()` and collect the full blob
48-
- `CasBlobAdapter` can already store attachment content in `git-cas`, but it
49-
still restores into one full buffer via `retrieve()`
50-
- `git-cas` streaming restore is already used in `CasSeekCacheAdapter`, but not
51-
yet exposed through attachment reads
39+
The streaming substrate is there. It is not expressed through the public API.
5240

53-
## Desired outcome
41+
## Why this matters
5442

55-
Make `git-cas` the first-class streaming attachment path without breaking the
56-
simple buffered paths.
43+
WARP graphs can carry attached documents, media, model weights, and other
44+
payloads that are legitimately large. The API should not force full in-memory
45+
buffering on either side of the I/O boundary.
5746

58-
Likely shape:
47+
- Callers writing large content should be able to pipe a stream in
48+
- Callers reading large content should be able to consume it incrementally
49+
- Every blob should get CDC chunking and deduplication as a substrate guarantee
50+
- The decision between buffered and streaming I/O should belong to the caller
5951

60-
- `getContentStream(nodeId)`
61-
- `getEdgeContentStream(from, to, label)`
62-
- `BlobStoragePort.retrieveStream(oid)`
63-
- `CasBlobAdapter.retrieveStream(oid)` backed by `git-cas restoreStream()`
64-
- a clear default/recommended way to wire `CasBlobAdapter` into `WarpApp.open()`
65-
/ `WarpCore.open()` for attachment storage
52+
## Current state
6653

67-
Buffered helpers should remain available for convenience, but they should be
68-
clearly layered on top of the stream-capable substrate.
54+
As of `v15.0.1`:
55+
56+
- `BlobStoragePort`: `store(content, options) → Promise<string>`,
57+
`retrieve(oid) → Promise<Uint8Array>` — both buffered
58+
- `CasBlobAdapter`: fully implemented CAS adapter with CDC chunking, optional
59+
encryption, backward-compat fallback to raw Git blobs — but only buffered I/O
60+
- `CasBlobAdapter` is internal (not exported from `index.js`)
61+
- `PatchBuilderV2.attachContent()`: accepts `Uint8Array | string`, uses
62+
`blobStorage.store()` if injected, else raw `persistence.writeBlob()`
63+
- `getContent()` / `getEdgeContent()`: returns `Promise<Uint8Array | null>`,
64+
uses `blobStorage.retrieve()` if injected, else raw `persistence.readBlob()`
65+
- `WarpApp` and `WarpCore` do not expose content read methods at all
66+
- `git-cas` streaming (`restoreStream()`) is already used in
67+
`CasSeekCacheAdapter` but not in blob reads
68+
- `InMemoryGraphAdapter` has `writeBlob()`/`readBlob()` for browser/test path
6969

70-
Longer-term, if attachment storage standardizes on `git-cas`, the builder story
71-
gets cleaner too:
70+
## Desired outcome
7271

73-
- large attachments become chunked CAS assets
74-
- reads can stream incrementally
75-
- dedupe happens below the API surface
76-
- legacy raw Git blob attachments can remain readable for compatibility
72+
1. CAS blob storage is mandatory — no fallback to raw `writeBlob()` for content
73+
2. Write path accepts streaming input and pipes through without buffering
74+
3. Read path returns a stream the caller can consume incrementally
75+
4. Buffered convenience methods remain available, layered on top of streams
76+
5. Browser and in-memory paths still work via a conforming adapter
77+
6. Legacy raw Git blob attachments remain readable for backward compatibility
7778

7879
## Acceptance criteria
7980

80-
1. `git-warp` exposes explicit streaming APIs for node and edge attachments.
81-
2. Callers can choose stream vs buffered read intentionally.
82-
3. `BlobStoragePort` grows a stream-capable retrieval contract.
83-
4. `CasBlobAdapter` supports streaming retrieval via `git-cas`.
84-
5. `git-cas` becomes the recommended path for large attachment storage.
85-
6. Legacy raw Git blob attachments remain readable for compatibility.
86-
7. Builder docs explain when to use buffered reads vs streams.
87-
8. Large attachment reads no longer require full in-memory buffering by
88-
default in the stream path.
81+
1. Every content blob written through `attachContent()` / `attachEdgeContent()`
82+
goes through `BlobStoragePort` — no raw `persistence.writeBlob()` fallback.
83+
2. `attachContent()` / `attachEdgeContent()` accept streaming input
84+
(`AsyncIterable<Uint8Array>`, `ReadableStream`, `Uint8Array`, `string`).
85+
3. New `getContentStream()` / `getEdgeContentStream()` return
86+
`AsyncIterable<Uint8Array>` for incremental consumption.
87+
4. Existing `getContent()` / `getEdgeContent()` remain as buffered convenience,
88+
implemented on top of the stream primitive.
89+
5. `BlobStoragePort` grows `storeStream()` and `retrieveStream()` methods.
90+
6. `CasBlobAdapter` implements streaming via `git-cas` natively.
91+
7. An `InMemoryBlobStorageAdapter` implements the port contract for browser and
92+
test paths.
93+
8. Legacy raw Git blob attachments remain readable through backward-compat
94+
fallback in `CasBlobAdapter.retrieveStream()`.
95+
9. Content stream methods are exposed on `WarpApp` and `WarpCore`.
8996

9097
## Non-goals
9198

92-
- no automatic conversion of all existing attachment reads to streams
93-
- no silent breaking change to `getContent()` / `getEdgeContent()`
94-
- no attempt to solve whole-state out-of-core replay here
99+
- No automatic migration of existing raw Git blobs to CAS format
100+
- No silent breaking change to existing `getContent()` / `getEdgeContent()`
101+
return types
102+
- No attempt to solve whole-state out-of-core replay (that is OG-013)
103+
- No encryption-by-default (encryption remains an opt-in CAS capability)
95104

96105
## Notes
97106

98-
This item is related to, but narrower than,
99-
`OG-013-out-of-core-materialization-and-streaming-reads.md`.
100-
`OG-013` is about whole-state and replay architecture.
101-
This item is specifically about attachment payload I/O and making
102-
`git-cas` the streaming/chunked attachment path.
107+
This item supersedes the original OG-014 scope, which covered only streaming
108+
reads. The expanded scope now includes mandatory CAS and streaming writes.
109+
110+
Related items:
111+
- `OG-013`: out-of-core materialization and streaming reads (broader, separate)
112+
- `B160`: blob attachments via CAS (done, but opt-in — this item makes it
113+
mandatory)
114+
- `B163`: streaming restore for seek cache (done, pattern to follow for blobs)

BACKLOG/OG-015-jsr-documentation-quality.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OG-015 — Raise JSR documentation quality score
22

3-
Status: QUEUED
3+
Status: DONE
44

55
Legend: Observer Geometry
66

BACKLOG/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# BACKLOG — Observer Geometry
22

3-
Last updated: 2026-03-28
3+
Last updated: 2026-03-29
44

55
This directory holds promotable pre-design items for the current Observer
66
Geometry tranche.
@@ -26,9 +26,9 @@ Workflow:
2626
| DONE | OG-007 | Expand hash-stability coverage across snapshot flavors | [OG-007-hash-stability-coverage.md](OG-007-hash-stability-coverage.md) |
2727
| DONE | OG-008 | Make retargeting compatibility a hard major-version cut | [OG-008-retargeting-compatibility.md](OG-008-retargeting-compatibility.md) |
2828
| QUEUED | OG-009 | Align playback-head and TTD consumers after read nouns stabilize | [OG-009-playback-head-alignment.md](OG-009-playback-head-alignment.md) |
29-
| ACTIVE | OG-010 | IBM Design Thinking pass over public APIs and README | [OG-010-public-api-design-thinking.md](OG-010-public-api-design-thinking.md) |
29+
| DONE | OG-010 | IBM Design Thinking pass over public APIs and README | [OG-010-public-api-design-thinking.md](OG-010-public-api-design-thinking.md) |
3030
| QUEUED | OG-011 | Publish a public API catalog and browser documentation playground | [OG-011-public-api-catalog-and-playground.md](OG-011-public-api-catalog-and-playground.md) |
3131
| DONE | OG-012 | Audit and reconcile the documentation corpus before v15 | [OG-012-documentation-corpus-audit.md](OG-012-documentation-corpus-audit.md) |
3232
| QUEUED | OG-013 | Design out-of-core materialization and streaming reads | [OG-013-out-of-core-materialization-and-streaming-reads.md](OG-013-out-of-core-materialization-and-streaming-reads.md) |
33-
| QUEUED | OG-014 | Stream content attachments through `git-cas` | [OG-014-streaming-content-attachments.md](OG-014-streaming-content-attachments.md) |
34-
| QUEUED | OG-015 | Raise JSR documentation quality score | [OG-015-jsr-documentation-quality.md](OG-015-jsr-documentation-quality.md) |
33+
| DONE | OG-014 | Mandatory CAS blob storage with streaming I/O | [OG-014-streaming-content-attachments.md](OG-014-streaming-content-attachments.md) |
34+
| DONE | OG-015 | Raise JSR documentation quality score | [OG-015-jsr-documentation-quality.md](OG-015-jsr-documentation-quality.md) |

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [16.0.0] — 2026-03-29
9+
10+
### Added
11+
12+
- **Streaming content attachment I/O (OG-014)**`getContentStream()` and `getEdgeContentStream()` return `AsyncIterable<Uint8Array>` for incremental consumption of large content blobs. `attachContent()` and `attachEdgeContent()` now accept `AsyncIterable<Uint8Array>`, `ReadableStream<Uint8Array>`, `Uint8Array`, or `string` — streaming inputs are piped directly to blob storage without intermediate buffering.
13+
- **`InMemoryBlobStorageAdapter`** — new domain-local adapter implementing `BlobStoragePort` with content-addressed `Map`-based storage for browser and test paths. Exported from the package surface.
14+
- **`BlobStoragePort.storeStream()` / `retrieveStream()`** — streaming variants of the blob storage port contract. `storeStream()` accepts `AsyncIterable<Uint8Array>`, `retrieveStream()` returns `AsyncIterable<Uint8Array>`.
15+
- **Content methods on `WarpApp` and `WarpCore`**`getContent()`, `getContentStream()`, `getContentOid()`, `getContentMeta()` and their edge equivalents are now exposed on both public API surfaces (previously only on `WarpRuntime`).
16+
17+
### Changed
18+
19+
- **CAS blob storage is now mandatory for content attachments**`attachContent()` and `attachEdgeContent()` always route through `BlobStoragePort`. The raw `persistence.writeBlob()` fallback has been removed. `WarpRuntime.open()` auto-constructs `CasBlobAdapter` for Git-backed persistence (when `plumbing` is available) or `InMemoryBlobStorageAdapter` otherwise.
20+
- **Content blob tree entries use tree mode** — patch commit trees and checkpoint trees now reference content blobs as `040000 tree` entries (matching CAS tree OIDs) instead of `100644 blob` entries.
21+
22+
### Removed
23+
24+
- **`TraversalService`** — deprecated alias removed. Use `CommitDagTraversalService` directly.
25+
- **`createWriter()`** — deprecated method removed from `WarpApp` and `WarpCore`. Use `writer()` or `writer(id)` instead.
26+
827
## [15.0.0] — 2026-03-28
928

1029
## [15.0.1] — 2026-03-28

MIGRATING.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Migrating to v16
2+
3+
This guide covers the breaking changes in v16.0.0 and how to update your code.
4+
5+
## Content attachments now require blob storage (OG-014)
6+
7+
**What changed:** `attachContent()` and `attachEdgeContent()` no longer fall
8+
back to raw `persistence.writeBlob()`. They always route through
9+
`BlobStoragePort`. Without blob storage, they throw `NO_BLOB_STORAGE`.
10+
11+
**Who is affected:** Only consumers who construct `PatchBuilderV2` directly
12+
(bypassing `WarpRuntime.open()`) without passing `blobStorage`. If you use
13+
`WarpApp.open()`, `WarpCore.open()`, or `WarpRuntime.open()`, blob storage
14+
is auto-constructed — no code changes needed.
15+
16+
**How to migrate:**
17+
18+
```javascript
19+
import InMemoryBlobStorageAdapter from '@git-stunts/git-warp/defaultBlobStorage';
20+
21+
// If you construct PatchBuilderV2 directly, add blobStorage:
22+
const builder = new PatchBuilderV2({
23+
persistence,
24+
writerId: 'alice',
25+
blobStorage: new InMemoryBlobStorageAdapter(), // or CasBlobAdapter for Git
26+
// ...other options
27+
});
28+
```
29+
30+
## Streaming content I/O
31+
32+
**What changed:** `attachContent()` and `attachEdgeContent()` now accept
33+
streaming input (`AsyncIterable<Uint8Array>`, `ReadableStream<Uint8Array>`)
34+
in addition to `Uint8Array` and `string`. New `getContentStream()` and
35+
`getEdgeContentStream()` methods return `AsyncIterable<Uint8Array>`.
36+
37+
**Who is affected:** No one — this is additive. Existing code continues to
38+
work. New streaming APIs are opt-in.
39+
40+
**How to use:**
41+
42+
```javascript
43+
// Streaming write — pipe a file directly
44+
import { createReadStream } from 'node:fs';
45+
const patch = await app.createPatch();
46+
patch.addNode('doc:1');
47+
await patch.attachContent('doc:1', createReadStream('large-file.bin'), {
48+
size: fileStat.size,
49+
mime: 'application/octet-stream',
50+
});
51+
await patch.commit();
52+
53+
// Streaming read — consume incrementally
54+
const stream = await app.getContentStream('doc:1');
55+
if (stream) {
56+
for await (const chunk of stream) {
57+
process.stdout.write(chunk);
58+
}
59+
}
60+
61+
// Buffered read — unchanged
62+
const buf = await app.getContent('doc:1');
63+
```
64+
65+
## Content blob tree entries use tree mode
66+
67+
**What changed:** Patch commit trees and checkpoint trees now reference
68+
content blobs as `040000 tree` entries (CAS tree OIDs) instead of
69+
`100644 blob` entries.
70+
71+
**Who is affected:** Consumers who parse raw Git commit trees and expect
72+
content anchor entries to use blob mode. This does not affect any public
73+
API — it is an internal storage format change.
74+
75+
**How to migrate:** If you parse `_content_<oid>` entries from commit trees,
76+
update your parser to accept `040000 tree` mode.
77+
78+
## `TraversalService` removed
79+
80+
**What changed:** The `TraversalService` export was a deprecated alias for
81+
`CommitDagTraversalService`. It has been removed.
82+
83+
**How to migrate:**
84+
85+
```javascript
86+
// Before
87+
import { TraversalService } from '@git-stunts/git-warp';
88+
89+
// After
90+
import { CommitDagTraversalService } from '@git-stunts/git-warp';
91+
```
92+
93+
## `createWriter()` removed
94+
95+
**What changed:** The `createWriter()` method on `WarpApp` was deprecated in
96+
v15 and has been removed. Use `writer()` instead.
97+
98+
**How to migrate:**
99+
100+
```javascript
101+
// Before
102+
const w = await app.createWriter();
103+
const w2 = await app.createWriter({ persist: 'config', alias: 'secondary' });
104+
105+
// After
106+
const w = await app.writer(); // resolves from git config or generates
107+
const w2 = await app.writer('secondary'); // explicit ID
108+
```

0 commit comments

Comments
 (0)