diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cd5faf3..38b26327 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 --bad --test --writer `. O(log N) materializations. Exit codes: 0=found, 1=usage, 2=range error, 3=internal. ### Changed +- **BREAKING: `getNodeProps()` returns `Record` instead of `Map` (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. @@ -25,6 +32,11 @@ 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):** + - **CLI hardening** — added `--writer` validation to bisect, SHA format regex on `--good`/`--bad`, rethrow ENOENT/EACCES from test command runner instead of swallowing. + - **BisectService cleanup** — removed dead code, added invariant comment, replaced `BisectResult` interface with discriminated union type, fixed exit code constant. + - **Prototype-pollution hardening** — `Object.create(null)` for property bags in `getNodeProps`, `getEdgeProps`, `getEdges`, `buildPropsSnapshot`; fixed indexed-path null masking in `getNodeProps`. + - **Docs housekeeping** — reconciled ROADMAP inventory counts (24→29 done), fixed M11 sequencing, removed done items from priority tiers, fixed stale test vector counts (6→9), corrected Deno test name, moved B100 to `### Changed`. ## [12.4.1] — 2026-02-28 @@ -1528,7 +1540,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` 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 diff --git a/README.md b/README.md index acf89d21..043283f4 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,14 @@ git-warp CLI demo

-## 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`** — 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. @@ -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 @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index bc252f37..b8872024 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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) --- @@ -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. @@ -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). --- @@ -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. | | 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 | @@ -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 @@ -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`. 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 | --- @@ -312,21 +284,21 @@ B5, B6, B13, B17, B18, B25, B45 — rejected 2026-02-17 with cause recorded in ` ## Execution Order -### Milestones: M10 → M12 → M13 → M14 → M11 +### Milestones: M10 → M12 → M13 → M11 → M14 -1. **M10 SENTINEL** — Trust + sync safety + correctness — DONE except B2 spec +1. **M10 SENTINEL** — Trust + sync safety + correctness — **DONE** 2. **M12 SCALPEL** — STANK audit cleanup (minus edge prop encoding) — **DONE** (all tasks complete, gate verified) 3. **M13 SCALPEL II** — Edge property canonicalization — **DONE** (internal model complete; wire-format cutover deferred by ADR 3) -4. **M14 HYGIENE** — Test quality, DRY extraction, SOLID quick-wins — **NEXT** (from HEX_AUDIT) -5. **M11 COMPASS II** — Developer experience (B2 impl, B3, B11) — after M14 +4. **M11 COMPASS II** — Developer experience (B2 impl, B3, B11) — ✅ **DONE** (v13.0.0), archived +5. **M14 HYGIENE** — Test quality, DRY extraction, SOLID quick-wins — **NEXT** (from HEX_AUDIT) ### Standalone Priority Sequence 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 @@ -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** total; 29 standalone done | | ### STANK.md Cross-Reference @@ -455,11 +427,11 @@ Pick opportunistically between milestones. Recommended order within tiers: ## Final Command Every milestone has a hard gate. No milestone blurs into the next. -Execution: M10 SENTINEL → **M12 SCALPEL** → **M13 SCALPEL II** → **M14 HYGIENE** → M11 COMPASS II. Standalone items fill the gaps. +Execution: M10 SENTINEL → **M12 SCALPEL** → **M13 SCALPEL II** → **M11 COMPASS II** → **M14 HYGIENE**. M11 is complete and archived. Standalone items fill the gaps. M12 is complete (including T8/T9). M13 internal canonicalization (ADR 1) is complete — canonical `NodePropSet`/`EdgePropSet` semantics, wire gate split, reserved-byte validation, version namespace separation. The persisted wire-format half of B116 is deferred by ADR 2 and governed by ADR 3 readiness gates. -M14 HYGIENE is the current priority — test hardening, DRY extraction, and SOLID quick-wins from the HEX_AUDIT. M11 follows after M14. +M14 HYGIENE is the current priority — test hardening, DRY extraction, and SOLID quick-wins from the HEX_AUDIT. M11 is complete and archived in COMPLETED.md. Rejected items live in `GRAVEYARD.md`. Resurrections require an RFC. `BACKLOG.md` retired — all intake goes directly into this file (policy in `CLAUDE.md`). diff --git a/bin/cli/commands/bisect.js b/bin/cli/commands/bisect.js new file mode 100644 index 00000000..17cf7179 --- /dev/null +++ b/bin/cli/commands/bisect.js @@ -0,0 +1,91 @@ +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 (/** @type {unknown} */ err) { + // Non-zero exit (err.status is a number) → test says "bad" + const asRecord = /** @type {Record} */ (err); + if (err && typeof asRecord.status === 'number') { + return false; + } + // Spawn failure (ENOENT, EACCES, etc.) → rethrow so the user sees the real error + throw err; + } +} + +/** + * 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 '); + } + + 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 }; +} diff --git a/bin/cli/commands/registry.js b/bin/cli/commands/registry.js index 5c8850d0..5b04ae94 100644 --- a/bin/cli/commands/registry.js +++ b/bin/cli/commands/registry.js @@ -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} */ export const COMMANDS = new Map(/** @type {[string, Function][]} */ ([ @@ -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], ])); diff --git a/bin/cli/infrastructure.js b/bin/cli/infrastructure.js index 48f76fc1..bcd2f21e 100644 --- a/bin/cli/infrastructure.js +++ b/bin/cli/infrastructure.js @@ -49,6 +49,7 @@ Commands: seek Time-travel: step through graph history by Lamport tick patch Decode and inspect raw patches tree ASCII tree traversal from root nodes + bisect Binary search for first bad patch in writer history view Interactive TUI graph browser (requires @git-stunts/git-warp-tui) install-hooks Install post-merge git hook @@ -119,6 +120,12 @@ Tree options: --edge