Skip to content

feat: op type class hierarchy (cycle 0009)#80

Open
flyingrobots wants to merge 8 commits intomainfrom
cycle/0009-op-type-class-hierarchy
Open

feat: op type class hierarchy (cycle 0009)#80
flyingrobots wants to merge 8 commits intomainfrom
cycle/0009-op-type-class-hierarchy

Conversation

@flyingrobots
Copy link
Copy Markdown
Member

@flyingrobots flyingrobots commented Apr 6, 2026

Summary

  • 8 typedef ops (NodeAdd, NodeRemove, EdgeAdd, EdgeRemove, NodePropSet, EdgePropSet, PropSet, BlobValue) promoted to frozen classes with constructor validation and instanceof dispatch
  • Base Op class (abstract) provides shared runtime identity
  • Factory functions in WarpTypesV2.js delegate to class constructors
  • All three JoinReducer paths (applyFast, applyWithReceipt, applyWithDiff) proven to work with class instances
  • 97 new tests across 4 test files; 5504 total tests passing
  • noCoordination safety gate: 7/7 pass
  • Wire format unchanged — zero behavioral change

What's deferred

  • Slice 4: Consumer instanceof migration (MessageSchemaDetector, presenter, TickReceipt) — backlog up-next/
  • Slice 5: CBOR hydration boundary (decode produces class instances) — backlog up-next/

Test plan

  • 97 new tests: constructor validation, freeze, instanceof isolation, factory delegation, OpNormalizer round-trip, JoinReducer integration
  • 35 existing WarpTypesV2 tests updated (real Dot instances, encoded dot strings)
  • noCoordination regression suite: 7/7
  • Full unit suite: 5504/5504
  • Lint: 0 errors, 0 warnings

Summary by CodeRabbit

  • New Features

    • Operation types now have stronger runtime identity, input validation, and immutability, improving correctness and predictable behavior.
    • createPatch() auto-materializes when autoMaterialize is enabled, so materialize() is not required before remove operations.
  • Bug Fixes

    • VersionVector now validates constructor input to prevent runtime errors from undefined entries.
  • Tests

    • Added 97 new tests covering operation types, factories, normalization, and reducer integration.

Pull PROTO_op-type-class-hierarchy (cool-ideas) and
CC_op-types-typedef-to-class (bad-code) into cycle 0009.

Hill: replace 8 typedef ops with frozen class hierarchy for runtime
identity, constructor validation, and instanceof dispatch.
Base Op class (abstract) + 8 concrete subclasses:
NodeAdd, NodeRemove, EdgeAdd, EdgeRemove, NodePropSet,
EdgePropSet, PropSet, BlobValue.

Each class: constructor validation, Object.freeze, instanceof
dispatch. Edge types use options objects (max-params compliance).
Shared validators in validate.js.

Cycle 0009 ��� op-type-class-hierarchy
Factory functions in WarpTypesV2.js now delegate to Op class
constructors. OpNormalizer.normalizeRawOp() and lowerCanonicalOp()
already return class instances via the factory functions.

Updated WarpTypesV2.test.js: real Dot instances replace plain objects,
encoded dot strings replace object arrays for observedDots.

15 new factory-integration tests + 35 updated existing tests.

Cycle 0009 — op-type-class-hierarchy
11 new tests verifying class-instance ops work through all three
reducer paths (applyFast, applyWithReceipt, applyWithDiff) and
OP_STRATEGIES dispatch. No source changes needed — the string-keyed
strategy Map already dispatches correctly for class instances since
they carry the same .type field.

Cycle 0009 — op-type-class-hierarchy
Cycle 0009 — op-type-class-hierarchy
Witness: hill met — all 8 agent questions answered YES (Q9 deferred).
All 3 human questions answered YES. Hard gates: noCoordination 7/7,
5504/5504 tests, lint clean.

Retro: fast RED→GREEN, edge options objects caught by lint, existing
tests had wrong Dot field names hidden by any-casts.

New backlog:
- PROTO_op-consumer-instanceof-migration (up-next, S)
- PROTO_cbor-op-hydration (up-next, M)
- CC_versionvector-constructor-no-validation (bad-code, S)

Cycle 0009 — op-type-class-hierarchy
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

This PR replaces plain-object op typedefs with a frozen Op class hierarchy (8 concrete subclasses), updates WarpTypesV2 factories to construct those classes, adds validation helpers and 97 tests, and documents design/retro/backlog items. CHANGELOG also notes auto-materialize-on-remove behavior for createPatch().

Changes

Cohort / File(s) Summary
Op class hierarchy & new ops
src/domain/types/ops/Op.js, src/domain/types/ops/NodeAdd.js, src/domain/types/ops/NodeRemove.js, src/domain/types/ops/EdgeAdd.js, src/domain/types/ops/EdgeRemove.js, src/domain/types/ops/PropSet.js, src/domain/types/ops/NodePropSet.js, src/domain/types/ops/EdgePropSet.js, src/domain/types/ops/BlobValue.js
Added an abstract Op base and eight concrete op subclasses. Each constructor performs validation, assigns public fields, and freezes instances for immutability; Op prevents direct instantiation and stores a type string matching wire format.
Validation utilities
src/domain/types/ops/validate.js
New exported helpers (assertNonEmptyString, assertNoReservedBytes, assertArray) used by op constructors to enforce argument shapes and provide descriptive errors.
Factory updates
src/domain/types/WarpTypesV2.js
WarpTypesV2 factory functions now instantiate and return op class instances (e.g., new NodeAdd(...), new EdgeAdd({...})) instead of returning plain object literals with type discriminators; argument shapes adjusted to match constructors.
Tests — op unit & validation
test/unit/domain/types/ops/Op.test.js, test/unit/domain/types/ops/validate.test.js
New comprehensive unit tests validating Op abstractness, subclass instanceof isolation, field correctness, freezing, and constructor validation behaviors (including reserved-byte checks and array/dot type enforcement).
Tests — factory & reducer integration
test/unit/domain/types/ops/factory-integration.test.js, test/unit/domain/types/ops/reducer-integration.test.js, test/unit/domain/types/WarpTypesV2.test.js
New integration suites asserting WarpTypesV2 factories return op class instances, OpNormalizer normalize/lower behavior for PropSet ↔ Node/Edge prop variants, and JoinReducer/patch application paths accept class-instance ops end-to-end (applyOpV2, applyFast, applyWithReceipt, applyWithDiff). Updated existing WarpTypesV2 factory tests to use Dot instances and instance checks.
Design, retro, backlog, and changelog docs
CHANGELOG.md, docs/design/0009-op-type-class-hierarchy/op-type-class-hierarchy.md, docs/design/0009-op-type-class-hierarchy/witness.md, docs/method/retro/0009-op-type-class-hierarchy/retro.md, docs/method/backlog/up-next/PROTO_cbor-op-hydration.md, docs/method/backlog/up-next/PROTO_op-consumer-instanceof-migration.md, docs/method/backlog/bad-code/CC_versionvector-constructor-no-validation.md
Added design spec, witness, retro, and new backlog/bug docs. CHANGELOG updated to announce the op class hierarchy and auto-materialize-on-remove behavior; noted follow-up slices (CBOR hydration, consumer instanceof migration).
Removed docs / archived notes
docs/method/backlog/cool-ideas/PROTO_op-type-class-hierarchy.md (deleted), docs/method/backlog/bad-code/CC_op-types-typedef-to-class.md (deleted)
Removed obsolete/duplicative backlog/idea pages replaced by the canonical design and retro artifacts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 New classes hop into the light,
Frozen fields and checks done right.
From plain tags to instanceof cheer,
Reducers nod — the path is clear.
Tiny paws stamp tests with delight. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: promoting typedef ops to a class hierarchy. It is specific, directly related to the core objective, and provides meaningful context.
Description check ✅ Passed The description comprehensively covers all required sections: a clear summary of changes, explicit test plan with checkboxes (all checked), and ADR compliance checks. It provides sufficient detail on the refactoring, deferred work, and test coverage.
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 cycle/0009-op-type-class-hierarchy

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.

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

🧹 Nitpick comments (1)
test/unit/domain/types/WarpTypesV2.test.js (1)

442-447: Use distinct Dot instances per add-op fixture.

This test currently reuses one Dot for both NodeAdd and EdgeAdd. Separate dots keep fixtures aligned with causal-identity expectations and avoid normalizing an invalid pattern.

Proposed test tweak
-      const dot = new Dot('w', 1);
+      const nodeDot = new Dot('w', 1);
+      const edgeDot = new Dot('w', 2);
       const ops = [
-        createNodeAddV2('n1', dot),
+        createNodeAddV2('n1', nodeDot),
         createNodeRemoveV2('n1', []),
-        createEdgeAddV2('n1', 'n2', 'r', dot),
+        createEdgeAddV2('n1', 'n2', 'r', edgeDot),
         createEdgeRemoveV2('n1', 'n2', 'r', []),
         createPropSetV2('n1', 'k', 'v'),
       ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/types/WarpTypesV2.test.js` around lines 442 - 447, The test
reuses one Dot instance for both NodeAdd and EdgeAdd which violates
causal-identity expectations; update the ops fixture so createNodeAddV2 and
createEdgeAddV2 each receive their own distinct Dot (e.g., two Dot(...)
instances) instead of sharing the same dot variable; locate the ops array
construction around the calls to createNodeAddV2, createNodeRemoveV2,
createEdgeAddV2, and createEdgeRemoveV2 and replace the single shared Dot with
separate Dot instances for the add operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/0009-op-type-class-hierarchy/op-type-class-hierarchy.md`:
- Around line 31-32: The design doc claims JoinReducer.OP_STRATEGIES dispatches
by instanceof but the implementation uses string-key lookup via op.type; update
the design doc text (references: JoinReducer.OP_STRATEGIES, JoinReducer.js line
showing OP_STRATEGIES.get(op.type)) to accurately state that strategy lookup is
performed by the `.type` string on operation instances, or explicitly document
that an instanceof-based dispatch approach is planned/deferred and not currently
used; ensure the doc references the `.type` property on class instances and
remove or rephrase any lines asserting instanceof dispatch.

In `@src/domain/types/ops/EdgeRemove.js`:
- Around line 36-40: The container-level assertArray call in the EdgeRemove
constructor is not enough—after assertArray(observedDots, 'EdgeRemove',
'observedDots') you must validate each element is a non-empty encoded dot string
before freezing; iterate observedDots and run the appropriate element validator
(e.g., an existing assertEncodedDot or a new small helper) on each entry
(include context 'EdgeRemove' and 'observedDots' and the element index in error
messages), then set this.observedDots = Object.freeze([...observedDots]).

In `@src/domain/types/ops/NodePropSet.js`:
- Around line 8-9: NodePropSet's constructor currently uses assertNonEmptyString
but does not reject NUL bytes for the identifiers; update the validation in the
NodePropSet constructor (the class/function named NodePropSet) to check both the
node and key parameters for banned NUL bytes (e.g., string.includes('\0')) and
throw/raise the same validation error when found; reuse or extend
assertNonEmptyString (or add a small helper) so both node and key are rejected
on construction if they contain '\0' (also apply the same check to the other
validation locations referenced in the comment around lines 33-35).

In `@src/domain/types/ops/NodeRemove.js`:
- Around line 29-31: The constructor for NodeRemove only asserts observedDots is
an array but doesn't validate elements; update the NodeRemove constructor to
iterate observedDots and validate each entry is a non-empty string (e.g., use or
add an assertString/assertNonEmpty helper) before freezing the array, and throw
a clear error mentioning 'NodeRemove' and 'observedDots' if any element is
invalid so downstream remove processing cannot receive bad entries.

In `@src/domain/types/ops/PropSet.js`:
- Around line 12-13: PropSet currently only calls assertNonEmptyString for the
key, which allows NUL bytes; update the PropSet constructor (or the helper it
uses) to reject any key containing '\0' by adding a NUL-byte check alongside the
existing assertNonEmptyString validation (e.g., throw if key.includes('\0'));
ensure the same change is applied for all PropSet key validations mentioned
(lines ~37-39) so every code path that sets PropSet.key enforces non-empty and
no NUL bytes, referencing the PropSet constructor and assertNonEmptyString
helper for where to add the check.

In `@test/unit/domain/types/ops/factory-integration.test.js`:
- Line 15: Remove the unused import by deleting the BlobValue import statement
(the "import BlobValue from ...BlobValue.js" line) from the test file; ensure
there are no remaining references to the BlobValue symbol in the file and run
the tests/linter to confirm the unused-import warning is resolved.
- Around line 120-129: The test's `canonical` variable is typed as a union so
TypeScript can't see `node`/`key` even after the runtime `toBeInstanceOf` check;
narrow the type before accessing properties by casting `canonical` to
`NodePropSet` (e.g., use a type assertion like `canonical as NodePropSet` or
assign `const nodeProp = canonical as NodePropSet`) and then assert
`nodeProp.node`, `nodeProp.key`, and `nodeProp.value`; keep using
`normalizeRawOp`, `NodePropSet`, and the `canonical` variable names to locate
the change.

---

Nitpick comments:
In `@test/unit/domain/types/WarpTypesV2.test.js`:
- Around line 442-447: The test reuses one Dot instance for both NodeAdd and
EdgeAdd which violates causal-identity expectations; update the ops fixture so
createNodeAddV2 and createEdgeAddV2 each receive their own distinct Dot (e.g.,
two Dot(...) instances) instead of sharing the same dot variable; locate the ops
array construction around the calls to createNodeAddV2, createNodeRemoveV2,
createEdgeAddV2, and createEdgeRemoveV2 and replace the single shared Dot with
separate Dot instances for the add operations.
🪄 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: ba3cdbe1-183c-4e0d-b5dd-cac2ae425741

📥 Commits

Reviewing files that changed from the base of the PR and between 7d64a95 and 87666b9.

📒 Files selected for processing (25)
  • CHANGELOG.md
  • docs/design/0009-op-type-class-hierarchy/op-type-class-hierarchy.md
  • docs/design/0009-op-type-class-hierarchy/witness.md
  • docs/method/backlog/bad-code/CC_op-types-typedef-to-class.md
  • docs/method/backlog/bad-code/CC_versionvector-constructor-no-validation.md
  • docs/method/backlog/cool-ideas/PROTO_op-type-class-hierarchy.md
  • docs/method/backlog/up-next/PROTO_cbor-op-hydration.md
  • docs/method/backlog/up-next/PROTO_op-consumer-instanceof-migration.md
  • docs/method/retro/0009-op-type-class-hierarchy/retro.md
  • src/domain/types/WarpTypesV2.js
  • src/domain/types/ops/BlobValue.js
  • src/domain/types/ops/EdgeAdd.js
  • src/domain/types/ops/EdgePropSet.js
  • src/domain/types/ops/EdgeRemove.js
  • src/domain/types/ops/NodeAdd.js
  • src/domain/types/ops/NodePropSet.js
  • src/domain/types/ops/NodeRemove.js
  • src/domain/types/ops/Op.js
  • src/domain/types/ops/PropSet.js
  • src/domain/types/ops/validate.js
  • test/unit/domain/types/WarpTypesV2.test.js
  • test/unit/domain/types/ops/Op.test.js
  • test/unit/domain/types/ops/factory-integration.test.js
  • test/unit/domain/types/ops/reducer-integration.test.js
  • test/unit/domain/types/ops/validate.test.js
💤 Files with no reviewable changes (2)
  • docs/method/backlog/bad-code/CC_op-types-typedef-to-class.md
  • docs/method/backlog/cool-ideas/PROTO_op-type-class-hierarchy.md

Comment on lines +36 to +40
assertArray(observedDots, 'EdgeRemove', 'observedDots');
this.from = from;
this.to = to;
this.label = label;
this.observedDots = Object.freeze([...observedDots]);
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

observedDots needs element-level validation.

At Line 36, container validation alone is insufficient. Validate each entry as a non-empty encoded dot string before freezing.

Proposed fix
   constructor({ from, to, label, observedDots }) {
     super('EdgeRemove');
     assertNonEmptyString(from, 'EdgeRemove', 'from');
     assertNonEmptyString(to, 'EdgeRemove', 'to');
     assertNonEmptyString(label, 'EdgeRemove', 'label');
     assertArray(observedDots, 'EdgeRemove', 'observedDots');
+    for (let i = 0; i < observedDots.length; i += 1) {
+      assertNonEmptyString(observedDots[i], 'EdgeRemove', `observedDots[${i}]`);
+    }
     this.from = from;
     this.to = to;
     this.label = label;
     this.observedDots = Object.freeze([...observedDots]);
     Object.freeze(this);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/types/ops/EdgeRemove.js` around lines 36 - 40, The container-level
assertArray call in the EdgeRemove constructor is not enough—after
assertArray(observedDots, 'EdgeRemove', 'observedDots') you must validate each
element is a non-empty encoded dot string before freezing; iterate observedDots
and run the appropriate element validator (e.g., an existing assertEncodedDot or
a new small helper) on each entry (include context 'EdgeRemove' and
'observedDots' and the element index in error messages), then set
this.observedDots = Object.freeze([...observedDots]).

Comment on lines +29 to +31
assertArray(observedDots, 'NodeRemove', 'observedDots');
this.node = node;
this.observedDots = Object.freeze([...observedDots]);
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

Validate each observedDots entry, not just the container.

At Line 29, only array shape is checked. Non-string entries can pass construction and fail semantically during remove processing. Please validate each element as a non-empty string.

Proposed fix
 import Op from './Op.js';
 import { assertNonEmptyString, assertArray } from './validate.js';
@@
   constructor(node, observedDots) {
     super('NodeRemove');
     assertNonEmptyString(node, 'NodeRemove', 'node');
     assertArray(observedDots, 'NodeRemove', 'observedDots');
+    for (let i = 0; i < observedDots.length; i += 1) {
+      assertNonEmptyString(observedDots[i], 'NodeRemove', `observedDots[${i}]`);
+    }
     this.node = node;
     this.observedDots = Object.freeze([...observedDots]);
     Object.freeze(this);
   }
📝 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
assertArray(observedDots, 'NodeRemove', 'observedDots');
this.node = node;
this.observedDots = Object.freeze([...observedDots]);
assertArray(observedDots, 'NodeRemove', 'observedDots');
for (let i = 0; i < observedDots.length; i += 1) {
assertNonEmptyString(observedDots[i], 'NodeRemove', `observedDots[${i}]`);
}
this.node = node;
this.observedDots = Object.freeze([...observedDots]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/types/ops/NodeRemove.js` around lines 29 - 31, The constructor for
NodeRemove only asserts observedDots is an array but doesn't validate elements;
update the NodeRemove constructor to iterate observedDots and validate each
entry is a non-empty string (e.g., use or add an assertString/assertNonEmpty
helper) before freezing the array, and throw a clear error mentioning
'NodeRemove' and 'observedDots' if any element is invalid so downstream remove
processing cannot receive bad entries.

Comment on lines +12 to +13
import { assertNonEmptyString } from './validate.js';

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

Enforce NUL-byte validation for PropSet.key.

key is only checked for non-empty string. That permits NUL bytes, which conflicts with the shared identifier constraints and weakens constructor boundary validation.

🔧 Proposed fix
-import { assertNonEmptyString } from './validate.js';
+import { assertNonEmptyString, assertNoBannedBytes } from './validate.js';
@@
     assertNonEmptyString(node, 'PropSet', 'node');
     assertNonEmptyString(key, 'PropSet', 'key');
+    assertNoBannedBytes(key, 'PropSet', 'key');
     this.node = node;

Also applies to: 37-39

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

In `@src/domain/types/ops/PropSet.js` around lines 12 - 13, PropSet currently only
calls assertNonEmptyString for the key, which allows NUL bytes; update the
PropSet constructor (or the helper it uses) to reject any key containing '\0' by
adding a NUL-byte check alongside the existing assertNonEmptyString validation
(e.g., throw if key.includes('\0')); ensure the same change is applied for all
PropSet key validations mentioned (lines ~37-39) so every code path that sets
PropSet.key enforces non-empty and no NUL bytes, referencing the PropSet
constructor and assertNonEmptyString helper for where to add the check.

import NodePropSet from '../../../../../src/domain/types/ops/NodePropSet.js';
import EdgePropSet from '../../../../../src/domain/types/ops/EdgePropSet.js';
import PropSetClass from '../../../../../src/domain/types/ops/PropSet.js';
import BlobValue from '../../../../../src/domain/types/ops/BlobValue.js';
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

Remove unused BlobValue import.

The static analysis correctly identifies that BlobValue is imported but never used in this test file.

🧹 Proposed fix
-import BlobValue from '../../../../../src/domain/types/ops/BlobValue.js';
📝 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
import BlobValue from '../../../../../src/domain/types/ops/BlobValue.js';
🧰 Tools
🪛 GitHub Check: preflight

[failure] 15-15:
'BlobValue' is declared but its value is never read.

🪛 GitHub Check: type-firewall

[failure] 15-15:
'BlobValue' is declared but its value is never read.

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

In `@test/unit/domain/types/ops/factory-integration.test.js` at line 15, Remove
the unused import by deleting the BlobValue import statement (the "import
BlobValue from ...BlobValue.js" line) from the test file; ensure there are no
remaining references to the BlobValue symbol in the file and run the
tests/linter to confirm the unused-import warning is resolved.

Comment on lines +120 to +129
it('normalizeRawOp converts PropSet (node) to NodePropSet instance', () => {
const raw = createPropSetV2('user:alice', 'name', 'Alice');
const canonical = normalizeRawOp(raw);

expect(canonical).toBeInstanceOf(NodePropSet);
expect(canonical.type).toBe('NodePropSet');
expect(canonical.node).toBe('user:alice');
expect(canonical.key).toBe('name');
expect(canonical.value).toBe('Alice');
});
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

Add type narrowing to fix TypeScript errors on canonical properties.

The static analysis reports that canonical.node and canonical.key don't exist on CanonicalOpV2 | { type: string; }. After confirming canonical is a NodePropSet instance (line 124), TypeScript needs help narrowing the type.

🔧 Proposed fix using type assertion
   it('normalizeRawOp converts PropSet (node) to NodePropSet instance', () => {
     const raw = createPropSetV2('user:alice', 'name', 'Alice');
     const canonical = normalizeRawOp(raw);

     expect(canonical).toBeInstanceOf(NodePropSet);
-    expect(canonical.type).toBe('NodePropSet');
-    expect(canonical.node).toBe('user:alice');
-    expect(canonical.key).toBe('name');
-    expect(canonical.value).toBe('Alice');
+    const np = /** `@type` {import('../../../../../src/domain/types/ops/NodePropSet.js').default} */ (canonical);
+    expect(np.type).toBe('NodePropSet');
+    expect(np.node).toBe('user:alice');
+    expect(np.key).toBe('name');
+    expect(np.value).toBe('Alice');
   });
📝 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
it('normalizeRawOp converts PropSet (node) to NodePropSet instance', () => {
const raw = createPropSetV2('user:alice', 'name', 'Alice');
const canonical = normalizeRawOp(raw);
expect(canonical).toBeInstanceOf(NodePropSet);
expect(canonical.type).toBe('NodePropSet');
expect(canonical.node).toBe('user:alice');
expect(canonical.key).toBe('name');
expect(canonical.value).toBe('Alice');
});
it('normalizeRawOp converts PropSet (node) to NodePropSet instance', () => {
const raw = createPropSetV2('user:alice', 'name', 'Alice');
const canonical = normalizeRawOp(raw);
expect(canonical).toBeInstanceOf(NodePropSet);
const np = /** `@type` {import('../../../../../src/domain/types/ops/NodePropSet.js').default} */ (canonical);
expect(np.type).toBe('NodePropSet');
expect(np.node).toBe('user:alice');
expect(np.key).toBe('name');
expect(np.value).toBe('Alice');
});
🧰 Tools
🪛 GitHub Check: preflight

[failure] 127-127:
Property 'key' does not exist on type 'CanonicalOpV2 | { type: string; }'.


[failure] 126-126:
Property 'node' does not exist on type 'CanonicalOpV2 | { type: string; }'.

🪛 GitHub Check: type-firewall

[failure] 127-127:
Property 'key' does not exist on type 'CanonicalOpV2 | { type: string; }'.


[failure] 126-126:
Property 'node' does not exist on type 'CanonicalOpV2 | { type: string; }'.

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

In `@test/unit/domain/types/ops/factory-integration.test.js` around lines 120 -
129, The test's `canonical` variable is typed as a union so TypeScript can't see
`node`/`key` even after the runtime `toBeInstanceOf` check; narrow the type
before accessing properties by casting `canonical` to `NodePropSet` (e.g., use a
type assertion like `canonical as NodePropSet` or assign `const nodeProp =
canonical as NodePropSet`) and then assert `nodeProp.node`, `nodeProp.key`, and
`nodeProp.value`; keep using `normalizeRawOp`, `NodePropSet`, and the
`canonical` variable names to locate the change.

Code review (12 issues, 0 remaining):

C1/M2/m1/m2: Add assertNoReservedBytes to all op constructors —
rejects NUL (\x00) and \x01 prefix in identifiers. Matches
PatchBuilderV2._assertNoReservedBytes. PropSet exempted (legitimately
carries \x01-prefixed nodes for edge prop wire encoding).

M1: Design doc Status DESIGN → HILL MET. Added Delivered vs. Deferred
table clarifying which scope items shipped.

n1: Blank line between frontmatter fields in design doc.
n2: Witness references test descriptions instead of line numbers.
n3: Retro file count 9 → 10.
n4: WarpTypesV2 import alias PropSetOpClass → PropSetClass.

15 new validation tests. 5519 total tests passing.

Cycle 0009 — op-type-class-hierarchy
Use case: mirroring git history into a warp graph. HEAD snapshot is
fast, but full history backfill is O(commits). Explores edge-based
chronological ordering, checkpoint seeding, wormhole compression,
and lazy materialization with continuation tokens.

Cycle 0009 — op-type-class-hierarchy
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

🤖 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/types/WarpTypesV2.js`:
- Around line 160-162: The JSDoc `@returns` annotations still reference
plain-object typedefs (e.g., OpV2NodeAdd) while the factory functions like
createNodeAddV2, createNodeRemoveV2, createEdgeAddV2, createEdgeRemoveV2,
createPropSetV2, createNodePropSetV2, and createEdgePropSetV2 now return class
instances (NodeAddClass, NodeRemoveClass, EdgeAddClass, EdgeRemoveClass,
PropSetClass, NodePropSetClass, EdgePropSetClass); update each function's
`@returns` tag to reference the corresponding class type (e.g., `@returns`
{NodeAddClass}) OR change the typedefs to alias the classes (e.g., `@typedef`
{NodeAddClass} OpV2NodeAdd) so TypeScript sees them as compatible.
🪄 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: 43fbdd8b-5655-418b-9c1d-4e366037d012

📥 Commits

Reviewing files that changed from the base of the PR and between 87666b9 and 90ce8e6.

📒 Files selected for processing (15)
  • docs/design/0009-op-type-class-hierarchy/op-type-class-hierarchy.md
  • docs/design/0009-op-type-class-hierarchy/witness.md
  • docs/method/backlog/cool-ideas/PROTO_incremental-history-backfill.md
  • docs/method/retro/0009-op-type-class-hierarchy/retro.md
  • src/domain/types/WarpTypesV2.js
  • src/domain/types/ops/BlobValue.js
  • src/domain/types/ops/EdgeAdd.js
  • src/domain/types/ops/EdgePropSet.js
  • src/domain/types/ops/EdgeRemove.js
  • src/domain/types/ops/NodeAdd.js
  • src/domain/types/ops/NodePropSet.js
  • src/domain/types/ops/NodeRemove.js
  • src/domain/types/ops/validate.js
  • test/unit/domain/types/ops/Op.test.js
  • test/unit/domain/types/ops/validate.test.js
✅ Files skipped from review due to trivial changes (6)
  • src/domain/types/ops/NodePropSet.js
  • docs/method/backlog/cool-ideas/PROTO_incremental-history-backfill.md
  • docs/method/retro/0009-op-type-class-hierarchy/retro.md
  • test/unit/domain/types/ops/validate.test.js
  • docs/design/0009-op-type-class-hierarchy/witness.md
  • docs/design/0009-op-type-class-hierarchy/op-type-class-hierarchy.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/domain/types/ops/EdgeAdd.js
  • src/domain/types/ops/EdgePropSet.js
  • src/domain/types/ops/NodeRemove.js
  • src/domain/types/ops/validate.js
  • src/domain/types/ops/EdgeRemove.js
  • test/unit/domain/types/ops/Op.test.js

Comment on lines 160 to 162
export function createNodeAddV2(node, dot) {
return { type: 'NodeAdd', node, dot };
return new NodeAddClass(node, dot);
}
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 | 🔴 Critical

TypeScript type mismatch causing pipeline failure.

The factory functions now return class instances, but the @returns JSDoc annotations still reference the plain-object typedefs (e.g., OpV2NodeAdd). TypeScript sees these as incompatible because class instances have a prototype chain and additional structure that doesn't match the typedef's plain object shape.

Update the return type annotations to reference the class types, or adjust the typedefs to describe the classes.

Proposed fix: Update `@returns` to reference class types
 /**
  * Creates a NodeAdd operation with a dot
  * `@param` {NodeId} node - Node ID to add
  * `@param` {Dot} dot - Causal identifier for this add
- * `@returns` {OpV2NodeAdd} NodeAdd operation
+ * `@returns` {NodeAddClass} NodeAdd operation
  */
 export function createNodeAddV2(node, dot) {
   return new NodeAddClass(node, dot);
 }

Apply the same pattern to the other factory functions:

  • createNodeRemoveV2: @returns {NodeRemoveClass}
  • createEdgeAddV2: @returns {EdgeAddClass}
  • createEdgeRemoveV2: @returns {EdgeRemoveClass}
  • createPropSetV2: @returns {PropSetClass}
  • createNodePropSetV2: @returns {NodePropSetClass}
  • createEdgePropSetV2: @returns {EdgePropSetClass}

Alternatively, if the typedefs are used elsewhere and need to remain as the "public API type", you could update them to use @typedef {NodeAddClass} OpV2NodeAdd so the typedef aliases the class.

Also applies to: 170-172, 182-184, 194-196, 207-209, 218-220, 231-233

🧰 Tools
🪛 GitHub Actions: Release Preflight (PR)

[error] 161-161: Typecheck failed (tsc --noEmit). TS2322: Type 'NodeAdd' is not assignable to type 'OpV2NodeAdd'.

🪛 GitHub Check: preflight

[failure] 161-161:
Type 'NodeAdd' is not assignable to type 'OpV2NodeAdd'.

🪛 GitHub Check: type-firewall

[failure] 161-161:
Type 'NodeAdd' is not assignable to type 'OpV2NodeAdd'.

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

In `@src/domain/types/WarpTypesV2.js` around lines 160 - 162, The JSDoc `@returns`
annotations still reference plain-object typedefs (e.g., OpV2NodeAdd) while the
factory functions like createNodeAddV2, createNodeRemoveV2, createEdgeAddV2,
createEdgeRemoveV2, createPropSetV2, createNodePropSetV2, and
createEdgePropSetV2 now return class instances (NodeAddClass, NodeRemoveClass,
EdgeAddClass, EdgeRemoveClass, PropSetClass, NodePropSetClass,
EdgePropSetClass); update each function's `@returns` tag to reference the
corresponding class type (e.g., `@returns` {NodeAddClass}) OR change the typedefs
to alias the classes (e.g., `@typedef` {NodeAddClass} OpV2NodeAdd) so TypeScript
sees them as compatible.

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