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:
104
kg/edge.go
Normal file
104
kg/edge.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package kg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/exo/core"
|
||||
"git.wntrmute.dev/kyle/exo/db"
|
||||
)
|
||||
|
||||
// EdgeRelation describes the type of relationship between two nodes.
|
||||
type EdgeRelation string
|
||||
|
||||
const (
|
||||
EdgeRelationChild EdgeRelation = "child"
|
||||
EdgeRelationParent EdgeRelation = "parent"
|
||||
EdgeRelationRelated EdgeRelation = "related"
|
||||
EdgeRelationArtifactLink EdgeRelation = "artifact_link"
|
||||
)
|
||||
|
||||
// Edge links a source node to a target node or artifact.
|
||||
type Edge struct {
|
||||
ID string
|
||||
SourceID string
|
||||
TargetID string
|
||||
Relation EdgeRelation
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
// NewEdge creates an Edge with a new UUID and current timestamp.
|
||||
func NewEdge(sourceID, targetID string, relation EdgeRelation) *Edge {
|
||||
return &Edge{
|
||||
ID: core.NewUUID(),
|
||||
SourceID: sourceID,
|
||||
TargetID: targetID,
|
||||
Relation: relation,
|
||||
Created: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// Store persists an Edge to the database.
|
||||
func (e *Edge) Store(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO edges (id, source_id, target_id, relation, created) VALUES (?, ?, ?, ?, ?)`,
|
||||
e.ID, e.SourceID, e.TargetID, string(e.Relation), db.ToDBTime(e.Created))
|
||||
if err != nil {
|
||||
return fmt.Errorf("kg: failed to store edge: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEdgesFrom retrieves all edges originating from a given node.
|
||||
func GetEdgesFrom(ctx context.Context, tx *sql.Tx, sourceID string) ([]*Edge, error) {
|
||||
rows, err := tx.QueryContext(ctx,
|
||||
`SELECT id, target_id, relation, created FROM edges WHERE source_id=?`, sourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kg: failed to retrieve edges: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var edges []*Edge
|
||||
for rows.Next() {
|
||||
e := &Edge{SourceID: sourceID}
|
||||
var relation, created string
|
||||
if err := rows.Scan(&e.ID, &e.TargetID, &relation, &created); err != nil {
|
||||
return nil, fmt.Errorf("kg: failed to scan edge: %w", err)
|
||||
}
|
||||
e.Relation = EdgeRelation(relation)
|
||||
e.Created, err = db.FromDBTime(created, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
edges = append(edges, e)
|
||||
}
|
||||
return edges, rows.Err()
|
||||
}
|
||||
|
||||
// GetEdgesTo retrieves all edges pointing to a given node/artifact.
|
||||
func GetEdgesTo(ctx context.Context, tx *sql.Tx, targetID string) ([]*Edge, error) {
|
||||
rows, err := tx.QueryContext(ctx,
|
||||
`SELECT id, source_id, relation, created FROM edges WHERE target_id=?`, targetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kg: failed to retrieve incoming edges: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var edges []*Edge
|
||||
for rows.Next() {
|
||||
e := &Edge{TargetID: targetID}
|
||||
var relation, created string
|
||||
if err := rows.Scan(&e.ID, &e.SourceID, &relation, &created); err != nil {
|
||||
return nil, fmt.Errorf("kg: failed to scan edge: %w", err)
|
||||
}
|
||||
e.Relation = EdgeRelation(relation)
|
||||
e.Created, err = db.FromDBTime(created, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
edges = append(edges, e)
|
||||
}
|
||||
return edges, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user