-
Notifications
You must be signed in to change notification settings - Fork 0
feat!: WARP-native content system (M13 VESSEL v4.0.1) #285
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
Changes from 6 commits
ccfaecc
ce9fbad
3626c9f
355d9f9
3e243ca
6ad851d
69d8def
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,66 +1,48 @@ | ||
| /** | ||
| * @module content | ||
| * Content-on-node: attach rich content (markdown, text, etc.) to graph nodes | ||
| * using git's native content-addressed storage. | ||
| * using git-warp's native content-addressed storage. | ||
| * | ||
| * Content is stored as git blobs via `git hash-object -w`. The blob SHA and | ||
| * metadata are recorded as WARP node properties under the `_content.` prefix. | ||
| * Content is stored as git blobs via WARP's patch.attachContent(). The blob OID | ||
| * and metadata are recorded as WARP node properties. | ||
| * | ||
| * Property convention: | ||
| * _content.sha — git blob SHA | ||
| * _content.mime — MIME type (e.g. "text/markdown") | ||
| * _content.size — byte count | ||
| * _content — git blob OID (managed by WARP via CONTENT_PROPERTY_KEY) | ||
| * _content.mime — MIME type (e.g. "text/markdown") | ||
| * _content.size — byte count | ||
| */ | ||
|
|
||
| import { execFileSync } from 'node:child_process'; | ||
| import { CONTENT_PROPERTY_KEY } from '@git-stunts/git-warp'; | ||
|
|
||
| /** Property key prefix for content metadata. */ | ||
| const PREFIX = '_content.'; | ||
|
|
||
| /** Known content property keys. */ | ||
| const KEYS = { | ||
| sha: `${PREFIX}sha`, | ||
| mime: `${PREFIX}mime`, | ||
| size: `${PREFIX}size`, | ||
| }; | ||
|
|
||
| /** Validates a string is a 40- or 64-hex-char git object hash (SHA-1 or SHA-256). */ | ||
| const SHA_RE = /^[0-9a-f]{40,64}$/; | ||
|
|
||
| /** @throws {Error} if sha is not a valid git object hash (40 or 64 hex chars). */ | ||
| function assertValidSha(sha) { | ||
| if (typeof sha !== 'string' || !SHA_RE.test(sha)) { | ||
| throw new Error(`Invalid content SHA: ${sha}`); | ||
| } | ||
| } | ||
| const MIME_KEY = '_content.mime'; | ||
| const SIZE_KEY = '_content.size'; | ||
|
|
||
| /** | ||
| * @typedef {object} ContentMeta | ||
| * @property {string} sha - Git blob SHA | ||
| * @property {string} sha - Git blob OID | ||
| * @property {string} mime - MIME type | ||
| * @property {number} size - Content size in bytes | ||
| */ | ||
|
|
||
| /** | ||
| * @typedef {object} WriteContentResult | ||
| * @property {string} nodeId - Target node | ||
| * @property {string} sha - Written blob SHA | ||
| * @property {string} sha - Written blob OID | ||
| * @property {string} mime - MIME type | ||
| * @property {number} size - Byte count | ||
| */ | ||
|
|
||
| /** | ||
| * Write content to a graph node. Stores the content as a git blob and records | ||
| * metadata as node properties. | ||
| * Write content to a graph node. Stores the content as a git blob via WARP's | ||
| * native content API and records metadata as node properties. | ||
| * | ||
| * @param {string} cwd - Repository working directory | ||
| * @param {import('@git-stunts/git-warp').default} graph - WARP graph instance | ||
| * @param {string} nodeId - Target node ID | ||
| * @param {Buffer|string} content - Content to store | ||
| * @param {{ mime?: string }} [opts] | ||
| * @returns {Promise<WriteContentResult>} | ||
| */ | ||
| export async function writeContent(cwd, graph, nodeId, content, opts = {}) { | ||
| export async function writeContent(graph, nodeId, content, opts = {}) { | ||
| const exists = await graph.hasNode(nodeId); | ||
| if (!exists) { | ||
| throw new Error(`Node not found: ${nodeId}`); | ||
|
|
@@ -70,70 +52,52 @@ export async function writeContent(cwd, graph, nodeId, content, opts = {}) { | |
| const mime = opts.mime ?? 'text/plain'; | ||
| const size = buf.length; | ||
|
|
||
| // Write blob to git object store | ||
| const sha = execFileSync('git', ['hash-object', '-w', '--stdin'], { | ||
| cwd, | ||
| input: buf, | ||
| encoding: 'utf-8', | ||
| }).trim(); | ||
|
|
||
| // Record metadata as node properties | ||
| const patch = await graph.createPatch(); | ||
| patch.setProperty(nodeId, KEYS.sha, sha); | ||
| patch.setProperty(nodeId, KEYS.mime, mime); | ||
| patch.setProperty(nodeId, KEYS.size, size); | ||
| await patch.attachContent(nodeId, buf); | ||
| patch.setProperty(nodeId, MIME_KEY, mime); | ||
| patch.setProperty(nodeId, SIZE_KEY, size); | ||
| await patch.commit(); | ||
|
|
||
| const sha = await graph.getContentOid(nodeId); | ||
|
|
||
| if (sha == null) { | ||
| throw new Error(`Failed to retrieve OID after writing content to node: ${nodeId}`); | ||
| } | ||
|
|
||
| return { nodeId, sha, mime, size }; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Read content attached to a graph node. Retrieves the blob from git's object | ||
| * store and verifies SHA integrity. | ||
| * Read content attached to a graph node. Retrieves the blob from WARP's | ||
| * native content store. | ||
| * | ||
| * @param {string} cwd - Repository working directory | ||
| * @param {import('@git-stunts/git-warp').default} graph - WARP graph instance | ||
| * @param {string} nodeId - Target node ID | ||
| * @returns {Promise<{ content: string, meta: ContentMeta }>} | ||
| */ | ||
| export async function readContent(cwd, graph, nodeId) { | ||
| export async function readContent(graph, nodeId) { | ||
| const meta = await getContentMeta(graph, nodeId); | ||
| if (!meta) { | ||
| throw new Error(`No content attached to node: ${nodeId}`); | ||
| } | ||
|
|
||
| // Validate SHA before passing to git | ||
| assertValidSha(meta.sha); | ||
|
|
||
| // Retrieve blob from git object store | ||
| let content; | ||
| let contentBuf; | ||
| try { | ||
| content = execFileSync('git', ['cat-file', 'blob', meta.sha], { | ||
| cwd, | ||
| encoding: 'utf-8', | ||
| }); | ||
| } catch { | ||
| contentBuf = await graph.getContent(nodeId); | ||
| } catch (err) { | ||
| throw new Error( | ||
| `Content blob ${meta.sha} not found in git object store for node: ${nodeId}`, | ||
| `Failed to retrieve content blob ${meta.sha} for node: ${nodeId}`, | ||
| { cause: err }, | ||
| ); | ||
| } | ||
|
|
||
| // Verify integrity: re-hash and compare | ||
| const verifyBuf = Buffer.from(content, 'utf-8'); | ||
| const verifySha = execFileSync('git', ['hash-object', '--stdin'], { | ||
| cwd, | ||
| input: verifyBuf, | ||
| encoding: 'utf-8', | ||
| }).trim(); | ||
|
|
||
| if (verifySha !== meta.sha) { | ||
| if (contentBuf == null || (contentBuf.length === 0 && meta.size > 0)) { | ||
| throw new Error( | ||
| `Content integrity check failed for node ${nodeId}: ` + | ||
| `expected ${meta.sha}, got ${verifySha}`, | ||
| `Failed to retrieve content blob ${meta.sha} for node: ${nodeId}`, | ||
| ); | ||
| } | ||
|
|
||
| return { content, meta }; | ||
| return { content: contentBuf.toString('utf-8'), meta }; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -150,14 +114,15 @@ export async function getContentMeta(graph, nodeId) { | |
| throw new Error(`Node not found: ${nodeId}`); | ||
| } | ||
|
|
||
| const propsMap = await graph.getNodeProps(nodeId); | ||
| const sha = propsMap?.get(KEYS.sha) ?? null; | ||
| const sha = await graph.getContentOid(nodeId); | ||
| if (!sha) return null; | ||
|
|
||
| const propsMap = await graph.getNodeProps(nodeId); | ||
|
|
||
| return { | ||
| sha, | ||
| mime: propsMap.get(KEYS.mime) ?? 'text/plain', | ||
| size: propsMap.get(KEYS.size) ?? 0, | ||
| mime: propsMap?.get(MIME_KEY) ?? 'text/plain', | ||
| size: propsMap?.get(SIZE_KEY) ?? 0, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -172,13 +137,12 @@ export async function hasContent(graph, nodeId) { | |
| const exists = await graph.hasNode(nodeId); | ||
| if (!exists) return false; | ||
|
|
||
| const propsMap = await graph.getNodeProps(nodeId); | ||
| const sha = propsMap?.get(KEYS.sha) ?? null; | ||
| return sha !== null; | ||
| const sha = await graph.getContentOid(nodeId); | ||
| return sha != null; | ||
| } | ||
|
|
||
| /** | ||
| * Delete content from a node by clearing the `_content.*` properties. | ||
| * Delete content from a node by clearing the content properties. | ||
| * The git blob remains in the object store (cleaned up by git gc). | ||
| * | ||
| * @param {import('@git-stunts/git-warp').default} graph - WARP graph instance | ||
|
|
@@ -191,17 +155,16 @@ export async function deleteContent(graph, nodeId) { | |
| throw new Error(`Node not found: ${nodeId}`); | ||
| } | ||
|
|
||
| const propsMap = await graph.getNodeProps(nodeId); | ||
| const previousSha = propsMap?.get(KEYS.sha) ?? null; | ||
| const previousSha = await graph.getContentOid(nodeId); | ||
|
|
||
| if (!previousSha) { | ||
| return { nodeId, removed: false, previousSha: null }; | ||
| } | ||
|
|
||
| const patch = await graph.createPatch(); | ||
| patch.setProperty(nodeId, KEYS.sha, null); | ||
| patch.setProperty(nodeId, KEYS.mime, null); | ||
| patch.setProperty(nodeId, KEYS.size, null); | ||
| patch.setProperty(nodeId, CONTENT_PROPERTY_KEY, null); | ||
| patch.setProperty(nodeId, MIME_KEY, null); | ||
| patch.setProperty(nodeId, SIZE_KEY, null); | ||
| await patch.commit(); | ||
|
Comment on lines
164
to
168
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, check if the WARP library is available in the repository
find . -name "git-warp*" -type d 2>/dev/null | head -10Repository: neuroglyph/git-mind Length of output: 45 🏁 Script executed: # Also search the codebase for WARP API usage patterns
rg "\.attachContent\(|\.setProperty\(|\.getContentOid\(|deleteContent" --include="*.js" -B 2 -A 2 2>/dev/null | head -60Repository: neuroglyph/git-mind Length of output: 45 🏁 Script executed: # Search for any documentation or types that might indicate WARP's content detach mechanisms
rg "detach|removeContent|deleteContent" --include="*.js" --include="*.md" --include="*.json" -B 2 -A 2 2>/dev/null | head -80Repository: neuroglyph/git-mind Length of output: 45 🏁 Script executed: # Check package.json for the exact version of `@git-stunts/git-warp` being used
cat package.json | grep -A 5 -B 5 "@git-stunts/git-warp" 2>/dev/null || echo "WARP dependency not found in package.json"Repository: neuroglyph/git-mind Length of output: 379 🏁 Script executed: wc -l src/content.jsRepository: neuroglyph/git-mind Length of output: 81 🏁 Script executed: cat -n src/content.js | sed -n '150,175p'Repository: neuroglyph/git-mind Length of output: 645 🏁 Script executed: # Search for the deleteContent function definition
rg "deleteContent\s*=" src/content.js -B 5 -A 20Repository: neuroglyph/git-mind Length of output: 45 🏁 Script executed: # Check for any imports or references to WARP patch methods
rg "import|require" src/content.js | head -20Repository: neuroglyph/git-mind Length of output: 518 🌐 Web query:
💡 Result: In @git-stunts/git-warp v11 there is no “delete/unset property” operation in the patch API— What you can do:
So: Sources: 🌐 Web query:
💡 Result: I couldn’t find any The closest match for
Sources: [1] [2] [3] 🏁 Script executed: rg "getContentOid" src/ --include="*.js" -B 3 -A 10Repository: neuroglyph/git-mind Length of output: 504 🏁 Script executed: # Also check if there's any documentation or comments about null handling
rg "CONTENT_PROPERTY_KEY" src/ --include="*.js" -B 2 -A 2Repository: neuroglyph/git-mind Length of output: 504 🏁 Script executed: rg "getContentOid" src/ -B 3 -A 10Repository: neuroglyph/git-mind Length of output: 2513 🏁 Script executed: rg "CONTENT_PROPERTY_KEY" src/ -B 2 -A 2Repository: neuroglyph/git-mind Length of output: 778
WARP v11 has no 🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the deep investigation! This is a known WARP v11 behavior pattern documented in our project notes. WARP v11 has no removeProperty() API — setProperty(key, null) is the established deletion sentinel. All our readers check values (truthiness/nullish), never key presence: getContentOid() returns null after deletion, hasContent() returns false, getContentMeta() returns null. The full delete/re-check cycle is covered by the deleteContent removes properties test. No action needed.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| return { nodeId, removed: true, previousSha }; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: neuroglyph/git-mind
Length of output: 489
🏁 Script executed:
Repository: neuroglyph/git-mind
Length of output: 392
🏁 Script executed:
Repository: neuroglyph/git-mind
Length of output: 3465
🏁 Script executed:
Repository: neuroglyph/git-mind
Length of output: 6789
Verify
@git-stunts/git-warp@11.5.0exports the required content API surface.The content system (src/content.js) actively uses
CONTENT_PROPERTY_KEY,patch.attachContent(),graph.getContent(),graph.getContentOid(), andgraph.getNodeProps(). All five are called at runtime across writeContent, readContent, getContentMeta, hasContent, and deleteContent. If any of these exports do not exist in v11.5.0, the content subsystem will fail at runtime.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for flagging! This is a false positive — CodeRabbit sandbox could not resolve node_modules, but the API surface is fully verified locally. All 5 symbols (CONTENT_PROPERTY_KEY, patch.attachContent, graph.getContent, graph.getContentOid, graph.getNodeProps) are exercised by the test suite. 37 content tests pass covering every code path. No action needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@flyingrobots, thank you for the clarification! You're absolutely right — the sandbox limitation prevented me from verifying the node_modules, but your local test coverage with 37 passing content tests clearly confirms that all five API symbols are present and functioning correctly in@git-stunts/git-warp@11.5.0. I appreciate you taking the time to confirm this. No action needed indeed!🧠 Learnings used