-
Notifications
You must be signed in to change notification settings - Fork 0
feat: op type class hierarchy (cycle 0009) #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
flyingrobots
wants to merge
11
commits into
main
Choose a base branch
from
cycle/0009-op-type-class-hierarchy
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
26a3411
docs: cycle 0009 design — op type class hierarchy
flyingrobots c393be5
feat: op type class hierarchy — Slice 1 (9 classes, 71 tests)
flyingrobots e97fbab
feat: wire factory functions + OpNormalizer to Op classes — Slice 2
flyingrobots 3b5fba3
test: prove Op class instances flow through JoinReducer — Slice 3
flyingrobots 977f324
docs: update CHANGELOG for op type class hierarchy
flyingrobots 87666b9
docs: cycle 0009 witness, retro, and new backlog items
flyingrobots db6a070
fix: code review — reserved-byte validation + doc corrections
flyingrobots 90ce8e6
docs: backlog cool-idea — incremental history backfill
flyingrobots c6eb313
fix: validate op removal and prop key inputs
flyingrobots cb5ab97
fix: align op types with class-backed factories
flyingrobots fed7fa4
docs: fix backlog code sample syntax
flyingrobots File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
145 changes: 145 additions & 0 deletions
145
docs/design/0009-op-type-class-hierarchy/op-type-class-hierarchy.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # Cycle 0009 — Op Type Class Hierarchy | ||
|
|
||
| **Status:** HILL MET | ||
|
|
||
| **Date:** 2026-04-05 | ||
|
|
||
| ## Sponsors | ||
|
|
||
| - **Human:** James Ross | ||
| - **Agent:** Claude (Opus) | ||
|
|
||
| ## Hill | ||
|
|
||
| Replace 8 typedef ops with a frozen class hierarchy so the domain | ||
| model has runtime identity, constructor validation, and `instanceof` | ||
| dispatch — eliminating all string-based tag switching. | ||
|
|
||
| ## Playback questions | ||
|
|
||
| ### Agent questions | ||
|
|
||
| 1. Does `new NodeAdd(nodeId, dot)` throw when `nodeId` is empty or | ||
| `dot` is not a `Dot`? | ||
| 2. Does `op instanceof NodeAdd` return true for NodeAdd instances and | ||
| false for EdgeAdd instances? | ||
| 3. Does `instanceof Op` return true for all 8 op subclasses? | ||
| 4. Are all op instances frozen (`Object.isFrozen(op) === true`)? | ||
| 5. Does `OpNormalizer.normalizeRawOp()` return canonical op class | ||
| instances (`NodePropSet`, `EdgePropSet`)? | ||
| 6. Does `OpNormalizer.lowerCanonicalOp()` return raw op class | ||
| instances (`PropSet`)? | ||
| 7. Does `JoinReducer.OP_STRATEGIES` dispatch by `instanceof` instead | ||
| of string keys? | ||
| 8. Do factory functions in WarpTypesV2.js delegate to constructors? | ||
| 9. Does the CBOR decode boundary produce op class instances (not | ||
| plain objects)? | ||
|
|
||
| ### Human questions | ||
|
|
||
| 1. Can I still do `patch.addNode('user:alice')` and have it just | ||
| work? | ||
| 2. Does `git warp history` still show op types correctly? | ||
| 3. Do existing patches in a real repo still materialize identically? | ||
|
|
||
| ## Scope | ||
|
|
||
| ### What changes | ||
|
|
||
| | Component | Change | | ||
| |---|---| | ||
| | `src/domain/types/ops/` | New directory. 8 op classes + base `Op` class. One file per class. | | ||
| | `src/domain/types/WarpTypesV2.js` | Factory functions delegate to constructors. Typedefs become re-exports. | | ||
| | `src/domain/services/OpNormalizer.js` | `normalizeRawOp` returns class instances. `lowerCanonicalOp` uses `instanceof`. | | ||
| | `src/domain/services/JoinReducer.js` | `OP_STRATEGIES` keyed by class reference, not string. Lookup via `instanceof` chain or class-to-strategy Map. | | ||
| | `src/domain/types/TickReceipt.js` | `OP_TYPES` array becomes class references or derives names from classes. | | ||
| | `src/domain/services/codec/MessageSchemaDetector.js` | `instanceof` checks replace string comparisons. | | ||
| | `bin/presenters/text.js` | `instanceof` checks replace string comparisons. | | ||
| | `src/domain/services/PatchBuilderV2.js` | Builds canonical op class instances internally. `build()`/`commit()` lower via `lowerCanonicalOp`. | | ||
| | CBOR decode boundary | `CborCodec` or `CborPatchJournalAdapter` hydrates plain objects into op classes. | | ||
|
|
||
| ### Delivered vs. Deferred | ||
|
|
||
| | Component | Status | | ||
| |---|---| | ||
| | `src/domain/types/ops/` | **Delivered** — 9 classes + validate.js | | ||
| | `src/domain/types/WarpTypesV2.js` | **Delivered** — factory functions delegate to constructors | | ||
| | `src/domain/services/OpNormalizer.js` | **Delivered** — returns class instances via factory functions | | ||
| | `src/domain/services/JoinReducer.js` | **Deferred** — OP_STRATEGIES still string-keyed (works with class instances via .type). See `PROTO_op-consumer-instanceof-migration` | | ||
| | `src/domain/types/TickReceipt.js` | **Deferred** — See `PROTO_op-consumer-instanceof-migration` | | ||
| | `src/domain/services/codec/MessageSchemaDetector.js` | **Deferred** — See `PROTO_op-consumer-instanceof-migration` | | ||
| | `bin/presenters/text.js` | **Deferred** — See `PROTO_op-consumer-instanceof-migration` | | ||
| | `src/domain/services/PatchBuilderV2.js` | **Not needed** — already uses factory functions which now produce class instances | | ||
| | CBOR decode boundary | **Deferred** — See `PROTO_cbor-op-hydration` | | ||
|
|
||
| ### What does NOT change | ||
|
|
||
| - Wire format. Persisted patches remain CBOR with `{ type: 'NodeAdd', ... }` plain objects. The class boundary is at decode, not encode. | ||
| - PatchV2 class. It holds `ops: Op[]` instead of `ops: OpV2[]` but the shape is the same. | ||
| - CRDT semantics. JoinReducer mutation logic is identical. | ||
| - Public API surface. `createPatch().addNode()` still works. | ||
|
|
||
| ## Non-goals | ||
|
|
||
| - Moving strategy methods onto op classes. That's the JoinReducer | ||
| strategy registry cycle (separate design doc exists). This cycle | ||
| gives ops runtime identity; behavior coupling is a follow-on. | ||
| - Changing the wire format. ADR-0002 defers that. | ||
| - Touching test files that construct ops with plain objects — those | ||
| become integration tests for the CBOR decode hydration path. | ||
|
|
||
| ## Accessibility / assistive reading posture | ||
|
|
||
| Not applicable — no UI changes. | ||
|
|
||
| ## Localization / directionality posture | ||
|
|
||
| Not applicable — no user-facing strings. | ||
|
|
||
| ## Agent inspectability / explainability posture | ||
|
|
||
| Op classes are `instanceof`-dispatchable and frozen. An agent can | ||
| inspect any op with `op.constructor.name` and get a meaningful | ||
| domain name. This is strictly better than the current string tags | ||
| for agent tooling. | ||
|
|
||
| ## Cut plan | ||
|
|
||
| ### Slice 1 — Op classes + tests (RED then GREEN) | ||
|
|
||
| - Base `Op` class (abstract-ish — no direct instantiation) | ||
| - 8 subclasses: `NodeAdd`, `NodeRemove`, `EdgeAdd`, `EdgeRemove`, | ||
| `NodePropSet`, `EdgePropSet`, `PropSet`, `BlobValue` | ||
| - Constructor validation, freeze, instanceof | ||
| - Factory functions in WarpTypesV2.js delegate to constructors | ||
|
|
||
| ### Slice 2 — OpNormalizer + tests | ||
|
|
||
| - `normalizeRawOp` returns class instances | ||
| - `lowerCanonicalOp` uses `instanceof` | ||
| - Round-trip: raw → canonical → raw preserves identity | ||
|
|
||
| ### Slice 3 — JoinReducer wiring + tests | ||
|
|
||
| - `OP_STRATEGIES` lookup by constructor, not string | ||
| - `RAW_KNOWN_OPS` / `CANONICAL_KNOWN_OPS` become class-based checks | ||
| - Existing noCoordination test suite must pass unchanged | ||
|
|
||
| ### Slice 4 — Consumer wiring (presenter, detector, receipt) | ||
|
|
||
| - `MessageSchemaDetector` uses `instanceof` | ||
| - `bin/presenters/text.js` uses `instanceof` | ||
| - `TickReceipt.OP_TYPES` derives from class hierarchy | ||
|
|
||
| ### Slice 5 — CBOR hydration boundary | ||
|
|
||
| - Decode path hydrates plain objects into op class instances | ||
| - Golden blob round-trip test: encode → decode → class instance | ||
|
|
||
| ## Hard gates | ||
|
|
||
| - **noCoordination test suite passes unchanged.** This is the | ||
| multi-writer safety regression suite. Non-negotiable. | ||
| - **Existing BATS CLI tests pass.** No user-visible behavior change. | ||
| - **Wire format compatibility.** Encode a patch with the new classes, | ||
| decode with the old code path — must produce identical state. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # Cycle 0009 — Witness | ||
|
|
||
| ## Agent questions | ||
|
|
||
| 1. **Does `new NodeAdd(nodeId, dot)` throw when `nodeId` is empty or `dot` is not a `Dot`?** | ||
| YES — `Op.test.js` "throws on empty nodeId", "throws on non-string nodeId", "throws when dot is not a Dot instance" tests in the NodeAdd describe block. | ||
|
|
||
| 2. **Does `op instanceof NodeAdd` return true for NodeAdd instances and false for EdgeAdd instances?** | ||
| YES — `Op.test.js` "cross-class instanceof isolation" > "no op is instanceof a sibling class" proves all 8x8 combinations. | ||
|
|
||
| 3. **Does `instanceof Op` return true for all 8 op subclasses?** | ||
| YES — `Op.test.js` "cross-class instanceof isolation" > "all ops share the Op base" iterates all 8. | ||
|
|
||
| 4. **Are all op instances frozen?** | ||
| YES — every class describe block includes an "is frozen" test. NodeRemove and EdgeRemove also have "freezes the observedDots array" tests. | ||
|
|
||
| 5. **Does `OpNormalizer.normalizeRawOp()` return canonical op class instances?** | ||
| YES — `factory-integration.test.js` "normalizeRawOp converts PropSet (node) to NodePropSet instance" and "normalizeRawOp converts PropSet (edge) to EdgePropSet instance" tests. | ||
|
|
||
| 6. **Does `OpNormalizer.lowerCanonicalOp()` return raw op class instances?** | ||
| YES — `factory-integration.test.js` "lowerCanonicalOp converts NodePropSet to PropSet instance" and "lowerCanonicalOp converts EdgePropSet to PropSet instance" tests. | ||
|
|
||
| 7. **Does `JoinReducer.OP_STRATEGIES` dispatch class instances?** | ||
| YES — `reducer-integration.test.js` "finds strategy for every class instance type" test: all 7 canonical types dispatch correctly. Strategy lookup is via `.type` string on the class instance. | ||
|
|
||
| 8. **Do factory functions delegate to constructors?** | ||
| YES — `factory-integration.test.js` "WarpTypesV2 factory functions produce Op class instances" describe block: every factory returns an `instanceof` the correct class. | ||
|
|
||
| 9. **Does the CBOR decode boundary produce op class instances?** | ||
| NOT YET — deferred to future cycle (Slice 5). Plain objects from CBOR still work through the reducer because dispatch is string-based. | ||
|
|
||
| ## Human questions | ||
|
|
||
| 1. **Can I still do `patch.addNode('user:alice')` and have it just work?** | ||
| YES — PatchBuilderV2 calls factory functions internally. No API change. | ||
|
|
||
| 2. **Does `git warp history` still show op types correctly?** | ||
| YES — presenter uses `.type` string which class instances carry. | ||
|
|
||
| 3. **Do existing patches in a real repo still materialize identically?** | ||
| YES — noCoordination test suite 7/7 pass. Wire format unchanged (CBOR still encodes `.type` strings). Factory functions produce structurally identical objects (same fields, same values). | ||
|
|
||
| ## Hard gates | ||
|
|
||
| - noCoordination: **7/7 PASS** | ||
| - Full unit suite: **5504/5504 PASS** (332 files) | ||
| - Lint: **0 errors, 0 warnings** | ||
| - Wire format: **unchanged** (class instances serialize to same CBOR) | ||
|
|
||
| ## Verdict | ||
|
|
||
| **Hill met.** 8 typedef ops replaced with frozen class hierarchy. Runtime identity, constructor validation, `instanceof` dispatch all proven. Consumer migration (Slice 4) and CBOR hydration (Slice 5) deferred — they're incremental and don't block the core value. |
20 changes: 0 additions & 20 deletions
20
docs/method/backlog/bad-code/CC_op-types-typedef-to-class.md
This file was deleted.
Oops, something went wrong.
29 changes: 29 additions & 0 deletions
29
docs/method/backlog/bad-code/CC_versionvector-constructor-no-validation.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # VersionVector constructor accepts undefined entries | ||
|
|
||
| **Effort:** S | ||
|
|
||
| ## What's Wrong | ||
|
|
||
| `new VersionVector()` (no args) sets `#entries` to `undefined`. Any | ||
| subsequent method call (`merge`, `get`, `[Symbol.iterator]`) throws a | ||
| confusing `TypeError: undefined is not iterable`. The constructor | ||
| should reject missing or non-Map arguments per P2 (boundary validation). | ||
|
|
||
| ## Suggested Fix | ||
|
|
||
| Add validation to the constructor: | ||
|
|
||
| ```js | ||
| class VersionVector { | ||
| constructor(entries) { | ||
| if (!(entries instanceof Map)) { | ||
| throw new CrdtError('VersionVector requires a Map<string, number>'); | ||
| } | ||
| this.#entries = entries; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Source | ||
|
|
||
| Discovered during cycle 0009 reducer integration tests. |
53 changes: 53 additions & 0 deletions
53
docs/method/backlog/cool-ideas/PROTO_incremental-history-backfill.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # Incremental History Backfill for Git Mirror Use Cases | ||
|
|
||
| **Effort:** L | ||
|
|
||
| ## Idea | ||
|
|
||
| Use case: mirroring git history into a warp graph — map each git | ||
| commit to an observation (one tick per commit). Works fine for HEAD | ||
| snapshots, but backfilling full history is O(commits) and takes | ||
| forever for large repos. | ||
|
|
||
| The append-only ledger makes incremental backfill hard: you can't | ||
| insert ticks at arbitrary points in the causal past. Once the | ||
| frontier has advanced, earlier observations can only arrive as new | ||
| patches, not as historical insertions. | ||
|
|
||
| ## Possible approaches | ||
|
|
||
| - **Edges as chronological ordering:** Nodes represent commits, | ||
| edges encode Lamport-ordered relationships between them. Backfill | ||
| adds nodes + edges in reverse chronological order. Each batch is | ||
| a new patch at the current frontier, but the edge structure | ||
| encodes the historical timeline. The graph's materialized state | ||
| shows the full DAG even though the patches arrived out of order. | ||
|
|
||
| - **Checkpoint-based seeding:** Snapshot HEAD as a checkpoint | ||
| (fast). Then backfill historical commits as patches that add | ||
| nodes/edges the snapshot doesn't cover. The checkpoint provides | ||
| the "current state" instantly; backfill adds provenance depth | ||
| incrementally. | ||
|
|
||
| - **Wormhole compression:** Paper III wormholes compress multi-tick | ||
| segments into single edges carrying sub-payloads. A "history | ||
| import" wormhole could represent N git commits as a single | ||
| compressed segment, expandable on demand. | ||
|
|
||
| - **Lazy materialization with continuation tokens:** Don't backfill | ||
| eagerly. Instead, when a query touches a node that references | ||
| unmaterialized history, fetch and materialize on demand. The | ||
| continuation token marks how far back the graph has been | ||
| materialized. | ||
|
|
||
| ## Why it matters | ||
|
|
||
| Git-warp-as-git-mirror is a compelling use case for the Inspector | ||
| and for agent collaboration. But any repo with >10K commits makes | ||
| naive tick-per-commit infeasible. The solution probably involves | ||
| separating "current state" (fast) from "historical provenance" | ||
| (incremental, lazy, or compressed). | ||
|
|
||
| ## Source | ||
|
|
||
| James Ross, 2026-04-05. Raw idea during cycle 0009 session. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.