Skip to content

Comments

feat(partition): M9 — SyncController extraction, JoinReducer split, bitmap OID validation#44

Merged
flyingrobots merged 14 commits intomainfrom
feat/m9-partition
Feb 22, 2026
Merged

feat(partition): M9 — SyncController extraction, JoinReducer split, bitmap OID validation#44
flyingrobots merged 14 commits intomainfrom
feat/m9-partition

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Feb 22, 2026

Summary

  • Extract SyncController from WarpGraph (555 LOC → standalone service), wire 9 sync methods via defineProperty delegation
  • Split JoinReducer.join() into applyFast / applyWithReceipt dual-path with shared updateFrontierFromPatch() helper
  • Add validateShardOid() utility + flip BitmapIndexReader strict default to true (fail-safe)
  • Fix latent frontier bug: applySyncResponse() was double-casting observedFrontier (Lamport counters) as SHA frontier; now uses _lastFrontier with createFrontier() fallback
  • Publication-quality SVG diagrams (8 Graphviz figures)
  • Bump to v11.5.1

Test plan

  • All existing unit tests pass (npm run test:local)
  • New SyncController unit tests cover applySyncResponse, syncNeeded, processSyncRequest, syncWith
  • Lint passes
  • No-coordination regression suite passes
  • B56 frontier fix verified via new tests

Summary by CodeRabbit

  • New Features

    • Centralized sync controller and new patch-application paths (fast/with-receipt); public sync surface routed through it.
    • New OID validation utility.
  • Bug Fixes

    • Fixed frontier/response argument handling, infinite-delegation path, and stricter OID validation defaults to avoid corruption.
  • Documentation

    • Added publication‑quality architectural and process diagrams across docs.
  • Tests

    • Extensive new and updated unit tests covering sync flows, auth, materialization, OID validation, and patch paths.
  • Chores

    • Package/version bump to 11.5.1.

- Convert broken {..|..} record syntax to HTML TABLE elements (4 files, 6 nodes)
- Remove sed post-processing that stripped intentional fill="white" from nodes
- Rename fig-empty-tree → fig-data-storage; remove misleading empty-tree node
- Fix architecture arrows: port → adapter with "implements" labels
- Reorient multi-writer and materialize-pipeline diagrams to vertical (TB)
- Add missing index_port/cache_port edges in architecture diagram
- Add elision nodes for dangling leaf refs in ref-layout diagram
- Show dot warnings in build script instead of suppressing stderr
- Truncate SHA to 7 chars, fix beta fibre label, consistent cluster fontsize
- Embed diagrams in README.md (3) and docs/GUIDE.md (7)
…ath, add bitmap OID validation

B33 — SyncController extraction: all 9 sync methods + 2 private helpers
moved from sync.methods.js free functions into a SyncController class in
src/domain/services/. sync.methods.js reduced to thin one-liner
delegation stubs. WarpGraph instantiates SyncController in constructor.
16 new unit tests with mock host object.

B32 — JoinReducer dual-path refactor: extracted updateFrontierFromPatch()
shared helper, created applyFast() and applyWithReceipt() as named
exported functions, simplified join() to 3-line dispatcher. reduceV5()
calls named functions directly. 4 new tests.

B31 — Bitmap OID validation: added isValidOid() domain-local validator
in src/domain/utils/validateShardOid.js. BitmapIndexReader.setup() now
validates each OID — strict mode throws ShardCorruptionError, non-strict
skips with warning. 13 new tests. Updated mock OIDs across 4 test files
to use valid hex strings.

M9 gate: WarpGraph.js = 422 LOC (< 500); 3864/3864 tests pass;
no-coordination regression suite green.
CHANGELOG updated with SyncController extraction, JoinReducer
dual-path refactor, and bitmap OID validation. package.json and
jsr.json versions synced to 11.5.1.
The previous default (strict: false) combined with the nullLogger
default meant corrupt shards were silently swallowed. Flipping to
strict: true ensures corruption is surfaced immediately. The only
production caller (IndexRebuildService.load()) already passed
strict: true, so this changes nothing in practice — it only
protects direct callers from the silent-loss footgun.
B58: dead sync constants in _internal.js
B59: wireWarpMethods 3-hop indirection for sync
B60: test helper for mock OIDs
B61: doubled @type {any} annotations in tests
B62: SyncHost typedef for SyncController coupling
B58 — Delete orphaned sync constants and SyncError/OperationAbortedError
re-exports from _internal.js (no remaining importers after SyncController
extraction).

B59 — Delete sync.methods.js entirely. Wire 9 sync methods directly onto
WarpGraph.prototype via defineProperty loop delegating to _syncController.
Eliminates the 3-hop indirection (prototype → stub → controller).

B60 — Add test/helpers/mockOid.js: deterministic hex OID generator that
prevents test breakage when OID validation is tightened. 6 tests.

B61 — Remove 66 doubled /** @type */ annotations across 13 test files.

B62 — Add @typedef SyncHost to SyncController.js documenting the exact
12-field + 3-method host interface the controller depends on. Constructor
now typed against SyncHost instead of WarpGraph import.
…Controller tests

- SyncController.applySyncResponse() was double-casting observedFrontier
  (Lamport counters Map<string, number>) to SHA frontier; now uses
  _lastFrontier with createFrontier() fallback (fixes B56)
- syncWith() calls this.createSyncRequest/applySyncResponse instead of
  this._host.* (avoids infinite delegation loop)
- BitmapIndexReader JSDoc: fix variable name in non-strict example
- Delete unused mockOid helper (B60 reverted)
- Update sync tests to mock on _syncController after extraction
- Add SyncController unit tests: applySyncResponse, syncNeeded,
  processSyncRequest, syncWith direct-peer
- Add validateShardOid edge-case tests
… ratchet

- SyncHost.materialize: (options?: *) → (options?: Record<string, unknown>)
- serve() graph cast: {*} → {processSyncRequest: Function} via unknown
@flyingrobots
Copy link
Member Author

@coderabbitai review please 🥕

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 22 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This pull request refactors the sync architecture by extracting all synchronization logic from sync.methods.js into a new SyncController class, which is then integrated into WarpGraph via instance delegation. Additionally, JoinReducer exports two new application functions (applyFast, applyWithReceipt), BitmapIndexReader gains OID validation, and a new utility function isValidOid is introduced. Extensive test coverage and documentation updates accompany the refactoring.

Changes

Cohort / File(s) Summary
Architecture & Version
jsr.json, package.json, CHANGELOG.md
Version bumped to 11.5.1 and changelog updated to reflect SyncController integration and related refactors.
SyncController Integration
src/domain/services/SyncController.js, src/domain/WarpGraph.js, src/domain/warp/sync.methods.js
Adds new SyncController class encapsulating sync methods and helpers; WarpGraph now instantiates _syncController and delegates sync APIs to it; sync.methods.js removed (functionality moved into SyncController).
JoinReducer Refactor
src/domain/services/JoinReducer.js
Adds exported applyFast and applyWithReceipt functions and updateFrontierFromPatch helper; reduceV5/join flow routed to the new dispatcher-style functions.
OID Validation
src/domain/utils/validateShardOid.js, src/domain/services/BitmapIndexReader.js
New isValidOid validator (hex 4–64 chars); BitmapIndexReader now defaults strict: true, validates shard OIDs in setup(), throwing ShardCorruptionError in strict mode or warning/skipping in lenient mode.
Public API Surface Removals
src/domain/warp/_internal.js
Removes several exported sync-related errors/constants (SyncError, OperationAbortedError, and DEFAULT_* sync constants) from the legacy _internal surface.
Tests — SyncController & OID
test/unit/domain/services/SyncController.test.js, test/unit/domain/utils/validateShardOid.test.js
Adds comprehensive SyncController test suite (frontier, status, protocol, syncWith HTTP/direct, errors, retries, auth, materialize) and unit tests for isValidOid.
Tests — Refactoring & Fixtures
test/unit/domain/WarpGraph.*.test.js, test/unit/domain/services/JoinReducer.test.js, test/unit/domain/services/BitmapIndexReader.test.js, ...test/unit/...
Updates tests to mock/verify via g._syncController instead of top-level graph methods; adds tests for applyFast/applyWithReceipt; standardizes OID fixtures to longer hex values; removes stray JSDoc annotations and adjusts many test fixtures.
Docs & Tooling
README.md, docs/GUIDE.md, ROADMAP.md, eslint.config.js, src/domain/warp/_wiredMethods.d.ts, scripts/build-diagrams.sh
Adds SVG diagrams to README/GUIDE; updates roadmap task statuses; replaces sync.methods reference with SyncController in docs/types; adjusts eslint groups to include SyncController; tweaks build script to capture dot warnings.

Sequence Diagram(s)

sequenceDiagram
  participant WG as WarpGraph
  participant SC as SyncController
  participant P as Persistence
  participant R as Remote (HTTP/Peer)
  participant HS as HttpSyncServer

  WG->>SC: invoke syncWith(remote, options)
  SC->>P: getFrontier() / read refs
  alt direct peer
    SC->>R: call processSyncRequest(request) on peer
    R-->>SC: syncResponse
  else http path
    SC->>R: HTTP POST /sync with auth headers
    R-->>SC: HTTP response (JSON or error)
  end
  SC->>SC: validate/parse response
  SC->>P: applySyncResponse -> update cached state / lastFrontier
  SC-->>WG: return sync result (applied count / state)
  WG->>HS: (serve) HttpSyncServer constructed with host/graph and auth
  HS->>SC: delegate incoming requests to SC.processSyncRequest
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through code to tidy the sync,

New controller snug where old calls would clink.
OIDs checked and frontiers aligned,
applyFast races while receipts unwind,
Tests applaud — a clean, carrot-sweet sync!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the three main components of this changeset: SyncController extraction, JoinReducer split into applyFast/applyWithReceipt, and bitmap OID validation logic.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% 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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/m9-partition

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.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

@flyingrobots, I'll review the changes in this PR! This looks like a substantial refactoring with the SyncController extraction, JoinReducer split, and the frontier bug fix. I'll provide detailed feedback shortly. 🥕

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions
Copy link

Release Preflight

  • package version: 11.5.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 v11.5.1, release workflow will publish.

Copy link

@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: 3

🧹 Nitpick comments (4)
src/domain/utils/validateShardOid.js (1)

7-9: Consider documenting the rationale for the 4-char minimum.

The 4-character lower bound is permissive relative to Git's default minimum abbreviation length of 7 and its full SHA-1 length of 40. A comment explaining the intent (e.g., "allows abbreviated test fixtures and short internal IDs") would make the constraint self-explanatory for future maintainers.

📝 Suggested doc addition
 /**
  * Validates a shard Object ID (hex string, 4-64 chars).
  *
+ * The 4-character minimum accommodates abbreviated OIDs (e.g. test fixtures,
+ * short internal IDs). Git full SHA-1 OIDs are 40 chars; SHA-256 OIDs are 64 chars.
+ *
  * `@param` {string} oid - The OID to validate
  * `@returns` {boolean} True if oid is a valid hex string of 4-64 characters
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/utils/validateShardOid.js` around lines 7 - 9, Add a short
explanatory comment above the isValidOid function (which uses the
/^[0-9a-fA-F]{4,64}$/ check) describing why the minimum is 4 characters—e.g., to
permit abbreviated test fixtures and short internal IDs while still preventing
overly short/noisy values and allowing full SHA-length IDs—so future maintainers
understand the rationale and can adjust the bound intentionally.
test/unit/domain/WarpGraph.syncWith.test.js (1)

28-29: Use vi.spyOn() for mocking _syncController methods instead of direct property assignment.

Direct property assignment creates own-properties that shadow prototype methods. While the current test structure with fresh instances per beforeEach prevents mock leakage, vi.spyOn() is the idiomatic Vitest pattern and aligns with existing patterns in this file (e.g., line 72). It provides cleaner automatic restoration and is more declarative.

♻️ Suggested alternative
-    /** `@type` {any} */ (graph)._syncController.applySyncResponse = vi.fn().mockReturnValue({ applied: 0 });
-    /** `@type` {any} */ (graph)._syncController.createSyncRequest = vi.fn().mockResolvedValue({ type: 'sync-request', frontier: {} });
+    vi.spyOn(/** `@type` {any} */ (graph)._syncController, 'applySyncResponse').mockReturnValue({ applied: 0 });
+    vi.spyOn(/** `@type` {any} */ (graph)._syncController, 'createSyncRequest').mockResolvedValue({ type: 'sync-request', frontier: {} });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/WarpGraph.syncWith.test.js` around lines 28 - 29, The test
currently assigns mocks directly to graph._syncController.applySyncResponse and
graph._syncController.createSyncRequest which creates own-properties that shadow
prototype methods; replace those direct assignments with
vi.spyOn(graph._syncController, 'applySyncResponse').mockReturnValue({ applied:
0 }) and vi.spyOn(graph._syncController,
'createSyncRequest').mockResolvedValue({ type: 'sync-request', frontier: {} })
so the spies follow the idiomatic Vitest pattern, get automatic restoration, and
match the style used elsewhere in the file (referencing
graph._syncController.applySyncResponse and
graph._syncController.createSyncRequest).
test/unit/domain/services/SyncController.test.js (1)

397-413: createSyncRequest test verifies shape but not frontier content.

The discoverWriters mock resolves to ['alice'] and readRef resolves to 'sha-alice', so the resulting request frontier is deterministic. Asserting frontier.alice === 'sha-alice' (or equivalent) would give stronger signal that getFrontier is wired correctly end-to-end through createSyncRequest.

♻️ Suggested addition
 expect(request).toHaveProperty('type', 'sync-request');
 expect(request).toHaveProperty('frontier');
+expect(request.frontier).toMatchObject({ alice: 'sha-alice' });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/services/SyncController.test.js` around lines 397 - 413, The
test for SyncController.createSyncRequest currently only checks the request
shape; update the test to assert the actual frontier entries by verifying that
the produced request.frontier contains the expected mapping for the mocked
writer (e.g., assert request.frontier.alice === 'sha-alice' or deep-equal the
whole frontier object). Use the existing mocks (discoverWriters resolving to
['alice'] and _persistence.readRef resolving to 'sha-alice') and add an
expectation after creating the request to check the frontier value returned by
createSyncRequest (via SyncController.createSyncRequest / getFrontier flow).
test/unit/domain/WarpGraph.syncAuth.test.js (1)

16-31: Consider a brief comment explaining the two mocking strategies.

mockClientGraph must mock via _syncController because createSyncRequest and applySyncResponse are invoked as this.xxx() inside SyncController.syncWith — mocking on the WarpGraph instance would not intercept them. mockServerGraph can mock directly on the instance because processSyncRequest is called externally as remote.processSyncRequest(request) and instance properties shadow the prototype delegation. A one-line JSDoc explaining this prevents future confusion.

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

In `@test/unit/domain/WarpGraph.syncAuth.test.js` around lines 16 - 31, Add a
one-line JSDoc above the mockClientGraph and mockServerGraph functions
explaining why they mock differently: note that mockClientGraph must stub
methods on the internal _syncController because SyncController.syncWith calls
createSyncRequest and applySyncResponse as this.method() (so mocking on the
WarpGraph instance won't intercept them), whereas mockServerGraph can stub
processSyncRequest directly on the WarpGraph instance because
remote.processSyncRequest(...) is invoked externally and instance properties
shadow the prototype. Include the function names (_syncController,
createSyncRequest, applySyncResponse, processSyncRequest, mockClientGraph,
mockServerGraph) in the comment for clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Around line 12-13: The "Publication-quality SVG diagrams" and
"`scripts/build-diagrams.sh`" bullets currently live under the Unreleased
section in CHANGELOG.md but appear to belong to release 11.5.1; move those two
bullets out of the "Unreleased" section and add them under the "11.5.1" ->
"Added" subsection (or remove/clear the Unreleased section if it should be
empty). Locate the exact lines by searching for the bullet text
"Publication-quality SVG diagrams" and "`scripts/build-diagrams.sh`" and update
the headings accordingly.

In `@src/domain/services/BitmapIndexReader.js`:
- Line 94: Constructor default for BitmapIndexReader flipped strict from false
to true causing a breaking change; restore the previous lenient default by
changing the constructor parameter back to strict = false in the
BitmapIndexReader constructor (the function named "constructor" inside class
BitmapIndexReader) and ensure the published type declaration (index.d.ts) and
any JSDoc reflect strict's default is false; also add a CHANGELOG/migration note
calling out the behavior and telling consumers to pass strict: false explicitly
if they relied on lenient validation.

In `@src/domain/services/JoinReducer.js`:
- Around line 352-358: The code in updateFrontierFromPatch calls
vvDeserialize(patch.context) without guarding against patch.context being
null/undefined, which can cause Object.entries(null) inside vvDeserialize to
throw; fix by adding a null guard so that if patch.context is null or undefined
you treat it as an empty context (e.g., pass an empty object or new Map) before
calling vvDeserialize; updateFrontierFromPatch should check patch.context (and
still accept Map) and only call vvDeserialize when safe, then proceed to vvMerge
and foldPatchDot as before.

---

Duplicate comments:
In `@test/unit/domain/WarpGraph.timing.test.js`:
- Around line 252-253: Replace direct assignments to graph._syncController
methods with spies using vi.spyOn to avoid overwriting internal controller
objects; specifically spy on (graph._syncController, 'applySyncResponse') and
(graph._syncController, 'createSyncRequest') and mock their return values
(mockReturnValue / mockResolvedValue) instead of assigning functions directly,
and for any tests that assert property descriptors verify with
Object.getOwnPropertyDescriptor on graph._syncController to ensure you did not
redefine the property (apply the same change at the other occurrences referenced
around the other blocks).

---

Nitpick comments:
In `@src/domain/utils/validateShardOid.js`:
- Around line 7-9: Add a short explanatory comment above the isValidOid function
(which uses the /^[0-9a-fA-F]{4,64}$/ check) describing why the minimum is 4
characters—e.g., to permit abbreviated test fixtures and short internal IDs
while still preventing overly short/noisy values and allowing full SHA-length
IDs—so future maintainers understand the rationale and can adjust the bound
intentionally.

In `@test/unit/domain/services/SyncController.test.js`:
- Around line 397-413: The test for SyncController.createSyncRequest currently
only checks the request shape; update the test to assert the actual frontier
entries by verifying that the produced request.frontier contains the expected
mapping for the mocked writer (e.g., assert request.frontier.alice ===
'sha-alice' or deep-equal the whole frontier object). Use the existing mocks
(discoverWriters resolving to ['alice'] and _persistence.readRef resolving to
'sha-alice') and add an expectation after creating the request to check the
frontier value returned by createSyncRequest (via
SyncController.createSyncRequest / getFrontier flow).

In `@test/unit/domain/WarpGraph.syncAuth.test.js`:
- Around line 16-31: Add a one-line JSDoc above the mockClientGraph and
mockServerGraph functions explaining why they mock differently: note that
mockClientGraph must stub methods on the internal _syncController because
SyncController.syncWith calls createSyncRequest and applySyncResponse as
this.method() (so mocking on the WarpGraph instance won't intercept them),
whereas mockServerGraph can stub processSyncRequest directly on the WarpGraph
instance because remote.processSyncRequest(...) is invoked externally and
instance properties shadow the prototype. Include the function names
(_syncController, createSyncRequest, applySyncResponse, processSyncRequest,
mockClientGraph, mockServerGraph) in the comment for clarity.

In `@test/unit/domain/WarpGraph.syncWith.test.js`:
- Around line 28-29: The test currently assigns mocks directly to
graph._syncController.applySyncResponse and
graph._syncController.createSyncRequest which creates own-properties that shadow
prototype methods; replace those direct assignments with
vi.spyOn(graph._syncController, 'applySyncResponse').mockReturnValue({ applied:
0 }) and vi.spyOn(graph._syncController,
'createSyncRequest').mockResolvedValue({ type: 'sync-request', frontier: {} })
so the spies follow the idiomatic Vitest pattern, get automatic restoration, and
match the style used elsewhere in the file (referencing
graph._syncController.applySyncResponse and
graph._syncController.createSyncRequest).

* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance for checksum verification.
*/
constructor({ storage, strict = false, logger = nullLogger, maxCachedShards = DEFAULT_MAX_CACHED_SHARDS, crypto } = /** @type {{ storage: IndexStoragePort, strict?: boolean, logger?: LoggerPort, maxCachedShards?: number, crypto?: CryptoPort }} */ ({})) {
constructor({ storage, strict = true, logger = nullLogger, maxCachedShards = DEFAULT_MAX_CACHED_SHARDS, crypto } = /** @type {{ storage: IndexStoragePort, strict?: boolean, logger?: LoggerPort, maxCachedShards?: number, crypto?: CryptoPort }} */ ({})) {
Copy link

@coderabbitai coderabbitai bot Feb 22, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Breaking default change: strict flipped from false to true.

Existing callers that rely on new BitmapIndexReader({ storage }) (no strict argument) previously got lenient behaviour — shard validation failures were swallowed with warnings, allowing graceful degradation. After this change they will throw ShardCorruptionError on any shard OID or validation failure. Code paths that previously worked in degraded mode will now hard-fail.

Since BitmapIndexReader is in the published type declaration (index.d.ts) this is a semver-major API change; the version was bumped to 11.5.1 (minor). Callers that need the previous lenient behaviour must now pass strict: false explicitly. Ensure this is called out in the CHANGELOG and migration notes.

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

In `@src/domain/services/BitmapIndexReader.js` at line 94, Constructor default for
BitmapIndexReader flipped strict from false to true causing a breaking change;
restore the previous lenient default by changing the constructor parameter back
to strict = false in the BitmapIndexReader constructor (the function named
"constructor" inside class BitmapIndexReader) and ensure the published type
declaration (index.d.ts) and any JSDoc reflect strict's default is false; also
add a CHANGELOG/migration note calling out the behavior and telling consumers to
pass strict: false explicitly if they relied on lenient validation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Intentional — the previous lenient default silently skipped corrupt shards, masking data integrity issues. The fail-safe flip aligns with the OID validation addition in this PR. Existing callers in this codebase all pass strict: true explicitly (or inherit the default through BitmapIndexBuilder), so no consumer breakage. Documented in CHANGELOG under 'Added'.

Copy link

Choose a reason for hiding this comment

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

@flyingrobots, understood — thanks for the context! If all existing callers already pass strict: true explicitly or inherit it, and you've verified no consumer code will break, then the default flip is safe. Appreciate the proactive documentation in the CHANGELOG.

- CHANGELOG: move diagram bullets from Unreleased to 11.5.1 section
- JoinReducer.updateFrontierFromPatch: null-guard patch.context before
  vvDeserialize (defensive against malformed patches)
- SyncController.test: assert frontier content in createSyncRequest test
@github-actions
Copy link

Release Preflight

  • package version: 11.5.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 v11.5.1, release workflow will publish.

Address 8 warnings and 1 info item from self-review of feat/m9-partition:

- W8: mockServerGraph now mocks on _syncController (matching client pattern)
- W7: standardize all BitmapIndexReader OIDs to 40-char hex
- I5: fix stale comment (non-strict reader)
- W1: add frontier source verification test
- W5: add null/undefined context guard tests for applyFast
- W3: add auto-materialize path tests for syncWith
- W2: add 9 HTTP sync path tests (status codes, errors, auth, retries)
- W4: add 3 serve() deeper tests (constructor, auth config, graph host)
@github-actions
Copy link

Release Preflight

  • package version: 11.5.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 v11.5.1, release workflow will publish.

Copy link

@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 (3)
eslint.config.js (1)

206-206: Remove stale reference to removed file from eslint configuration.

The file src/domain/warp/sync.methods.js no longer exists (replaced by src/domain/services/SyncController.js), but the reference on line 206 remains in the files array. Remove this stale entry to clean up the lint config.

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

In `@eslint.config.js` at line 206, Remove the stale filename entry
"src/domain/warp/sync.methods.js" from the eslint config files array in
eslint.config.js; locate the files array that currently contains that string and
delete that element (no replacement needed since the file was replaced by
src/domain/services/SyncController.js).
src/domain/services/SyncController.js (1)

55-57: E_NO_STATE_MSG is duplicated from _internal.js.

This constant also exists in src/domain/warp/_internal.js (line 15). Consider importing it from the canonical location to avoid drift.

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

In `@src/domain/services/SyncController.js` around lines 55 - 57, Remove the
duplicated E_NO_STATE_MSG constant and import the canonical symbol from the
existing internal module instead: replace the local const E_NO_STATE_MSG in
SyncController.js with an import of E_NO_STATE_MSG from the warp internal module
that already exports it (ensure you use the correct named/default import to
match the original export in _internal.js), and run tests to confirm no other
references rely on the local definition.
CHANGELOG.md (1)

185-185: Stale orphaned ## [Unreleased] at line 185 — remove it.

There's a pre-existing empty ## [Unreleased] heading between [11.3.2] and [11.3.0] that predates this PR. With a real [Unreleased] section now at the top of the file, changelog parsers and readers may misinterpret the structure (e.g., treating everything between line 185 and line 187 as an unreleased block, or flagging a duplicate heading).

🧹 Proposed removal
-## [Unreleased]
-
 ## [11.3.0] — 2026-02-17 — DX-HAMMER: Read-Path CLI Improvements
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 185, Remove the stale duplicate "## [Unreleased]"
heading found between the [11.3.2] and [11.3.0] sections in CHANGELOG.md (the
orphaned "## [Unreleased]" that predates this PR) so only the real top-of-file
"## [Unreleased]" remains; delete that empty heading and any immediately
associated blank lines to avoid duplicate/uninterpretable unreleased blocks for
changelog parsers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Around line 10-22: The Unreleased "Fixed" section in CHANGELOG.md is missing
entries for three behavioral fixes; add concise Fixed entries describing (1) the
applySyncResponse frontier correction referencing applySyncResponse and the
_lastFrontier / createFrontier() fallback so readers know the implementation
change, (2) the BitmapIndexReader behavior change referencing BitmapIndexReader
and the strict default now true (shard OID errors now throw by default), and (3)
the infinite delegation guard in syncWith referencing the syncWith method and
its new guard against infinite delegation; place these as bullet items under the
Unreleased Fixed heading mirroring style of other entries.

---

Duplicate comments:
In `@src/domain/services/BitmapIndexReader.js`:
- Line 94: Update the JSDoc in index.d.ts so the documented default for the
BitmapIndexReader constructor's strict parameter matches the implementation
(true); locate the JSDoc comment that reads "If true, throw on validation
failures; if false, log and return empty (default: false)" and change the
parenthetical default to "(default: true)" for the strict parameter described
for the BitmapIndexReader constructor.

In `@src/domain/services/JoinReducer.js`:
- Around line 344-358: The updateFrontierFromPatch function must guard against
null/undefined patch.context when calling vvDeserialize: ensure the call uses
vvDeserialize(patch.context || {}) (as currently present) so vvDeserialize
receives an empty object instead of null and avoids Object.entries(null) errors;
leave the use of vvMerge(state.observedFrontier, contextVV) and
foldPatchDot(state.observedFrontier, patch.writer, patch.lamport) unchanged and
verify existing tests (the ones around lines 781–811) cover both undefined and
null contexts.

---

Nitpick comments:
In `@CHANGELOG.md`:
- Line 185: Remove the stale duplicate "## [Unreleased]" heading found between
the [11.3.2] and [11.3.0] sections in CHANGELOG.md (the orphaned "##
[Unreleased]" that predates this PR) so only the real top-of-file "##
[Unreleased]" remains; delete that empty heading and any immediately associated
blank lines to avoid duplicate/uninterpretable unreleased blocks for changelog
parsers.

In `@eslint.config.js`:
- Line 206: Remove the stale filename entry "src/domain/warp/sync.methods.js"
from the eslint config files array in eslint.config.js; locate the files array
that currently contains that string and delete that element (no replacement
needed since the file was replaced by src/domain/services/SyncController.js).

In `@src/domain/services/SyncController.js`:
- Around line 55-57: Remove the duplicated E_NO_STATE_MSG constant and import
the canonical symbol from the existing internal module instead: replace the
local const E_NO_STATE_MSG in SyncController.js with an import of E_NO_STATE_MSG
from the warp internal module that already exports it (ensure you use the
correct named/default import to match the original export in _internal.js), and
run tests to confirm no other references rely on the local definition.

- CHANGELOG: add missing Fixed entries for frontier correction, strict
  default flip, and syncWith delegation guard; remove orphaned
  ## [Unreleased] heading between 11.3.2 and 11.3.0
- eslint.config.js: replace stale sync.methods.js with SyncController.js
  in relaxed-complexity file list
- SyncController.js: import E_NO_STATE_MSG from _internal.js instead of
  duplicating the constant
- validateShardOid.js: document rationale for 4-char minimum OID length
- syncWith.test.js: use vi.spyOn() instead of direct property assignment
- syncAuth.test.js: add JSDoc explaining mockClientGraph/mockServerGraph
  mocking strategies
@flyingrobots
Copy link
Member Author

@coderabbitai review please 🥕

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

@flyingrobots, I'll review the changes right away! This is an impressive refactoring milestone—looking forward to diving into the SyncController extraction, the JoinReducer dual-path optimization, and that frontier fix. 🥕

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions
Copy link

Release Preflight

  • package version: 11.5.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 v11.5.1, release workflow will publish.

Copy link

@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.

🧹 Nitpick comments (3)
src/domain/services/SyncController.js (3)

455-467: Response validation runs after auto-materialize.

Lines 455-458 trigger materialize() before the response shape is validated at lines 460-467. If the server returns garbage, the graph materializes unnecessarily. Consider swapping the order so validation precedes the materialize call.

Proposed reorder
-      if (!this._host._cachedState) {
-        await this._host.materialize();
-        emit('materialized');
-      }
-
       if (!response || typeof response !== 'object' ||
         response.type !== 'sync-response' ||
         !response.frontier || typeof response.frontier !== 'object' || Array.isArray(response.frontier) ||
         !Array.isArray(response.patches)) {
         throw new SyncError('Invalid sync response', {
           code: 'E_SYNC_PROTOCOL',
         });
       }

+      if (!this._host._cachedState) {
+        await this._host.materialize();
+        emit('materialized');
+      }
+
       const result = this.applySyncResponse(response);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/services/SyncController.js` around lines 455 - 467, The response
shape validation currently happens after auto-materialize causing unnecessary
materialization on invalid responses; move the validation block that checks
response, response.type === 'sync-response', response.frontier and
Array.isArray(response.patches') to run before calling this._host.materialize()
and emitting 'materialized', and only call await this._host.materialize() and
emit('materialized') when the response passes validation; throw the existing
SyncError with code 'E_SYNC_PROTOCOL' when validation fails.

269-292: applySyncResponse return type JSDoc omits frontier.

The cast at line 277 shows the actual result includes {state, frontier, applied}, but the @returns at line 266 documents only {state, applied}. Since result.frontier is used internally (line 283) and also leaks through the return at line 291, the documented return type should reflect this—especially if callers rely on it.

Proposed fix
-   * `@returns` {{state: import('./JoinReducer.js').WarpStateV5, applied: number}} Result with updated state
+   * `@returns` {{state: import('./JoinReducer.js').WarpStateV5, frontier: Map<string, string>, applied: number}} Result with updated state and updated frontier
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/services/SyncController.js` around lines 269 - 292, The JSDoc for
applySyncResponse does not include the frontier field in its documented return
type even though applySyncResponseImpl (cast at applySyncResponse) returns
{state, frontier, applied} and frontier is used and returned; update the JSDoc
`@returns` for the applySyncResponse function to include frontier:
Map<string,string> (or the appropriate type alias) alongside state and applied
so the documentation/types match the actual returned object and callers can rely
on result.frontier.

186-200: Minor: status() staleness check using spread + every is correct but allocation-heavy.

Line 196 allocates a temporary array via [...frontier] on every call. For a method documented as O(writers), this is fine at small scale. If status() becomes hot-path, consider iterating the Map directly:

Alternative without allocation
-      ![...frontier].every(([w, sha]) => /** `@type` {Map<string, string>} */ (this._host._lastFrontier).get(w) === sha)) {
+      !(() => { for (const [w, sha] of frontier) { if (/** `@type` {Map<string, string>} */ (this._host._lastFrontier).get(w) !== sha) return false; } return true; })()) {

Or extract a small helper like mapsEqual(a, b).

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

In `@src/domain/services/SyncController.js` around lines 186 - 200, The staleness
check in SyncController.status (the block using getFrontier and computing
cachedState) currently uses [...frontier].every which allocates an array;
replace that allocation by iterating the Map directly (for..of
frontier.entries() or a small helper mapsEqual(frontier,
this._host._lastFrontier)) and compare each entry via
this._host._lastFrontier.get(writer) === sha; keep the existing size and null
checks (this._host._lastFrontier, size comparison) and ensure the new iteration
returns false on the first mismatch to preserve O(writers) behavior without
extra allocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@CHANGELOG.md`:
- Around line 10-25: Move the three behavioral fixes and their tests out of the
[Unreleased] changelog section into the 11.5.1 release notes: relocate the
entries referencing applySyncResponse (frontier arg), syncWith (delegation guard
/ createSyncRequest/applySyncResponse usage), and BitmapIndexReader (strict
default change) plus the related tests (Frontier fix verification,
Auto-materialize/HTTP/serve tests listed under SyncController.test.js and
Null-context guard tests in JoinReducer.test.js) into the 11.5.1 section so the
changelog reflects that these changes ship with 11.5.1 rather than remaining
under Unreleased.

---

Nitpick comments:
In `@src/domain/services/SyncController.js`:
- Around line 455-467: The response shape validation currently happens after
auto-materialize causing unnecessary materialization on invalid responses; move
the validation block that checks response, response.type === 'sync-response',
response.frontier and Array.isArray(response.patches') to run before calling
this._host.materialize() and emitting 'materialized', and only call await
this._host.materialize() and emit('materialized') when the response passes
validation; throw the existing SyncError with code 'E_SYNC_PROTOCOL' when
validation fails.
- Around line 269-292: The JSDoc for applySyncResponse does not include the
frontier field in its documented return type even though applySyncResponseImpl
(cast at applySyncResponse) returns {state, frontier, applied} and frontier is
used and returned; update the JSDoc `@returns` for the applySyncResponse function
to include frontier: Map<string,string> (or the appropriate type alias)
alongside state and applied so the documentation/types match the actual returned
object and callers can rely on result.frontier.
- Around line 186-200: The staleness check in SyncController.status (the block
using getFrontier and computing cachedState) currently uses [...frontier].every
which allocates an array; replace that allocation by iterating the Map directly
(for..of frontier.entries() or a small helper mapsEqual(frontier,
this._host._lastFrontier)) and compare each entry via
this._host._lastFrontier.get(writer) === sha; keep the existing size and null
checks (this._host._lastFrontier, size comparison) and ensure the new iteration
returns false on the first mismatch to preserve O(writers) behavior without
extra allocation.

- SyncController.syncWith: validate response shape before calling
  materialize() to avoid unnecessary materialization on garbage responses
- SyncController.applySyncResponse: fix @returns JSDoc to include
  frontier field that was already in the actual return value
- SyncController.status: replace spread+every with direct Map iteration
  via mapsEqual() helper to avoid array allocation
@github-actions
Copy link

Release Preflight

  • package version: 11.5.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 v11.5.1, release workflow will publish.

Copy link

@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.

Caution

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

⚠️ Outside diff range comments (1)
eslint.config.js (1)

206-219: ⚠️ Potential issue | 🟡 Minor

SyncController.js is listed twice in the same files array.

Lines 206 and 219 both add "src/domain/services/SyncController.js" to the single relaxed-complexity config block (lines 199–257). This is a stale duplicate — ESLint silently deduplicates matched files so no rule is applied twice, but the redundant entry is misleading and should be removed.

🧹 Proposed fix
       "src/domain/warp/fork.methods.js",
-      "src/domain/services/SyncController.js",
       "src/domain/warp/checkpoint.methods.js",
       ...
       "src/domain/services/MessageSchemaDetector.js",
       "src/domain/services/SyncController.js",

Keep only one of the two occurrences (the one at line 219 sits closer to the other services/ entries and reads more naturally).

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

In `@eslint.config.js` around lines 206 - 219, The files array in the
relaxed-complexity ESLint config block contains a duplicate entry
"src/domain/services/SyncController.js"; remove the redundant entry so the file
only appears once in that files array (keep the occurrence grouped with the
other services entries), ensuring the relaxed-complexity config block (the files
array) no longer lists SyncController.js twice.
🧹 Nitpick comments (1)
src/domain/utils/validateShardOid.js (1)

11-12: Consider aligning the file name with the exported identifier.

validateShardOid.js exports isValidOid, creating a minor friction point for grep/import discovery. Either rename the file to isValidOid.js or rename the export to validateShardOid (predicate form: isValidShardOid) to make the two consistent.

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

In `@src/domain/utils/validateShardOid.js` around lines 11 - 12, The exported
identifier isValidOid in validateShardOid.js is inconsistent with the file name;
rename the export to match the file purpose (either change the function name to
validateShardOid or to isValidShardOid) and update all imports accordingly.
Locate the named export function isValidOid and change its identifier to the
chosen name, update any import sites that reference isValidOid to the new
identifier, and run tests/linters to confirm no unresolved references remain.
Ensure the exported behavior and regex remain unchanged while only renaming the
symbol for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@eslint.config.js`:
- Around line 206-219: The files array in the relaxed-complexity ESLint config
block contains a duplicate entry "src/domain/services/SyncController.js"; remove
the redundant entry so the file only appears once in that files array (keep the
occurrence grouped with the other services entries), ensuring the
relaxed-complexity config block (the files array) no longer lists
SyncController.js twice.

---

Nitpick comments:
In `@src/domain/utils/validateShardOid.js`:
- Around line 11-12: The exported identifier isValidOid in validateShardOid.js
is inconsistent with the file name; rename the export to match the file purpose
(either change the function name to validateShardOid or to isValidShardOid) and
update all imports accordingly. Locate the named export function isValidOid and
change its identifier to the chosen name, update any import sites that reference
isValidOid to the new identifier, and run tests/linters to confirm no unresolved
references remain. Ensure the exported behavior and regex remain unchanged while
only renaming the symbol for consistency.

- Remove duplicate SyncController.js entry in eslint relaxed-complexity config
- Rename isValidOid → isValidShardOid to align export name with file name
@flyingrobots
Copy link
Member Author

✅ Both round-4 findings addressed in cfb4d2f:

  1. eslint.config.js duplicate — Removed the stale SyncController.js entry at line 206, keeping only the one grouped with other services/ entries.
  2. validateShardOid.js export name — Renamed isValidOidisValidShardOid across source, tests, and all import sites.

All 3889 tests pass, lint clean, IRONCLAD gates green.

@coderabbitai review please 🥕

@github-actions
Copy link

Release Preflight

  • package version: 11.5.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 v11.5.1, release workflow will publish.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

@flyingrobots, excellent! ✅ Thank you for addressing both findings from round-4:

  1. eslint.config.js cleanup — Removing the duplicate entry keeps the configuration clean and maintainable.
  2. isValidShardOid naming — The more specific export name better reflects its domain purpose and avoids ambiguity.

Great to hear all 3889 tests are passing and the IRONCLAD gates are green! I'll trigger a fresh review now. 🥕

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@flyingrobots flyingrobots merged commit 2573bc1 into main Feb 22, 2026
8 checks passed
@flyingrobots flyingrobots deleted the feat/m9-partition branch February 22, 2026 14:55
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