From ddc15bbf6d08ce47fb703ba416a57548ab77d771 Mon Sep 17 00:00:00 2001 From: Smit Bafna Date: Fri, 3 Jul 2026 11:04:49 +0530 Subject: [PATCH] add-graph-object-lookup-and-related-memory-cli-commands --- apps/cli/main.ts | 70 +++++++++++++++ libs/knowledge-graph/service.ts | 17 +++- libs/storage/sqlite/repository.ts | 144 ++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 1 deletion(-) diff --git a/apps/cli/main.ts b/apps/cli/main.ts index a74bdeb..f6bdb44 100644 --- a/apps/cli/main.ts +++ b/apps/cli/main.ts @@ -6,6 +6,7 @@ import { basename, dirname, join, resolve } from "node:path"; import { tmpdir } from "node:os"; import { createLocalKnowledgeGraphService, KnowledgeGraphService } from "../../libs/knowledge-graph/service.js"; import type { ClaimAnchorAuditResult, RepoRef } from "../../libs/knowledge-graph/service.js"; +import type { GraphObjectType } from "../../libs/knowledge-graph/schema.js"; import { envVarSource, loadRepoEnv, type LoadedRepoEnv } from "../../libs/env/load-local-env.js"; import { ensureGreplicaConfig, @@ -89,6 +90,20 @@ const cliCommands = [ handler: runGraphReadCommand, showInTopLevelHelp: true, }, + { + key: "graphGet", + path: ["graph", "get"], + usage: "graph get ", + handler: runGraphGetCommand, + showInTopLevelHelp: true, + }, + { + key: "graphTraverse", + path: ["graph", "traverse"], + usage: "graph traverse [--depth ]", + handler: runGraphTraverseCommand, + showInTopLevelHelp: true, + }, { key: "graphContext", path: ["graph", "context"], @@ -258,6 +273,33 @@ function runGraphReadCommand(_args: string[]): void { printSection("Edges", graph.edges, (item) => `${field(item, "from_type")}:${field(item, "from_id")} -[${field(item, "kind")}]-> ${field(item, "to_type")}:${field(item, "to_id")}`); } +function runGraphGetCommand(args: string[]): void { + const type = requireGraphObjectType(args[0], usage("graphGet")); + const id = requireFile(args[1], usage("graphGet")); + const { repo, service } = createCommandContext(); + const obj = service.lookupObject(repo, type, id); + if (obj === undefined) { + console.log(`No ${type} found with id '${id}'`); + process.exitCode = 1; + return; + } + console.log(JSON.stringify(obj, null, 2)); +} + +function runGraphTraverseCommand(args: string[]): void { + const type = requireGraphObjectType(args[0], usage("graphTraverse")); + const id = requireFile(args[1], usage("graphTraverse")); + const depth = parseGraphTraverseDepth(args); + const { repo, service } = createCommandContext(); + const graph = service.traverseGraph(repo, type, id, depth); + console.log(`Traversal from ${type}:${id} (depth=${depth}):`); + printSection("Components", graph.components, (item) => `${named(item)} ${anchor(item)}`.trim()); + printSection("Flows", graph.flows, named); + printSection("Claims", graph.claims, (item) => `${field(item, "kind")}: ${field(item, "text")}`); + printSection("Sources", graph.sources, (item) => `${field(item, "kind")}: ${field(item, "title") || field(item, "ref")}`); + printSection("Edges", graph.edges, (item) => `${field(item, "from_type")}:${field(item, "from_id")} -[${field(item, "kind")}]-> ${field(item, "to_type")}:${field(item, "to_id")}`); +} + async function runGraphContextCommand(args: string[]): Promise { const output = parseGraphContextOutput(args); const query = args.filter((arg) => arg !== "--debug").join(" ").trim(); @@ -853,6 +895,34 @@ function requireFile(file: string | undefined, usage: string): string { return file; } +function requireGraphObjectType(value: string | undefined, usage: string): GraphObjectType { + const validTypes = ["component", "flow", "claim", "edge", "source"] as const; + if (value === undefined || !validTypes.includes(value as GraphObjectType)) { + throw new Error(`Invalid graph object type '${value}'. Expected one of: ${validTypes.join(", ")}.\n${usage}`); + } + return value as GraphObjectType; +} + +function parseGraphTraverseDepth(args: string[]): number { + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--depth") { + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) throw new Error(`Missing value for --depth.\n${usage("graphTraverse")}`); + const depth = Number.parseInt(value, 10); + if (Number.isNaN(depth) || depth < 1) throw new Error(`Invalid --depth value '${value}'; expected a positive integer.\n${usage("graphTraverse")}`); + return depth; + } + if (arg.startsWith("--depth=")) { + const value = arg.slice("--depth=".length); + const depth = Number.parseInt(value, 10); + if (Number.isNaN(depth) || depth < 1) throw new Error(`Invalid --depth value '${value}'; expected a positive integer.\n${usage("graphTraverse")}`); + return depth; + } + } + return 1; // default depth +} + function parseRequiredOption(args: string[], name: string, usage: string): string { for (let index = 0; index < args.length; index += 1) { const arg = args[index]; diff --git a/libs/knowledge-graph/service.ts b/libs/knowledge-graph/service.ts index efb6e65..af42926 100644 --- a/libs/knowledge-graph/service.ts +++ b/libs/knowledge-graph/service.ts @@ -2,7 +2,7 @@ import { normalizeProposal } from "./proposal.js"; import { validateProposal, type ProposalValidationResult } from "./validate-proposal.js"; import type { Claim } from "./claim.js"; import type { Edge } from "./edge.js"; -import type { Component, Flow, Source } from "./schema.js"; +import type { Component, Flow, GraphObjectType, Source } from "./schema.js"; import { GraphContextBuilder } from "./graph-context/context-builder.js"; import { graphContextConfig, type GraphContextConfig } from "./graph-context/config.js"; import type { EmbeddingStatus, GraphContextResult } from "./graph-context/types.js"; @@ -178,6 +178,21 @@ export class KnowledgeGraphService { }; } + lookupObject(input: RepoRef, type: GraphObjectType, id: string): Component | Flow | Claim | Source | Edge | undefined { + this.requireRepo(input); + return this.repository.getObjectById(type, id); + } + + traverseGraph( + input: RepoRef, + type: GraphObjectType, + id: string, + maxDepth: number = 1, + ): GraphReadResult { + const initialized = this.requireRepo(input); + return this.repository.getRelatedObjects(initialized.repo_id, type, id, maxDepth); + } + } function anchorAuditErrors(result: ClaimAnchorAuditResult): string[] { diff --git a/libs/storage/sqlite/repository.ts b/libs/storage/sqlite/repository.ts index de8f4f4..d532609 100644 --- a/libs/storage/sqlite/repository.ts +++ b/libs/storage/sqlite/repository.ts @@ -442,6 +442,150 @@ export class SqliteRepository { })); } + getObjectById(type: GraphObjectType, id: string): Component | Flow | Claim | Source | Edge | undefined { + const table = tableForType(type); + const row = this.db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(id) as Record | undefined; + if (!row) return undefined; + + switch (type) { + case "component": { + const comp = row as ComponentRow; + return { id: comp.id, name: comp.name, code_anchor: comp.code_anchor ?? undefined } as Component; + } + case "flow": + return row as Flow; + case "claim": { + const claim = row as ClaimRow; + return { + id: claim.id, + kind: claim.kind, + text: claim.text, + truth: claim.truth, + intent: claim.intent, + code_anchors: claim.code_anchors === null ? undefined : JSON.parse(claim.code_anchors) as Claim["code_anchors"], + } as Claim; + } + case "source": + return row as Source; + case "edge": { + const edge = row as EdgeRow; + return { + ...edge, + metadata: edge.metadata === null ? undefined : JSON.parse(edge.metadata) as Record, + } as Edge; + } + } + } + + getEdgesForObject(type: GraphObjectType, id: string): Edge[] { + const rows = this.db + .prepare( + `SELECT * FROM edges WHERE (from_type = ? AND from_id = ?) OR (to_type = ? AND to_id = ?)`, + ) + .all(type, id, type, id) as EdgeRow[]; + return rows.map((row) => ({ + ...row, + metadata: row.metadata === null ? undefined : (JSON.parse(row.metadata) as Record), + })); + } + + getRelatedObjects( + repoId: string, + seedType: GraphObjectType, + seedId: string, + maxDepth: number, + ): { + components: Component[]; + flows: Flow[]; + claims: Claim[]; + sources: Source[]; + edges: Edge[]; + } { + const scopeIds = this.currentScopeIds(repoId); + const memberships = this.membershipsForScopes(scopeIds); + const allEdges = this.loadEdges(selectIds(memberships, "edge")); + const active = activeSubjectKeys(memberships, allEdges); + + // Build index of edges by connected object for efficient traversal + const edgesByObject = new Map(); + for (const edge of allEdges) { + if (!active.has(subjectKey("edge", edge.id))) continue; + const fromKey = subjectKey(edge.from_type, edge.from_id); + const toKey = subjectKey(edge.to_type, edge.to_id); + if (!edgesByObject.has(fromKey)) edgesByObject.set(fromKey, []); + if (!edgesByObject.has(toKey)) edgesByObject.set(toKey, []); + edgesByObject.get(fromKey)!.push(edge); + edgesByObject.get(toKey)!.push(edge); + } + + // BFS traversal from seed object + const visited = new Set(); + const collectedEdges = new Map(); + const collectedTypes = new Map(); + + const queue: Array<{ type: GraphObjectType; id: string; depth: number }> = [ + { type: seedType, id: seedId, depth: 0 }, + ]; + visited.add(subjectKey(seedType, seedId)); + + while (queue.length > 0) { + const current = queue.shift()!; + if (current.depth >= maxDepth) continue; + + const edges = this.getEdgesForObject(current.type, current.id); + for (const edge of edges) { + if (!active.has(subjectKey("edge", edge.id))) continue; + if (!collectedEdges.has(edge.id)) { + collectedEdges.set(edge.id, edge); + } + + // Determine neighbor + const neighborType = edge.from_type === current.type && edge.from_id === current.id + ? edge.to_type + : edge.from_type; + const neighborId = edge.from_type === current.type && edge.from_id === current.id + ? edge.to_id + : edge.from_id; + const neighborKey = subjectKey(neighborType, neighborId); + + if (!visited.has(neighborKey) && neighborType !== "source") { + visited.add(neighborKey); + collectedTypes.set(neighborKey, neighborType); + queue.push({ type: neighborType, id: neighborId, depth: current.depth + 1 }); + } + } + } + + // Collect all objects by type + const componentIds: string[] = []; + const flowIds: string[] = []; + const claimIds: string[] = []; + const sourceIds: string[] = []; + + for (const [key, objType] of collectedTypes) { + const id = key.slice(key.indexOf(":") + 1); + switch (objType) { + case "component": componentIds.push(id); break; + case "flow": flowIds.push(id); break; + case "claim": claimIds.push(id); break; + case "source": sourceIds.push(id); break; + } + } + + // Also include sources referenced by collected edges + for (const edge of collectedEdges.values()) { + if (edge.to_type === "source") sourceIds.push(edge.to_id); + } + + return { + components: this.loadComponents(componentIds), + flows: this.loadFlows(flowIds), + claims: this.loadClaims(claimIds), + sources: this.loadSources([...new Set(sourceIds)]), + edges: [...collectedEdges.values()], + }; + } + private loadByIds(table: string, ids: string[]): T[] { if (ids.length === 0) return []; return this.db.prepare(`SELECT * FROM ${table} WHERE id IN (${placeholders(ids)})`).all(...ids) as T[];