From 81a3470407ad0cdfaa9e06c3224536b369040e21 Mon Sep 17 00:00:00 2001 From: Hein Puth Date: Tue, 14 Jul 2026 15:18:57 +0200 Subject: [PATCH] feat(tools): link thoughts and learnings --- .gitignore | 1 + internal/app/app.go | 45 +++--- internal/mcpserver/server.go | 50 +++++- .../mcpserver/streamable_integration_test.go | 51 +++--- internal/store/thought_learning_links.go | 148 +++++++++++++++++ internal/tools/thought_learning_links.go | 152 ++++++++++++++++++ internal/types/project.go | 19 +++ migrations/030_thought_learning_links.sql | 26 +++ schema/core.dbml | 14 ++ 9 files changed, 451 insertions(+), 55 deletions(-) create mode 100644 internal/store/thought_learning_links.go create mode 100644 internal/tools/thought_learning_links.go create mode 100644 migrations/030_thought_learning_links.sql diff --git a/.gitignore b/.gitignore index 1492131..e3dc00c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ ui/.svelte-kit/ internal/app/ui/dist/* !internal/app/ui/dist/placeholder.txt .codex +.worktrees/ diff --git a/internal/app/app.go b/internal/app/app.go index 480118f..8a65214 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -194,28 +194,29 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger) toolSet := mcpserver.ToolSet{ - Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool), - Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects), - List: tools.NewListTool(db, cfg.Search, activeProjects), - Stats: tools.NewStatsTool(db), - Get: tools.NewGetTool(db), - Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger), - Delete: tools.NewDeleteTool(db), - Archive: tools.NewArchiveTool(db), - Projects: tools.NewProjectsTool(db, activeProjects), - Version: tools.NewVersionTool(cfg.MCP.ServerName, info), - Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search), - Plans: tools.NewPlansTool(db, activeProjects, cfg.Search), - ProjectPersonas: tools.NewProjectPersonasTool(db, activeProjects), - WorldModel: tools.NewWorldModelTool(db, activeProjects), - Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects), - Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects), - Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects), - Links: tools.NewLinksTool(db, embeddings, cfg.Search), - Files: filesTool, - Backfill: backfillTool, - Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger), - RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer), + Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool), + Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects), + List: tools.NewListTool(db, cfg.Search, activeProjects), + Stats: tools.NewStatsTool(db), + Get: tools.NewGetTool(db), + Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger), + Delete: tools.NewDeleteTool(db), + Archive: tools.NewArchiveTool(db), + Projects: tools.NewProjectsTool(db, activeProjects), + Version: tools.NewVersionTool(cfg.MCP.ServerName, info), + Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search), + Plans: tools.NewPlansTool(db, activeProjects, cfg.Search), + ProjectPersonas: tools.NewProjectPersonasTool(db, activeProjects), + WorldModel: tools.NewWorldModelTool(db, activeProjects), + ThoughtLearningLinks: tools.NewThoughtLearningLinksTool(db), + Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects), + Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects), + Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects), + Links: tools.NewLinksTool(db, embeddings, cfg.Search), + Files: filesTool, + Backfill: backfillTool, + Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger), + RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer), //Maintenance: tools.NewMaintenanceTool(db), Skills: tools.NewSkillsTool(db, activeProjects), Personas: tools.NewAgentPersonasTool(db), diff --git a/internal/mcpserver/server.go b/internal/mcpserver/server.go index ad62aa4..f3a127e 100644 --- a/internal/mcpserver/server.go +++ b/internal/mcpserver/server.go @@ -38,14 +38,15 @@ type ToolSet struct { Reparse *tools.ReparseMetadataTool RetryMetadata *tools.RetryEnrichmentTool //Maintenance *tools.MaintenanceTool - Skills *tools.SkillsTool - Personas *tools.AgentPersonasTool - ChatHistory *tools.ChatHistoryTool - Describe *tools.DescribeTool - Learnings *tools.LearningsTool - Plans *tools.PlansTool - ProjectPersonas *tools.ProjectPersonasTool - WorldModel *tools.WorldModelTool + Skills *tools.SkillsTool + Personas *tools.AgentPersonasTool + ChatHistory *tools.ChatHistoryTool + Describe *tools.DescribeTool + Learnings *tools.LearningsTool + Plans *tools.PlansTool + ProjectPersonas *tools.ProjectPersonasTool + WorldModel *tools.WorldModelTool + ThoughtLearningLinks *tools.ThoughtLearningLinksTool } // Handlers groups the HTTP handlers produced for an MCP server instance. @@ -91,6 +92,7 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS registerProjectTools, registerWorldModelTools, registerLearningTools, + registerThoughtLearningLinkTools, registerPlanTools, registerFileTools, registerMaintenanceTools, @@ -308,6 +310,34 @@ func registerLearningTools(server *mcp.Server, logger *slog.Logger, toolSet Tool return nil } +func registerThoughtLearningLinkTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error { + if err := addTool(server, logger, &mcp.Tool{ + Name: "link_thought_learning", + Description: "Create or update an explicit association between a raw thought and a curated learning.", + }, toolSet.ThoughtLearningLinks.Link); err != nil { + return err + } + if err := addTool(server, logger, &mcp.Tool{ + Name: "unlink_thought_learning", + Description: "Remove an explicit association between a thought and a learning.", + }, toolSet.ThoughtLearningLinks.Unlink); err != nil { + return err + } + if err := addTool(server, logger, &mcp.Tool{ + Name: "get_thought_learnings", + Description: "List curated learnings explicitly linked to a thought.", + }, toolSet.ThoughtLearningLinks.GetThoughtLearnings); err != nil { + return err + } + if err := addTool(server, logger, &mcp.Tool{ + Name: "get_learning_thoughts", + Description: "List raw thoughts explicitly linked to a learning.", + }, toolSet.ThoughtLearningLinks.GetLearningThoughts); err != nil { + return err + } + return nil +} + func registerPlanTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error { if err := addTool(server, logger, &mcp.Tool{ Name: "create_plan", @@ -755,6 +785,10 @@ func BuildToolCatalog() []tools.ToolEntry { {Name: "add_learning", Description: "Create a curated learning record distinct from raw thoughts.", Category: "projects"}, {Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"}, {Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"}, + {Name: "link_thought_learning", Description: "Create or update a lightweight explicit association between a raw thought and a curated learning.", Category: "projects"}, + {Name: "unlink_thought_learning", Description: "Remove a lightweight explicit association between a thought and a learning.", Category: "projects"}, + {Name: "get_thought_learnings", Description: "List curated learnings explicitly associated with a thought.", Category: "projects"}, + {Name: "get_learning_thoughts", Description: "List raw source thoughts explicitly associated with a learning.", Category: "projects"}, // plans {Name: "create_plan", Description: "Create a structured plan with status, priority, owner, due date, and optional project link.", Category: "plans"}, diff --git a/internal/mcpserver/streamable_integration_test.go b/internal/mcpserver/streamable_integration_test.go index e616ecf..a4868ea 100644 --- a/internal/mcpserver/streamable_integration_test.go +++ b/internal/mcpserver/streamable_integration_test.go @@ -189,30 +189,31 @@ func TestStreamableHTTPReturnsStructuredToolErrors(t *testing.T) { func streamableTestToolSet() ToolSet { return ToolSet{ - Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}), - Capture: new(tools.CaptureTool), - Search: new(tools.SearchTool), - List: new(tools.ListTool), - Stats: new(tools.StatsTool), - Get: new(tools.GetTool), - Update: new(tools.UpdateTool), - Delete: new(tools.DeleteTool), - Archive: new(tools.ArchiveTool), - Projects: new(tools.ProjectsTool), - Context: new(tools.ContextTool), - Recall: new(tools.RecallTool), - Summarize: new(tools.SummarizeTool), - Links: new(tools.LinksTool), - Files: new(tools.FilesTool), - Backfill: new(tools.BackfillTool), - Reparse: new(tools.ReparseMetadataTool), - RetryMetadata: new(tools.RetryEnrichmentTool), - Skills: new(tools.SkillsTool), - ChatHistory: new(tools.ChatHistoryTool), - Describe: new(tools.DescribeTool), - Learnings: new(tools.LearningsTool), - Plans: new(tools.PlansTool), - ProjectPersonas: new(tools.ProjectPersonasTool), - WorldModel: new(tools.WorldModelTool), + Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}), + Capture: new(tools.CaptureTool), + Search: new(tools.SearchTool), + List: new(tools.ListTool), + Stats: new(tools.StatsTool), + Get: new(tools.GetTool), + Update: new(tools.UpdateTool), + Delete: new(tools.DeleteTool), + Archive: new(tools.ArchiveTool), + Projects: new(tools.ProjectsTool), + Context: new(tools.ContextTool), + Recall: new(tools.RecallTool), + Summarize: new(tools.SummarizeTool), + Links: new(tools.LinksTool), + Files: new(tools.FilesTool), + Backfill: new(tools.BackfillTool), + Reparse: new(tools.ReparseMetadataTool), + RetryMetadata: new(tools.RetryEnrichmentTool), + Skills: new(tools.SkillsTool), + ChatHistory: new(tools.ChatHistoryTool), + Describe: new(tools.DescribeTool), + Learnings: new(tools.LearningsTool), + Plans: new(tools.PlansTool), + ProjectPersonas: new(tools.ProjectPersonasTool), + WorldModel: new(tools.WorldModelTool), + ThoughtLearningLinks: new(tools.ThoughtLearningLinksTool), } } diff --git a/internal/store/thought_learning_links.go b/internal/store/thought_learning_links.go new file mode 100644 index 0000000..0c4a26d --- /dev/null +++ b/internal/store/thought_learning_links.go @@ -0,0 +1,148 @@ +package store + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "git.warky.dev/wdevs/amcs/internal/generatedmodels" + thoughttypes "git.warky.dev/wdevs/amcs/internal/types" +) + +// LinkThoughtLearning creates or updates an association between a thought (by GUID) and a learning (by numeric ID). +// If the pair already exists, the relation label is updated. +func (db *DB) LinkThoughtLearning(ctx context.Context, thoughtGUID uuid.UUID, learningID int64, relation string) error { + _, err := db.pool.Exec(ctx, ` + insert into thought_learning_links (thought_id, learning_id, relation) + select t.id, $2, $3 + from thoughts t + where t.guid = $1 + on conflict (thought_id, learning_id) do update set relation = excluded.relation + `, thoughtGUID, learningID, relation) + if err != nil { + return fmt.Errorf("link thought learning: %w", err) + } + return nil +} + +// UnlinkThoughtLearning removes the association between a thought (by GUID) and a learning (by numeric ID). +func (db *DB) UnlinkThoughtLearning(ctx context.Context, thoughtGUID uuid.UUID, learningID int64) error { + _, err := db.pool.Exec(ctx, ` + delete from thought_learning_links + where thought_id = (select id from thoughts where guid = $1) + and learning_id = $2 + `, thoughtGUID, learningID) + if err != nil { + return fmt.Errorf("unlink thought learning: %w", err) + } + return nil +} + +// GetLinkedLearnings returns all learnings explicitly associated with a thought (by GUID). +func (db *DB) GetLinkedLearnings(ctx context.Context, thoughtGUID uuid.UUID) ([]thoughttypes.LinkedLearning, error) { + rows, err := db.pool.Query(ctx, ` + select l.id, l.guid, l.summary, l.details, l.category, l.area, l.status, l.priority, l.confidence, + l.action_required, l.source_type, l.source_ref, l.project_id, l.related_thought_id, + l.related_skill_id, l.reviewed_by, l.reviewed_at, l.duplicate_of_learning_id, + l.supersedes_learning_id, l.tags::text[], l.created_at, l.updated_at, + tll.relation, tll.created_at as linked_at + from thought_learning_links tll + join learnings l on l.id = tll.learning_id + where tll.thought_id = (select id from thoughts where guid = $1) + order by tll.created_at desc + `, thoughtGUID) + if err != nil { + return nil, fmt.Errorf("query linked learnings: %w", err) + } + defer rows.Close() + + items := make([]thoughttypes.LinkedLearning, 0) + for rows.Next() { + var tags []string + var linked thoughttypes.LinkedLearning + var m generatedmodels.ModelPublicLearnings + + if err := rows.Scan( + &m.ID, + &m.GUID, + &m.Summary, + &m.Details, + &m.Category, + &m.Area, + &m.Status, + &m.Priority, + &m.Confidence, + &m.ActionRequired, + &m.SourceType, + &m.SourceRef, + &m.ProjectID, + &m.RelatedThoughtID, + &m.RelatedSkillID, + &m.ReviewedBy, + &m.ReviewedAt, + &m.DuplicateOfLearningID, + &m.SupersedesLearningID, + &tags, + &m.CreatedAt, + &m.UpdatedAt, + &linked.Relation, + &linked.CreatedAt, + ); err != nil { + return nil, fmt.Errorf("scan linked learning: %w", err) + } + linked.Learning = learningFromModel(m, tags) + items = append(items, linked) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate linked learnings: %w", err) + } + return items, nil +} + +// GetLinkedThoughtsForLearning returns all thoughts explicitly associated with a learning (by numeric ID). +func (db *DB) GetLinkedThoughtsForLearning(ctx context.Context, learningID int64) ([]thoughttypes.LinkedLearningThought, error) { + rows, err := db.pool.Query(ctx, ` + select t.id, t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, + tll.relation, tll.created_at as linked_at + from thought_learning_links tll + join thoughts t on t.id = tll.thought_id + where tll.learning_id = $1 + order by tll.created_at desc + `, learningID) + if err != nil { + return nil, fmt.Errorf("query linked thoughts for learning: %w", err) + } + defer rows.Close() + + items := make([]thoughttypes.LinkedLearningThought, 0) + for rows.Next() { + var linked thoughttypes.LinkedLearningThought + var m generatedmodels.ModelPublicThoughts + + if err := rows.Scan( + &m.ID, + &m.GUID, + &m.Content, + &m.Metadata, + &m.ProjectID, + &m.ArchivedAt, + &m.CreatedAt, + &m.UpdatedAt, + &linked.Relation, + &linked.CreatedAt, + ); err != nil { + return nil, fmt.Errorf("scan linked thought for learning: %w", err) + } + thought, err := thoughtFromModel(m) + if err != nil { + return nil, fmt.Errorf("map linked thought for learning: %w", err) + } + linked.Thought = thought + items = append(items, linked) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate linked thoughts for learning: %w", err) + } + return items, nil +} diff --git a/internal/tools/thought_learning_links.go b/internal/tools/thought_learning_links.go new file mode 100644 index 0000000..72d186d --- /dev/null +++ b/internal/tools/thought_learning_links.go @@ -0,0 +1,152 @@ +package tools + +import ( + "context" + "strconv" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "git.warky.dev/wdevs/amcs/internal/store" + thoughttypes "git.warky.dev/wdevs/amcs/internal/types" +) + +type ThoughtLearningLinksTool struct { + store *store.DB +} + +type LinkThoughtLearningInput struct { + ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought to link"` + LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning to link"` + Relation string `json:"relation,omitempty" jsonschema:"relationship label, e.g. source, derived_from, related; defaults to source"` +} + +type LinkThoughtLearningOutput struct { + Linked bool `json:"linked"` +} + +type UnlinkThoughtLearningInput struct { + ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought"` + LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning"` +} + +type UnlinkThoughtLearningOutput struct { + Unlinked bool `json:"unlinked"` +} + +type GetThoughtLearningsInput struct { + ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought"` +} + +type GetThoughtLearningsOutput struct { + Learnings []thoughttypes.LinkedLearning `json:"learnings"` +} + +type GetLearningThoughtsInput struct { + LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning"` +} + +type GetLearningThoughtsOutput struct { + Thoughts []thoughttypes.LinkedLearningThought `json:"thoughts"` +} + +func NewThoughtLearningLinksTool(db *store.DB) *ThoughtLearningLinksTool { + return &ThoughtLearningLinksTool{store: db} +} + +func (t *ThoughtLearningLinksTool) Link(ctx context.Context, _ *mcp.CallToolRequest, in LinkThoughtLearningInput) (*mcp.CallToolResult, LinkThoughtLearningOutput, error) { + if err := t.ensureConfigured(); err != nil { + return nil, LinkThoughtLearningOutput{}, err + } + + thoughtGUID, err := parseUUID(in.ThoughtID) + if err != nil { + return nil, LinkThoughtLearningOutput{}, err + } + if in.LearningID <= 0 { + return nil, LinkThoughtLearningOutput{}, errRequiredField("learning_id") + } + relation := strings.TrimSpace(in.Relation) + if relation == "" { + relation = "source" + } + + if _, err := t.store.GetThought(ctx, thoughtGUID); err != nil { + return nil, LinkThoughtLearningOutput{}, errEntityNotFound("thought", "thought_id", in.ThoughtID) + } + if _, err := t.store.GetLearning(ctx, in.LearningID); err != nil { + return nil, LinkThoughtLearningOutput{}, errEntityNotFound("learning", "learning_id", strconv.FormatInt(in.LearningID, 10)) + } + + if err := t.store.LinkThoughtLearning(ctx, thoughtGUID, in.LearningID, relation); err != nil { + return nil, LinkThoughtLearningOutput{}, err + } + return nil, LinkThoughtLearningOutput{Linked: true}, nil +} + +func (t *ThoughtLearningLinksTool) Unlink(ctx context.Context, _ *mcp.CallToolRequest, in UnlinkThoughtLearningInput) (*mcp.CallToolResult, UnlinkThoughtLearningOutput, error) { + if err := t.ensureConfigured(); err != nil { + return nil, UnlinkThoughtLearningOutput{}, err + } + + thoughtGUID, err := parseUUID(in.ThoughtID) + if err != nil { + return nil, UnlinkThoughtLearningOutput{}, err + } + if in.LearningID <= 0 { + return nil, UnlinkThoughtLearningOutput{}, errRequiredField("learning_id") + } + + if err := t.store.UnlinkThoughtLearning(ctx, thoughtGUID, in.LearningID); err != nil { + return nil, UnlinkThoughtLearningOutput{}, err + } + return nil, UnlinkThoughtLearningOutput{Unlinked: true}, nil +} + +func (t *ThoughtLearningLinksTool) GetThoughtLearnings(ctx context.Context, _ *mcp.CallToolRequest, in GetThoughtLearningsInput) (*mcp.CallToolResult, GetThoughtLearningsOutput, error) { + if err := t.ensureConfigured(); err != nil { + return nil, GetThoughtLearningsOutput{}, err + } + + thoughtGUID, err := parseUUID(in.ThoughtID) + if err != nil { + return nil, GetThoughtLearningsOutput{}, err + } + + if _, err := t.store.GetThought(ctx, thoughtGUID); err != nil { + return nil, GetThoughtLearningsOutput{}, errEntityNotFound("thought", "thought_id", in.ThoughtID) + } + + learnings, err := t.store.GetLinkedLearnings(ctx, thoughtGUID) + if err != nil { + return nil, GetThoughtLearningsOutput{}, err + } + return nil, GetThoughtLearningsOutput{Learnings: learnings}, nil +} + +func (t *ThoughtLearningLinksTool) GetLearningThoughts(ctx context.Context, _ *mcp.CallToolRequest, in GetLearningThoughtsInput) (*mcp.CallToolResult, GetLearningThoughtsOutput, error) { + if err := t.ensureConfigured(); err != nil { + return nil, GetLearningThoughtsOutput{}, err + } + + if in.LearningID <= 0 { + return nil, GetLearningThoughtsOutput{}, errRequiredField("learning_id") + } + + if _, err := t.store.GetLearning(ctx, in.LearningID); err != nil { + return nil, GetLearningThoughtsOutput{}, errEntityNotFound("learning", "learning_id", strconv.FormatInt(in.LearningID, 10)) + } + + thoughts, err := t.store.GetLinkedThoughtsForLearning(ctx, in.LearningID) + if err != nil { + return nil, GetLearningThoughtsOutput{}, err + } + return nil, GetLearningThoughtsOutput{Thoughts: thoughts}, nil +} + +func (t *ThoughtLearningLinksTool) ensureConfigured() error { + if t == nil || t.store == nil { + return errInvalidInput("thought learning links tool is not configured") + } + return nil +} diff --git a/internal/types/project.go b/internal/types/project.go index d5eefc9..d07d12b 100644 --- a/internal/types/project.go +++ b/internal/types/project.go @@ -39,3 +39,22 @@ type LinkedThought struct { Direction string `json:"direction"` CreatedAt time.Time `json:"created_at"` } + +type ThoughtLearningLink struct { + ThoughtID int64 `json:"thought_id"` + LearningID int64 `json:"learning_id"` + Relation string `json:"relation"` + CreatedAt time.Time `json:"created_at"` +} + +type LinkedLearning struct { + Learning Learning `json:"learning"` + Relation string `json:"relation"` + CreatedAt time.Time `json:"created_at"` +} + +type LinkedLearningThought struct { + Thought Thought `json:"thought"` + Relation string `json:"relation"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/migrations/030_thought_learning_links.sql b/migrations/030_thought_learning_links.sql new file mode 100644 index 0000000..66cdf6f --- /dev/null +++ b/migrations/030_thought_learning_links.sql @@ -0,0 +1,26 @@ +-- Many-to-many join table between thoughts and learnings. +-- Allows a learning to reference multiple source thoughts and a thought to +-- expose multiple related learnings, queryable in both directions. + +CREATE SEQUENCE IF NOT EXISTS public.identity_thought_learning_links_id + INCREMENT 1 + MINVALUE 1 + MAXVALUE 9223372036854775807 + START 1 + CACHE 1; + +CREATE TABLE IF NOT EXISTS public.thought_learning_links ( + id bigint NOT NULL DEFAULT nextval('public.identity_thought_learning_links_id'), + thought_id bigint NOT NULL REFERENCES public.thoughts(id) ON DELETE CASCADE, + learning_id bigint NOT NULL REFERENCES public.learnings(id) ON DELETE CASCADE, + relation text NOT NULL DEFAULT 'source', + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT thought_learning_links_pkey PRIMARY KEY (id), + CONSTRAINT thought_learning_links_unique UNIQUE (thought_id, learning_id) +); + +CREATE INDEX IF NOT EXISTS idx_thought_learning_links_thought_id + ON public.thought_learning_links (thought_id); + +CREATE INDEX IF NOT EXISTS idx_thought_learning_links_learning_id + ON public.thought_learning_links (learning_id); diff --git a/schema/core.dbml b/schema/core.dbml index c44f6c0..8790a87 100644 --- a/schema/core.dbml +++ b/schema/core.dbml @@ -32,6 +32,20 @@ Table thought_links { } } +Table thought_learning_links { + id bigserial [pk] + thought_id bigint [not null, ref: > thoughts.id] + learning_id bigint [not null, ref: > learnings.id] + relation text [not null, default: `'source'`] + created_at timestamptz [not null, default: `now()`] + + indexes { + (thought_id, learning_id) [unique] + thought_id + learning_id + } +} + Table embeddings { id bigserial [pk] guid uuid [unique, not null, default: `gen_random_uuid()`]