Skip to content

feat!: mandatory CAS blob storage with streaming I/O (OG-014)#72

Merged
flyingrobots merged 16 commits intomainfrom
slice/og-014-streaming-cas-blob-storage
Mar 29, 2026
Merged

feat!: mandatory CAS blob storage with streaming I/O (OG-014)#72
flyingrobots merged 16 commits intomainfrom
slice/og-014-streaming-cas-blob-storage

Conversation

@flyingrobots
Copy link
Copy Markdown
Member

@flyingrobots flyingrobots commented Mar 29, 2026

Summary

  • CAS blob storage is now mandatory for content attachmentsattachContent() / attachEdgeContent() always route through BlobStoragePort. The raw persistence.writeBlob() fallback is removed. WarpRuntime.open() auto-constructs CasBlobAdapter for Git-backed persistence or InMemoryBlobStorageAdapter for browser/test paths.
  • Streaming write pathattachContent() and attachEdgeContent() now accept AsyncIterable<Uint8Array>, ReadableStream<Uint8Array>, Uint8Array, or string. Streaming inputs pipe directly to blob storage without intermediate buffering.
  • Streaming read path — new getContentStream() / getEdgeContentStream() return AsyncIterable<Uint8Array> for incremental consumption. Buffered getContent() / getEdgeContent() remain as convenience.
  • Content methods on WarpApp and WarpCore — all 8 content read methods now exposed on both public API surfaces (previously only on WarpRuntime).
  • JSR symbol documentation at 100% — all 227 exported symbols in index.d.ts now have JSDoc comments (up from 65%).
  • OG-010 closed — IBM Design Thinking pass shipped in v15.0.0, formally closed.

Breaking Changes

  • attachContent() / attachEdgeContent() without blob storage now throw E_NO_BLOB_STORAGE
  • Content blob tree entries use 040000 tree mode (CAS trees) instead of 100644 blob
  • WarpRuntime.open() auto-constructs blob storage when none provided

New Public API

  • BlobStoragePort.storeStream() / retrieveStream()
  • InMemoryBlobStorageAdapter (exported)
  • WarpApp.getContentStream() / getEdgeContentStream()
  • WarpCore.getContentStream() / getEdgeContentStream()

Backlog Items Closed

  • OG-014: Mandatory CAS blob storage with streaming I/O (design → spec → semantic → surface → retro)
  • OG-015: JSR symbol documentation at 100%
  • OG-010: IBM Design Thinking pass (formal close)

Test Plan

  • 5203 unit + integration tests pass (verified locally, 322 files)
  • All pre-push gates pass: tsc, ESLint, surface validator, policy ratchet, markdown lint
  • Integration content-attachment tests verify CAS round-trip with real Git repos
  • Streaming input tests cover AsyncIterable and ReadableStream paths
  • Legacy raw blob backward-compat tested via CasBlobAdapter fallback
  • API surface snapshot updated for new methods

Summary by CodeRabbit

  • New Features

    • End-to-end streaming for content attachments and reads; streaming methods exposed on public surfaces.
    • New in-memory, content-addressed blob adapter for non-Git environments.
  • Documentation

    • Added design spec, migration guide, and changelog entries for v16.0.0.
  • Breaking Changes

    • CAS-backed blob storage is now mandatory; no raw write fallback.
    • Content anchor storage format changed to tree-mode entries.
  • Removals

    • Deprecated writer-creation helper and TraversalService alias removed.

Expand OG-014 scope from streaming reads only to three requirements:
mandatory CAS blob storage, streaming writes, and streaming reads.

Add design doc at docs/design/streaming-cas-blob-storage.md with IBM
Design Thinking framing, hills, invariants, checkpoint gates, and
affected files inventory.
Checkpoint 2 (spec) for the streaming CAS blob storage design.

New and expanded test coverage:

- BlobStoragePort: storeStream() and retrieveStream() contract stubs
- InMemoryBlobStorageAdapter: full adapter test suite (14 tests)
- CasBlobAdapter: storeStream(), retrieveStream(), legacy fallback (6 tests)
- PatchBuilderV2: streaming input (AsyncIterable, ReadableStream),
  no-raw-fallback assertion (4 tests)
- WarpGraph: getContentStream(), getEdgeContentStream() (5 tests)
- WarpRuntime: blob storage auto-construction (4 tests)

All 35 new tests fail against current code (red phase).
All 5171 existing tests pass — zero regressions.
… propagation

PatchBuilderV2.attachContent() and attachEdgeContent() now require
blobStorage (E_NO_BLOB_STORAGE). Updated all tests that constructed
builders without blobStorage to inject createMockBlobStorage().

Also fixed a bug where setProperty for _content.size and _content.mime
used raw metadata instead of normalizedMeta for non-streaming inputs,
causing size to be null when not explicitly provided.

Fixed pre-existing lint errors in defaultBlobStorage.js (require-await),
streamUtils.js (require-await), CasBlobAdapter.js (return-await), and
WarpRuntime.js (bare wildcard JSDoc type).
Checkpoint 3 (semantic) for the streaming CAS blob storage design.

BREAKING CHANGES:

- attachContent() and attachEdgeContent() now require blob storage.
  Calling without blobStorage throws E_NO_BLOB_STORAGE instead of
  falling back to raw persistence.writeBlob().

- WarpRuntime.open() auto-constructs blob storage when none is
  provided: CasBlobAdapter for Git-backed persistence (has plumbing),
  InMemoryBlobStorageAdapter for in-memory/browser paths.

- Content blob tree entries now use 040000 tree mode (CAS trees)
  instead of 100644 blob mode (raw Git blobs).

Implementation:

- BlobStoragePort: added storeStream() and retrieveStream() abstract
  methods for streaming I/O
- InMemoryBlobStorageAdapter: new domain-local adapter (defaultBlobStorage.js)
  with content-addressed Map storage, streaming round-trip support
- CasBlobAdapter: added storeStream() piping through git-cas store(),
  retrieveStream() using git-cas restoreStream() with legacy blob fallback,
  Buffer→Uint8Array normalization on retrieve()
- PatchBuilderV2: accepts AsyncIterable/ReadableStream content input,
  routes streaming input to storeStream(), removes raw writeBlob fallback
- query.methods.js: added getContentStream() and getEdgeContentStream()
  returning AsyncIterable<Uint8Array> for incremental consumption
- streamUtils.js: normalizeToAsyncIterable() and isStreamingInput()
  domain utilities for input normalization
- WarpRuntime: auto-constructs blob storage in open(), new streaming
  read methods auto-wired via wireWarpMethods

All 5203 tests pass across 322 files.
…int 4)

Complete the OG-014 surface checkpoint:

- WarpApp: getContent, getContentStream, getContentOid, getContentMeta
  and edge equivalents — all 8 content read methods now on the product
  surface
- WarpCore: same 8 methods via WarpRuntime.prototype delegation
- index.js: export InMemoryBlobStorageAdapter
- index.d.ts: streaming types on BlobStoragePort (storeStream,
  retrieveStream), InMemoryBlobStorageAdapter class, streaming input
  union on attachContent/attachEdgeContent, getContentStream and
  getEdgeContentStream on WarpCoreBase and WarpApp
- CHANGELOG: [Unreleased] section with OG-014 additions and breaking
  changes
- ROADMAP: updated to v15.0.1 current / v16.0.0 next
- OG-014 and design doc marked DONE / IMPLEMENTED

BREAKING CHANGE: CAS blob storage is mandatory for content attachments.
attachContent() without blobStorage now throws E_NO_BLOB_STORAGE.
WarpRuntime.open() auto-constructs blob storage when none is provided.
Design alignment audit: 6/6 design decisions aligned (2 partially —
adapter placement and dynamic import, both accepted as deliberate
trade-offs). Two unanticipated drift items documented: content anchor
tree mode change and retrieve() independence from retrieveStream().
The WarpApp/WarpCore split, Observer/Lens public noun cuts, Strand
rename, README rewrite, and cost-signaling work all landed in v15.0.0.
No remaining open items under this cycle.
Document 29 visible state, coordinate comparison, and transfer plan
type exports that were missing JSDoc comments (lines ~3155-3484).
Add JSDoc comments to all 80 previously undocumented exported symbols
in index.d.ts:

- 14 traversal, observer, content types (L248-L1629)
- 18 conflict analysis and strand types (L2819-L2994)
- 9 op interfaces + 9 factory functions + 1 migration function (L3039-L3131)
- 29 visible state, comparison, and transfer plan types (L3159-L3478)
- 1 default export

Symbol documentation coverage: 65% → 100% (227/227 exports).
All 227 exported symbols in index.d.ts now have JSDoc comments.
Coverage raised from 65% to 100%.
- Fix tsc errors: import content query functions directly in WarpApp
  and WarpCore instead of accessing dynamically-wired prototype methods,
  narrow streaming union type in PatchBuilderV2, fix crypto.subtle.digest
  typing in defaultBlobStorage, add _asRuntime() helper in WarpCore for
  type-safe delegation
- Add InMemoryBlobStorageAdapter to type-surface.m8.json manifest
- Fix markdown code sample lint: use text blocks for pseudocode signatures
  in design doc
- Clean up CasBlobAdapter test mock: declare restoreStream in constructor
  instead of prototype patching
- Add JSDoc types to integration test helper openGraph params
- Restore README.md (reverted unintended agent modification)

All gates pass: tsc, surface validator, policy ratchet, markdown lint,
5203 tests green.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 195aa7b9-cbc8-4187-8fc7-8d3eb56affeb

📥 Commits

Reviewing files that changed from the base of the PR and between 024b0bb and 5677907.

📒 Files selected for processing (2)
  • contracts/type-surface.m8.json
  • index.d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/type-surface.m8.json

📝 Walkthrough

Walkthrough

Adds mandatory content-addressed blob storage with streaming I/O: new streaming methods on BlobStoragePort, an InMemoryBlobStorageAdapter, streaming attach/read APIs on app/core, PatchBuilderV2 enforces CAS and streaming, WarpRuntime auto-constructs blob storage, and content anchors changed to tree-mode entries.

Changes

Cohort / File(s) Summary
Backlog & Release Docs
BACKLOG/OG-010-public-api-design-thinking.md, BACKLOG/OG-014-streaming-content-attachments.md, BACKLOG/OG-015-jsr-documentation-quality.md, BACKLOG/README.md, docs/ROADMAP.md
Marked tasks DONE, renamed OG-014 to mandatory CAS + streaming, bumped release metadata.
Design & Migration
docs/design/streaming-cas-blob-storage.md, docs/design/streaming-cas-blob-storage.retro.md, CHANGELOG.md, MIGRATING.md
Added design/retro, documented new streaming APIs, mandatory CAS, tree-mode anchors, and migration notes.
Port Contracts & Types
src/ports/BlobStoragePort.js, contracts/type-surface.m8.json, index.d.ts
Added storeStream/retrieveStream to BlobStoragePort; updated type surface and d.ts; exported InMemoryBlobStorageAdapter; removed deprecated aliases and createWriter declarations.
Blob Storage Adapters
src/domain/utils/defaultBlobStorage.js, src/infrastructure/adapters/CasBlobAdapter.js
Added InMemoryBlobStorageAdapter (content-addressed) and implemented streaming store/retrieve + legacy fallback in CasBlobAdapter.
Domain Services & Patch Handling
src/domain/services/PatchBuilderV2.js, src/domain/services/CheckpointService.js, src/domain/errors/WriterError.js
PatchBuilderV2 accepts streaming inputs, normalizes/streams to blob storage, throws NO_BLOB_STORAGE if absent; checkpoint content entries now use 040000 tree; added NO_BLOB_STORAGE error code.
Stream Utilities
src/domain/utils/streamUtils.js
New utilities: isStreamingInput, normalizeToAsyncIterable, collectAsyncIterable for input normalization and collection.
Query & Public Surfaces
src/domain/warp/query.methods.js, src/domain/WarpApp.js, src/domain/WarpCore.js, index.js
Added getContentStream/getEdgeContentStream and related getContent/getContentMeta/getContentOid on WarpApp/WarpCore; re-exported InMemory adapter; removed deprecated exports.
Runtime Wiring & Tests Setup
src/domain/WarpRuntime.js, test/integration/api/helpers/setup.js
WarpRuntime.open() auto-constructs blob storage (Cas if plumbing present, else InMemory); integration test setup updated to pass CasBlobAdapter.
Tests — Blob & Streaming
test/unit/infrastructure/adapters/InMemoryBlobStorageAdapter.test.js, test/unit/infrastructure/adapters/CasBlobAdapter.test.js, test/unit/ports/BlobStoragePort.test.js, test/unit/domain/WarpGraph.content.test.js
Added/updated tests for InMemory adapter, Cas streaming and fallback, port stubs, and WarpGraph streaming read paths.
Tests — Patch/Checkpoint/Trees
test/unit/domain/services/PatchBuilderV2.content.test.js, test/unit/domain/services/CheckpointService.test.js, test/unit/domain/services/TreeConstruction.determinism.test.js
Updated tests to assert CAS store/storeStream, no persistence.writeBlob fallback, and 040000 tree content entries; injected InMemory adapter where needed.
Exports, Cleanup & Release
index.js, package.json, jsr.json, .gitignore, eslint.config.js, src/domain/services/TraversalService.js
Re-exported InMemory adapter, removed TraversalService file and export alias, bumped version to v16.0.0, updated ignore and ESLint overrides.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant WarpApp
    participant PatchBuilderV2
    participant BlobStorage
    participant Persistence_CAS

    Client->>WarpApp: attachContent(nodeId, asyncIterable)
    WarpApp->>PatchBuilderV2: attachContent(nodeId, asyncIterable, metadata?)
    PatchBuilderV2->>PatchBuilderV2: isStreamingInput? / normalizeToAsyncIterable
    PatchBuilderV2->>BlobStorage: storeStream(normalized, options)
    BlobStorage->>Persistence_CAS: stream chunks to git-cas
    Persistence_CAS-->>BlobStorage: return tree OID
    BlobStorage-->>PatchBuilderV2: return OID
    PatchBuilderV2->>PatchBuilderV2: write content metadata + anchor (tree-mode)
    PatchBuilderV2-->>WarpApp: patch queued
    WarpApp-->>Client: success
Loading
sequenceDiagram
    participant Client
    participant WarpApp
    participant WarpRuntime
    participant BlobStorage
    participant Persistence_CAS

    Client->>WarpApp: getContentStream(nodeId)
    WarpApp->>WarpRuntime: getContentStream(nodeId)
    WarpRuntime->>WarpRuntime: locate content OID
    WarpRuntime->>BlobStorage: retrieveStream(oid)
    alt BlobStorage supports streaming
        BlobStorage->>Persistence_CAS: retrieve via git-cas stream
        Persistence_CAS-->>BlobStorage: AsyncIterable<Uint8Array>
    else Fallback to buffered read
        BlobStorage->>Persistence_CAS: readBlob(oid)
        Persistence_CAS-->>BlobStorage: Uint8Array
        BlobStorage->>BlobStorage: wrap in single-chunk AsyncIterable
    end
    BlobStorage-->>WarpRuntime: AsyncIterable<Uint8Array>
    WarpRuntime-->>WarpApp: AsyncIterable<Uint8Array>
    WarpApp-->>Client: AsyncIterable<Uint8Array>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰
I hop where chunks of starlight stream,
CAS nests each crumb in a tidy beam,
No fallback now — the tunnels sing,
Trees hold the crumbs, and patches spring,
Hop, fetch, and patch — a rabbit's dream.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides a comprehensive summary of changes, implementation details, breaking changes, new public APIs, and backlog closures. However, it does not follow the required template structure with explicit ADR checks section. Add the required ADR checks section with checkboxes as specified in the template, even if marking items as not applicable (N/A) with explanations.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: mandatory CAS blob storage with streaming I/O implementation for OG-014. It is concise, specific, and clearly identifies the primary feature delivered.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slice/og-014-streaming-cas-blob-storage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Release Preflight

  • package version: 15.0.1
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v15.0.1, release workflow will publish.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/infrastructure/adapters/CasBlobAdapter.js (1)

157-165: ⚠️ Potential issue | 🟡 Minor

Normalize the legacy raw-blob fallback too.

The CAS path now converts Buffer to plain Uint8Array, but the raw-blob fallback still returns persistence.readBlob() verbatim. That makes the BlobStoragePort.retrieve() contract in src/ports/BlobStoragePort.js:23-32 depend on how the attachment was originally stored.

🩹 Normalize the fallback path too
       const blob = await this._persistence.readBlob(oid);
       if (blob === null || blob === undefined) {
         throw new PersistenceError(
           `Missing Git object: ${oid}`,
           PersistenceError.E_MISSING_OBJECT,
           { context: { oid } },
         );
       }
-      return blob;
+      return blob instanceof Uint8Array && blob.constructor !== Uint8Array
+        ? new Uint8Array(blob.buffer, blob.byteOffset, blob.byteLength)
+        : blob;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/infrastructure/adapters/CasBlobAdapter.js` around lines 157 - 165, The
legacy fallback in CasBlobAdapter (where you call
this._persistence.readBlob(oid)) must normalize returned Buffer to a plain
Uint8Array to match the CAS path and the BlobStoragePort.retrieve contract;
update the retrieval path in the CasBlobAdapter method that returns blob (the
same block that throws PersistenceError on missing object) to detect Buffer/Node
buffer-like values and convert them to Uint8Array before returning so callers
always receive a Uint8Array.
CHANGELOG.md (1)

22-33: ⚠️ Potential issue | 🟡 Minor

Remove the duplicate empty 15.0.0 header to keep version chronology unambiguous.

## [15.0.0] appears twice, and the first instance is empty. This can break changelog consumers/parsers and makes release history unclear.

✂️ Proposed fix
-## [15.0.0] — 2026-03-28
-
 ## [15.0.1] — 2026-03-28

Based on learnings: Follow Keep a Changelog conventions; duplicate subheadings across versions are acceptable, but version entries should remain structurally clear.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 22 - 33, Remove the duplicate empty "## [15.0.0] —
2026-03-28" heading so the changelog has a single clear entry for 15.0.0; locate
the first occurrence of the header string "## [15.0.0] — 2026-03-28" (the empty
block above the 15.0.1 section) and delete that header line (and any immediately
adjacent blank lines) so only the intended 15.0.0 entry remains and
chronology/parsing is unambiguous.
🧹 Nitpick comments (3)
src/domain/utils/streamUtils.js (1)

108-128: Consider adding throw() for complete iterator cleanup.

The async iterator returned by readableStreamToAsyncIterable implements next() and return() but omits throw(). While throw() is optional per the async iteration protocol, implementing it would ensure the reader lock is released if an error is thrown into the iterator.

This is a minor concern since return() handles most cleanup scenarios (break, return, normal completion).

♻️ Optional: Add throw() method
       return() {
         reader.releaseLock();
         return Promise.resolve({ value: undefined, done: true });
       },
+      throw(err) {
+        reader.releaseLock();
+        return Promise.reject(err);
+      },
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/utils/streamUtils.js` around lines 108 - 128, The async iterator
produced by readableStreamToAsyncIterable lacks a throw() method, so if an error
is thrown into the iterator the reader lock may not be released; add an async
throw(err) method to the iterator returned from [Symbol.asyncIterator]()
(alongside next() and return()) that calls reader.releaseLock() and then
rethrows or returns a rejected promise with the provided error to ensure proper
cleanup of the stream reader in error scenarios.
test/unit/domain/WarpRuntime.blobAutoConstruct.test.js (1)

11-13: Stale comment: tests are no longer in "red phase".

The comment states tests should "all FAIL against the current code (red phase)", but per the PR description, the implementation is complete and tests are green. Consider updating or removing this comment.

📝 Suggested update
-/**
- * Spec tests for OG-014: auto-construction of blob storage.
- *
- * When no explicit `blobStorage` is provided to `WarpRuntime.open()`,
- * the runtime should auto-construct the appropriate adapter:
- * - `CasBlobAdapter` when persistence has plumbing (Git-backed)
- * - `InMemoryBlobStorageAdapter` when persistence lacks plumbing (in-memory)
- *
- * These tests should all FAIL against the current code (red phase).
- */
+/**
+ * Spec tests for OG-014: auto-construction of blob storage.
+ *
+ * When no explicit `blobStorage` is provided to `WarpRuntime.open()`,
+ * the runtime should auto-construct the appropriate adapter:
+ * - `CasBlobAdapter` when persistence has plumbing (Git-backed)
+ * - `InMemoryBlobStorageAdapter` when persistence lacks plumbing (in-memory)
+ */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/WarpRuntime.blobAutoConstruct.test.js` around lines 11 - 13,
The block comment in test/unit/domain/WarpRuntime.blobAutoConstruct.test.js that
reads "These tests should all FAIL against the current code (red phase)" is
stale; remove or update it to reflect the current state (tests are implemented
and passing). Locate the comment text in the file (search for "These tests
should all FAIL against the current code (red phase)") and either delete the
line or replace it with a short, accurate note (e.g., "Tests validate blob
auto-construction; implementation complete") so the comment matches the current
behavior of the WarpRuntime.blobAutoConstruct tests.
test/unit/domain/services/PatchBuilderV2.content.test.js (1)

492-503: Assert the streamed metadata path, not just the call count.

These cases only prove that storeStream() was reached. They do not verify { slug, mime, size } forwarding or the _content.size / _content.mime patch entries, so a regression in the streamed metadata path would still pass.

Also applies to: 532-535, 563-566

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/services/PatchBuilderV2.content.test.js` around lines 492 -
503, The tests currently only assert that blobStorage.storeStream was called;
update the assertions around builder.attachContent and builder.build to also
verify the streamed metadata and resulting patch entries: assert
blobStorage.storeStream was called with the expected metadata object including
slug, mime and size (from the source()), and assert the built patch.ops contains
PropSet entries for '_content.size' and '_content.mime' (and the forwarded slug
if applicable) alongside the existing '_content' entry; reference the existing
calls to builder.attachContent, blobStorage.storeStream and
builder.build/patch.ops to locate where to add these extra assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@BACKLOG/OG-014-streaming-content-attachments.md`:
- Around line 87-88: The buffered helpers getContent() and getEdgeContent()
currently call retrieve() directly (see retrieve()), which contradicts the
stated design that they should be implemented on top of the stream primitive;
update those functions so they route through the stream-based path instead of
calling retrieve() directly — e.g., have getContent()/getEdgeContent() call the
stream primitive (the existing streaming retrieval function) and then collect
the stream into a buffer before returning, or alternatively relax the spec text
to reflect current implementation if you don't want to change code; ensure you
update references to retrieve(), getContent(), and getEdgeContent() so the
buffered getters either wrap the stream primitive or the doc is revised.

In `@index.d.ts`:
- Line 256: The deprecation comment for LogicalTraversal incorrectly tells users
to "Use GraphTraversal directly" even though GraphTraversal isn't exported;
either export GraphTraversal or (recommended) update the deprecation text on
LogicalTraversal to point to the actual public replacement — e.g., "Use the
traverse APIs exposed on WarpAppObserver, Worldline, or WarpCoreBase" — so
change the comment attached to LogicalTraversal to reference those public
traverse properties (or export GraphTraversal if you prefer that path).

In `@src/domain/utils/defaultBlobStorage.js`:
- Around line 24-40: The contentHash function may pass the entire underlying
ArrayBuffer via bytes.buffer to crypto.subtle.digest, which can include extra
data if bytes is a view; update the crypto path in contentHash to pass the
Uint8Array (bytes) directly as the BufferSource to crypto.subtle.digest (leave
hexEncode usage and the FNV-1a fallback intact) so only the intended slice is
hashed.

In `@src/domain/warp/query.methods.js`:
- Around line 866-868: The current logic calls this._persistence.readBlob(oid)
and immediately wraps the result with singleChunkAsyncIterable(buf), which will
yield a null chunk if readBlob returns null; change the flow in the method that
calls readBlob so it checks the result (buf) and if buf is null return null (or
throw a clear error) instead of wrapping it in singleChunkAsyncIterable; update
the branch around this._persistence.readBlob(oid) to return null when buf ===
null and only call singleChunkAsyncIterable(buf) when buf is non-null so callers
of this method (which expect either a null or an async iterable) get the correct
behavior.
- Around line 838-841: In the fallback path where _blobStorage is missing,
ensure you handle a null return from _persistence.readBlob(oid) — call readBlob,
and if buf === null return null immediately (rather than wrapping it with
singleChunkAsyncIterable), so getContentStream/getContent behavior matches the
buffered getContent(); reference symbols: _blobStorage, _persistence.readBlob,
singleChunkAsyncIterable, getContent, and consider consistency with
CasBlobAdapter / PersistenceError.E_MISSING_OBJECT if you later decide to throw
instead.

In `@src/domain/WarpRuntime.js`:
- Around line 53-62: autoConstructBlobStorage currently returns a new
InMemoryBlobStorageAdapter() every time (in autoConstructBlobStorage), which
loses prior in-memory blobs across open() calls; change it to cache and reuse a
single InMemoryBlobStorageAdapter instance per persistence object (keyed by the
persistence reference) so subsequent calls return the same adapter for that
persistence. Implement similar caching wherever the same one-off creation occurs
(the other spot around lines 415-418), ensuring you return the cached adapter
when p.plumbing is absent and only create a new InMemoryBlobStorageAdapter once
per persistence object.

In `@src/infrastructure/adapters/CasBlobAdapter.js`:
- Around line 210-268: The current retrieveStream wrapper (retrieveStream) only
implements next() so return()/throw() never propagate to the underlying iterator
returned by _resolveStreamIterator, and streamed chunks are not normalized to
true Uint8Array like retrieve() does; convert retrieveStream into an async
generator that awaits the iterator from _resolveStreamIterator (or uses the
iterator returned by cas.restoreStream/singleChunkIterator) and uses a
normalization helper (same logic as retrieve() that converts Buffer subclasses
to Uint8Array) before yielding each chunk so both buffered and streamed paths
behave identically, and rely on generator semantics to delegate return()/throw()
to the inner iterator to ensure cleanup; add a unit test that consumes the async
iterable and breaks early (calls return on the iterator or uses break in a
for-await loop) to assert the underlying cas.restoreStream iterator saw
cancellation/cleanup.

In `@test/unit/domain/services/PatchBuilderV2.content.test.js`:
- Around line 618-620: Replace the generic error message assertion with an
explicit check for the error code E_NO_BLOB_STORAGE when calling
builder.attachContent('node:1', 'hello') — e.g. use await
expect(builder.attachContent(...)).rejects.toMatchObject({code:
'E_NO_BLOB_STORAGE'}) or await expect(...).rejects.toHaveProperty('code',
'E_NO_BLOB_STORAGE') and keep the persistence.writeBlob non-call assertion;
apply the same change for the other case referenced (OG-014 at the later
assertion).

---

Outside diff comments:
In `@CHANGELOG.md`:
- Around line 22-33: Remove the duplicate empty "## [15.0.0] — 2026-03-28"
heading so the changelog has a single clear entry for 15.0.0; locate the first
occurrence of the header string "## [15.0.0] — 2026-03-28" (the empty block
above the 15.0.1 section) and delete that header line (and any immediately
adjacent blank lines) so only the intended 15.0.0 entry remains and
chronology/parsing is unambiguous.

In `@src/infrastructure/adapters/CasBlobAdapter.js`:
- Around line 157-165: The legacy fallback in CasBlobAdapter (where you call
this._persistence.readBlob(oid)) must normalize returned Buffer to a plain
Uint8Array to match the CAS path and the BlobStoragePort.retrieve contract;
update the retrieval path in the CasBlobAdapter method that returns blob (the
same block that throws PersistenceError on missing object) to detect Buffer/Node
buffer-like values and convert them to Uint8Array before returning so callers
always receive a Uint8Array.

---

Nitpick comments:
In `@src/domain/utils/streamUtils.js`:
- Around line 108-128: The async iterator produced by
readableStreamToAsyncIterable lacks a throw() method, so if an error is thrown
into the iterator the reader lock may not be released; add an async throw(err)
method to the iterator returned from [Symbol.asyncIterator]() (alongside next()
and return()) that calls reader.releaseLock() and then rethrows or returns a
rejected promise with the provided error to ensure proper cleanup of the stream
reader in error scenarios.

In `@test/unit/domain/services/PatchBuilderV2.content.test.js`:
- Around line 492-503: The tests currently only assert that
blobStorage.storeStream was called; update the assertions around
builder.attachContent and builder.build to also verify the streamed metadata and
resulting patch entries: assert blobStorage.storeStream was called with the
expected metadata object including slug, mime and size (from the source()), and
assert the built patch.ops contains PropSet entries for '_content.size' and
'_content.mime' (and the forwarded slug if applicable) alongside the existing
'_content' entry; reference the existing calls to builder.attachContent,
blobStorage.storeStream and builder.build/patch.ops to locate where to add these
extra assertions.

In `@test/unit/domain/WarpRuntime.blobAutoConstruct.test.js`:
- Around line 11-13: The block comment in
test/unit/domain/WarpRuntime.blobAutoConstruct.test.js that reads "These tests
should all FAIL against the current code (red phase)" is stale; remove or update
it to reflect the current state (tests are implemented and passing). Locate the
comment text in the file (search for "These tests should all FAIL against the
current code (red phase)") and either delete the line or replace it with a
short, accurate note (e.g., "Tests validate blob auto-construction;
implementation complete") so the comment matches the current behavior of the
WarpRuntime.blobAutoConstruct tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 618511ac-4c40-4485-a0ca-09bffdfb570e

📥 Commits

Reviewing files that changed from the base of the PR and between 3d185b5 and ec783ce.

⛔ Files ignored due to path filters (1)
  • test/unit/domain/__snapshots__/WarpRuntime.apiSurface.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (33)
  • BACKLOG/OG-010-public-api-design-thinking.md
  • BACKLOG/OG-014-streaming-content-attachments.md
  • BACKLOG/OG-015-jsr-documentation-quality.md
  • BACKLOG/README.md
  • CHANGELOG.md
  • README.md
  • contracts/type-surface.m8.json
  • docs/ROADMAP.md
  • docs/design/streaming-cas-blob-storage.md
  • docs/design/streaming-cas-blob-storage.retro.md
  • index.d.ts
  • index.js
  • src/domain/WarpApp.js
  • src/domain/WarpCore.js
  • src/domain/WarpRuntime.js
  • src/domain/errors/WriterError.js
  • src/domain/services/CheckpointService.js
  • src/domain/services/PatchBuilderV2.js
  • src/domain/utils/defaultBlobStorage.js
  • src/domain/utils/streamUtils.js
  • src/domain/warp/query.methods.js
  • src/infrastructure/adapters/CasBlobAdapter.js
  • src/ports/BlobStoragePort.js
  • test/integration/api/helpers/setup.js
  • test/unit/domain/WarpGraph.content.test.js
  • test/unit/domain/WarpRuntime.blobAutoConstruct.test.js
  • test/unit/domain/services/CheckpointService.test.js
  • test/unit/domain/services/PatchBuilderV2.content.test.js
  • test/unit/domain/services/TreeConstruction.determinism.test.js
  • test/unit/infrastructure/adapters/CasBlobAdapter.test.js
  • test/unit/infrastructure/adapters/InMemoryBlobStorageAdapter.test.js
  • test/unit/ports/BlobStoragePort.test.js
  • test/unit/scripts/release-policy-shape.test.js

Comment on lines +87 to +88
4. Existing `getContent()` / `getEdgeContent()` remain as buffered convenience,
implemented on top of the stream primitive.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Criterion 4 does not match the current implementation.

src/domain/warp/query.methods.js:737-760 and src/domain/warp/query.methods.js:803-826 still call retrieve() directly for the buffered getters, so they are not yet “implemented on top of the stream primitive” as written. Please either relax this wording or route the buffered helpers through the stream path before closing OG-014.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@BACKLOG/OG-014-streaming-content-attachments.md` around lines 87 - 88, The
buffered helpers getContent() and getEdgeContent() currently call retrieve()
directly (see retrieve()), which contradicts the stated design that they should
be implemented on top of the stream primitive; update those functions so they
route through the stream-based path instead of calling retrieve() directly —
e.g., have getContent()/getEdgeContent() call the stream primitive (the existing
streaming retrieval function) and then collect the stream into a buffer before
returning, or alternatively relax the spec text to reflect current
implementation if you don't want to change code; ensure you update references to
retrieve(), getContent(), and getEdgeContent() so the buffered getters either
wrap the stream primitive or the doc is revised.

Comment on lines +838 to +841
// Fallback: wrap buffered read as single-chunk async iterable
const buf = await this._persistence.readBlob(oid);
return singleChunkAsyncIterable(buf);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential null propagation in fallback path.

When _blobStorage is unavailable and persistence.readBlob(oid) returns null (e.g., missing Git object), singleChunkAsyncIterable(buf) will yield null as a chunk. This violates the AsyncIterable<Uint8Array> return type contract.

The buffered getContent() at line 748 returns the raw null directly, which is semantically different (return null vs. yield null as a chunk).

Consider either:

  1. Checking for null and returning null before wrapping (matching buffered behavior)
  2. Throwing PersistenceError.E_MISSING_OBJECT (matching CasBlobAdapter behavior)
🛡️ Suggested fix (option 1 - match buffered API)
   // Fallback: wrap buffered read as single-chunk async iterable
   const buf = await this._persistence.readBlob(oid);
+  if (buf === null) {
+    return null;
+  }
   return singleChunkAsyncIterable(buf);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Fallback: wrap buffered read as single-chunk async iterable
const buf = await this._persistence.readBlob(oid);
return singleChunkAsyncIterable(buf);
}
// Fallback: wrap buffered read as single-chunk async iterable
const buf = await this._persistence.readBlob(oid);
if (buf === null) {
return null;
}
return singleChunkAsyncIterable(buf);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/warp/query.methods.js` around lines 838 - 841, In the fallback
path where _blobStorage is missing, ensure you handle a null return from
_persistence.readBlob(oid) — call readBlob, and if buf === null return null
immediately (rather than wrapping it with singleChunkAsyncIterable), so
getContentStream/getContent behavior matches the buffered getContent();
reference symbols: _blobStorage, _persistence.readBlob,
singleChunkAsyncIterable, getContent, and consider consistency with
CasBlobAdapter / PersistenceError.E_MISSING_OBJECT if you later decide to throw
instead.

Comment on lines +866 to +868
const buf = await this._persistence.readBlob(oid);
return singleChunkAsyncIterable(buf);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same null handling issue in edge content fallback.

The same concern applies here: if persistence.readBlob(oid) returns null, the streaming API will yield null as a chunk rather than returning null or throwing.

🛡️ Suggested fix
   const buf = await this._persistence.readBlob(oid);
+  if (buf === null) {
+    return null;
+  }
   return singleChunkAsyncIterable(buf);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const buf = await this._persistence.readBlob(oid);
return singleChunkAsyncIterable(buf);
}
const buf = await this._persistence.readBlob(oid);
if (buf === null) {
return null;
}
return singleChunkAsyncIterable(buf);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/warp/query.methods.js` around lines 866 - 868, The current logic
calls this._persistence.readBlob(oid) and immediately wraps the result with
singleChunkAsyncIterable(buf), which will yield a null chunk if readBlob returns
null; change the flow in the method that calls readBlob so it checks the result
(buf) and if buf is null return null (or throw a clear error) instead of
wrapping it in singleChunkAsyncIterable; update the branch around
this._persistence.readBlob(oid) to return null when buf === null and only call
singleChunkAsyncIterable(buf) when buf is non-null so callers of this method
(which expect either a null or an async iterable) get the correct behavior.

Comment on lines +53 to +62
async function autoConstructBlobStorage(persistence) {
const p = /** @type {{ plumbing?: unknown }} */ (persistence);
if (p.plumbing) {
const { default: CasBlobAdapter } = await import(
/* webpackIgnore: true */ '../infrastructure/adapters/CasBlobAdapter.js'
);
return new CasBlobAdapter({ plumbing: p.plumbing, persistence });
}
return new InMemoryBlobStorageAdapter();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persist auto-constructed in-memory blob storage across open() calls.

Line 61 creates a brand-new InMemoryBlobStorageAdapter on every open(). With mandatory blob storage, that can make prior content OIDs unreadable after reopening against the same in-memory persistence instance.

💡 Proposed fix (cache auto-constructed in-memory adapter per persistence object)
+const _autoInMemoryBlobStorageByPersistence = new WeakMap();

 async function autoConstructBlobStorage(persistence) {
   const p = /** `@type` {{ plumbing?: unknown }} */ (persistence);
   if (p.plumbing) {
     const { default: CasBlobAdapter } = await import(
       /* webpackIgnore: true */ '../infrastructure/adapters/CasBlobAdapter.js'
     );
     return new CasBlobAdapter({ plumbing: p.plumbing, persistence });
   }
-  return new InMemoryBlobStorageAdapter();
+  if (typeof persistence === 'object' && persistence !== null) {
+    const key = /** `@type` {object} */ (persistence);
+    const existing = _autoInMemoryBlobStorageByPersistence.get(key);
+    if (existing) {
+      return existing;
+    }
+    const storage = new InMemoryBlobStorageAdapter();
+    _autoInMemoryBlobStorageByPersistence.set(key, storage);
+    return storage;
+  }
+  return new InMemoryBlobStorageAdapter();
 }

Also applies to: 415-418

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/WarpRuntime.js` around lines 53 - 62, autoConstructBlobStorage
currently returns a new InMemoryBlobStorageAdapter() every time (in
autoConstructBlobStorage), which loses prior in-memory blobs across open()
calls; change it to cache and reuse a single InMemoryBlobStorageAdapter instance
per persistence object (keyed by the persistence reference) so subsequent calls
return the same adapter for that persistence. Implement similar caching wherever
the same one-off creation occurs (the other spot around lines 415-418), ensuring
you return the cached adapter when p.plumbing is absent and only create a new
InMemoryBlobStorageAdapter once per persistence object.

Comment on lines +210 to +268
retrieveStream(oid) {
const self = this;
return /** @type {AsyncIterable<Uint8Array>} */ ({
[Symbol.asyncIterator]() {
/** @type {AsyncIterator<Uint8Array>|null} */
let inner = null;
let initialized = false;
return {
async next() {
if (!initialized) {
initialized = true;
inner = await self._resolveStreamIterator(oid);
}
return await /** @type {AsyncIterator<Uint8Array>} */ (inner).next();
},
};
},
});
}

/**
* Resolves the inner async iterator for retrieveStream().
*
* @private
* @param {string} oid
* @returns {Promise<AsyncIterator<Uint8Array>>}
*/
async _resolveStreamIterator(oid) {
const cas = await this._getCas();

try {
const manifest = await cas.readManifest({ treeOid: oid });
/** @type {{ manifest: *, encryptionKey?: Uint8Array }} */
const restoreOpts = { manifest };
if (this._encryptionKey) {
restoreOpts.encryptionKey = this._encryptionKey;
}

if (typeof cas.restoreStream === 'function') {
const stream = cas.restoreStream(restoreOpts);
return stream[Symbol.asyncIterator]();
}

// Fallback: buffered restore as single chunk
const { buffer } = await cas.restore(restoreOpts);
return singleChunkIterator(buffer);
} catch (err) {
if (!isLegacyBlobError(err)) {
throw err;
}
const blob = await this._persistence.readBlob(oid);
if (blob === null || blob === undefined) {
throw new PersistenceError(
`Missing Git object: ${oid}`,
PersistenceError.E_MISSING_OBJECT,
{ context: { oid } },
);
}
return singleChunkIterator(blob);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the full file to understand the context
wc -l src/infrastructure/adapters/CasBlobAdapter.js

Repository: git-stunts/git-warp

Length of output: 112


🏁 Script executed:

# Check the retrieve() method to see the Uint8Array normalization
rg -A 30 "retrieve\(oid\)" src/infrastructure/adapters/CasBlobAdapter.js | head -50

Repository: git-stunts/git-warp

Length of output: 1292


🏁 Script executed:

# Look for any return() method handling in the file
rg "return\(\)" src/infrastructure/adapters/CasBlobAdapter.js

Repository: git-stunts/git-warp

Length of output: 45


🏁 Script executed:

# Check if there's any async generator or similar pattern in the file
rg "async\s*\*|Symbol\.asyncIterator" src/infrastructure/adapters/CasBlobAdapter.js -A 3

Repository: git-stunts/git-warp

Length of output: 319


🏁 Script executed:

# Get the full retrieveStream() method implementation
sed -n '210,230p' src/infrastructure/adapters/CasBlobAdapter.js

Repository: git-stunts/git-warp

Length of output: 628


🏁 Script executed:

# Look for tests related to retrieveStream or stream cleanup
find . -type f -name "*.test.js" -o -name "*.spec.js" | xargs grep -l "retrieveStream" 2>/dev/null | head -5

Repository: git-stunts/git-warp

Length of output: 353


🏁 Script executed:

# Check if there's a return() method implementation anywhere in the file
sed -n '1,290p' src/infrastructure/adapters/CasBlobAdapter.js | grep -n "return\s*(\|throw\s*(" | grep -v "//"

Repository: git-stunts/git-warp

Length of output: 45


🏁 Script executed:

# Look at the complete _resolveStreamIterator method to see the full logic
sed -n '228,268p' src/infrastructure/adapters/CasBlobAdapter.js

Repository: git-stunts/git-warp

Length of output: 1309


🏁 Script executed:

# Check what singleChunkIterator does
rg -B 3 -A 10 "singleChunkIterator" src/infrastructure/adapters/CasBlobAdapter.js | head -30

Repository: git-stunts/git-warp

Length of output: 833


🏁 Script executed:

# Look at the test file to see if early-break tests exist
rg "retrieveStream|early.*break|return\(\)" test/unit/infrastructure/adapters/CasBlobAdapter.test.js -A 5 -B 2 | head -50

Repository: git-stunts/git-warp

Length of output: 1220


🏁 Script executed:

# Check the full singleChunkIterator implementation to see if it handles cleanup
sed -n '273,290p' src/infrastructure/adapters/CasBlobAdapter.js

Repository: git-stunts/git-warp

Length of output: 462


🏁 Script executed:

# Search for any async-generator patterns or return() handling elsewhere in the file
rg "async\s*\*.*{|\.return\(|\.throw\(" src/infrastructure/adapters/CasBlobAdapter.js

Repository: git-stunts/git-warp

Length of output: 192


🏁 Script executed:

# Let's verify if there are any tests that break early from retrieveStream
rg -A 20 "retrieveStream" test/unit/infrastructure/adapters/CasBlobAdapter.test.js | grep -A 20 "break\|return\|early"

Repository: git-stunts/git-warp

Length of output: 803


🏁 Script executed:

# Check if there's Uint8Array normalization applied to the chunks in any way
rg -B 5 -A 5 "Uint8Array" src/infrastructure/adapters/CasBlobAdapter.js | grep -A 5 -B 5 "normali\|constructor"

Repository: git-stunts/git-warp

Length of output: 995


Add return() and throw() delegation to the async iterator wrapper, and normalize Uint8Array chunks in retrieveStream().

The wrapper at lines 210–220 only implements next(), missing the return() and throw() methods needed for cleanup. If a consumer breaks early from the stream, the underlying cas.restoreStream() iterator never receives the cancellation signal, leaking resources. Additionally, the retrieve() method normalizes Buffer subclasses to true Uint8Array instances (lines 168–170), but retrieveStream() skips this normalization entirely—the streamed chunks may not match the buffered path's behavior.

Convert the wrapper to an async generator, which automatically delegates cleanup methods and provides a clearer control flow for applying the normalization. Add a test that breaks early from the stream to verify cleanup is properly invoked.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/infrastructure/adapters/CasBlobAdapter.js` around lines 210 - 268, The
current retrieveStream wrapper (retrieveStream) only implements next() so
return()/throw() never propagate to the underlying iterator returned by
_resolveStreamIterator, and streamed chunks are not normalized to true
Uint8Array like retrieve() does; convert retrieveStream into an async generator
that awaits the iterator from _resolveStreamIterator (or uses the iterator
returned by cas.restoreStream/singleChunkIterator) and uses a normalization
helper (same logic as retrieve() that converts Buffer subclasses to Uint8Array)
before yielding each chunk so both buffered and streamed paths behave
identically, and rely on generator semantics to delegate return()/throw() to the
inner iterator to ensure cleanup; add a unit test that consumes the async
iterable and breaks early (calls return on the iterator or uses break in a
for-await loop) to assert the underlying cas.restoreStream iterator saw
cancellation/cleanup.

Comment on lines +618 to +620
await expect(builder.attachContent('node:1', 'hello'))
.rejects.toThrow('Cannot attach content without blob storage');
expect(persistence.writeBlob).not.toHaveBeenCalled();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Check E_NO_BLOB_STORAGE explicitly.

This breaking change is the error code, not just the message. These expectations still pass if the implementation regresses to a generic Error, and the OG-014 case below duplicates the same behavior without tightening the contract.

Also applies to: 642-644

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/services/PatchBuilderV2.content.test.js` around lines 618 -
620, Replace the generic error message assertion with an explicit check for the
error code E_NO_BLOB_STORAGE when calling builder.attachContent('node:1',
'hello') — e.g. use await
expect(builder.attachContent(...)).rejects.toMatchObject({code:
'E_NO_BLOB_STORAGE'}) or await expect(...).rejects.toHaveProperty('code',
'E_NO_BLOB_STORAGE') and keep the persistence.writeBlob non-call assertion;
apply the same change for the other case referenced (OG-014 at the later
assertion).

Critical:
- Fix contentHash to slice ArrayBuffer view correctly (was hashing
  entire backing buffer for sub-array inputs)

High:
- Remove dead legacy blob-mode branch in PatchBuilderV2 tree construction
- Guard isStreamingInput against Uint8Array/string before async checks

Medium:
- Add return() to CasBlobAdapter retrieveStream iterator and
  singleChunkIterator for proper early-break cleanup
- Fully revert README.md (unintended agent modification)

Low:
- Move collectAsyncIterable to streamUtils.js (eliminate duplication)
- Rename error code E_NO_BLOB_STORAGE → NO_BLOB_STORAGE (convention)
- Add FNV-1a OID length documentation comment

Nit:
- Fix stale comment in WarpApp content section
- Collapse multi-line JSDoc in WarpCore getEdgeContentMeta
@github-actions
Copy link
Copy Markdown

Release Preflight

  • package version: 15.0.1
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v15.0.1, release workflow will publish.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/infrastructure/adapters/CasBlobAdapter.js (1)

210-231: ⚠️ Potential issue | 🟠 Major

return() does not delegate to inner iterator; streamed chunks lack normalization.

Two issues remain from the prior review feedback:

  1. Incomplete cleanup: Line 225-227 returns immediately without calling inner.return(). If a consumer breaks early from a cas.restoreStream() iterator, the underlying stream never receives cancellation, potentially leaking resources.

  2. Missing normalization: Unlike retrieve() (lines 146-149), retrieveStream() yields chunks without normalizing Buffer subclasses to Uint8Array. This inconsistency could cause instanceof Uint8Array checks to fail downstream (see comparison.methods.js:874).

🔒 Proposed fix: delegate return() and normalize chunks
   retrieveStream(oid) {
     const self = this;
     return /** `@type` {AsyncIterable<Uint8Array>} */ ({
       [Symbol.asyncIterator]() {
         /** `@type` {AsyncIterator<Uint8Array>|null} */
         let inner = null;
         let initialized = false;
         return {
           async next() {
             if (!initialized) {
               initialized = true;
               inner = await self._resolveStreamIterator(oid);
             }
-            return await /** `@type` {AsyncIterator<Uint8Array>} */ (inner).next();
+            const result = await /** `@type` {AsyncIterator<Uint8Array>} */ (inner).next();
+            if (result.done) {
+              return result;
+            }
+            // Normalize Buffer to Uint8Array for domain boundary compliance
+            const chunk = result.value;
+            const normalized = chunk instanceof Uint8Array && chunk.constructor !== Uint8Array
+              ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)
+              : chunk;
+            return { value: normalized, done: false };
           },
-          return() {
-            return Promise.resolve({ value: undefined, done: true });
+          async return() {
+            if (inner && typeof inner.return === 'function') {
+              await inner.return();
+            }
+            return { value: undefined, done: true };
           },
         };
       },
     });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/infrastructure/adapters/CasBlobAdapter.js` around lines 210 - 231, In
retrieveStream(), ensure early cancellation delegates to the underlying iterator
and that yielded chunks are normalized to Uint8Array: when implementing the
iterator returned by retrieveStream(), call await inner.return() (if present)
inside the return() method rather than immediately resolving, and when pulling
values from inner (in next()) coerce any Buffer or Buffer subclass to a
Uint8Array (matching retrieve()'s normalization) before returning; locate
symbols retrieveStream, _resolveStreamIterator, inner, and return() to apply
these changes.
🧹 Nitpick comments (2)
src/domain/utils/streamUtils.js (1)

89-104: Missing return() handler for iterator cleanup consistency.

The readableStreamToAsyncIterable function (lines 125-128) includes a return() handler for cleanup, but singleValueAsyncIterable does not. While there's nothing to clean up for a single value, adding a no-op return() maintains consistency and ensures proper protocol compliance when consumers call break or return early in a for-await loop.

♻️ Suggested addition for consistency
       next() {
         if (done) {
           return Promise.resolve({ value: undefined, done: true });
         }
         done = true;
         return Promise.resolve({ value, done: false });
       },
+      return() {
+        done = true;
+        return Promise.resolve({ value: undefined, done: true });
+      },
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/utils/streamUtils.js` around lines 89 - 104, Add a no-op return()
method to the iterator returned by singleValueAsyncIterable so it implements the
async iterator cleanup protocol like readableStreamToAsyncIterable; update the
object returned by [Symbol.asyncIterator]() (the iterator with next()) to also
include a return() that sets done = true and returns Promise.resolve({ value:
undefined, done: true }) to gracefully handle early termination in for-await
loops.
src/domain/utils/defaultBlobStorage.js (1)

97-117: Consider adding return() handler for iterator protocol completeness.

The async iterator lacks a return() method. While cleanup isn't strictly needed for in-memory reads, adding it would make the implementation consistent with CasBlobAdapter.singleChunkIterator and fully conform to the async iterator protocol.

🔧 Optional: Add return() handler
       return {
         next() {
           if (done) {
             return Promise.resolve({ value: undefined, done: true });
           }
           done = true;
           return Promise.resolve({ value: chunk, done: false });
         },
+        return() {
+          done = true;
+          return Promise.resolve({ value: undefined, done: true });
+        },
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/utils/defaultBlobStorage.js` around lines 97 - 117, The async
iterator returned by retrieveStream(oid) is missing a return() method; add a
return() handler on the iterator object returned by [Symbol.asyncIterator]()
that sets the internal done flag to true and returns a resolved Promise of {
value: undefined, done: true } so the iterator conforms to the async iterator
protocol (mirror the behavior used by CasBlobAdapter.singleChunkIterator),
ensuring proper cleanup/early termination support.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/domain/utils/streamUtils.js`:
- Around line 112-132: The iterator returned by readableStreamToAsyncIterable
can leak the reader lock if reader.read() throws; modify the iterator so
reader.releaseLock() is always called on error by wrapping the await
reader.read() in try/catch/finally (or implement a throw(error) method on the
iterator that releases the lock before rethrowing), ensure next() uses
try/finally to release on thrown errors, and keep the existing return() behavior
to release the lock when iteration finishes; reference the
readableStreamToAsyncIterable function, its inner [Symbol.asyncIterator]()
returning next() and return(), and the reader.read()/reader.releaseLock() calls
when making the change.

---

Duplicate comments:
In `@src/infrastructure/adapters/CasBlobAdapter.js`:
- Around line 210-231: In retrieveStream(), ensure early cancellation delegates
to the underlying iterator and that yielded chunks are normalized to Uint8Array:
when implementing the iterator returned by retrieveStream(), call await
inner.return() (if present) inside the return() method rather than immediately
resolving, and when pulling values from inner (in next()) coerce any Buffer or
Buffer subclass to a Uint8Array (matching retrieve()'s normalization) before
returning; locate symbols retrieveStream, _resolveStreamIterator, inner, and
return() to apply these changes.

---

Nitpick comments:
In `@src/domain/utils/defaultBlobStorage.js`:
- Around line 97-117: The async iterator returned by retrieveStream(oid) is
missing a return() method; add a return() handler on the iterator object
returned by [Symbol.asyncIterator]() that sets the internal done flag to true
and returns a resolved Promise of { value: undefined, done: true } so the
iterator conforms to the async iterator protocol (mirror the behavior used by
CasBlobAdapter.singleChunkIterator), ensuring proper cleanup/early termination
support.

In `@src/domain/utils/streamUtils.js`:
- Around line 89-104: Add a no-op return() method to the iterator returned by
singleValueAsyncIterable so it implements the async iterator cleanup protocol
like readableStreamToAsyncIterable; update the object returned by
[Symbol.asyncIterator]() (the iterator with next()) to also include a return()
that sets done = true and returns Promise.resolve({ value: undefined, done: true
}) to gracefully handle early termination in for-await loops.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62637179-3ec1-4f0b-a491-6d2d4d591612

📥 Commits

Reviewing files that changed from the base of the PR and between ec783ce and bc12f76.

📒 Files selected for processing (8)
  • src/domain/WarpApp.js
  • src/domain/WarpCore.js
  • src/domain/errors/WriterError.js
  • src/domain/services/PatchBuilderV2.js
  • src/domain/utils/defaultBlobStorage.js
  • src/domain/utils/streamUtils.js
  • src/infrastructure/adapters/CasBlobAdapter.js
  • test/unit/domain/services/PatchBuilderV2.content.test.js
✅ Files skipped from review due to trivial changes (2)
  • src/domain/errors/WriterError.js
  • src/domain/services/PatchBuilderV2.js

Comment on lines +112 to +132
function readableStreamToAsyncIterable(stream) {
return {
[Symbol.asyncIterator]() {
const reader = stream.getReader();
return {
async next() {
const { value, done } = await reader.read();
if (done) {
reader.releaseLock();
return { value: undefined, done: true };
}
return { value, done: false };
},
return() {
reader.releaseLock();
return Promise.resolve({ value: undefined, done: true });
},
};
},
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential resource leak if reader.read() throws.

If reader.read() throws an error, releaseLock() is never called, leaving the stream locked. Consider wrapping the read in try-catch or adding a throw() method to the iterator.

🛡️ Proposed fix to ensure lock release on error
 function readableStreamToAsyncIterable(stream) {
   return {
     [Symbol.asyncIterator]() {
       const reader = stream.getReader();
       return {
         async next() {
-          const { value, done } = await reader.read();
-          if (done) {
+          try {
+            const { value, done } = await reader.read();
+            if (done) {
+              reader.releaseLock();
+              return { value: undefined, done: true };
+            }
+            return { value, done: false };
+          } catch (err) {
             reader.releaseLock();
-            return { value: undefined, done: true };
+            throw err;
           }
-          return { value, done: false };
         },
         return() {
           reader.releaseLock();
           return Promise.resolve({ value: undefined, done: true });
         },
       };
     },
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/utils/streamUtils.js` around lines 112 - 132, The iterator
returned by readableStreamToAsyncIterable can leak the reader lock if
reader.read() throws; modify the iterator so reader.releaseLock() is always
called on error by wrapping the await reader.read() in try/catch/finally (or
implement a throw(error) method on the iterator that releases the lock before
rethrowing), ensure next() uses try/finally to release on thrown errors, and
keep the existing return() behavior to release the lock when iteration finishes;
reference the readableStreamToAsyncIterable function, its inner
[Symbol.asyncIterator]() returning next() and return(), and the
reader.read()/reader.releaseLock() calls when making the change.

BREAKING CHANGE: TraversalService alias and createWriter() method removed.

- TraversalService: deleted source file and test, removed from index.js,
  index.d.ts, type-surface manifest, ESLint config, and export tests.
  Use CommitDagTraversalService directly.

- createWriter(): removed from patch.methods.js, WarpApp, index.d.ts,
  type-surface manifest, and all test files. Use writer() or writer(id).

LogicalTraversal retained as internal implementation — traverse property
still works on WarpRuntime, Observer, and Worldline.

5192 tests pass across 321 files.
Bump package.json and jsr.json to 16.0.0.
Date CHANGELOG [Unreleased] section to [16.0.0] — 2026-03-29.
Add MIGRATING.md with upgrade guide for all breaking changes.
Update ROADMAP header to v16.0.0 current.

BREAKING CHANGE: see MIGRATING.md for full upgrade guide.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
index.d.ts (1)

366-367: Narrow the GraphPersistencePort summary to match declared methods.

The description currently mentions refs/blobs/trees, but this interface only declares commit/node/log/ping/count operations.

Doc wording tweak
-/** Abstract port for Git persistence operations (commits, refs, blobs, trees). */
+/** Abstract port for Git commit persistence and graph-walk operations. */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.d.ts` around lines 366 - 367, Update the JSDoc summary for class
GraphPersistencePort to accurately reflect only the operations declared (commit,
node, log, ping, count) instead of mentioning refs/blobs/trees; locate the
GraphPersistencePort class and edit its top comment to describe it as an
abstract port for commit/node/log/ping/count persistence operations and remove
references to refs, blobs, and trees so the doc matches the implemented methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@index.d.ts`:
- Around line 251-254: The union type WeightedCostSelector currently allows `{}`
because both properties are optional; change the union so each branch requires
its respective provider: make the first branch require weightFn (and keep
nodeWeightFn as never/optional) and make the second branch require nodeWeightFn
(and keep weightFn as never/optional) so consumers must supply exactly one of
EdgeWeightFn or NodeWeightFn; refer to WeightedCostSelector, EdgeWeightFn and
NodeWeightFn when updating the type.

---

Nitpick comments:
In `@index.d.ts`:
- Around line 366-367: Update the JSDoc summary for class GraphPersistencePort
to accurately reflect only the operations declared (commit, node, log, ping,
count) instead of mentioning refs/blobs/trees; locate the GraphPersistencePort
class and edit its top comment to describe it as an abstract port for
commit/node/log/ping/count persistence operations and remove references to refs,
blobs, and trees so the doc matches the implemented methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a24a88d2-7191-49db-8f34-63b4d36dc9c7

📥 Commits

Reviewing files that changed from the base of the PR and between bc12f76 and 024b0bb.

⛔ Files ignored due to path filters (1)
  • test/unit/domain/__snapshots__/WarpRuntime.apiSurface.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (22)
  • .gitignore
  • CHANGELOG.md
  • MIGRATING.md
  • contracts/type-surface.m8.json
  • docs/ROADMAP.md
  • eslint.config.js
  • index.d.ts
  • index.js
  • jsr.json
  • package.json
  • src/domain/WarpApp.js
  • src/domain/services/TraversalService.js
  • src/domain/warp/_wiredMethods.d.ts
  • src/domain/warp/patch.methods.js
  • test/type-check/consumer.ts
  • test/unit/domain/WarpGraph.prototypeWiring.test.js
  • test/unit/domain/WarpGraph.test.js
  • test/unit/domain/WarpGraph.writerApi.test.js
  • test/unit/domain/WarpGraph.writerInvalidation.test.js
  • test/unit/domain/index.exports.test.js
  • test/unit/domain/services/TraversalService.test.js
  • test/unit/scripts/release-policy-shape.test.js
💤 Files with no reviewable changes (8)
  • src/domain/services/TraversalService.js
  • test/type-check/consumer.ts
  • test/unit/domain/services/TraversalService.test.js
  • eslint.config.js
  • src/domain/warp/_wiredMethods.d.ts
  • test/unit/domain/WarpGraph.test.js
  • test/unit/domain/index.exports.test.js
  • test/unit/domain/WarpGraph.writerApi.test.js
✅ Files skipped from review due to trivial changes (5)
  • .gitignore
  • package.json
  • jsr.json
  • test/unit/scripts/release-policy-shape.test.js
  • MIGRATING.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/ROADMAP.md
  • index.js
  • contracts/type-surface.m8.json

Comment on lines +251 to 254
/** Selector for weighted cost traversal mode — supply either an edge or node weight function, not both. */
export type WeightedCostSelector =
| { weightFn?: EdgeWeightFn; nodeWeightFn?: never }
| { nodeWeightFn?: NodeWeightFn; weightFn?: never };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Require exactly one provider in WeightedCostSelector.

The current union still accepts {} because both branches are optional, which conflicts with the declared contract (“either edge or node weight function”).

Suggested type fix
 export type WeightedCostSelector =
-  | { weightFn?: EdgeWeightFn; nodeWeightFn?: never }
-  | { nodeWeightFn?: NodeWeightFn; weightFn?: never };
+  | { weightFn: EdgeWeightFn; nodeWeightFn?: never }
+  | { nodeWeightFn: NodeWeightFn; weightFn?: never };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Selector for weighted cost traversal mode — supply either an edge or node weight function, not both. */
export type WeightedCostSelector =
| { weightFn?: EdgeWeightFn; nodeWeightFn?: never }
| { nodeWeightFn?: NodeWeightFn; weightFn?: never };
/** Selector for weighted cost traversal mode — supply either an edge or node weight function, not both. */
export type WeightedCostSelector =
| { weightFn: EdgeWeightFn; nodeWeightFn?: never }
| { nodeWeightFn: NodeWeightFn; weightFn?: never };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.d.ts` around lines 251 - 254, The union type WeightedCostSelector
currently allows `{}` because both properties are optional; change the union so
each branch requires its respective provider: make the first branch require
weightFn (and keep nodeWeightFn as never/optional) and make the second branch
require nodeWeightFn (and keep weightFn as never/optional) so consumers must
supply exactly one of EdgeWeightFn or NodeWeightFn; refer to
WeightedCostSelector, EdgeWeightFn and NodeWeightFn when updating the type.

Keep LogicalTraversal interface and traverse property in index.d.ts
and type-surface manifest — the runtime traverse property is still
used by consumers via graph.traverse.bfs() etc. Only TraversalService
alias and createWriter() are removed as breaking changes.
@github-actions
Copy link
Copy Markdown

Release Preflight

  • package version: 16.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v16.0.0, release workflow will publish.

@flyingrobots flyingrobots merged commit a4d9dda into main Mar 29, 2026
7 checks passed
@flyingrobots flyingrobots deleted the slice/og-014-streaming-cas-blob-storage branch March 29, 2026 21:48
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