Files
amcs/internal/store/learnings.go
sgcommand 3e832eea98
Some checks failed
CI / build-and-test (push) Failing after -32m34s
CI / build-and-test (pull_request) Failing after -32m27s
feat(learnings): add store and MCP tool layer
2026-04-22 14:00:12 +02:00

216 lines
6.0 KiB
Go

package store
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
)
func (db *DB) CreateLearning(ctx context.Context, learning thoughttypes.Learning) (thoughttypes.Learning, error) {
row := db.pool.QueryRow(ctx, `
insert into learnings (
summary, details, category, area, status, priority, confidence,
action_required, source_type, source_ref, project_id, related_thought_id,
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
supersedes_learning_id, tags
) values (
$1, $2, $3, $4, $5, $6, $7,
$8, $9, $10, $11, $12,
$13, $14, $15, $16,
$17, $18
)
returning id, created_at, updated_at
`,
strings.TrimSpace(learning.Summary),
strings.TrimSpace(learning.Details),
strings.TrimSpace(learning.Category),
strings.TrimSpace(learning.Area),
string(learning.Status),
string(learning.Priority),
string(learning.Confidence),
learning.ActionRequired,
nullableText(learning.SourceType),
nullableText(learning.SourceRef),
learning.ProjectID,
learning.RelatedThoughtID,
learning.RelatedSkillID,
nullableTextPtr(learning.ReviewedBy),
learning.ReviewedAt,
learning.DuplicateOfLearningID,
learning.SupersedesLearningID,
learning.Tags,
)
created := learning
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
return thoughttypes.Learning{}, fmt.Errorf("create learning: %w", err)
}
return created, nil
}
func (db *DB) GetLearning(ctx context.Context, id uuid.UUID) (thoughttypes.Learning, error) {
row := db.pool.QueryRow(ctx, `
select id, summary, details, category, area, status, priority, confidence,
action_required, source_type, source_ref, project_id, related_thought_id,
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
supersedes_learning_id, tags, created_at, updated_at
from learnings
where id = $1
`, id)
learning, err := scanLearning(row)
if err != nil {
if err == pgx.ErrNoRows {
return thoughttypes.Learning{}, fmt.Errorf("learning not found: %s", id)
}
return thoughttypes.Learning{}, fmt.Errorf("get learning: %w", err)
}
return learning, nil
}
func (db *DB) ListLearnings(ctx context.Context, filter thoughttypes.LearningFilter) ([]thoughttypes.Learning, error) {
args := make([]any, 0, 8)
conditions := make([]string, 0, 8)
if filter.ProjectID != nil {
args = append(args, *filter.ProjectID)
conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args)))
}
if value := strings.TrimSpace(filter.Category); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("category = $%d", len(args)))
}
if value := strings.TrimSpace(filter.Area); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("area = $%d", len(args)))
}
if value := strings.TrimSpace(filter.Status); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("status = $%d", len(args)))
}
if value := strings.TrimSpace(filter.Priority); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("priority = $%d", len(args)))
}
if value := strings.TrimSpace(filter.Tag); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args)))
}
if value := strings.TrimSpace(filter.Query); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("to_tsvector('simple', summary || ' ' || coalesce(details, '')) @@ websearch_to_tsquery('simple', $%d)", len(args)))
}
query := `
select id, summary, details, category, area, status, priority, confidence,
action_required, source_type, source_ref, project_id, related_thought_id,
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
supersedes_learning_id, tags, created_at, updated_at
from learnings
`
if len(conditions) > 0 {
query += " where " + strings.Join(conditions, " and ")
}
query += " order by updated_at desc"
if filter.Limit > 0 {
args = append(args, filter.Limit)
query += fmt.Sprintf(" limit $%d", len(args))
}
rows, err := db.pool.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("list learnings: %w", err)
}
defer rows.Close()
items := make([]thoughttypes.Learning, 0)
for rows.Next() {
item, err := scanLearning(rows)
if err != nil {
return nil, fmt.Errorf("scan learning: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate learnings: %w", err)
}
return items, nil
}
type learningScanner interface {
Scan(dest ...any) error
}
func scanLearning(row learningScanner) (thoughttypes.Learning, error) {
var learning thoughttypes.Learning
var sourceType pgtype.Text
var sourceRef pgtype.Text
var reviewedBy pgtype.Text
var tags []string
err := row.Scan(
&learning.ID,
&learning.Summary,
&learning.Details,
&learning.Category,
&learning.Area,
&learning.Status,
&learning.Priority,
&learning.Confidence,
&learning.ActionRequired,
&sourceType,
&sourceRef,
&learning.ProjectID,
&learning.RelatedThoughtID,
&learning.RelatedSkillID,
&reviewedBy,
&learning.ReviewedAt,
&learning.DuplicateOfLearningID,
&learning.SupersedesLearningID,
&tags,
&learning.CreatedAt,
&learning.UpdatedAt,
)
if err != nil {
return thoughttypes.Learning{}, err
}
learning.SourceType = sourceType.String
learning.SourceRef = sourceRef.String
if reviewedBy.Valid {
value := reviewedBy.String
learning.ReviewedBy = &value
}
if tags == nil {
learning.Tags = []string{}
} else {
learning.Tags = tags
}
return learning, nil
}
func nullableText(value string) *string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
return &trimmed
}
func nullableTextPtr(value *string) *string {
if value == nil {
return nil
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return nil
}
return &trimmed
}