Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dfcb553
docs: add markdownlint CI and code sample linter to backlog
flyingrobots Feb 21, 2026
df5c519
feat(content): add CONTENT_PROPERTY_KEY and PatchBuilderV2.attachCont…
flyingrobots Feb 21, 2026
495f839
feat(content): add PatchSession.attachContent() pass-through
flyingrobots Feb 21, 2026
83fed75
feat(content): add getContent/getContentOid query methods
flyingrobots Feb 21, 2026
2527b91
feat(content): type declarations and surface manifest
flyingrobots Feb 21, 2026
4961e7f
test(content): integration tests for content attachment
flyingrobots Feb 21, 2026
d7e0bcb
docs: update content attachment spec and changelog
flyingrobots Feb 21, 2026
6adecfd
chore: bump version to 11.5.0
flyingrobots Feb 21, 2026
0aee7f0
docs: add ADR-001 Folds design document
flyingrobots Feb 21, 2026
da8670f
fix(content): embed content blob OIDs in checkpoint tree
flyingrobots Feb 21, 2026
759b4b0
fix: ensure readBlob() always returns a real Node Buffer
flyingrobots Feb 21, 2026
b5270f2
test(content): checkpoint + GC durability integration test
flyingrobots Feb 21, 2026
7b34065
docs: document checkpoint anchoring and readBlob fix
flyingrobots Feb 21, 2026
6b7bb9b
docs: add content attachment to README, clean up ADR-001
flyingrobots Feb 21, 2026
6f8fea5
docs: add content attachment to README, clean up ADR-001
flyingrobots Feb 21, 2026
4cb8744
fix: update API surface snapshots for content attachment methods
flyingrobots Feb 21, 2026
003bae4
fix(docs): clean up ADR-001-Folds.md formatting and LLM residue
flyingrobots Feb 21, 2026
a2d0fa5
refactor: rename _blob_N tree entries to _content_<oid>
flyingrobots Feb 21, 2026
7f1c4f0
docs: add JSDoc notes, inline comments, and remove stale spec bullet
flyingrobots Feb 21, 2026
9923f26
docs(backlog): mark B-FEAT-1 done, add B-FEAT-2/B-FEAT-3/B-TYPE-3
flyingrobots Feb 21, 2026
c8af9ec
fix: reorder attachEdgeContent to avoid orphaned blobs + deduplicate …
flyingrobots Feb 21, 2026
12e5b9c
fix(deno): import Buffer from node:buffer in GitGraphAdapter
flyingrobots Feb 21, 2026
009fece
docs: fix review feedback — code fence tag, patch reuse, signatures, …
flyingrobots Feb 21, 2026
1d31fc1
fix: fold patch own dot into observedFrontier (#43)
flyingrobots Feb 21, 2026
618a39c
docs: add @throws to getEdgeContent JSDoc
flyingrobots Feb 21, 2026
c355f07
chore: address code review nits
flyingrobots Feb 21, 2026
5395fb4
fix: move foldPatchDot above join JSDoc to restore type annotations
flyingrobots Feb 21, 2026
a072239
fix(types): narrow join() union return type in receipt test
flyingrobots Feb 21, 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
51 changes: 41 additions & 10 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,26 +88,57 @@ The surface check shows `Manifest entries: 70` vs `index.js exports: 66`. This m

**Files:** `contracts/type-surface.m8.json`, `index.js`

### B-TYPE-3: Establish test-file wildcard ratchet

`ts-policy-check.js` only scans `src/`, `bin/`, and `scripts/` by design — test files are excluded. This means `@type {any}` usages across test files are unchecked. Options:
- Add a separate ratchet for test files with a higher threshold
- Add per-file caps to prevent individual test files from growing unbounded
- Document the exclusion as intentional and accept it

**Files:** `scripts/ts-policy-check.js`

---

## Feature: Content Attachment

### B-FEAT-1: Implement content attachment (`Atom(p)` payloads on nodes)
### B-FEAT-1: ~~Implement content attachment~~ DONE (v11.5.0)

Shipped in v11.5.0. See `docs/specs/CONTENT_ATTACHMENT.md` and CHANGELOG.

### B-FEAT-2: Determinism fuzzer for tree construction

Content blob tree entries are sorted by filename for deterministic tree OIDs. A property-based test should randomize:
- content blob insertion order in `PatchBuilderV2`
- content OID iteration order in `CheckpointService.createV5()`

and verify the resulting tree OID is identical regardless of insertion order. This would catch any accidental order-dependence in tree construction.

**Files:** new test in `test/unit/domain/services/`

Full spec in `docs/specs/CONTENT_ATTACHMENT.md`. Proposal to attach content-addressed blobs to graph nodes as first-class payloads, bridging git-warp's flat key-value properties to the paper's `α(v)` vertex attachment model.
### B-FEAT-3: Reconcile Map vs Record asymmetry in getNodeProps/getEdgeProps

**Core idea:** Store blobs in the Git object store (already a CAS), reference them via a `_content` property on nodes. This gets CRDT merge (LWW on the SHA), time-travel (`materialize({ ceiling })`), and observer scoping for free — zero changes to the CRDT model.
`getNodeProps()` returns a `Map<string, unknown>` while `getEdgeProps()` returns a plain `Record<string, unknown>`. This forces callers to use `.get()` for node props and `[key]` for edge props — an easy source of bugs. Options:
- Both return Map (breaking change for edge prop consumers)
- Both return Record (breaking change for node prop consumers)
- Keep both, document the asymmetry prominently

**Key decisions needed:**
**Files:** `src/domain/warp/query.methods.js`, `src/domain/WarpGraph.js`

---

## Documentation Quality

### B-DOC-1: Add markdownlint to CI

Add a markdownlint check to the CI pipeline to catch MD040 (missing code fence language tags) and similar doc issues automatically. Currently these are only caught by CodeRabbit review, which is slow and non-blocking.

**File:** `.github/workflows/ci.yml`

- **API shape:** Property convention only (zero new API) vs dedicated `patch.attachContent()` / `graph.getContent()` methods vs hybrid. Spec recommends hybrid.
- **Metadata properties:** Whether to store `_content.size`, `_content.mime`, `_content.encoding` at the substrate level or leave to consumers.
- **Dependency:** `git-cas` package or equivalent `git hash-object -w` / `git cat-file blob` via existing plumbing.
- **Edge attachments:** Deferred to v2 unless trivially included. Same mechanism — `_content` property on edges.
### B-DOC-2: Add a code sample linter for markdown files

**Out of scope (future):** Nested WARP attachments (where `α(v)` is itself a full WARP graph, not just an atom), content-level merge, MIME handling, content GC protection.
Syntax-check JS/TS code blocks embedded in markdown files (specs, guides, etc.) to catch issues like duplicate `const` declarations, TDZ errors, and other syntax errors before they reach review. Could use `eslint-plugin-markdown` (runs ESLint natively on fenced blocks) or a custom script that extracts code blocks and pipes them through `eslint --stdin`.

**Spec:** `docs/specs/CONTENT_ATTACHMENT.md`
**Files:** new script or CI step, `docs/**/*.md`

---

Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ 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).

## [11.5.0] — 2026-02-20 — Content Attachment (Paper I `Atom(p)`)

Implements content attachment — the ability to attach content-addressed blobs
to WARP graph nodes and edges as first-class payloads. A blob OID stored as
a `_content` string property gets CRDT merge (LWW), time-travel, and observer
scoping for free — zero changes to JoinReducer, serialization, or the CRDT layer.

### Added

- **`CONTENT_PROPERTY_KEY`** constant (`'_content'`) exported from `KeyCodec` and `index.js`.
- **`PatchBuilderV2.attachContent(nodeId, content)`** — writes blob to Git object store, sets `_content` property, tracks OID for GC anchoring.
- **`PatchBuilderV2.attachEdgeContent(from, to, label, content)`** — same for edges.
- **`PatchSession.attachContent()`** / **`attachEdgeContent()`** — async pass-through delegates.
- **`WarpGraph.getContent(nodeId)`** — returns `Buffer | null` from the content blob.
- **`WarpGraph.getContentOid(nodeId)`** — returns hex OID or null.
- **`WarpGraph.getEdgeContent(from, to, label)`** / **`getEdgeContentOid(from, to, label)`** — edge variants.
- **Blob anchoring** — content blob OIDs embedded in patch commit tree as `_content_<oid>` entries (self-documenting, unique by construction). Survives `git gc --prune=now`.
- **Type declarations** — all new methods in `index.d.ts`, `type-surface.m8.json`, `consumer.ts`.
- **Integration tests** — 11 tests covering single-writer, LWW, time-travel, deletion, Writer API, GC durability, binary round-trip.
- **Unit tests** — 23 tests for PatchBuilderV2 content ops and WarpGraph query methods.
- **ADR-001 Folds** — design document for future recursive attachments (structural zoom portals). Deferred; documents the path from `Atom(p)` to full `α(v) → WARP` recursion.

### Fixed

- **Checkpoint content anchoring** — `CheckpointService.createV5()` now scans `state.prop` for `_content` values and embeds the referenced blob OIDs in the checkpoint tree as `_content_<oid>` entries. This ensures content survives `git gc` even if patch commits are ever pruned.
- **`GitGraphAdapter.readBlob()`** — Now always returns a real Node `Buffer` (wraps `Uint8Array` from plumbing with `Buffer.from()`). Consumers can call `.toString('utf8')` directly.
- **`observedFrontier` staleness (#43)** — `JoinReducer.join()` now folds the patch's own dot (`{writer, lamport}`) into `observedFrontier`. Previously the frontier only reflected patch context VVs (pre-creation state), lagging by one tick per writer. The graph's `_versionVector` — cloned from `observedFrontier` after materialization — now reflects actual Lamport ticks.

## [11.4.0] — 2026-02-20 — M8 IRONCLAD Phase 3: Declaration Surface Automation

Completes M8 IRONCLAD with automated declaration surface validation and expanded
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,29 @@ const sha = await (await graph.createPatch())

Each `commit()` creates one Git commit containing all the operations, advances the writer's Lamport clock, and updates the writer's ref via compare-and-swap.

### Content Attachment

Attach content-addressed blobs to nodes and edges as first-class payloads (Paper I `Atom(p)`). Blobs are stored in the Git object store, referenced by SHA, and inherit CRDT merge, time-travel, and observer scoping automatically.

```javascript
const patch = await graph.createPatch();
patch.addNode('adr:0007'); // sync — queues a NodeAdd op
await patch.attachContent('adr:0007', '# ADR 0007\n\nDecision text...'); // async — writes blob
await patch.commit();

// Read content back
const buffer = await graph.getContent('adr:0007'); // Buffer | null
const oid = await graph.getContentOid('adr:0007'); // hex SHA or null

// Edge content works the same way (assumes nodes and edge already exist)
const patch2 = await graph.createPatch();
await patch2.attachEdgeContent('a', 'b', 'rel', 'edge payload');
await patch2.commit();
const edgeBuf = await graph.getEdgeContent('a', 'b', 'rel');
```

Content blobs survive `git gc` — their OIDs are embedded in the patch commit tree and checkpoint tree, keeping them reachable.

### Writer API

For repeated writes, the Writer API is more convenient:
Expand Down Expand Up @@ -508,7 +531,7 @@ npm run test:matrix # All runtimes in parallel
## When NOT to Use It

- **High-throughput transactional workloads.** If you need thousands of writes per second with immediate consistency, use Postgres or Redis.
- **Large binary or blob storage.** Data lives in Git commit messages (default cap 1 MB). Use object storage for images or videos.
- **Large binary or blob storage.** Properties live in Git commit messages; content blobs live in the Git object store. Neither is optimized for large media files. Use object storage for images or videos.
- **Sub-millisecond read latency.** Materialization has overhead. Use an in-memory database for real-time gaming physics or HFT.
- **Simple key-value storage.** If you don't have relationships or need traversals, a graph database is overkill.
- **Non-Git environments.** The value proposition depends on Git infrastructure (push/pull, content-addressing).
Expand Down
33 changes: 33 additions & 0 deletions contracts/type-surface.m8.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,34 @@
],
"returns": "Promise<Record<string, unknown> | null>"
},
"getContentOid": {
"async": true,
"params": [{ "name": "nodeId", "type": "string" }],
"returns": "Promise<string | null>"
},
"getContent": {
"async": true,
"params": [{ "name": "nodeId", "type": "string" }],
"returns": "Promise<Buffer | null>"
},
"getEdgeContentOid": {
"async": true,
"params": [
{ "name": "from", "type": "string" },
{ "name": "to", "type": "string" },
{ "name": "label", "type": "string" }
],
"returns": "Promise<string | null>"
},
"getEdgeContent": {
"async": true,
"params": [
{ "name": "from", "type": "string" },
{ "name": "to", "type": "string" },
{ "name": "label", "type": "string" }
],
"returns": "Promise<Buffer | null>"
},
"neighbors": {
"async": true,
"params": [
Expand Down Expand Up @@ -310,6 +338,8 @@
"removeEdge": { "params": [{ "name": "from", "type": "string" }, { "name": "to", "type": "string" }, { "name": "label", "type": "string" }], "returns": "PatchBuilderV2" },
"setProperty": { "params": [{ "name": "nodeId", "type": "string" }, { "name": "key", "type": "string" }, { "name": "value", "type": "unknown" }], "returns": "PatchBuilderV2" },
"setEdgeProperty": { "params": [{ "name": "from", "type": "string" }, { "name": "to", "type": "string" }, { "name": "label", "type": "string" }, { "name": "key", "type": "string" }, { "name": "value", "type": "unknown" }], "returns": "PatchBuilderV2" },
"attachContent": { "async": true, "params": [{ "name": "nodeId", "type": "string" }, { "name": "content", "type": "Buffer | string" }], "returns": "Promise<PatchBuilderV2>" },
"attachEdgeContent": { "async": true, "params": [{ "name": "from", "type": "string" }, { "name": "to", "type": "string" }, { "name": "label", "type": "string" }, { "name": "content", "type": "Buffer | string" }], "returns": "Promise<PatchBuilderV2>" },
"build": { "params": [], "returns": "PatchV2" },
"commit": { "async": true, "params": [], "returns": "Promise<string>" }
},
Expand All @@ -326,6 +356,8 @@
"removeEdge": { "params": [{ "name": "from", "type": "string" }, { "name": "to", "type": "string" }, { "name": "label", "type": "string" }], "returns": "this" },
"setProperty": { "params": [{ "name": "nodeId", "type": "string" }, { "name": "key", "type": "string" }, { "name": "value", "type": "unknown" }], "returns": "this" },
"setEdgeProperty": { "params": [{ "name": "from", "type": "string" }, { "name": "to", "type": "string" }, { "name": "label", "type": "string" }, { "name": "key", "type": "string" }, { "name": "value", "type": "unknown" }], "returns": "this" },
"attachContent": { "async": true, "params": [{ "name": "nodeId", "type": "string" }, { "name": "content", "type": "Buffer | string" }], "returns": "Promise<this>" },
"attachEdgeContent": { "async": true, "params": [{ "name": "from", "type": "string" }, { "name": "to", "type": "string" }, { "name": "label", "type": "string" }, { "name": "content", "type": "Buffer | string" }], "returns": "Promise<this>" },
"build": { "params": [], "returns": "PatchV2" },
"commit": { "async": true, "params": [], "returns": "Promise<string>" }
},
Expand Down Expand Up @@ -413,6 +445,7 @@
"encodeEdgePropKey": { "kind": "function", "params": [{ "name": "from", "type": "string" }, { "name": "to", "type": "string" }, { "name": "label", "type": "string" }, { "name": "propKey", "type": "string" }], "returns": "string" },
"decodeEdgePropKey": { "kind": "function", "params": [{ "name": "encoded", "type": "string" }], "returns": "{ from: string; to: string; label: string; propKey: string }" },
"isEdgePropKey": { "kind": "function", "params": [{ "name": "key", "type": "string" }], "returns": "boolean" },
"CONTENT_PROPERTY_KEY": { "kind": "const" },
"computeTranslationCost": { "kind": "function" },
"migrateV4toV5": { "kind": "function" },

Expand Down
Loading
Loading