feat(graph,mcp): annotate_nodes — write external metadata back onto graph nodes#162
feat(graph,mcp): annotate_nodes — write external metadata back onto graph nodes#162avfirsov wants to merge 1 commit into
Conversation
…nodes Add a sanctioned, additive write-back path so a downstream tool (an LLM, an analysis pass, an editor extension) can enrich existing graph nodes with its own metadata — without re-indexing and without racing the sharded in-memory store. - graph.Store.MergeNodeMeta(id, kv) (changed, found): the only sanctioned way for a Store holder to mutate Node.Meta. Additive + idempotent (deep-equal delta via metaDelta), shard-locked, and never touches structural fields (id/kind/name/path/lines). Implemented for both the in-memory and SQLite backends and covered by a shared store-conformance case so they behave identically. - mcp annotate_nodes tool (also served at POST /v1/tools/annotate_nodes through the shared registry): merges a free-form per-node `meta` map under a caller-chosen `namespace` (default "ext") so annotations can never shadow indexer-owned keys, and optionally adds idempotent semantically_related edges between node pairs. Tests: pure metaDelta unit, in-memory + SQLite conformance, and MCP handler round-trip / idempotency / namespace / edge / bad-input plus a registration guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XcQY8fFFyDQidwVC8dKHWh
|
@avfirsov thank you for submitting an interesting feature. Overall, it looks good to me, excluding only one aspect: What about adding an additional attribute to the table and persisting user input metadata into an additional JSON column, and loading it when it is necessary (not on every node extraction)? Second point, which may not be expected from the user POV - an in-session reindex of a changed file evicts and re-parses that file's nodes — which drops both the merged ext_* Meta and the semantically_related edges, on the SQLite backend too (reindex INSERT OR REPLACE overwrites Meta). So annotations are lost on any edit to the annotated file, not just on restart. Also, Minor DRY: the SQLite path re-implements the metaDelta loop inline instead of reusing the pure helper. It's forced today (the helper is unexported in package graph); exporting graph.MetaDelta and calling it would remove the duplication the PR itself flags in a comment. |
What
Adds a sanctioned, additive write-back path for external metadata so a downstream tool — an LLM enrichment pass, an analysis stage, an editor extension — can attach its own metadata to existing graph nodes without re-indexing and without racing the sharded in-memory store.
Today there is no safe way for a caller holding a
graph.Store(rather than a concrete*Graph) to mutateNode.Meta: the shard lock the in-memory backend takes is private, so reaching intoGetNode(id).Metadirectly races the sharded map and panics on a live daemon. This PR closes that gap.How
graph.Store.MergeNodeMeta(id, kv) (changed, found)— the only sanctionedNode.Metamutation path for aStoreholder. Additive and idempotent (deep-equal delta via the puremetaDeltahelper), taken under the node's shard write lock, and it never touches structural fields (id / kind / name / path / lines). Implemented for both the in-memory and SQLite backends and covered by a shared store-conformance case so the two behave identically.annotate_nodesMCP tool (also served atPOST /v1/tools/annotate_nodesthrough the shared registry — no separate HTTP code): merges a free-form per-nodemetamap under a caller-chosennamespace(defaultext). Every key is stored as<namespace>_<key>, so an annotation can never shadow an indexer-owned Meta key. Optionally adds idempotentsemantically_relatededges between node pairs. Returns{annotated, unchanged, not_found, edges_added}.The merge is deliberately scoped: never deletes keys, never mutates structural data, non-fatal per item (an unknown id is recorded in
not_found, the batch continues).Tests
metaDeltaunit (delta/idempotency/nil-handling).MergeNodeMeta): additive merge, deep-equal idempotency, found semantics for an unknown id, lazyMetainit, structural fields untouched.namespaceprefixing (incl. no double-prefix),semantically_relatededge add + dedup, default score, bad input, and a registration guard.All green;
go build ./...,go vet, andgofmtclean on the changed files.Notes / scope
🤖 Generated with Claude Code