diff --git a/CHANGELOG.md b/CHANGELOG.md index af4c5ebb..0ffd058d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.0.0] - 2026-02-25 + +### Breaking + +- **`getNodes(graph)` wrapper** — Use `graph.getNodes()` directly (#295) +- **`hasNode(graph, id)` wrapper** — Use `graph.hasNode(id)` directly (#295) +- **`saveGraph(graph)` wrapper** — Dead code with zero call sites (#295) +- **`queryEdges(graph, filter)` wrapper** — Use `graph.getEdges()` with inline filter (#295) +- **`getNodesByPrefix(graph, prefix)` wrapper** — Use `graph.getNodes()` with `startsWith()` filter (#295) + +### Changed + +- **All internal `loadGraph()` calls replaced with `initGraph()`** — `loadGraph` kept as deprecated alias for public API backward compatibility (#295) + ## [4.0.1] - 2026-02-22 ### Fixed @@ -357,6 +371,11 @@ Complete rewrite from C23 to Node.js on `@git-stunts/git-warp`. - Docker-based CI/CD - All C-specific documentation +[5.0.0]: https://github.com/neuroglyph/git-mind/releases/tag/v5.0.0 +[4.0.1]: https://github.com/neuroglyph/git-mind/releases/tag/v4.0.1 +[4.0.0]: https://github.com/neuroglyph/git-mind/releases/tag/v4.0.0 +[3.3.0]: https://github.com/neuroglyph/git-mind/releases/tag/v3.3.0 +[3.2.0]: https://github.com/neuroglyph/git-mind/releases/tag/v3.2.0 [3.1.0]: https://github.com/neuroglyph/git-mind/releases/tag/v3.1.0 [3.0.0]: https://github.com/neuroglyph/git-mind/releases/tag/v3.0.0 [2.0.0-alpha.5]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.5 diff --git a/package-lock.json b/package-lock.json index 5ca4d88b..d18a6fad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@neuroglyph/git-mind", - "version": "4.0.1", + "version": "5.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@neuroglyph/git-mind", - "version": "4.0.1", + "version": "5.0.0", "license": "Apache-2.0", "dependencies": { "@git-stunts/git-warp": "^11.5.0", diff --git a/package.json b/package.json index 0257fb6f..03136bf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neuroglyph/git-mind", - "version": "4.0.1", + "version": "5.0.0", "description": "A project knowledge graph tool built on git-warp", "type": "module", "license": "Apache-2.0", diff --git a/src/cli/commands.js b/src/cli/commands.js index 0fbdccd8..ccaa1a1c 100644 --- a/src/cli/commands.js +++ b/src/cli/commands.js @@ -6,9 +6,9 @@ import { execFileSync } from 'node:child_process'; import { writeFile, chmod, access, constants, readFile } from 'node:fs/promises'; import { join, extname } from 'node:path'; -import { initGraph, loadGraph } from '../graph.js'; -import { createEdge, queryEdges, removeEdge } from '../edges.js'; -import { getNodes, getNode, getNodesByPrefix, setNodeProperty, unsetNodeProperty } from '../nodes.js'; +import { initGraph } from '../graph.js'; +import { createEdge, removeEdge } from '../edges.js'; +import { getNode, setNodeProperty, unsetNodeProperty } from '../nodes.js'; import { computeStatus } from '../status.js'; import { importFile } from '../import.js'; import { importFromMarkdown } from '../frontmatter.js'; @@ -62,11 +62,11 @@ export async function resolveContext(cwd, envelope) { let graph; if (asOf === 'HEAD') { - graph = await loadGraph(cwd, { writerId: 'ctx-reader' }); + graph = await initGraph(cwd, { writerId: 'ctx-reader' }); } else { // Time-travel: resolve git ref → Lamport tick → materialize // Use a separate resolver instance so we can materialize a fresh one. - const resolver = await loadGraph(cwd, { writerId: 'ctx-resolver' }); + const resolver = await initGraph(cwd, { writerId: 'ctx-resolver' }); const result = await getEpochForRef(resolver, cwd, asOf); if (!result) { throw new Error( @@ -76,7 +76,7 @@ export async function resolveContext(cwd, envelope) { } resolvedTick = result.epoch.tick; // materialize({ ceiling }) is destructive — use a dedicated instance - graph = await loadGraph(cwd, { writerId: 'ctx-temporal' }); + graph = await initGraph(cwd, { writerId: 'ctx-temporal' }); await graph.materialize({ ceiling: resolvedTick }); } @@ -139,7 +139,7 @@ export async function link(cwd, source, target, opts = {}) { const tgt = opts.remote ? qualifyNodeId(target, opts.remote) : target; try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); await createEdge(graph, { source: src, target: tgt, type, confidence: opts.confidence }); console.log(success(`${src} --[${type}]--> ${tgt}`)); } catch (err) { @@ -208,8 +208,14 @@ export async function view(cwd, viewSpec, opts = {}) { */ export async function list(cwd, filter = {}) { try { - const graph = await loadGraph(cwd); - const edges = await queryEdges(graph, filter); + const graph = await initGraph(cwd); + const allEdges = await graph.getEdges(); + const edges = allEdges.filter(edge => { + if (filter.source && edge.from !== filter.source) return false; + if (filter.target && edge.to !== filter.target) return false; + if (filter.type && edge.label !== filter.type) return false; + return true; + }); if (edges.length === 0) { console.log(info('No edges found')); @@ -237,7 +243,7 @@ export async function remove(cwd, source, target, opts = {}) { const type = opts.type ?? 'relates-to'; try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); await removeEdge(graph, source, target, type); console.log(success(`Removed: ${source} --[${type}]--> ${target}`)); } catch (err) { @@ -299,7 +305,7 @@ export async function processCommitCmd(cwd, sha) { try { execFileSync('git', ['rev-parse', '--verify', sha], { cwd, encoding: 'utf-8' }); const message = execFileSync('git', ['log', '-1', '--format=%B', sha], { cwd, encoding: 'utf-8' }); - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const directives = await processCommit(graph, { sha, message }); if (directives.length > 0) { @@ -340,9 +346,10 @@ export async function nodes(cwd, opts = {}) { } // List nodes (optionally filtered by prefix) + const allNodes = await graph.getNodes(); const nodeList = opts.prefix - ? await getNodesByPrefix(graph, opts.prefix) - : await getNodes(graph); + ? allNodes.filter(n => n.startsWith(opts.prefix + ':')) + : allNodes; if (opts.json) { outputJson('nodes', { nodes: nodeList, resolvedContext }); @@ -398,7 +405,7 @@ export async function at(cwd, ref, opts = {}) { } try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await getEpochForRef(graph, cwd, ref); if (!result) { @@ -441,7 +448,7 @@ export async function at(cwd, ref, opts = {}) { */ export async function importCmd(cwd, filePath, opts = {}) { try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await importFile(graph, filePath, { dryRun: opts.dryRun }); if (opts.json) { @@ -467,7 +474,7 @@ export async function importCmd(cwd, filePath, opts = {}) { */ export async function importMarkdownCmd(cwd, pattern, opts = {}) { try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await importFromMarkdown(graph, cwd, pattern, { dryRun: opts.dryRun }); if (opts.json) { @@ -534,7 +541,7 @@ export async function mergeCmd(cwd, opts = {}) { } try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await mergeFromRepo(graph, opts.from, { repoName: opts.repoName, dryRun: opts.dryRun, @@ -594,7 +601,7 @@ export async function doctor(cwd, opts = {}) { */ export async function suggest(cwd, opts = {}) { try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await generateSuggestions(cwd, graph, { agent: opts.agent, range: opts.context, @@ -618,7 +625,7 @@ export async function suggest(cwd, opts = {}) { */ export async function review(cwd, opts = {}) { try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); // Batch mode if (opts.batch) { @@ -736,7 +743,7 @@ export async function set(cwd, nodeId, key, value, opts = {}) { } try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await setNodeProperty(graph, nodeId, key, value); if (opts.json) { @@ -769,7 +776,7 @@ export async function unsetCmd(cwd, nodeId, key, opts = {}) { } try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await unsetNodeProperty(graph, nodeId, key); if (opts.json) { @@ -838,7 +845,7 @@ export async function contentSet(cwd, nodeId, filePath, opts = {}) { const buf = await readFile(filePath); const mime = opts.mime ?? MIME_MAP[extname(filePath).toLowerCase()] ?? 'application/octet-stream'; - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await writeContent(graph, nodeId, buf, { mime }); if (opts.json) { @@ -861,7 +868,7 @@ export async function contentSet(cwd, nodeId, filePath, opts = {}) { */ export async function contentShow(cwd, nodeId, opts = {}) { try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const { content, meta } = await readContent(graph, nodeId); if (opts.json) { @@ -890,7 +897,7 @@ export async function contentShow(cwd, nodeId, opts = {}) { */ export async function contentMeta(cwd, nodeId, opts = {}) { try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const meta = await getContentMeta(graph, nodeId); if (!meta) { @@ -921,7 +928,7 @@ export async function contentMeta(cwd, nodeId, opts = {}) { */ export async function contentDelete(cwd, nodeId, opts = {}) { try { - const graph = await loadGraph(cwd); + const graph = await initGraph(cwd); const result = await deleteContent(graph, nodeId); if (opts.json) { diff --git a/src/diff.js b/src/diff.js index 8f4dc2c2..ee10768c 100644 --- a/src/diff.js +++ b/src/diff.js @@ -18,7 +18,7 @@ * endpoints pass the prefix filter**. No partial cross-prefix edges. */ -import { loadGraph } from './graph.js'; +import { initGraph } from './graph.js'; import { getEpochForRef } from './epoch.js'; import { extractPrefix } from './validators.js'; @@ -279,7 +279,7 @@ export function collectDiffPositionals(args) { */ export async function computeDiff(cwd, refA, refB, opts = {}) { // 1. Resolve both refs to epochs using a resolver graph - const resolver = await loadGraph(cwd, { writerId: 'diff-resolver' }); + const resolver = await initGraph(cwd, { writerId: 'diff-resolver' }); const resultA = await getEpochForRef(resolver, cwd, refA); if (!resultA) { @@ -326,12 +326,12 @@ export async function computeDiff(cwd, refA, refB, opts = {}) { // 2. Materialize two separate graph instances const startA = Date.now(); - const graphA = await loadGraph(cwd, { writerId: 'diff-a' }); + const graphA = await initGraph(cwd, { writerId: 'diff-a' }); await graphA.materialize({ ceiling: tickA }); const msA = Date.now() - startA; const startB = Date.now(); - const graphB = await loadGraph(cwd, { writerId: 'diff-b' }); + const graphB = await initGraph(cwd, { writerId: 'diff-b' }); await graphB.materialize({ ceiling: tickB }); const msB = Date.now() - startB; diff --git a/src/edges.js b/src/edges.js index 94d7a462..e86ff5c9 100644 --- a/src/edges.js +++ b/src/edges.js @@ -1,6 +1,6 @@ /** * @module edges - * Edge creation, querying, and removal for git-mind. + * Edge creation and removal for git-mind. */ import { validateEdge } from './validators.js'; @@ -53,31 +53,6 @@ export async function createEdge(graph, { source, target, type, confidence = 1.0 await patch.commit(); } -/** - * @typedef {object} EdgeQuery - * @property {string} [source] - Filter by source node - * @property {string} [target] - Filter by target node - * @property {string} [type] - Filter by edge type - */ - -/** - * Query edges from the graph. - * - * @param {import('@git-stunts/git-warp').default} graph - * @param {EdgeQuery} [filter={}] - * @returns {Promise>} - */ -export async function queryEdges(graph, filter = {}) { - const allEdges = await graph.getEdges(); - - return allEdges.filter(edge => { - if (filter.source && edge.from !== filter.source) return false; - if (filter.target && edge.to !== filter.target) return false; - if (filter.type && edge.label !== filter.type) return false; - return true; - }); -} - /** * Remove an edge from the graph. * diff --git a/src/graph.js b/src/graph.js index a0f61344..43d5c74a 100644 --- a/src/graph.js +++ b/src/graph.js @@ -44,20 +44,11 @@ export async function initGraph(repoPath, opts = {}) { /** * Load an existing git-mind graph from a repository. + * @deprecated Use initGraph — WarpGraph.open is idempotent (init and load are the same call). * @param {string} repoPath - Path to the Git repository * @param {{ writerId?: string }} [opts] * @returns {Promise} */ export async function loadGraph(repoPath, opts = {}) { - // Same operation — WarpGraph.open is idempotent return initGraph(repoPath, opts); } - -/** - * Save (checkpoint) the graph state. - * @param {import('@git-stunts/git-warp').default} graph - * @returns {Promise} checkpoint SHA - */ -export async function saveGraph(graph) { - return graph.createCheckpoint(); -} diff --git a/src/index.js b/src/index.js index 9a910e39..bdff0fc2 100644 --- a/src/index.js +++ b/src/index.js @@ -3,9 +3,9 @@ * Public API for git-mind — a project knowledge graph tool built on git-warp. */ -export { initGraph, loadGraph, saveGraph } from './graph.js'; -export { createEdge, queryEdges, removeEdge, EDGE_TYPES } from './edges.js'; -export { getNodes, hasNode, getNode, getNodesByPrefix, setNodeProperty, unsetNodeProperty } from './nodes.js'; +export { initGraph, loadGraph } from './graph.js'; +export { createEdge, removeEdge, EDGE_TYPES } from './edges.js'; +export { getNode, setNodeProperty, unsetNodeProperty } from './nodes.js'; export { computeStatus } from './status.js'; export { importFile, importData, parseImportFile, validateImportData } from './import.js'; export { importFromMarkdown, parseFrontmatter } from './frontmatter.js'; diff --git a/src/nodes.js b/src/nodes.js index 4713cb5b..bc4180a2 100644 --- a/src/nodes.js +++ b/src/nodes.js @@ -13,27 +13,6 @@ import { extractPrefix, classifyPrefix } from './validators.js'; * @property {Record} properties - Node properties */ -/** - * Get all node IDs from the graph. - * - * @param {import('@git-stunts/git-warp').default} graph - * @returns {Promise} - */ -export async function getNodes(graph) { - return graph.getNodes(); -} - -/** - * Check if a node exists in the graph. - * - * @param {import('@git-stunts/git-warp').default} graph - * @param {string} id - Node ID to check - * @returns {Promise} - */ -export async function hasNode(graph, id) { - return graph.hasNode(id); -} - /** * Get a node by ID with prefix classification and properties. * Returns null if the node doesn't exist. @@ -65,19 +44,6 @@ export async function getNode(graph, id) { }; } -/** - * Get all nodes matching a given prefix. - * - * @param {import('@git-stunts/git-warp').default} graph - * @param {string} prefix - Prefix to filter by (without colon) - * @returns {Promise} - */ -export async function getNodesByPrefix(graph, prefix) { - const nodes = await graph.getNodes(); - const needle = prefix + ':'; - return nodes.filter(n => n.startsWith(needle)); -} - /** * @typedef {object} SetPropertyResult * @property {string} id - Node ID diff --git a/test/edges.test.js b/test/edges.test.js index 308d3e70..5fac1c65 100644 --- a/test/edges.test.js +++ b/test/edges.test.js @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { execSync } from 'node:child_process'; import { initGraph } from '../src/graph.js'; -import { createEdge, queryEdges, removeEdge, EDGE_TYPES } from '../src/edges.js'; +import { createEdge, removeEdge, EDGE_TYPES } from '../src/edges.js'; describe('edges', () => { let tempDir; @@ -35,7 +35,7 @@ describe('edges', () => { type: 'implements', }); - const edges = await queryEdges(graph); + const edges = await graph.getEdges(); expect(edges.length).toBe(1); expect(edges[0].from).toBe('file:src/auth.js'); expect(edges[0].to).toBe('spec:auth-spec'); @@ -51,7 +51,7 @@ describe('edges', () => { rationale: 'test rationale', }); - const edges = await queryEdges(graph); + const edges = await graph.getEdges(); expect(edges[0].props.confidence).toBe(0.7); expect(edges[0].props.rationale).toBe('test rationale'); }); @@ -86,29 +86,29 @@ describe('edges', () => { ).rejects.toThrow(/must be a number/); }); - it('queryEdges filters by source', async () => { + it('getEdges filters by source', async () => { await createEdge(graph, { source: 'task:a', target: 'task:b', type: 'relates-to' }); await createEdge(graph, { source: 'task:c', target: 'task:d', type: 'implements' }); - const filtered = await queryEdges(graph, { source: 'task:a' }); + const filtered = (await graph.getEdges()).filter(e => e.from === 'task:a'); expect(filtered.length).toBe(1); expect(filtered[0].from).toBe('task:a'); }); - it('queryEdges filters by type', async () => { + it('getEdges filters by type', async () => { await createEdge(graph, { source: 'task:a', target: 'task:b', type: 'relates-to' }); await createEdge(graph, { source: 'task:c', target: 'task:d', type: 'implements' }); - const filtered = await queryEdges(graph, { type: 'implements' }); + const filtered = (await graph.getEdges()).filter(e => e.label === 'implements'); expect(filtered.length).toBe(1); expect(filtered[0].label).toBe('implements'); }); it('removeEdge removes an edge', async () => { await createEdge(graph, { source: 'task:a', target: 'task:b', type: 'relates-to' }); - expect((await queryEdges(graph)).length).toBe(1); + expect((await graph.getEdges()).length).toBe(1); await removeEdge(graph, 'task:a', 'task:b', 'relates-to'); - expect((await queryEdges(graph)).length).toBe(0); + expect((await graph.getEdges()).length).toBe(0); }); }); diff --git a/test/graph.test.js b/test/graph.test.js index 22b7fd99..3fe458a2 100644 --- a/test/graph.test.js +++ b/test/graph.test.js @@ -3,7 +3,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { execSync } from 'node:child_process'; -import { initGraph, loadGraph, saveGraph } from '../src/graph.js'; +import { initGraph } from '../src/graph.js'; describe('graph', () => { let tempDir; @@ -24,13 +24,13 @@ describe('graph', () => { expect(typeof graph.getNodes).toBe('function'); }); - it('loadGraph returns a graph (same as init — idempotent)', async () => { + it('initGraph is idempotent — calling twice returns a valid graph', async () => { await initGraph(tempDir); - const graph = await loadGraph(tempDir); + const graph = await initGraph(tempDir); expect(graph).toBeDefined(); }); - it('round-trip: add node, save, reload, verify', async () => { + it('round-trip: add node, reload via initGraph, verify', async () => { const graph = await initGraph(tempDir); const patch = await graph.createPatch(); @@ -39,7 +39,7 @@ describe('graph', () => { await patch.commit(); // Reload in a new instance - const graph2 = await loadGraph(tempDir); + const graph2 = await initGraph(tempDir); const hasNode = await graph2.hasNode('test-node'); expect(hasNode).toBe(true); diff --git a/test/hooks.test.js b/test/hooks.test.js index 9e3a9239..89caf6a1 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -4,7 +4,6 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { execSync } from 'node:child_process'; import { initGraph } from '../src/graph.js'; -import { queryEdges } from '../src/edges.js'; import { parseDirectives, processCommit } from '../src/hooks.js'; describe('hooks', () => { @@ -78,7 +77,7 @@ RELATES-TO: module:session`; expect(directives.length).toBe(1); - const edges = await queryEdges(graph); + const edges = await graph.getEdges(); expect(edges.length).toBe(1); expect(edges[0].from).toBe('commit:abc123def456'); expect(edges[0].to).toBe('spec:auth'); @@ -93,7 +92,7 @@ RELATES-TO: module:session`; }); expect(directives).toEqual([]); - const edges = await queryEdges(graph); + const edges = await graph.getEdges(); expect(edges.length).toBe(0); }); }); diff --git a/test/merge.test.js b/test/merge.test.js index b960842e..d317a3a1 100644 --- a/test/merge.test.js +++ b/test/merge.test.js @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { execSync } from 'node:child_process'; import { initGraph } from '../src/graph.js'; -import { createEdge, queryEdges } from '../src/edges.js'; +import { createEdge } from '../src/edges.js'; import { detectRepoIdentifier, mergeFromRepo } from '../src/merge.js'; describe('merge', () => { @@ -75,7 +75,7 @@ describe('merge', () => { expect(nodes).toContain('repo:other/repo:module:auth'); // Check qualified edges - const edges = await queryEdges(localGraph); + const edges = await localGraph.getEdges(); expect(edges).toHaveLength(1); expect(edges[0].from).toBe('repo:other/repo:spec:auth'); expect(edges[0].to).toBe('repo:other/repo:module:auth'); @@ -104,7 +104,7 @@ describe('merge', () => { expect(nodes).toContain('spec:b'); expect(nodes).toContain('repo:other/repo:crate:core'); - const edges = await queryEdges(localGraph); + const edges = await localGraph.getEdges(); expect(edges).toHaveLength(2); // local + remote }); @@ -119,7 +119,7 @@ describe('merge', () => { await mergeFromRepo(localGraph, remoteDir, { repoName: 'other/repo' }); - const edges = await queryEdges(localGraph); + const edges = await localGraph.getEdges(); expect(edges[0].props.confidence).toBe(0.7); expect(edges[0].props.rationale).toBe('Test rationale'); }); @@ -166,7 +166,7 @@ describe('merge', () => { const qualifiedNodes = nodes.filter(n => n.startsWith('repo:')); expect(qualifiedNodes).toHaveLength(2); - const edges = await queryEdges(localGraph); + const edges = await localGraph.getEdges(); expect(edges).toHaveLength(1); }); diff --git a/test/nodes.test.js b/test/nodes.test.js index dee6d09c..5c7f0dc4 100644 --- a/test/nodes.test.js +++ b/test/nodes.test.js @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'; import { execSync } from 'node:child_process'; import { initGraph } from '../src/graph.js'; import { createEdge } from '../src/edges.js'; -import { getNodes, hasNode, getNode, getNodesByPrefix, setNodeProperty, unsetNodeProperty } from '../src/nodes.js'; +import { getNode, setNodeProperty, unsetNodeProperty } from '../src/nodes.js'; describe('nodes', () => { let tempDir; @@ -21,9 +21,9 @@ describe('nodes', () => { await rm(tempDir, { recursive: true, force: true }); }); - describe('getNodes', () => { + describe('graph.getNodes (native API)', () => { it('returns empty array for a fresh graph', async () => { - const nodes = await getNodes(graph); + const nodes = await graph.getNodes(); expect(nodes).toEqual([]); }); @@ -34,7 +34,7 @@ describe('nodes', () => { type: 'implements', }); - const nodes = await getNodes(graph); + const nodes = await graph.getNodes(); expect(nodes).toContain('file:src/auth.js'); expect(nodes).toContain('spec:auth'); expect(nodes.length).toBe(2); @@ -52,7 +52,7 @@ describe('nodes', () => { type: 'documents', }); - const nodes = await getNodes(graph); + const nodes = await graph.getNodes(); expect(nodes).toContain('file:a.js'); expect(nodes).toContain('spec:auth'); expect(nodes).toContain('doc:readme'); @@ -60,9 +60,9 @@ describe('nodes', () => { }); }); - describe('hasNode', () => { + describe('graph.hasNode (native API)', () => { it('returns false for non-existent node', async () => { - expect(await hasNode(graph, 'task:nonexistent')).toBe(false); + expect(await graph.hasNode('task:nonexistent')).toBe(false); }); it('returns true for node created via edge', async () => { @@ -72,8 +72,8 @@ describe('nodes', () => { type: 'implements', }); - expect(await hasNode(graph, 'task:auth')).toBe(true); - expect(await hasNode(graph, 'spec:auth')).toBe(true); + expect(await graph.hasNode('task:auth')).toBe(true); + expect(await graph.hasNode('spec:auth')).toBe(true); }); }); @@ -123,32 +123,36 @@ describe('nodes', () => { }); }); - describe('getNodesByPrefix', () => { + describe('prefix filtering via graph.getNodes()', () => { beforeEach(async () => { await createEdge(graph, { source: 'task:auth', target: 'spec:auth', type: 'implements' }); await createEdge(graph, { source: 'task:login', target: 'spec:session', type: 'implements' }); await createEdge(graph, { source: 'file:src/auth.js', target: 'spec:auth', type: 'documents' }); }); - it('returns nodes matching the prefix', async () => { - const tasks = await getNodesByPrefix(graph, 'task'); + it('filters nodes by prefix using startsWith', async () => { + const allNodes = await graph.getNodes(); + const tasks = allNodes.filter(n => n.startsWith('task:')); expect(tasks).toContain('task:auth'); expect(tasks).toContain('task:login'); expect(tasks.length).toBe(2); }); it('returns empty array for non-matching prefix', async () => { - const modules = await getNodesByPrefix(graph, 'module'); + const allNodes = await graph.getNodes(); + const modules = allNodes.filter(n => n.startsWith('module:')); expect(modules).toEqual([]); }); it('does not match partial prefixes', async () => { - const results = await getNodesByPrefix(graph, 'tas'); + const allNodes = await graph.getNodes(); + const results = allNodes.filter(n => n.startsWith('tas:')); expect(results).toEqual([]); }); it('returns spec nodes correctly', async () => { - const specs = await getNodesByPrefix(graph, 'spec'); + const allNodes = await graph.getNodes(); + const specs = allNodes.filter(n => n.startsWith('spec:')); expect(specs).toContain('spec:auth'); expect(specs).toContain('spec:session'); expect(specs.length).toBe(2); diff --git a/test/remote.test.js b/test/remote.test.js index 2dd80921..943e8edb 100644 --- a/test/remote.test.js +++ b/test/remote.test.js @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { execSync } from 'node:child_process'; import { initGraph } from '../src/graph.js'; -import { createEdge, queryEdges } from '../src/edges.js'; +import { createEdge } from '../src/edges.js'; import { parseCrossRepoId, buildCrossRepoId, isCrossRepoId, extractRepo, qualifyNodeId, CROSS_REPO_ID_REGEX, @@ -161,7 +161,7 @@ describe('remote', () => { type: 'implements', }); - const edges = await queryEdges(graph); + const edges = await graph.getEdges(); expect(edges).toHaveLength(1); expect(edges[0].to).toBe('repo:neuroglyph/echo:spec:auth'); }); @@ -173,7 +173,7 @@ describe('remote', () => { type: 'depends-on', }); - const edges = await queryEdges(graph, { source: 'repo:neuroglyph/echo:crate:echo-core' }); + const edges = (await graph.getEdges()).filter(e => e.from === 'repo:neuroglyph/echo:crate:echo-core'); expect(edges).toHaveLength(1); expect(edges[0].label).toBe('depends-on'); }); diff --git a/test/resolve-context.test.js b/test/resolve-context.test.js index 02e248af..c731ff01 100644 --- a/test/resolve-context.test.js +++ b/test/resolve-context.test.js @@ -2,7 +2,7 @@ * @module test/resolve-context * Unit tests for the resolveContext() CLI boundary utility. * - * resolveContext() is tested by mocking loadGraph and getEpochForRef so + * resolveContext() is tested by mocking initGraph and getEpochForRef so * no real git repo or CRDT graph is required. */ @@ -11,7 +11,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // ── Mock graph module before importing commands ────────────────────────────── vi.mock('../src/graph.js', () => ({ - loadGraph: vi.fn(), initGraph: vi.fn(), })); @@ -20,7 +19,7 @@ vi.mock('../src/epoch.js', () => ({ })); // Import after mocks are set up -const { loadGraph } = await import('../src/graph.js'); +const { initGraph } = await import('../src/graph.js'); const { getEpochForRef } = await import('../src/epoch.js'); const { resolveContext } = await import('../src/cli/commands.js'); const { DEFAULT_CONTEXT, createContext } = await import('../src/context-envelope.js'); @@ -46,13 +45,13 @@ describe('resolveContext()', () => { }); describe('default context (HEAD, no observer)', () => { - it('returns graph from loadGraph with resolvedTick null', async () => { + it('returns graph from initGraph with resolvedTick null', async () => { const fakeGraph = makeFakeGraph(); - loadGraph.mockResolvedValue(fakeGraph); + initGraph.mockResolvedValue(fakeGraph); const { graph, resolvedContext } = await resolveContext('/repo', DEFAULT_CONTEXT); - expect(loadGraph).toHaveBeenCalledWith('/repo', { writerId: 'ctx-reader' }); + expect(initGraph).toHaveBeenCalledWith('/repo', { writerId: 'ctx-reader' }); expect(graph).toBe(fakeGraph); expect(resolvedContext.asOf).toBe('HEAD'); expect(resolvedContext.resolvedTick).toBeNull(); @@ -62,7 +61,7 @@ describe('resolveContext()', () => { it('does not call materialize for HEAD', async () => { const fakeGraph = makeFakeGraph(); - loadGraph.mockResolvedValue(fakeGraph); + initGraph.mockResolvedValue(fakeGraph); await resolveContext('/repo', DEFAULT_CONTEXT); @@ -71,7 +70,7 @@ describe('resolveContext()', () => { it('does not call observer() for null observer', async () => { const fakeGraph = makeFakeGraph(); - loadGraph.mockResolvedValue(fakeGraph); + initGraph.mockResolvedValue(fakeGraph); await resolveContext('/repo', DEFAULT_CONTEXT); @@ -83,8 +82,8 @@ describe('resolveContext()', () => { it('opens resolver + fresh temporal instance when asOf != HEAD', async () => { const resolver = makeFakeGraph(); const temporal = makeFakeGraph(); - // First loadGraph call → resolver; second → temporal - loadGraph + // First initGraph call → resolver; second → temporal + initGraph .mockResolvedValueOnce(resolver) .mockResolvedValueOnce(temporal); @@ -93,8 +92,8 @@ describe('resolveContext()', () => { const envelope = createContext({ asOf: 'main~5' }); const { graph, resolvedContext } = await resolveContext('/repo', envelope); - expect(loadGraph).toHaveBeenCalledWith('/repo', { writerId: 'ctx-resolver' }); - expect(loadGraph).toHaveBeenCalledWith('/repo', { writerId: 'ctx-temporal' }); + expect(initGraph).toHaveBeenCalledWith('/repo', { writerId: 'ctx-resolver' }); + expect(initGraph).toHaveBeenCalledWith('/repo', { writerId: 'ctx-temporal' }); expect(getEpochForRef).toHaveBeenCalledWith(resolver, '/repo', 'main~5'); expect(temporal.materialize).toHaveBeenCalledWith({ ceiling: 42 }); expect(graph).toBe(temporal); @@ -104,7 +103,7 @@ describe('resolveContext()', () => { it('throws when no epoch found for ref', async () => { const resolver = makeFakeGraph(); - loadGraph.mockResolvedValue(resolver); + initGraph.mockResolvedValue(resolver); getEpochForRef.mockResolvedValue(null); const envelope = createContext({ asOf: 'does-not-exist' }); @@ -123,7 +122,7 @@ describe('resolveContext()', () => { getNodeProps: vi.fn().mockResolvedValue(propsMap), observer: vi.fn().mockResolvedValue(fakeView), }); - loadGraph.mockResolvedValue(fakeGraph); + initGraph.mockResolvedValue(fakeGraph); const envelope = createContext({ observer: 'security-team' }); const { graph, resolvedContext } = await resolveContext('/repo', envelope); @@ -144,7 +143,7 @@ describe('resolveContext()', () => { getNodeProps: vi.fn().mockResolvedValue(propsMap), observer: vi.fn().mockResolvedValue(fakeView), }); - loadGraph.mockResolvedValue(fakeGraph); + initGraph.mockResolvedValue(fakeGraph); const envelope = createContext({ observer: 'minimal' }); await resolveContext('/repo', envelope); @@ -154,7 +153,7 @@ describe('resolveContext()', () => { it('throws when observer node does not exist', async () => { const fakeGraph = makeFakeGraph({ getNodeProps: vi.fn().mockResolvedValue(null) }); - loadGraph.mockResolvedValue(fakeGraph); + initGraph.mockResolvedValue(fakeGraph); const envelope = createContext({ observer: 'missing-profile' }); @@ -167,7 +166,7 @@ describe('resolveContext()', () => { describe('resolvedContext fields', () => { it('passes trustPolicy through to resolvedContext', async () => { const fakeGraph = makeFakeGraph(); - loadGraph.mockResolvedValue(fakeGraph); + initGraph.mockResolvedValue(fakeGraph); const envelope = createContext({ trustPolicy: 'approved-only' }); const { resolvedContext } = await resolveContext('/repo', envelope); @@ -177,7 +176,7 @@ describe('resolveContext()', () => { it('passes extensionLock through to resolvedContext', async () => { const fakeGraph = makeFakeGraph(); - loadGraph.mockResolvedValue(fakeGraph); + initGraph.mockResolvedValue(fakeGraph); const envelope = createContext({ extensionLock: 'deadbeef' }); const { resolvedContext } = await resolveContext('/repo', envelope); diff --git a/test/review.test.js b/test/review.test.js index 87e10548..bdfd5532 100644 --- a/test/review.test.js +++ b/test/review.test.js @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { execSync } from 'node:child_process'; import { initGraph } from '../src/graph.js'; -import { createEdge, queryEdges } from '../src/edges.js'; +import { createEdge } from '../src/edges.js'; import { getPendingSuggestions, acceptSuggestion, @@ -15,6 +15,11 @@ import { batchDecision, } from '../src/review.js'; +/** Filter edges by (from, to, label) — avoids repeating the pattern in every assertion. */ +async function findEdges(graph, from, to, label) { + return (await graph.getEdges()).filter(e => e.from === from && e.to === to && e.label === label); +} + describe('review', () => { let tempDir; let graph; @@ -71,7 +76,7 @@ describe('review', () => { expect(decision.reviewer).toBe('james'); // Check edge was updated - const edges = await queryEdges(graph, { source: 'task:a', target: 'spec:b', type: 'implements' }); + const edges = await findEdges(graph, 'task:a', 'spec:b', 'implements'); expect(edges[0].props.confidence).toBe(1.0); expect(edges[0].props.reviewedAt).toBeTruthy(); @@ -92,7 +97,7 @@ describe('review', () => { expect(decision.action).toBe('reject'); // Edge should be gone - const edges = await queryEdges(graph, { source: 'task:a', target: 'spec:b', type: 'implements' }); + const edges = await findEdges(graph, 'task:a', 'spec:b', 'implements'); expect(edges).toHaveLength(0); // Decision node persists @@ -113,7 +118,7 @@ describe('review', () => { expect(decision.confidence).toBe(0.9); // Check edge was updated - const edges = await queryEdges(graph, { source: 'task:a', target: 'spec:b', type: 'implements' }); + const edges = await findEdges(graph, 'task:a', 'spec:b', 'implements'); expect(edges[0].props.confidence).toBe(0.9); }); @@ -123,7 +128,7 @@ describe('review', () => { const original = { source: 'task:a', target: 'spec:b', type: 'implements', confidence: 0.3 }; await adjustSuggestion(graph, original, { type: 'augments' }); - const edges = await queryEdges(graph, { source: 'task:a', target: 'spec:b', type: 'augments' }); + const edges = await findEdges(graph, 'task:a', 'spec:b', 'augments'); expect(edges).toHaveLength(1); expect(edges[0].props.reviewedAt).toBeTruthy(); }); @@ -139,7 +144,7 @@ describe('review', () => { expect(decision.action).toBe('skip'); // Edge untouched - const edges = await queryEdges(graph, { source: 'task:a', target: 'spec:b', type: 'implements' }); + const edges = await findEdges(graph, 'task:a', 'spec:b', 'implements'); expect(edges[0].props.confidence).toBe(0.3); // No decision node in graph (skip doesn't write) @@ -175,7 +180,7 @@ describe('review', () => { expect(decision.confidence).toBe(0.3); - const edges = await queryEdges(graph, { source: 'task:a', target: 'spec:b', type: 'implements' }); + const edges = await findEdges(graph, 'task:a', 'spec:b', 'implements'); expect(edges[0].props.confidence).toBe(0.3); }); @@ -206,7 +211,7 @@ describe('review', () => { expect(result.decisions[0].action).toBe('accept'); // Confirm edge was promoted - const edges = await queryEdges(graph, { source: 'task:a', target: 'spec:b', type: 'implements' }); + const edges = await findEdges(graph, 'task:a', 'spec:b', 'implements'); expect(edges[0].props.confidence).toBe(1.0); }); });