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:
109
kg/cell.go
Normal file
109
kg/cell.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package kg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/exo/core"
|
||||
"git.wntrmute.dev/kyle/exo/db"
|
||||
)
|
||||
|
||||
// CellType distinguishes content types within a note.
|
||||
type CellType string
|
||||
|
||||
const (
|
||||
CellTypeMarkdown CellType = "markdown"
|
||||
CellTypeCode CellType = "code"
|
||||
CellTypePlain CellType = "plain"
|
||||
)
|
||||
|
||||
// Cell is a content unit within a note. A note is composed of
|
||||
// multiple cells of different types.
|
||||
type Cell struct {
|
||||
ID string
|
||||
NodeID string
|
||||
Type CellType
|
||||
Contents []byte
|
||||
Ordinal int
|
||||
Created time.Time
|
||||
Modified time.Time
|
||||
}
|
||||
|
||||
// NewCell creates a Cell with a new UUID and current timestamps.
|
||||
func NewCell(nodeID string, cellType CellType, contents []byte) *Cell {
|
||||
now := time.Now().UTC()
|
||||
return &Cell{
|
||||
ID: core.NewUUID(),
|
||||
NodeID: nodeID,
|
||||
Type: cellType,
|
||||
Contents: contents,
|
||||
Created: now,
|
||||
Modified: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Store persists a Cell to the database.
|
||||
func (c *Cell) Store(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO cells (id, node_id, type, contents, ordinal, created, modified) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
c.ID, c.NodeID, string(c.Type), c.Contents, c.Ordinal, db.ToDBTime(c.Created), db.ToDBTime(c.Modified))
|
||||
if err != nil {
|
||||
return fmt.Errorf("kg: failed to store cell: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a Cell by its ID.
|
||||
func (c *Cell) Get(ctx context.Context, tx *sql.Tx) error {
|
||||
if c.ID == "" {
|
||||
return fmt.Errorf("kg: cell missing ID: %w", core.ErrNoID)
|
||||
}
|
||||
|
||||
var cellType, created, modified string
|
||||
row := tx.QueryRowContext(ctx,
|
||||
`SELECT node_id, type, contents, ordinal, created, modified FROM cells WHERE id=?`, c.ID)
|
||||
if err := row.Scan(&c.NodeID, &cellType, &c.Contents, &c.Ordinal, &created, &modified); err != nil {
|
||||
return fmt.Errorf("kg: failed to retrieve cell: %w", err)
|
||||
}
|
||||
c.Type = CellType(cellType)
|
||||
|
||||
var err error
|
||||
c.Created, err = db.FromDBTime(created, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Modified, err = db.FromDBTime(modified, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCellsForNode retrieves all cells for a node, ordered by ordinal.
|
||||
func GetCellsForNode(ctx context.Context, tx *sql.Tx, nodeID string) ([]*Cell, error) {
|
||||
rows, err := tx.QueryContext(ctx,
|
||||
`SELECT id, type, contents, ordinal, created, modified FROM cells WHERE node_id=? ORDER BY ordinal`, nodeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kg: failed to retrieve cells for node: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var cells []*Cell
|
||||
for rows.Next() {
|
||||
c := &Cell{NodeID: nodeID}
|
||||
var cellType, created, modified string
|
||||
if err := rows.Scan(&c.ID, &cellType, &c.Contents, &c.Ordinal, &created, &modified); err != nil {
|
||||
return nil, fmt.Errorf("kg: failed to scan cell: %w", err)
|
||||
}
|
||||
c.Type = CellType(cellType)
|
||||
c.Created, err = db.FromDBTime(created, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Modified, err = db.FromDBTime(modified, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cells = append(cells, c)
|
||||
}
|
||||
return cells, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user