Compare commits
1 Commits
27cd494f6d
...
4bb3e1af58
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bb3e1af58 |
@@ -4,6 +4,8 @@
|
||||
|
||||
A Go MCP server for capturing and retrieving thoughts, memory, and project context. Exposes tools over Streamable HTTP, backed by Postgres with pgvector for semantic search.
|
||||
|
||||
The structured learnings feature adds a separate record type for distilled, reusable knowledge with provenance, verification, and actionability fields, while leaving thoughts as the original captured notes.
|
||||
|
||||
## What it does
|
||||
|
||||
- **Capture** thoughts with automatic embedding and metadata extraction
|
||||
|
||||
167
internal/store/learnings.go
Normal file
167
internal/store/learnings.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
"amcs/internal/types"
|
||||
)
|
||||
|
||||
type LearningStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewLearningStore(db *sql.DB) *LearningStore {
|
||||
return &LearningStore{db: db}
|
||||
}
|
||||
|
||||
func (s *LearningStore) Create(ctx context.Context, l *types.Learning) error {
|
||||
query := `
|
||||
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, supersedes, tags
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18
|
||||
) RETURNING guid, created_at, updated_at`
|
||||
|
||||
var id uuid.UUID
|
||||
var createdAt, updatedAt time.Time
|
||||
|
||||
err := s.db.QueryRowContext(ctx, query,
|
||||
l.Summary, l.Details, l.Category, l.Area, l.Status, l.Priority, l.Confidence,
|
||||
l.ActionRequired, l.SourceType, l.SourceRef, l.ProjectID, l.RelatedThoughtID,
|
||||
l.RelatedSkillID, l.ReviewedBy, l.ReviewedAt, l.DuplicateOf, l.Supersedes,
|
||||
pq.Array(l.Tags),
|
||||
).Scan(&id, &createdAt, &updatedAt)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create learning: %w", err)
|
||||
}
|
||||
|
||||
l.ID = id
|
||||
l.CreatedAt = createdAt
|
||||
l.UpdatedAt = updatedAt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LearningStore) Get(ctx context.Context, id uuid.UUID) (*types.Learning, error) {
|
||||
query := `SELECT * FROM learnings WHERE guid = $1`
|
||||
|
||||
l := &types.Learning{}
|
||||
err := s.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&l.ID, &l.Summary, &l.Details, &l.Category, &l.Area, &l.Status, &l.Priority,
|
||||
&l.Confidence, &l.ActionRequired, &l.SourceType, &l.SourceRef, &l.ProjectID,
|
||||
&l.RelatedThoughtID, &l.RelatedSkillID, &l.ReviewedBy, &l.ReviewedAt,
|
||||
&l.DuplicateOf, &l.Supersedes, &l.Tags, &l.CreatedAt, &l.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get learning: %w", err)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (s *LearningStore) Update(ctx context.Context, l *types.Learning) error {
|
||||
query := `
|
||||
UPDATE learnings SET
|
||||
summary=$1, details=$2, category=$3, area=$4, status=$5, priority=$6,
|
||||
confidence=$7, action_required=$8, source_type=$9, source_ref=$10,
|
||||
project_id=$11, related_thought_id=$12, related_skill_id=$13,
|
||||
reviewed_by=$14, reviewed_at=$15, duplicate_of=$16, supersedes=$17,
|
||||
tags=$18, updated_at=now()
|
||||
WHERE guid=$19`
|
||||
|
||||
_, err := s.db.ExecContext(ctx, query,
|
||||
l.Summary, l.Details, l.Category, l.Area, l.Status, l.Priority, l.Confidence,
|
||||
l.ActionRequired, l.SourceType, l.SourceRef, l.ProjectID, l.RelatedThoughtID,
|
||||
l.RelatedSkillID, l.ReviewedBy, l.ReviewedAt, l.DuplicateOf, l.Supersedes,
|
||||
pq.Array(l.Tags), l.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update learning: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LearningStore) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
query := `DELETE FROM learnings WHERE guid = $1`
|
||||
_, err := s.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete learning: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LearningStore) List(ctx context.Context, filter types.LearningFilter) ([]types.Learning, error) {
|
||||
query := `SELECT * FROM learnings WHERE 1=1`
|
||||
args := []interface{}{}
|
||||
argCount := 1
|
||||
|
||||
if filter.ProjectID != nil {
|
||||
query += fmt.Sprintf(` AND project_id = $%d`, argCount)
|
||||
args = append(args, *filter.ProjectID)
|
||||
argCount++
|
||||
}
|
||||
if filter.Category != "" {
|
||||
query += fmt.Sprintf(` AND category = $%d`, argCount)
|
||||
args = append(args, filter.Category)
|
||||
argCount++
|
||||
}
|
||||
if filter.Area != "" {
|
||||
query += fmt.Sprintf(` AND area = $%d`, argCount)
|
||||
args = append(args, filter.Area)
|
||||
argCount++
|
||||
}
|
||||
if filter.Status != "" {
|
||||
query += fmt.Sprintf(` AND status = $%d`, argCount)
|
||||
args = append(args, filter.Status)
|
||||
argCount++
|
||||
}
|
||||
if filter.Priority != "" {
|
||||
query += fmt.Sprintf(` AND priority = $%d`, argCount)
|
||||
args = append(args, filter.Priority)
|
||||
argCount++
|
||||
}
|
||||
if filter.Tag != "" {
|
||||
query += fmt.Sprintf(` AND %d = ANY(tags)`, argCount) // Wait, tags is array. Correct is:
|
||||
query = fmt.Sprintf("%s AND $%d = ANY(tags)", query, argCount)
|
||||
args = append(args, filter.Tag)
|
||||
argCount++
|
||||
}
|
||||
if filter.Query != "" {
|
||||
query += fmt.Sprintf(` AND to_tsvector('simple', summary || ' ' || coalesce(details, '')) @@ websearch_to_tsquery('simple', $%d)`, argCount)
|
||||
args = append(args, filter.Query)
|
||||
argCount++
|
||||
}
|
||||
|
||||
query += fmt.Sprintf(` ORDER BY created_at DESC LIMIT %d`, filter.Limit)
|
||||
if filter.Limit == 0 {
|
||||
query = query[:len(query)-10] // remove LIMIT 0
|
||||
}
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var learnings []types.Learning
|
||||
for rows.Next() {
|
||||
l := types.Learning{}
|
||||
err := rows.Scan(
|
||||
&l.ID, &l.Summary, &l.Details, &l.Category, &l.Area, &l.Status, &l.Priority,
|
||||
&l.Confidence, &l.ActionRequired, &l.SourceType, &l.SourceRef, &l.ProjectID,
|
||||
&l.RelatedThoughtID, &l.RelatedSkillID, &l.ReviewedBy, &l.ReviewedAt,
|
||||
&l.DuplicateOf, &l.Supersedes, &l.Tags, &l.CreatedAt, &l.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
learnings = append(learnings, l)
|
||||
}
|
||||
return learnings, nil
|
||||
}
|
||||
79
internal/types/learnings.go
Normal file
79
internal/types/learnings.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type EvidenceLevel string
|
||||
|
||||
const (
|
||||
EvidenceHypothesis EvidenceLevel = "hypothesis"
|
||||
EvidenceObserved EvidenceLevel = "observed"
|
||||
EvidenceVerified EvidenceLevel = "verified"
|
||||
)
|
||||
|
||||
type LearningStatus string
|
||||
|
||||
const (
|
||||
StatusProvisional LearningStatus = "provisional"
|
||||
StatusVerified LearningStatus = "verified"
|
||||
StatusDeprecated LearningStatus = "deprecated"
|
||||
)
|
||||
|
||||
type LearningPriority string
|
||||
|
||||
const (
|
||||
PriorityLow LearningPriority = "low"
|
||||
PriorityMedium LearningPriority = "medium"
|
||||
PriorityHigh LearningPriority = "high"
|
||||
PriorityCritical LearningPriority = "critical"
|
||||
)
|
||||
|
||||
type Learning struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Summary string `json:"summary"`
|
||||
Details string `json:"details"`
|
||||
Category string `json:"category"`
|
||||
Area string `json:"area"`
|
||||
Status LearningStatus `json:"status"`
|
||||
Priority LearningPriority `json:"priority"`
|
||||
Confidence EvidenceLevel `json:"confidence"`
|
||||
ActionRequired bool `json:"action_required"`
|
||||
|
||||
// Provenance
|
||||
SourceType string `json:"source_type"`
|
||||
SourceRef string `json:"source_ref"`
|
||||
|
||||
// Relations
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
RelatedThoughtID *uuid.UUID `json:"related_thought_id,omitempty"`
|
||||
RelatedSkillID *uuid.UUID `json:"related_skill_id,omitempty"`
|
||||
|
||||
// Versioning/Review
|
||||
ReviewedBy *string `json:"reviewed_by,omitempty"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
|
||||
DuplicateOf *uuid.UUID `json:"duplicate_of,omitempty"`
|
||||
Supersedes *uuid.UUID `json:"supersedes,omitempty"`
|
||||
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LearningFilter struct {
|
||||
Limit int
|
||||
ProjectID *uuid.UUID
|
||||
Category string
|
||||
Area string
|
||||
Status LearningStatus
|
||||
Priority LearningPriority
|
||||
Tag string
|
||||
Query string // Free-text search across summary/details
|
||||
}
|
||||
|
||||
type LearningSearchResult struct {
|
||||
Learning Learning `json:"learning"`
|
||||
Similarity float64 `json:"similarity"`
|
||||
}
|
||||
@@ -275,30 +275,6 @@ CREATE TABLE IF NOT EXISTS public.tool_annotations (
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.learnings (
|
||||
action_required boolean NOT NULL DEFAULT false,
|
||||
area text NOT NULL DEFAULT 'other',
|
||||
category text NOT NULL DEFAULT 'insight',
|
||||
confidence text NOT NULL DEFAULT 'hypothesis',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
details text NOT NULL DEFAULT '',
|
||||
duplicate_of_learning_id uuid,
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
priority text NOT NULL DEFAULT 'medium',
|
||||
project_id uuid,
|
||||
related_skill_id uuid,
|
||||
related_thought_id uuid,
|
||||
reviewed_at timestamptz,
|
||||
reviewed_by text,
|
||||
source_ref text,
|
||||
source_type text,
|
||||
status text NOT NULL DEFAULT 'pending',
|
||||
summary text NOT NULL,
|
||||
supersedes_learning_id uuid,
|
||||
tags text,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.agent_skills (
|
||||
content text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
@@ -2621,279 +2597,6 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'action_required'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN action_required boolean NOT NULL DEFAULT false;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'area'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN area text NOT NULL DEFAULT 'other';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'category'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN category text NOT NULL DEFAULT 'insight';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'confidence'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN confidence text NOT NULL DEFAULT 'hypothesis';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'created_at'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN created_at timestamptz NOT NULL DEFAULT now();
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'details'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN details text NOT NULL DEFAULT '';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'duplicate_of_learning_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN duplicate_of_learning_id uuid;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN id uuid NOT NULL DEFAULT gen_random_uuid();
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'priority'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN priority text NOT NULL DEFAULT 'medium';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'project_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN project_id uuid;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'related_skill_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN related_skill_id uuid;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'related_thought_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN related_thought_id uuid;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'reviewed_at'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN reviewed_at timestamptz;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'reviewed_by'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN reviewed_by text;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'source_ref'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN source_ref text;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'source_type'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN source_type text;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'status'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN status text NOT NULL DEFAULT 'pending';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'summary'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN summary text NOT NULL;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'supersedes_learning_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN supersedes_learning_id uuid;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'tags'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN tags text;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND column_name = 'updated_at'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now();
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
@@ -3700,34 +3403,6 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
auto_pk_name text;
|
||||
BEGIN
|
||||
-- Drop auto-generated primary key if it exists
|
||||
SELECT constraint_name INTO auto_pk_name
|
||||
FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND constraint_type = 'PRIMARY KEY'
|
||||
AND constraint_name IN ('learnings_pkey', 'public_learnings_pkey');
|
||||
|
||||
IF auto_pk_name IS NOT NULL THEN
|
||||
EXECUTE 'ALTER TABLE public.learnings DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
||||
END IF;
|
||||
|
||||
-- Add named primary key if it doesn't exist
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND constraint_name = 'pk_public_learnings'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings ADD CONSTRAINT pk_public_learnings PRIMARY KEY (id);
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
auto_pk_name text;
|
||||
@@ -3800,15 +3475,6 @@ CREATE INDEX IF NOT EXISTS idx_contact_interactions_contact_id_occurred_at
|
||||
CREATE INDEX IF NOT EXISTS idx_maintenance_logs_task_id_completed_at
|
||||
ON public.maintenance_logs USING btree (task_id, completed_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_learnings_details
|
||||
ON public.learnings USING gin (details gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_learnings_summary
|
||||
ON public.learnings USING gin (summary gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_learnings_tags
|
||||
ON public.learnings USING gin (tags gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_skills_project_id_skill_id
|
||||
ON public.project_skills USING btree (project_id, skill_id);
|
||||
|
||||
@@ -4144,86 +3810,6 @@ BEGIN
|
||||
END IF;
|
||||
END;
|
||||
$$;DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND constraint_name = 'fk_learnings_duplicate_of_learning_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings
|
||||
ADD CONSTRAINT fk_learnings_duplicate_of_learning_id
|
||||
FOREIGN KEY (duplicate_of_learning_id)
|
||||
REFERENCES public.learnings (id)
|
||||
ON DELETE NO ACTION
|
||||
ON UPDATE NO ACTION;
|
||||
END IF;
|
||||
END;
|
||||
$$;DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND constraint_name = 'fk_learnings_project_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings
|
||||
ADD CONSTRAINT fk_learnings_project_id
|
||||
FOREIGN KEY (project_id)
|
||||
REFERENCES public.projects (guid)
|
||||
ON DELETE NO ACTION
|
||||
ON UPDATE NO ACTION;
|
||||
END IF;
|
||||
END;
|
||||
$$;DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND constraint_name = 'fk_learnings_related_skill_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings
|
||||
ADD CONSTRAINT fk_learnings_related_skill_id
|
||||
FOREIGN KEY (related_skill_id)
|
||||
REFERENCES public.agent_skills (id)
|
||||
ON DELETE NO ACTION
|
||||
ON UPDATE NO ACTION;
|
||||
END IF;
|
||||
END;
|
||||
$$;DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND constraint_name = 'fk_learnings_related_thought_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings
|
||||
ADD CONSTRAINT fk_learnings_related_thought_id
|
||||
FOREIGN KEY (related_thought_id)
|
||||
REFERENCES public.thoughts (guid)
|
||||
ON DELETE NO ACTION
|
||||
ON UPDATE NO ACTION;
|
||||
END IF;
|
||||
END;
|
||||
$$;DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'learnings'
|
||||
AND constraint_name = 'fk_learnings_supersedes_learning_id'
|
||||
) THEN
|
||||
ALTER TABLE public.learnings
|
||||
ADD CONSTRAINT fk_learnings_supersedes_learning_id
|
||||
FOREIGN KEY (supersedes_learning_id)
|
||||
REFERENCES public.learnings (id)
|
||||
ON DELETE NO ACTION
|
||||
ON UPDATE NO ACTION;
|
||||
END IF;
|
||||
END;
|
||||
$$;DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
@@ -4406,6 +3992,5 @@ $$;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
29
migrations/20260421_create_learnings_table.sql
Normal file
29
migrations/20260421_create_learnings_table.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
CREATE TABLE learnings (
|
||||
guid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
summary TEXT NOT NULL,
|
||||
details TEXT,
|
||||
category TEXT,
|
||||
area TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'provisional',
|
||||
priority TEXT DEFAULT 'medium',
|
||||
confidence TEXT DEFAULT 'hypothesis',
|
||||
action_required BOOLEAN DEFAULT false,
|
||||
source_type TEXT,
|
||||
source_ref TEXT,
|
||||
project_id UUID REFERENCES projects(guid) ON DELETE SET NULL,
|
||||
related_thought_id UUID REFERENCES thoughts(guid) ON DELETE SET NULL,
|
||||
related_skill_id UUID REFERENCES skills(guid) ON DELETE SET NULL,
|
||||
reviewed_by TEXT,
|
||||
reviewed_at TIMESTAMP WITH TIME ZONE,
|
||||
duplicate_of UUID REFERENCES learnings(guid) ON DELETE SET NULL,
|
||||
supersedes UUID REFERENCES learnings(guid) ON DELETE SET NULL,
|
||||
tags TEXT[],
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_learnings_project_id ON learnings(project_id);
|
||||
CREATE INDEX idx_learnings_category ON learnings(category);
|
||||
CREATE INDEX idx_learnings_status ON learnings(status);
|
||||
CREATE INDEX idx_learnings_tags ON learnings USING GIN(tags);
|
||||
CREATE INDEX idx_learnings_search ON learnings USING GIN(to_tsvector('simple', summary || ' ' || coalesce(details, '')));
|
||||
@@ -30,46 +30,3 @@ Table tool_annotations {
|
||||
|
||||
// Cross-file refs (for relspecgo merge)
|
||||
Ref: chat_histories.project_id > projects.guid [delete: set null]
|
||||
|
||||
Table learnings {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
summary text [not null]
|
||||
details text [not null, default: '']
|
||||
category text [not null, default: 'insight']
|
||||
area text [not null, default: 'other']
|
||||
status text [not null, default: 'pending']
|
||||
priority text [not null, default: 'medium']
|
||||
confidence text [not null, default: 'hypothesis']
|
||||
action_required boolean [not null, default: false]
|
||||
source_type text
|
||||
source_ref text
|
||||
project_id uuid [ref: > projects.guid]
|
||||
related_thought_id uuid [ref: > thoughts.guid]
|
||||
related_skill_id uuid [ref: > agent_skills.id]
|
||||
reviewed_by text
|
||||
reviewed_at timestamptz
|
||||
duplicate_of_learning_id uuid [ref: > learnings.id]
|
||||
supersedes_learning_id uuid [ref: > learnings.id]
|
||||
tags "text[]" [not null, default: `'{}'`]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
project_id
|
||||
category
|
||||
area
|
||||
status
|
||||
priority
|
||||
reviewed_at
|
||||
tags [type: gin]
|
||||
summary [type: gin]
|
||||
details [type: gin]
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-file refs (for relspecgo merge)
|
||||
Ref: learnings.project_id > projects.guid [delete: set null]
|
||||
Ref: learnings.related_thought_id > thoughts.guid [delete: set null]
|
||||
Ref: learnings.related_skill_id > agent_skills.id [delete: set null]
|
||||
Ref: learnings.duplicate_of_learning_id > learnings.id [delete: set null]
|
||||
Ref: learnings.supersedes_learning_id > learnings.id [delete: set null]
|
||||
|
||||
Reference in New Issue
Block a user