Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
afe9dc9
feat(api)!: normalize getNodeProps() to return Record (B100)
flyingrobots Mar 3, 2026
243713a
refactor(types): unify CorePersistence/FullPersistence typedefs (B146)
flyingrobots Mar 3, 2026
4e20ea3
feat(api): stabilize Observer API — subscribe() and watch() (B3)
flyingrobots Mar 3, 2026
42e5377
feat(api): add graph.patchMany() batch patch API (B11)
flyingrobots Mar 3, 2026
a8c241b
feat(bisect): add BisectService for binary search over WARP history
flyingrobots Mar 3, 2026
138f03b
feat(bisect): implement causality bisect CLI and service (B2)
flyingrobots Mar 3, 2026
774d697
test(subscribe): add unsubscribe-during-callback E2E tests (B44)
flyingrobots Mar 3, 2026
b07d64d
test(trust): add CLI vs service payload parity tests (B124)
flyingrobots Mar 3, 2026
815e6e2
test(CachedValue): add null-payload semantic tests (B125)
flyingrobots Mar 3, 2026
001d0c1
release: v13.0.0
flyingrobots Mar 3, 2026
1ea6bc4
fix(release): resolve TypeScript and declaration surface errors for v…
flyingrobots Mar 3, 2026
b87b83b
fix(bisect): harden CLI validation and remove dead code (B148)
flyingrobots Mar 3, 2026
d7cf956
fix(docs): reconcile inventory counts and fix review nits (B148)
flyingrobots Mar 3, 2026
c1d73ad
docs(changelog): add B148 follow-up details
flyingrobots Mar 4, 2026
367efdf
fix(docs,types): address code review round 2 findings (B148)
flyingrobots Mar 4, 2026
1b3cfcc
fix(security,cli,docs): address CodeRabbit review feedback (B148)
flyingrobots Mar 4, 2026
65f0f56
fix(docs,security): address CodeRabbit round 4 nits (B148)
flyingrobots Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [13.0.0] — 2026-03-03

### Added

- **Observer API stabilized (B3)** — `subscribe()` and `watch()` promoted to `@stability stable` with `@since 13.0.0` annotations. Fixed `onError` callback type from `(error: Error)` to `(error: unknown)` to match runtime catch semantics. `watch()` pattern param now correctly typed as `string | string[]` in `_wiredMethods.d.ts`.
- **`graph.patchMany()` batch patch API (B11)** — applies multiple patch callbacks sequentially. Each callback sees state from prior commits. Returns array of commit SHAs. Inherits reentrancy guard from `graph.patch()`.
- **Causality bisect (B2)** — `BisectService` performs binary search over a writer's patch chain to find the first bad patch. CLI: `git warp bisect --good <sha> --bad <sha> --test <cmd> --writer <id>`. O(log N) materializations. Exit codes: 0=found, 1=usage, 2=range error, 3=internal.

### Changed

- **BREAKING: `getNodeProps()` returns `Record<string, unknown>` instead of `Map<string, unknown>` (B100)** — aligns with `getEdgeProps()` which already returns a plain object. Callers must replace `.get('key')` with `.key` or `['key']`, `.has('key')` with `'key' in props`, and `.size` with `Object.keys(props).length`. `ObserverView.getNodeProps()` follows the same change.
- **GraphPersistencePort narrowing (B145)** — domain services now declare focused port intersections (`CommitPort & BlobPort`, etc.) in JSDoc instead of the 23-method composite `GraphPersistencePort`. Removed `ConfigPort` from the composite (23 → 21 methods); adapters still implement `configGet`/`configSet` on their prototypes. Zero behavioral change.
- **Codec trailer validation extraction (B134, B138)** — created `TrailerValidation.js` with `requireTrailer()`, `parsePositiveIntTrailer()`, `validateKindDiscriminator()`. All 4 message codec decoders now use shared helpers exclusively. Patch and Checkpoint decoders now also perform semantic field validation (graph name, writer ID, OID, SHA-256) matching the Audit decoder pattern. Internal refactor for valid inputs, with stricter rejection of malformed messages.
- **HTTP adapter shared utilities (B135)** — created `httpAdapterUtils.js` with `MAX_BODY_BYTES`, `readStreamBody()`, `noopLogger`. Eliminates duplication across Node/Bun/Deno HTTP adapters. Internal refactor, no behavioral change.
Expand All @@ -25,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Fake timer lifecycle (B131)** — moved `vi.useFakeTimers()` from `beforeAll` to `beforeEach` and `vi.useRealTimers()` into `afterEach` in `WarpGraph.watch.test.js`.
- **Test determinism (B132)** — seeded `Math.random()` in benchmarks with Mulberry32 RNG (`0xDEADBEEF`), added `seed: 42` to all fast-check property tests, replaced random delays in stress test with deterministic values.
- **Global mutation documentation (B133)** — documented intentional `globalThis.Buffer` mutation in `noBufferGlobal.test.js` and `crypto.randomUUID()` usage in `SyncAuthService.test.js`.
- **Code review fixes (B148)** — removed dead code from BisectService, added `--writer` validation to bisect CLI, fixed exit code constant. Follow-up: reconciled ROADMAP inventory counts (24→29 done), fixed M11 placement in COMPLETED.md, corrected stale Deno test name, added invariant comment in BisectService. Round 2: moved B100 from `### Breaking` to `### Changed` in CHANGELOG, removed done items from ROADMAP priority tiers, fixed stale test vector counts (6→9), replaced `BisectResult` interface with discriminated union type, added SHA format validation to bisect CLI schema.

## [12.4.1] — 2026-02-28

Expand Down Expand Up @@ -1528,7 +1536,7 @@ Implements [Paper III](https://doi.org/10.5281/zenodo.17963669) (Computational H

#### Query API (V7 Task 7)
- **`graph.hasNode(nodeId)`** - Check if node exists in materialized state
- **`graph.getNodeProps(nodeId)`** - Get all properties for a node as Map
- **`graph.getNodeProps(nodeId)`** - Get all properties for a node (returns `Record<string, unknown>` since v13.0.0)
- **`graph.neighbors(nodeId, dir?, label?)`** - Get neighbors with direction/label filtering
- **`graph.getNodes()`** - Get all visible node IDs
- **`graph.getEdges()`** - Get all visible edges as `{from, to, label}` array
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
<img src="docs/images/hero.gif" alt="git-warp CLI demo" width="600">
</p>

## What's New in v12.4.1
## What's New in v13.0.0

- **JSDoc total coverage** — eliminated all unsafe `{Object}`, `{Function}`, `{*}` type patterns across 135 files (190+ sites), replacing them with precise inline typed shapes.
- **Zero tsc errors** — fixed tsconfig split-config includes and type divergences; 0 errors across all three tsconfig targets.
- **JSR dry-run fix** — worked around a deno_ast 0.52.0 panic caused by overlapping text-change entries for duplicate import specifiers.
- **`check-dts-surface.js` regex fix** — default-export parsing now correctly captures identifiers instead of keywords for `export default class/function` patterns.
- **BREAKING: `getNodeProps()` returns `Record<string, unknown>`** — aligns with `getEdgeProps()`. Replace `.get('key')` with `.key`, `.has('key')` with `'key' in props`, `.size` with `Object.keys(props).length`.
- **BREAKING: Removed `PerformanceClockAdapter` and `GlobalClockAdapter`** — use `ClockAdapter` directly.
- **`graph.patchMany()`** — batch multiple patches sequentially; each callback sees prior state.
- **`git warp bisect`** — binary search over writer patch history to find the first bad commit. O(log N) materializations.
- **Observer API stable** — `subscribe()` and `watch()` promoted to stable with `@since 13.0.0`.
- **`BisectService`** — domain service exported for programmatic use.

See the [full changelog](CHANGELOG.md) for details.

Expand Down Expand Up @@ -183,7 +185,7 @@ Query methods auto-materialize by default. Just open a graph and start querying:
```javascript
await graph.getNodes(); // ['user:alice', 'user:bob']
await graph.hasNode('user:alice'); // true
await graph.getNodeProps('user:alice'); // Map { 'name' => 'Alice', 'role' => 'admin' }
await graph.getNodeProps('user:alice'); // { name: 'Alice', role: 'admin' }
await graph.neighbors('user:alice', 'outgoing'); // [{ nodeId: 'user:bob', label: 'manages', direction: 'outgoing' }]
await graph.getEdges(); // [{ from: 'user:alice', to: 'user:bob', label: 'manages', props: {} }]
await graph.getEdgeProps('user:alice', 'user:bob', 'manages'); // { weight: 0.9 } or null
Expand Down Expand Up @@ -371,7 +373,7 @@ const view = await graph.observer('publicApi', {
});

const users = await view.getNodes(); // only user:* nodes
const props = await view.getNodeProps('user:alice'); // Map without ssn/password
const props = await view.getNodeProps('user:alice'); // { name: 'Alice', ... } without ssn/password
const result = await view.query().match('user:*').where({ role: 'admin' }).run();

// Measure information loss between two observer perspectives
Expand Down
62 changes: 17 additions & 45 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ROADMAP — @git-stunts/git-warp

> **Current version:** v12.4.1
> **Last reconciled:** 2026-03-02 (M14 HYGIENE added from HEX_AUDIT; completed items archived to COMPLETED.md; BACKLOG.md retired)
> **Current version:** v13.0.0
> **Last reconciled:** 2026-03-03 (v13.0.0 release: M11 COMPASS II complete, B100/B140 breaking, B44/B124/B125/B146 done)
> **Completed milestones:** [docs/ROADMAP/COMPLETED.md](docs/ROADMAP/COMPLETED.md)

---
Expand All @@ -25,11 +25,11 @@

### M10.T4 — Causality Bisect Spec

- **Status:** `PENDING`
- **Status:** `DONE` (spec existed; implementation completed in M11)

**Items:**

- **B2 (spec only)** (CAUSALITY BISECT) — design the bisect CLI contract + data model. Commit spec with test vectors. Full implementation deferred to M11 — but the spec lands here so bisect is available as a debugging tool during M10 trust hardening.
- **B2 (spec only)** (CAUSALITY BISECT) — Spec committed at `docs/specs/BISECT_V1.md`. Full implementation shipped in M11/v13.0.0.

**M10 Gate:** Signed ingress enforced end-to-end; trust E2E receipts green; B63 GC isolation verified under concurrent writes; B64 sync payload validation green; B65 divergence logging verified; B2 spec committed with test vectors.

Expand Down Expand Up @@ -165,37 +165,9 @@ Design-only items. RFCs filed — implementation deferred to future milestones.

---

## Milestone 11 — COMPASS II
## Milestone 11 — COMPASS II ✅ COMPLETE (v13.0.0)

**Theme:** Developer experience
**Objective:** Ship bisect, public observer API, and batch patch ergonomics.
**Triage date:** 2026-02-17

### M11.T1 — Causality Bisect (Implementation)

- **Status:** `PENDING`

**Items:**

- **B2 (implementation)** (CAUSALITY BISECT) — full implementation building on M10 spec. Binary search for first bad tick/invariant failure. `git bisect` for WARP.

### M11.T2 — Observer API

- **Status:** `PENDING`

**Items:**

- **B3** (OBSERVER API) — public event contract. Internal soak period over (shipped in PULSE, used internally since). Stabilize the public surface.

### M11.T3 — Batch Patch API

- **Status:** `PENDING`

**Items:**

- **B11** (`graph.patchMany(fns)` BATCH API) — sequence multiple patch callbacks atomically, each seeing the ref left by the previous. Natural complement to `graph.patch()`.

**M11 Gate:** Bisect correctness verified on seeded regressions; observer contract snapshot-tested; patchMany passes no-coordination suite.
Archived to [COMPLETED.md](docs/ROADMAP/COMPLETED.md#milestone-11--compass-ii).
Comment thread
coderabbitai[bot] marked this conversation as resolved.

---

Expand All @@ -209,10 +181,10 @@ Items picked up opportunistically without blocking milestones. No milestone assi

| ID | Item |
|----|------|
| B124 | **TRUST PAYLOAD PARITY TESTS** — assert CLI `trust` and `AuditVerifierService.evaluateTrust()` emit shape-compatible error payloads. From BACKLOG 2026-02-27. |
| B125 | **`CachedValue` NULL-PAYLOAD SEMANTIC TESTS** — document and test whether `null` is a valid cached value. From BACKLOG 2026-02-27. |
| ~~B124~~ | ✅ ~~**TRUST PAYLOAD PARITY TESTS**~~22 tests verifying CLI vs service shape parity. Done in v13.0.0. |
| ~~B125~~ | ✅ ~~**`CachedValue` NULL-PAYLOAD SEMANTIC TESTS**~~3 tests documenting null = "no value" sentinel. Done in v13.0.0. |
| B127 | **DENO SMOKE TEST** — `npm run test:deno:smoke` for fast local pre-push confidence without full Docker matrix. From BACKLOG 2026-02-25. |
| B44 | **SUBSCRIBER UNSUBSCRIBE-DURING-CALLBACK E2E** — event system edge case; known bug class that bites silently |
| ~~B44~~ | ✅ ~~**SUBSCRIBER UNSUBSCRIBE-DURING-CALLBACK E2E**~~3 edge-case tests (cross-unsubscribe, subscribe-during-callback, unsubscribe-in-onError). Done in v13.0.0. |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| B34 | **DOCS: SECURITY_SYNC.md** — extract threat model from JSDoc into operator doc |
| B35 | **DOCS: README INSTALL SECTION** — Quick Install with Docker + native paths |
| B36 | **FLUENT STATE BUILDER FOR TESTS** — `StateBuilder` helper replacing manual `WarpStateV5` literals |
Expand All @@ -229,7 +201,7 @@ Items picked up opportunistically without blocking milestones. No milestone assi
| B79 | **WARPGRAPH CONSTRUCTOR LIFECYCLE DOCS** — document cache invalidation strategy for 25 instance variables: which operations dirty which caches, which flush them. From B-AUDIT-16 (TSK TSK). **File:** `src/domain/WarpGraph.js:69-198` |
| B80 | **CHECKPOINTSERVICE CONTENT BLOB UNBOUNDED MEMORY** — iterates all properties into single `Set` before tree serialization. Stream content OIDs in batches. From B-AUDIT-10 (JANK). **File:** `src/domain/services/CheckpointService.js:224-226` |
| B81 | **`attachContent` ORPHAN BLOB GUARD** — `attachContent()` unconditionally writes blob before `setProperty()`. Validate before push to prevent orphan blobs. From B-CODE-2. **File:** `src/domain/services/PatchBuilderV2.js` |
| B146 | **UNIFY `CorePersistence` / `FullPersistence` TYPEDEFS** — `CorePersistence` (`WarpPersistence.js`) and `FullPersistence` (`WarpGraph.js`) are identical `CommitPort & BlobPort & TreePort & RefPort` intersections. Consolidate into one canonical typedef and update all import sites. From B145 PR review. |
| ~~B146~~ | ✅ ~~**UNIFY `CorePersistence` / `FullPersistence` TYPEDEFS**~~replaced `FullPersistence` with imported `CorePersistence`. Done in v13.0.0. |
| B147 | **RFC FIELD COUNT DRIFT DETECTOR** — script that counts WarpGraph instance fields (grep `this._` in constructor) and warns if design RFC field counts diverge. Prevents stale numbers in `warpgraph-decomposition.md`. From B145 PR review. |

### CI & Tooling Pack
Expand Down Expand Up @@ -299,7 +271,7 @@ Items parked with explicit conditions for promotion.
| B20 | **TRUST RECORD ROUND-TRIP SNAPSHOT TEST** | Promote if trust record schema changes |
| B21 | **TRUST SCHEMA DISCRIMINATED UNION** | Promote if superRefine causes a bug or blocks a feature |
| B27 | **`TrustKeyStore` PRE-VALIDATED KEY CACHE** | Promote when `verifySignature` appears in any p95 flame graph above 5% of call time |
| B100 | **MAP vs RECORD ASYMMETRY** — `getNodeProps()` returns Map, `getEdgeProps()` returns Record. Breaking change either way. From B-FEAT-3. | Promote with next major version RFC |
| ~~B100~~ | ✅ ~~**MAP vs RECORD ASYMMETRY**~~ — `getNodeProps()` now returns `Record<string, unknown>`. Done in v13.0.0. | ~~Promote with next major version RFC~~ |
| B101 | **MERMAID `~~~` INVISIBLE-LINK FRAGILITY** — undocumented Mermaid feature for positioning. From B-DIAG-3. | Promote if Mermaid renderer update breaks `~~~` positioning |

---
Expand All @@ -325,8 +297,8 @@ B5, B6, B13, B17, B18, B25, B45 — rejected 2026-02-17 with cause recorded in `
Pick opportunistically between milestones. Recommended order within tiers:

1. ~~**Immediate** (B46, B47, B26, B71, B126)~~ — **ALL DONE.**
2. **Near-term correctness** (B44, B76, B80, B81, B124) — prioritize items touching core services
3. **Near-term DX** (B36, B37, B43, B125, B127) — test ergonomics and developer velocity
2. **Near-term correctness** (B76, B80, B81) — prioritize items touching core services
3. **Near-term DX** (B36, B37, B43, B127) — test ergonomics and developer velocity
4. **Near-term docs/types** (B34, B35) — alignment and documentation
5. **Near-term tooling** (B12, B48, B49, B53, B54, B57, B28) — remaining type safety items
6. **CI & Tooling Pack** (B83, B85–B88, B119, B123, B128) — batch as one PR
Expand All @@ -349,11 +321,11 @@ Pick opportunistically between milestones. Recommended order within tiers:
| **Milestone (M12)** | 18 | B66, B67, B70, B73, B75, B105–B115, B117, B118 |
| **Milestone (M13)** | 1 | B116 (internal: DONE; wire-format: DEFERRED) |
| **Milestone (M14)** | 16 | B130–B145 |
| **Standalone** | 39 | B12, B19, B22, B28, B34–B37, B43, B44, B48, B49, B53, B54, B57, B76, B79–B81, B83, B85–B88, B95–B99, B102–B104, B119, B123–B125, B127–B129, B146, B147 |
| **Standalone (done)** | 23 | B26, B46, B47, B50–B52, B55, B71, B72, B77, B78, B82, B84, B89–B94, B120–B122, B126 |
| **Deferred** | 8 | B4, B7, B16, B20, B21, B27, B100, B101 |
| **Standalone** | 35 | B12, B19, B22, B28, B34–B37, B43, B48, B49, B53, B54, B57, B76, B79–B81, B83, B85–B88, B95–B99, B102–B104, B119, B123, B127–B129, B147 |
| **Standalone (done)** | 29 | B26, B44, B46, B47, B50–B52, B55, B71, B72, B77, B78, B82, B84, B89–B94, B100, B120–B122, B124, B125, B126, B146, B148 |
| **Deferred** | 7 | B4, B7, B16, B20, B21, B27, B101 |
| **Rejected** | 7 | B5, B6, B13, B17, B18, B25, B45 |
| **Total tracked** | **122** (23 done) | |
| **Total tracked** | **123** (29 done) | |

### STANK.md Cross-Reference

Expand Down
85 changes: 85 additions & 0 deletions bin/cli/commands/bisect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { execSync } from 'node:child_process';
import { EXIT_CODES, parseCommandArgs, usageError } from '../infrastructure.js';
import { bisectSchema } from '../schemas.js';
import { openGraph } from '../shared.js';
import BisectService from '../../../src/domain/services/BisectService.js';

/** @typedef {import('../types.js').CliOptions} CliOptions */

const BISECT_OPTIONS = {
good: { type: 'string' },
bad: { type: 'string' },
test: { type: 'string' },
};

/** @param {string[]} args */
function parseBisectArgs(args) {
const { values } = parseCommandArgs(args, BISECT_OPTIONS, bisectSchema);
return values;
}

/**
* Runs a shell command as the bisect test.
*
* @param {string} testCmd - Shell command to execute
* @param {string} sha - Candidate patch SHA (passed as env var)
* @param {string} graphName - Graph name (passed as env var)
* @returns {boolean} true if the command exits 0 (good), false otherwise (bad)
*/
function runTestCommand(testCmd, sha, graphName) {
try {
execSync(testCmd, {
stdio: 'pipe',
env: {
...process.env,
WARP_BISECT_SHA: sha,
WARP_BISECT_GRAPH: graphName,
},
});
return true;
} catch {
return false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Handles the `bisect` command: binary search over patch history.
* @param {{options: CliOptions, args: string[]}} params
* @returns {Promise<{payload: unknown, exitCode: number}>}
*/
export default async function handleBisect({ options, args }) {
if (options.writer === 'cli') {
throw usageError('bisect requires --writer <id>');
}

const { good, bad, test: testCmd } = parseBisectArgs(args);
const { graph, graphName } = await openGraph(options);
const writerId = options.writer;

const bisect = new BisectService({ graph });

const result = await bisect.run({
good,
bad,
writerId,
testFn: (_state, sha) => Promise.resolve(runTestCommand(testCmd, sha, graphName)),
});

if (result.result === 'range-error') {
return {
payload: { error: { code: 'E_BISECT_RANGE', message: result.message } },
exitCode: EXIT_CODES.NOT_FOUND,
};
}

const payload = {
result: 'found',
firstBadPatch: result.firstBadPatch,
writerId: result.writerId,
lamport: result.lamport,
steps: result.steps,
totalCandidates: result.totalCandidates,
};

return { payload, exitCode: EXIT_CODES.OK };
}
2 changes: 2 additions & 0 deletions bin/cli/commands/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import handleInstallHooks from './install-hooks.js';
import handleTrust from './trust.js';
import handlePatch from './patch.js';
import handleTree from './tree.js';
import handleBisect from './bisect.js';

/** @type {Map<string, Function>} */
export const COMMANDS = new Map(/** @type {[string, Function][]} */ ([
Expand All @@ -31,6 +32,7 @@ export const COMMANDS = new Map(/** @type {[string, Function][]} */ ([
['trust', handleTrust],
['patch', handlePatch],
['tree', handleTree],
['bisect', handleBisect],
['view', handleView],
['install-hooks', handleInstallHooks],
]));
Loading
Loading