Add Phase 4 knowledge graph: nodes, cells, facts, edges, gRPC service

Build the knowledge graph pillar with the kg package:
- Node: hierarchical notes with parent/children, C2 wiki-style naming,
  shared tag/category pool with artifacts
- Cell: content units (markdown, code, plain) with ordinal ordering
- Fact: EAV tuples with transaction timestamps and retraction support
- Edge: directed graph links (child, parent, related, artifact_link)

Includes schema migration (002_knowledge_graph.sql), protobuf definitions
(kg.proto), full gRPC KnowledgeGraphService implementation, CLI commands
(node create/get), and comprehensive test coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 10:05:43 -07:00
parent a336dc1ebb
commit 051a85d846
14 changed files with 3283 additions and 6 deletions

1347
proto/exo/v1/kg.pb.go Normal file

File diff suppressed because it is too large Load Diff

145
proto/exo/v1/kg.proto Normal file
View File

@@ -0,0 +1,145 @@
syntax = "proto3";
package exo.v1;
option go_package = "git.wntrmute.dev/kyle/exo/proto/exo/v1;exov1";
import "exo/v1/common.proto";
// Node is an entity in the knowledge graph.
message Node {
string id = 1;
string parent_id = 2;
string name = 3;
string type = 4; // "note" or "artifact_link"
string created = 5;
string modified = 6;
repeated string children = 7;
repeated string tags = 8;
repeated string categories = 9;
}
// Cell is a content unit within a note.
message Cell {
string id = 1;
string node_id = 2;
string type = 3; // "markdown", "code", "plain"
bytes contents = 4;
int32 ordinal = 5;
string created = 6;
string modified = 7;
}
// Fact records an EAV relationship with transactional history.
message Fact {
string id = 1;
string entity_id = 2;
string entity_name = 3;
string attribute_id = 4;
string attribute_name = 5;
Value value = 6;
int64 tx_timestamp = 7;
bool retraction = 8;
}
// Edge links nodes to other nodes or artifacts.
message Edge {
string id = 1;
string source_id = 2;
string target_id = 3;
string relation = 4;
string created = 5;
}
// --- Service messages ---
message CreateNodeRequest {
string name = 1;
string type = 2;
string parent_id = 3;
repeated string tags = 4;
repeated string categories = 5;
}
message CreateNodeResponse {
string id = 1;
}
message GetNodeRequest {
string id = 1;
}
message GetNodeResponse {
Node node = 1;
repeated Cell cells = 2;
}
message AddCellRequest {
string node_id = 1;
string type = 2;
bytes contents = 3;
int32 ordinal = 4;
}
message AddCellResponse {
string id = 1;
}
message RecordFactRequest {
string entity_id = 1;
string entity_name = 2;
string attribute_id = 3;
string attribute_name = 4;
Value value = 5;
bool retraction = 6;
}
message RecordFactResponse {
string id = 1;
}
message GetFactsRequest {
string entity_id = 1;
bool current_only = 2;
}
message GetFactsResponse {
repeated Fact facts = 1;
}
message AddEdgeRequest {
string source_id = 1;
string target_id = 2;
string relation = 3;
}
message AddEdgeResponse {
string id = 1;
}
message GetEdgesRequest {
string node_id = 1;
string direction = 2; // "from", "to", or "both"
}
message GetEdgesResponse {
repeated Edge edges = 1;
}
// KnowledgeGraphService provides operations for the knowledge graph pillar.
service KnowledgeGraphService {
// Nodes
rpc CreateNode(CreateNodeRequest) returns (CreateNodeResponse);
rpc GetNode(GetNodeRequest) returns (GetNodeResponse);
// Cells
rpc AddCell(AddCellRequest) returns (AddCellResponse);
// Facts
rpc RecordFact(RecordFactRequest) returns (RecordFactResponse);
rpc GetFacts(GetFactsRequest) returns (GetFactsResponse);
// Edges
rpc AddEdge(AddEdgeRequest) returns (AddEdgeResponse);
rpc GetEdges(GetEdgesRequest) returns (GetEdgesResponse);
}

361
proto/exo/v1/kg_grpc.pb.go Normal file
View File

@@ -0,0 +1,361 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: exo/v1/kg.proto
package exov1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
KnowledgeGraphService_CreateNode_FullMethodName = "/exo.v1.KnowledgeGraphService/CreateNode"
KnowledgeGraphService_GetNode_FullMethodName = "/exo.v1.KnowledgeGraphService/GetNode"
KnowledgeGraphService_AddCell_FullMethodName = "/exo.v1.KnowledgeGraphService/AddCell"
KnowledgeGraphService_RecordFact_FullMethodName = "/exo.v1.KnowledgeGraphService/RecordFact"
KnowledgeGraphService_GetFacts_FullMethodName = "/exo.v1.KnowledgeGraphService/GetFacts"
KnowledgeGraphService_AddEdge_FullMethodName = "/exo.v1.KnowledgeGraphService/AddEdge"
KnowledgeGraphService_GetEdges_FullMethodName = "/exo.v1.KnowledgeGraphService/GetEdges"
)
// KnowledgeGraphServiceClient is the client API for KnowledgeGraphService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// KnowledgeGraphService provides operations for the knowledge graph pillar.
type KnowledgeGraphServiceClient interface {
// Nodes
CreateNode(ctx context.Context, in *CreateNodeRequest, opts ...grpc.CallOption) (*CreateNodeResponse, error)
GetNode(ctx context.Context, in *GetNodeRequest, opts ...grpc.CallOption) (*GetNodeResponse, error)
// Cells
AddCell(ctx context.Context, in *AddCellRequest, opts ...grpc.CallOption) (*AddCellResponse, error)
// Facts
RecordFact(ctx context.Context, in *RecordFactRequest, opts ...grpc.CallOption) (*RecordFactResponse, error)
GetFacts(ctx context.Context, in *GetFactsRequest, opts ...grpc.CallOption) (*GetFactsResponse, error)
// Edges
AddEdge(ctx context.Context, in *AddEdgeRequest, opts ...grpc.CallOption) (*AddEdgeResponse, error)
GetEdges(ctx context.Context, in *GetEdgesRequest, opts ...grpc.CallOption) (*GetEdgesResponse, error)
}
type knowledgeGraphServiceClient struct {
cc grpc.ClientConnInterface
}
func NewKnowledgeGraphServiceClient(cc grpc.ClientConnInterface) KnowledgeGraphServiceClient {
return &knowledgeGraphServiceClient{cc}
}
func (c *knowledgeGraphServiceClient) CreateNode(ctx context.Context, in *CreateNodeRequest, opts ...grpc.CallOption) (*CreateNodeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateNodeResponse)
err := c.cc.Invoke(ctx, KnowledgeGraphService_CreateNode_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *knowledgeGraphServiceClient) GetNode(ctx context.Context, in *GetNodeRequest, opts ...grpc.CallOption) (*GetNodeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetNodeResponse)
err := c.cc.Invoke(ctx, KnowledgeGraphService_GetNode_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *knowledgeGraphServiceClient) AddCell(ctx context.Context, in *AddCellRequest, opts ...grpc.CallOption) (*AddCellResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AddCellResponse)
err := c.cc.Invoke(ctx, KnowledgeGraphService_AddCell_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *knowledgeGraphServiceClient) RecordFact(ctx context.Context, in *RecordFactRequest, opts ...grpc.CallOption) (*RecordFactResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RecordFactResponse)
err := c.cc.Invoke(ctx, KnowledgeGraphService_RecordFact_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *knowledgeGraphServiceClient) GetFacts(ctx context.Context, in *GetFactsRequest, opts ...grpc.CallOption) (*GetFactsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetFactsResponse)
err := c.cc.Invoke(ctx, KnowledgeGraphService_GetFacts_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *knowledgeGraphServiceClient) AddEdge(ctx context.Context, in *AddEdgeRequest, opts ...grpc.CallOption) (*AddEdgeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AddEdgeResponse)
err := c.cc.Invoke(ctx, KnowledgeGraphService_AddEdge_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *knowledgeGraphServiceClient) GetEdges(ctx context.Context, in *GetEdgesRequest, opts ...grpc.CallOption) (*GetEdgesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetEdgesResponse)
err := c.cc.Invoke(ctx, KnowledgeGraphService_GetEdges_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// KnowledgeGraphServiceServer is the server API for KnowledgeGraphService service.
// All implementations must embed UnimplementedKnowledgeGraphServiceServer
// for forward compatibility.
//
// KnowledgeGraphService provides operations for the knowledge graph pillar.
type KnowledgeGraphServiceServer interface {
// Nodes
CreateNode(context.Context, *CreateNodeRequest) (*CreateNodeResponse, error)
GetNode(context.Context, *GetNodeRequest) (*GetNodeResponse, error)
// Cells
AddCell(context.Context, *AddCellRequest) (*AddCellResponse, error)
// Facts
RecordFact(context.Context, *RecordFactRequest) (*RecordFactResponse, error)
GetFacts(context.Context, *GetFactsRequest) (*GetFactsResponse, error)
// Edges
AddEdge(context.Context, *AddEdgeRequest) (*AddEdgeResponse, error)
GetEdges(context.Context, *GetEdgesRequest) (*GetEdgesResponse, error)
mustEmbedUnimplementedKnowledgeGraphServiceServer()
}
// UnimplementedKnowledgeGraphServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedKnowledgeGraphServiceServer struct{}
func (UnimplementedKnowledgeGraphServiceServer) CreateNode(context.Context, *CreateNodeRequest) (*CreateNodeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateNode not implemented")
}
func (UnimplementedKnowledgeGraphServiceServer) GetNode(context.Context, *GetNodeRequest) (*GetNodeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetNode not implemented")
}
func (UnimplementedKnowledgeGraphServiceServer) AddCell(context.Context, *AddCellRequest) (*AddCellResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AddCell not implemented")
}
func (UnimplementedKnowledgeGraphServiceServer) RecordFact(context.Context, *RecordFactRequest) (*RecordFactResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RecordFact not implemented")
}
func (UnimplementedKnowledgeGraphServiceServer) GetFacts(context.Context, *GetFactsRequest) (*GetFactsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetFacts not implemented")
}
func (UnimplementedKnowledgeGraphServiceServer) AddEdge(context.Context, *AddEdgeRequest) (*AddEdgeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AddEdge not implemented")
}
func (UnimplementedKnowledgeGraphServiceServer) GetEdges(context.Context, *GetEdgesRequest) (*GetEdgesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetEdges not implemented")
}
func (UnimplementedKnowledgeGraphServiceServer) mustEmbedUnimplementedKnowledgeGraphServiceServer() {}
func (UnimplementedKnowledgeGraphServiceServer) testEmbeddedByValue() {}
// UnsafeKnowledgeGraphServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to KnowledgeGraphServiceServer will
// result in compilation errors.
type UnsafeKnowledgeGraphServiceServer interface {
mustEmbedUnimplementedKnowledgeGraphServiceServer()
}
func RegisterKnowledgeGraphServiceServer(s grpc.ServiceRegistrar, srv KnowledgeGraphServiceServer) {
// If the following call panics, it indicates UnimplementedKnowledgeGraphServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&KnowledgeGraphService_ServiceDesc, srv)
}
func _KnowledgeGraphService_CreateNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateNodeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KnowledgeGraphServiceServer).CreateNode(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: KnowledgeGraphService_CreateNode_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KnowledgeGraphServiceServer).CreateNode(ctx, req.(*CreateNodeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KnowledgeGraphService_GetNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetNodeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KnowledgeGraphServiceServer).GetNode(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: KnowledgeGraphService_GetNode_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KnowledgeGraphServiceServer).GetNode(ctx, req.(*GetNodeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KnowledgeGraphService_AddCell_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AddCellRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KnowledgeGraphServiceServer).AddCell(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: KnowledgeGraphService_AddCell_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KnowledgeGraphServiceServer).AddCell(ctx, req.(*AddCellRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KnowledgeGraphService_RecordFact_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RecordFactRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KnowledgeGraphServiceServer).RecordFact(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: KnowledgeGraphService_RecordFact_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KnowledgeGraphServiceServer).RecordFact(ctx, req.(*RecordFactRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KnowledgeGraphService_GetFacts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetFactsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KnowledgeGraphServiceServer).GetFacts(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: KnowledgeGraphService_GetFacts_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KnowledgeGraphServiceServer).GetFacts(ctx, req.(*GetFactsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KnowledgeGraphService_AddEdge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AddEdgeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KnowledgeGraphServiceServer).AddEdge(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: KnowledgeGraphService_AddEdge_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KnowledgeGraphServiceServer).AddEdge(ctx, req.(*AddEdgeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _KnowledgeGraphService_GetEdges_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetEdgesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KnowledgeGraphServiceServer).GetEdges(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: KnowledgeGraphService_GetEdges_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KnowledgeGraphServiceServer).GetEdges(ctx, req.(*GetEdgesRequest))
}
return interceptor(ctx, in, info, handler)
}
// KnowledgeGraphService_ServiceDesc is the grpc.ServiceDesc for KnowledgeGraphService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var KnowledgeGraphService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "exo.v1.KnowledgeGraphService",
HandlerType: (*KnowledgeGraphServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreateNode",
Handler: _KnowledgeGraphService_CreateNode_Handler,
},
{
MethodName: "GetNode",
Handler: _KnowledgeGraphService_GetNode_Handler,
},
{
MethodName: "AddCell",
Handler: _KnowledgeGraphService_AddCell_Handler,
},
{
MethodName: "RecordFact",
Handler: _KnowledgeGraphService_RecordFact_Handler,
},
{
MethodName: "GetFacts",
Handler: _KnowledgeGraphService_GetFacts_Handler,
},
{
MethodName: "AddEdge",
Handler: _KnowledgeGraphService_AddEdge_Handler,
},
{
MethodName: "GetEdges",
Handler: _KnowledgeGraphService_GetEdges_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "exo/v1/kg.proto",
}