Skip to content

Commit 6bd1e56

Browse files
authored
Merge pull request #285 from neuroglyph/feat/warp-native-content
Resolved 6 CodeRabbit issues (4 code fixes, 2 false positives); v4.0.1.
2 parents 3c4439f + 69d8def commit 6bd1e56

File tree

7 files changed

+114
-129
lines changed

7 files changed

+114
-129
lines changed

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [4.0.1] - 2026-02-22
11+
12+
### Fixed
13+
14+
- **OID guard in `writeContent()`** — Throw if `getContentOid()` returns null after a successful write, enforcing the `WriteContentResult.sha: string` contract (#284)
15+
- **Error cause chain in `readContent()`** — Capture original error via `{ cause: err }` so callers can distinguish blob-not-found from infrastructure failures (#284)
16+
- **Null guard in `readContent()`** — Changed `!contentBuf` to explicit `contentBuf == null` for clearer null/undefined intent (#284)
17+
- **Null guard in `hasContent()`** — Changed `sha !== null` to `sha != null` to catch both null and undefined from `getContentOid()`, consistent with other callers (#284)
18+
- **ROADMAP stale integrity-check language** — Corrected binary content backlog item to reflect WARP-native blob storage; reframed `--verify` flag as OID existence check (#284)
19+
- **JSDoc typedef terminology** — Changed "Git blob SHA" / "Written blob SHA" to "Git blob OID" in `ContentMeta` and `WriteContentResult` typedefs (#284)
20+
- **Dead `execSync` import** — Removed unused `execSync` from `test/content.test.js`; only `execFileSync` is used (#284)
21+
22+
### Added
23+
24+
- **Empty content edge-case test** — Verifies `writeContent()` handles empty string input correctly (size 0, round-trip intact) (#284)
25+
26+
## [4.0.0] - 2026-02-22
27+
28+
### Changed
29+
30+
- **BREAKING: Migrate content system to git-warp native API** — Replaced custom CAS layer (`git hash-object` / `git cat-file`) with `@git-stunts/git-warp` native `setContent()` / `getContent()` API. Content properties now use WARP's `CONTENT_PROPERTY_KEY` instead of custom `_content.sha`. Removes all direct git subprocess calls from content module (#284)
31+
- **Test count** — 577 tests across 29 files (was 571)
32+
1033
## [3.3.0] - 2026-02-22
1134

1235
### Added

ROADMAP.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2189,13 +2189,20 @@ Two issues were filed during the M12 extension polish pass and intentionally def
21892189

21902190
### Content system enhancements (from M13 VESSEL review)
21912191

2192-
- **`git mind content list`** — Query all nodes that have `_content.sha` properties. Currently there's no way to discover which nodes carry content without inspecting each one individually.
2193-
- **Binary content support** — Add base64 encoding for non-text MIME types. Currently the content system is text-only (UTF-8); non-UTF-8 blobs fail the integrity check by design. Requires reintroducing encoding metadata and updating `readContent()` to handle buffer round-trips.
2194-
- **`content meta --verify` flag**Run the SHA integrity check without dumping the full content body. Useful for bulk health checks across all content-bearing nodes.
2192+
- **`git mind content list`** — Query all nodes that have `_content` properties. Currently there's no way to discover which nodes carry content without inspecting each one individually.
2193+
- **Binary content support** — Add base64 encoding for non-text MIME types. Currently the content system is text-only (UTF-8); WARP stores blobs natively but `readContent()` always calls `.toString('utf-8')`, so binary round-trips are lossy. Requires returning a `Buffer` for non-text MIME types and reintroducing encoding metadata.
2194+
- **`content meta --verify` flag**Verify the content blob OID exists in the git object store without dumping the full body. Useful for bulk health checks across all content-bearing nodes.
21952195

21962196
### Codebase hardening (from M13 VESSEL review)
21972197

2198-
- **Standardize all git subprocess calls to `execFileSync`**`src/content.js` now uses `execFileSync` exclusively, but other modules (e.g. `processCommitCmd` in `commands.js`) still use `execSync` with string interpolation. Audit and migrate for consistency and defense-in-depth.
2198+
- **Standardize all git subprocess calls to `execFileSync`**`src/content.js` eliminated all subprocess calls via WARP-native migration, but other modules (e.g. `processCommitCmd` in `commands.js`) still use `execSync` with string interpolation. Audit and migrate for consistency and defense-in-depth.
2199+
2200+
### Content module error handling (from #284 CodeRabbit review)
2201+
2202+
- **Establish error message conventions** — Content module errors lack a consistent prefix/format. Consider a `[content] <operation> failed: <detail>` convention or a `ContentError` class hierarchy for typed catch handling.
2203+
- **Audit `try/catch` blocks for cause preservation** — The `readContent()` catch now chains `{ cause: err }`, but other modules may swallow root causes. Audit all catch blocks in domain code for cause propagation.
2204+
- **Integration test for `error.cause` chain** — Verify callers of `readContent()` can access `error.cause` when blob retrieval fails. Currently only the error message is tested.
2205+
- **`--verbose` flag for content CLI** — Dump the full `error.cause` chain when content operations fail. Helps diagnose infrastructure vs. missing-blob issues.
21992206

22002207
### Other backlog items
22012208

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@neuroglyph/git-mind",
3-
"version": "3.3.0",
3+
"version": "4.0.1",
44
"description": "A project knowledge graph tool built on git-warp",
55
"type": "module",
66
"license": "Apache-2.0",

src/cli/commands.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@ export async function contentSet(cwd, nodeId, filePath, opts = {}) {
838838
const mime = opts.mime ?? MIME_MAP[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
839839

840840
const graph = await loadGraph(cwd);
841-
const result = await writeContent(cwd, graph, nodeId, buf, { mime });
841+
const result = await writeContent(graph, nodeId, buf, { mime });
842842

843843
if (opts.json) {
844844
outputJson('content-set', result);
@@ -861,7 +861,7 @@ export async function contentSet(cwd, nodeId, filePath, opts = {}) {
861861
export async function contentShow(cwd, nodeId, opts = {}) {
862862
try {
863863
const graph = await loadGraph(cwd);
864-
const { content, meta } = await readContent(cwd, graph, nodeId);
864+
const { content, meta } = await readContent(graph, nodeId);
865865

866866
if (opts.json) {
867867
outputJson('content-show', { nodeId, content, ...meta });

src/content.js

Lines changed: 46 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,48 @@
11
/**
22
* @module content
33
* Content-on-node: attach rich content (markdown, text, etc.) to graph nodes
4-
* using git's native content-addressed storage.
4+
* using git-warp's native content-addressed storage.
55
*
6-
* Content is stored as git blobs via `git hash-object -w`. The blob SHA and
7-
* metadata are recorded as WARP node properties under the `_content.` prefix.
6+
* Content is stored as git blobs via WARP's patch.attachContent(). The blob OID
7+
* and metadata are recorded as WARP node properties.
88
*
99
* Property convention:
10-
* _content.sha — git blob SHA
11-
* _content.mime — MIME type (e.g. "text/markdown")
12-
* _content.size — byte count
10+
* _content — git blob OID (managed by WARP via CONTENT_PROPERTY_KEY)
11+
* _content.mime — MIME type (e.g. "text/markdown")
12+
* _content.size — byte count
1313
*/
1414

15-
import { execFileSync } from 'node:child_process';
15+
import { CONTENT_PROPERTY_KEY } from '@git-stunts/git-warp';
1616

17-
/** Property key prefix for content metadata. */
18-
const PREFIX = '_content.';
19-
20-
/** Known content property keys. */
21-
const KEYS = {
22-
sha: `${PREFIX}sha`,
23-
mime: `${PREFIX}mime`,
24-
size: `${PREFIX}size`,
25-
};
26-
27-
/** Validates a string is a 40- or 64-hex-char git object hash (SHA-1 or SHA-256). */
28-
const SHA_RE = /^[0-9a-f]{40,64}$/;
29-
30-
/** @throws {Error} if sha is not a valid git object hash (40 or 64 hex chars). */
31-
function assertValidSha(sha) {
32-
if (typeof sha !== 'string' || !SHA_RE.test(sha)) {
33-
throw new Error(`Invalid content SHA: ${sha}`);
34-
}
35-
}
17+
const MIME_KEY = '_content.mime';
18+
const SIZE_KEY = '_content.size';
3619

3720
/**
3821
* @typedef {object} ContentMeta
39-
* @property {string} sha - Git blob SHA
22+
* @property {string} sha - Git blob OID
4023
* @property {string} mime - MIME type
4124
* @property {number} size - Content size in bytes
4225
*/
4326

4427
/**
4528
* @typedef {object} WriteContentResult
4629
* @property {string} nodeId - Target node
47-
* @property {string} sha - Written blob SHA
30+
* @property {string} sha - Written blob OID
4831
* @property {string} mime - MIME type
4932
* @property {number} size - Byte count
5033
*/
5134

5235
/**
53-
* Write content to a graph node. Stores the content as a git blob and records
54-
* metadata as node properties.
36+
* Write content to a graph node. Stores the content as a git blob via WARP's
37+
* native content API and records metadata as node properties.
5538
*
56-
* @param {string} cwd - Repository working directory
5739
* @param {import('@git-stunts/git-warp').default} graph - WARP graph instance
5840
* @param {string} nodeId - Target node ID
5941
* @param {Buffer|string} content - Content to store
6042
* @param {{ mime?: string }} [opts]
6143
* @returns {Promise<WriteContentResult>}
6244
*/
63-
export async function writeContent(cwd, graph, nodeId, content, opts = {}) {
45+
export async function writeContent(graph, nodeId, content, opts = {}) {
6446
const exists = await graph.hasNode(nodeId);
6547
if (!exists) {
6648
throw new Error(`Node not found: ${nodeId}`);
@@ -70,70 +52,52 @@ export async function writeContent(cwd, graph, nodeId, content, opts = {}) {
7052
const mime = opts.mime ?? 'text/plain';
7153
const size = buf.length;
7254

73-
// Write blob to git object store
74-
const sha = execFileSync('git', ['hash-object', '-w', '--stdin'], {
75-
cwd,
76-
input: buf,
77-
encoding: 'utf-8',
78-
}).trim();
79-
80-
// Record metadata as node properties
8155
const patch = await graph.createPatch();
82-
patch.setProperty(nodeId, KEYS.sha, sha);
83-
patch.setProperty(nodeId, KEYS.mime, mime);
84-
patch.setProperty(nodeId, KEYS.size, size);
56+
await patch.attachContent(nodeId, buf);
57+
patch.setProperty(nodeId, MIME_KEY, mime);
58+
patch.setProperty(nodeId, SIZE_KEY, size);
8559
await patch.commit();
8660

61+
const sha = await graph.getContentOid(nodeId);
62+
63+
if (sha == null) {
64+
throw new Error(`Failed to retrieve OID after writing content to node: ${nodeId}`);
65+
}
66+
8767
return { nodeId, sha, mime, size };
8868
}
8969

9070
/**
91-
* Read content attached to a graph node. Retrieves the blob from git's object
92-
* store and verifies SHA integrity.
71+
* Read content attached to a graph node. Retrieves the blob from WARP's
72+
* native content store.
9373
*
94-
* @param {string} cwd - Repository working directory
9574
* @param {import('@git-stunts/git-warp').default} graph - WARP graph instance
9675
* @param {string} nodeId - Target node ID
9776
* @returns {Promise<{ content: string, meta: ContentMeta }>}
9877
*/
99-
export async function readContent(cwd, graph, nodeId) {
78+
export async function readContent(graph, nodeId) {
10079
const meta = await getContentMeta(graph, nodeId);
10180
if (!meta) {
10281
throw new Error(`No content attached to node: ${nodeId}`);
10382
}
10483

105-
// Validate SHA before passing to git
106-
assertValidSha(meta.sha);
107-
108-
// Retrieve blob from git object store
109-
let content;
84+
let contentBuf;
11085
try {
111-
content = execFileSync('git', ['cat-file', 'blob', meta.sha], {
112-
cwd,
113-
encoding: 'utf-8',
114-
});
115-
} catch {
86+
contentBuf = await graph.getContent(nodeId);
87+
} catch (err) {
11688
throw new Error(
117-
`Content blob ${meta.sha} not found in git object store for node: ${nodeId}`,
89+
`Failed to retrieve content blob ${meta.sha} for node: ${nodeId}`,
90+
{ cause: err },
11891
);
11992
}
12093

121-
// Verify integrity: re-hash and compare
122-
const verifyBuf = Buffer.from(content, 'utf-8');
123-
const verifySha = execFileSync('git', ['hash-object', '--stdin'], {
124-
cwd,
125-
input: verifyBuf,
126-
encoding: 'utf-8',
127-
}).trim();
128-
129-
if (verifySha !== meta.sha) {
94+
if (contentBuf == null || (contentBuf.length === 0 && meta.size > 0)) {
13095
throw new Error(
131-
`Content integrity check failed for node ${nodeId}: ` +
132-
`expected ${meta.sha}, got ${verifySha}`,
96+
`Failed to retrieve content blob ${meta.sha} for node: ${nodeId}`,
13397
);
13498
}
13599

136-
return { content, meta };
100+
return { content: contentBuf.toString('utf-8'), meta };
137101
}
138102

139103
/**
@@ -150,14 +114,15 @@ export async function getContentMeta(graph, nodeId) {
150114
throw new Error(`Node not found: ${nodeId}`);
151115
}
152116

153-
const propsMap = await graph.getNodeProps(nodeId);
154-
const sha = propsMap?.get(KEYS.sha) ?? null;
117+
const sha = await graph.getContentOid(nodeId);
155118
if (!sha) return null;
156119

120+
const propsMap = await graph.getNodeProps(nodeId);
121+
157122
return {
158123
sha,
159-
mime: propsMap.get(KEYS.mime) ?? 'text/plain',
160-
size: propsMap.get(KEYS.size) ?? 0,
124+
mime: propsMap?.get(MIME_KEY) ?? 'text/plain',
125+
size: propsMap?.get(SIZE_KEY) ?? 0,
161126
};
162127
}
163128

@@ -172,13 +137,12 @@ export async function hasContent(graph, nodeId) {
172137
const exists = await graph.hasNode(nodeId);
173138
if (!exists) return false;
174139

175-
const propsMap = await graph.getNodeProps(nodeId);
176-
const sha = propsMap?.get(KEYS.sha) ?? null;
177-
return sha !== null;
140+
const sha = await graph.getContentOid(nodeId);
141+
return sha != null;
178142
}
179143

180144
/**
181-
* Delete content from a node by clearing the `_content.*` properties.
145+
* Delete content from a node by clearing the content properties.
182146
* The git blob remains in the object store (cleaned up by git gc).
183147
*
184148
* @param {import('@git-stunts/git-warp').default} graph - WARP graph instance
@@ -191,17 +155,16 @@ export async function deleteContent(graph, nodeId) {
191155
throw new Error(`Node not found: ${nodeId}`);
192156
}
193157

194-
const propsMap = await graph.getNodeProps(nodeId);
195-
const previousSha = propsMap?.get(KEYS.sha) ?? null;
158+
const previousSha = await graph.getContentOid(nodeId);
196159

197160
if (!previousSha) {
198161
return { nodeId, removed: false, previousSha: null };
199162
}
200163

201164
const patch = await graph.createPatch();
202-
patch.setProperty(nodeId, KEYS.sha, null);
203-
patch.setProperty(nodeId, KEYS.mime, null);
204-
patch.setProperty(nodeId, KEYS.size, null);
165+
patch.setProperty(nodeId, CONTENT_PROPERTY_KEY, null);
166+
patch.setProperty(nodeId, MIME_KEY, null);
167+
patch.setProperty(nodeId, SIZE_KEY, null);
205168
await patch.commit();
206169

207170
return { nodeId, removed: true, previousSha };

0 commit comments

Comments
 (0)