skill

package
v0.0.0-...-7cce3c2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package skill provides CRUD and search operations for skills in the knowledge graph.

Package skill — tenant_store.go provides a tenant-aware wrapper around the skill CRUD operations. Every query is scoped to a tenant_id extracted from the request context, enforcing data isolation at the application layer.

When no tenant_id is present in context (single-tenant mode), queries are NOT filtered by tenant_id, preserving full backward compatibility with existing data that has NULL tenant_id columns (see migrations/004_enterprise.up.sql §2).

The tenant_id is expected to be injected into context by tenant middleware (e.g. an HTTP middleware that reads an X-Tenant-ID header or JWT claim and stores it under the "tenant" context key).

Index

Constants

View Source
const (
	// TenantKey is the context key used by tenant middleware to pass the
	// tenant UUID. Middleware should call context.WithValue(ctx, TenantKey, id)
	// before the handler runs.
	TenantKey tenantContextKey = "tenant"
)

Variables

View Source
var (
	// ErrSkillNotFound indicates the requested skill does not exist.
	ErrSkillNotFound = errors.New("skill not found")
	// ErrSkillExists indicates a skill with the same unique name already exists.
	ErrSkillExists = errors.New("skill already exists")
	// ErrInvalidSkill indicates a skill failed structural or semantic validation.
	ErrInvalidSkill = errors.New("invalid skill")
	// ErrDependencyNotFound indicates a referenced dependency skill does not exist.
	ErrDependencyNotFound = errors.New("dependency skill not found")
	// ErrCycleDetected indicates an operation would introduce a dependency cycle.
	ErrCycleDetected = errors.New("dependency cycle detected")
)

Sentinel errors returned by the skill store and graph operations. Callers should compare against these with errors.Is rather than matching strings.

Functions

func TenantFromContext

func TenantFromContext(ctx context.Context) (uuid.UUID, bool)

TenantFromContext extracts the tenant UUID from ctx. Returns uuid.Nil and false when no tenant is set (single-tenant mode). Callers that need to distinguish "no tenant" from "zero tenant" should check the boolean.

Types

type ListOpts

type ListOpts struct {
	Status models.SkillStatus
	Limit  int
	Offset int
}

ListOpts controls the behaviour of ListSkills. All fields are optional; zero values mean "no filter" / "no limit".

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store provides data access for skills and related entities.

func NewStore

func NewStore(pool *db.Pool) *Store

NewStore creates a new skill store. logger defaults to zap.NewNop() (see WithLogger) so a Store used without WithLogger -- every existing test helper and any future construction path that does not opt in -- behaves exactly as before this field was added: Store's internal diagnostics are silently discarded, never a nil-pointer panic.

func (*Store) AddDependency

func (s *Store) AddDependency(ctx context.Context, skillID, dependsOn uuid.UUID, relType models.DependencyType) error

AddDependency adds a directed edge from skillID to dependsOn with cycle detection. The relation type must be one of: requires, extends, recommends, composes, related_to, alternative_to (research/skill_granularity_and_composition.md §4.1).

func (*Store) AddEvidence

func (s *Store) AddEvidence(ctx context.Context, evidence *models.Evidence) error

AddEvidence attaches a new evidence record to a skill.

func (*Store) AddResource

func (s *Store) AddResource(ctx context.Context, resource *models.Resource) error

AddResource attaches a new external resource to a skill.

func (*Store) BulkAddEvidence

func (s *Store) BulkAddEvidence(ctx context.Context, skillID uuid.UUID, evidences []models.Evidence) error

BulkAddEvidence adds multiple evidence records to a skill in a single transaction.

func (*Store) BulkAddResources

func (s *Store) BulkAddResources(ctx context.Context, skillID uuid.UUID, resources []models.Resource) error

BulkAddResources adds multiple resources to a skill in a single transaction.

func (*Store) Create

func (s *Store) Create(ctx context.Context, skill *models.Skill) error

Create inserts a new skill into the database.

func (*Store) CreateFromTOML

func (s *Store) CreateFromTOML(ctx context.Context, wrapper *models.TOMLSkillWrapper) (*models.Skill, error)

CreateFromTOML creates a skill from a TOML skill wrapper.

§G59: CreateFromTOML delegates to Create (below) for the actual skill-row write, so it inherits Create's embedding write-through automatically -- no separate embedWriteThrough call is needed here. There is likewise no separate `Update` method on Store: Create's `ON CONFLICT (name) DO UPDATE` clause IS this package's skill update path (an upsert keyed on the unique `name` column), so wiring the embedding write into Create alone covers create, update, and CreateFromTOML uniformly.

func (*Store) DeleteEvidence

func (s *Store) DeleteEvidence(ctx context.Context, evidenceID uuid.UUID) error

DeleteEvidence removes an evidence record.

func (*Store) DeleteResource

func (s *Store) DeleteResource(ctx context.Context, resourceID uuid.UUID) error

DeleteResource removes a resource from a skill.

func (*Store) ExportToTOML

func (s *Store) ExportToTOML(ctx context.Context, skillName string) ([]byte, error)

ExportToTOML exports a skill and its dependencies and resources as TOML.

func (*Store) GetAllDependencies

func (s *Store) GetAllDependencies(ctx context.Context, skillID uuid.UUID) ([]models.Skill, error)

GetAllDependencies returns a flat list of all transitive dependencies for a skill. Depth is capped at 50 levels to prevent runaway queries on large/cyclic graphs.

func (*Store) GetByName

func (s *Store) GetByName(ctx context.Context, name string) (*models.Skill, error)

GetByName retrieves a complete skill by its unique name.

func (*Store) GetCoverage

func (s *Store) GetCoverage(ctx context.Context, domain string) (map[string]interface{}, error)

GetCoverage returns coverage statistics for a domain. Consolidated into a SINGLE query using conditional aggregation instead of 5 separate COUNT round-trips (performance: eliminates N+1 at the query level).

func (*Store) GetDependencyTree

func (s *Store) GetDependencyTree(ctx context.Context, rootName string, maxDepth int) (*models.SkillTreeNode, error)

GetDependencyTree returns the full dependency tree starting from a root skill name, using a PostgreSQL recursive CTE. Results are limited to maxDepth levels.

func (*Store) GetDependents

func (s *Store) GetDependents(ctx context.Context, skillID uuid.UUID) ([]models.Skill, error)

GetDependents returns all skills that directly depend on the given skill.

func (*Store) GetEvidence

func (s *Store) GetEvidence(ctx context.Context, skillID uuid.UUID) ([]models.Evidence, error)

GetEvidence returns all evidence records attached to a skill.

func (*Store) GetEvidenceByID

func (s *Store) GetEvidenceByID(ctx context.Context, evidenceID uuid.UUID) (*models.Evidence, error)

GetEvidenceByID retrieves a single evidence record by its ID.

func (*Store) GetEvidenceByLanguage

func (s *Store) GetEvidenceByLanguage(ctx context.Context, language string) ([]models.Evidence, error)

GetEvidenceByLanguage returns all evidence records for a specific programming language.

func (*Store) GetEvidenceByPattern

func (s *Store) GetEvidenceByPattern(ctx context.Context, pattern string) ([]models.Evidence, error)

GetEvidenceByPattern returns evidence records matching a pattern (substring search).

func (*Store) GetEvidenceByProject

func (s *Store) GetEvidenceByProject(ctx context.Context, project string) ([]models.Evidence, error)

GetEvidenceByProject returns all evidence records from a specific source project.

func (*Store) GetMissingSkills

func (s *Store) GetMissingSkills(ctx context.Context, domain string) ([]models.SkillRegistryEntry, error)

GetMissingSkills returns skills with missing dependencies (gaps in the graph).

func (*Store) GetResourceByID

func (s *Store) GetResourceByID(ctx context.Context, resourceID uuid.UUID) (*models.Resource, error)

GetResourceByID retrieves a single resource by its ID.

func (*Store) GetResources

func (s *Store) GetResources(ctx context.Context, skillID uuid.UUID) ([]models.Resource, error)

GetResources returns all resources attached to a skill.

func (*Store) GetResourcesNeedingValidation

func (s *Store) GetResourcesNeedingValidation(ctx context.Context, olderThan time.Duration) ([]models.Resource, error)

GetResourcesNeedingValidation returns resources that haven't been validated recently.

func (*Store) GetTree

func (s *Store) GetTree(ctx context.Context, name string, maxDepth int) (*models.SkillTreeNode, error)

GetTree returns the dependency tree for a skill up to the specified depth. Uses a single recursive CTE to fetch all reachable skills and edges, then assembles the tree in Go — O(1) queries instead of O(N) (§11.4.82).

func (*Store) ImportFromTOML

func (s *Store) ImportFromTOML(ctx context.Context, tomlData []byte) (*models.Skill, error)

ImportFromTOML parses a TOML skill definition and creates the skill along with its dependencies and resources in a single transaction. When a query-side embedder is configured (Store.WithEmbedder) it also write-through embeds the new skill immediately after the transaction commits (§G59 F1) -- this is the function the live MCP skill_create tool calls directly (internal/mcp/tools.go), so it must carry the same embedding write-through as Store.Create/ CreateFromTOML or a skill created via the deployed MCP tool would be invisible to vector-KNN.

func (*Store) InvalidateEvidence

func (s *Store) InvalidateEvidence(ctx context.Context, evidenceID uuid.UUID) error

InvalidateEvidence marks an evidence record as not validated.

func (*Store) InvalidateResourceCache

func (s *Store) InvalidateResourceCache(ctx context.Context, resourceID uuid.UUID) error

InvalidateResourceCache marks a resource's cached content as stale.

func (*Store) ListSkills

func (s *Store) ListSkills(ctx context.Context, status models.SkillStatus, limit, offset int) ([]models.Skill, error)

ListSkills returns all skills with optional filtering.

func (*Store) Pool

func (s *Store) Pool() *db.Pool

Pool returns the underlying database pool for operations that need direct database access (e.g., audit logging from other packages).

func (*Store) RemoveDependency

func (s *Store) RemoveDependency(ctx context.Context, skillID, dependsOn uuid.UUID) error

RemoveDependency removes a directed edge between two skills.

func (*Store) Search

func (s *Store) Search(ctx context.Context, query string, limit int) ([]models.SearchResult, error)

Search performs a hybrid search over the skill graph.

When a query-side embedder is configured (WithEmbedder) it runs two candidate retrievals — a pgvector cosine-KNN over skills.embedding (semantic recall, via VectorSearch) and a pg_trgm/ILIKE keyword match (lexical precision) — and fuses them with weighted Reciprocal Rank Fusion. Because the fusion is by RANK (not by raw score) the incomparable score scales of the two paths cannot distort each other, and a semantically-near skill whose text does NOT contain the query as a substring can both surface and outrank a purely-lexical match — the recall the keyword-only path structurally cannot deliver.

When no embedder is configured, OR the query embedding fails (e.g. an embedding provider is temporarily unreachable), Search transparently degrades to the keyword-only path rather than returning an error, so keyword search keeps working everywhere. A genuine failure of the vector KNN query itself is NOT masked — it is returned — because that signals a real misconfiguration (e.g. an embedding/column dimension mismatch) that must surface (§11.4.6).

The returned SearchResult.Score is the pg_trgm similarity for the keyword-only path, and the fused RRF relevance score for the hybrid path.

func (*Store) SubmitLearningJob

func (s *Store) SubmitLearningJob(ctx context.Context, projectPath string, languages []string) (*models.LearningJob, error)

SubmitLearningJob creates a new learning job for project analysis.

func (*Store) TouchSkillUpdatedAt

func (s *Store) TouchSkillUpdatedAt(ctx context.Context, skillID uuid.UUID) error

TouchSkillUpdatedAt updates the updated_at timestamp for a skill.

func (*Store) UpdateRegistryAfterChange

func (s *Store) UpdateRegistryAfterChange(ctx context.Context, skillID uuid.UUID) error

UpdateRegistryAfterChange recalculates registry state for affected skills. Should be called after any skill or dependency mutation.

func (*Store) UpdateResourceHash

func (s *Store) UpdateResourceHash(ctx context.Context, resourceID uuid.UUID, hash string) error

UpdateResourceHash updates the content hash and validation timestamp for a resource.

func (*Store) UpdateStatus

func (s *Store) UpdateStatus(ctx context.Context, skillID uuid.UUID, newStatus models.SkillStatus) error

UpdateStatus changes the status of a skill by ID. Returns ErrSkillNotFound when the skill does not exist. The updated_at timestamp is refreshed automatically. An audit log entry is recorded for the status change.

§G03 Validation pipeline promotion: used by the validation worker cycle to promote skills from draft → validated → active after passing all stages.

func (*Store) ValidateEvidence

func (s *Store) ValidateEvidence(ctx context.Context, evidenceID uuid.UUID) error

ValidateEvidence marks an evidence record as validated.

func (*Store) VectorSearch

func (s *Store) VectorSearch(ctx context.Context, embedding []float32, limit int) ([]models.SearchResult, error)

VectorSearch performs vector similarity search using pgvector.

func (*Store) WithEmbedder

func (s *Store) WithEmbedder(e db.Embedder) *Store

WithEmbedder configures the query-side embedder that turns Search into a genuine hybrid (vector KNN + trigram) search and returns the receiver for fluent wiring (§G29). Passing a nil embedder resets Search to keyword-only. This is an explicit opt-in: callers that never invoke it (every current test helper, and any deployment with no embedding provider configured) keep the keyword-only path and never issue an embedding request.

Concurrency contract (Fable code-review remediation, finding 6b): WithEmbedder is a plain, unsynchronized field write -- it is SAFE ONLY as a one-time construction-time wire-up that happens-before any concurrent Search call, NOT as a live runtime reconfiguration switch. The sole production caller, internal/mcp.NewMCPServer, relies on exactly this: it calls WithEmbedder on the shared *Store it was handed, synchronously, before returning the *MCPServer to its caller -- and every transport (stdio/HTTP/ACP) that could concurrently invoke Search is started strictly AFTER NewMCPServer returns (cmd/server wires the Store, then NewMCPServer, then RegisterTools/ ListenAndServe/RunStdio/RunACP). There is a SINGLE construction call over the Store's lifetime; calling WithEmbedder again after any transport has started serving requests is a data race with concurrent Search readers of s.embedder and is NOT supported.

func (*Store) WithLogger

func (s *Store) WithLogger(logger *zap.Logger) *Store

WithLogger wires the real application logger into Store so its own diagnostics (currently just warnEmbeddingDegraded) reach a real sink at runtime instead of the package-level zap.L() no-op default (re-review remediation, MAJOR finding; see the logger field's doc comment). Mirrors the WithEmbedder fluent-option convention above; returns the receiver for fluent wiring. A nil logger is a no-op (the field keeps whatever it already had -- the zap.NewNop() default from NewStore unless WithLogger was already called) rather than falling back to zap.L(), which would silently reintroduce the exact dead-sink class this method exists to close.

Concurrency contract: identical to WithEmbedder -- a plain, unsynchronized field write, safe ONLY as a one-time construction-time wire-up that happens-before any concurrent Search call. internal/mcp.NewMCPServer calls WithLogger synchronously, before returning the *MCPServer, alongside its existing WithEmbedder call.

type TenantStore

type TenantStore struct {
	// contains filtered or unexported fields
}

TenantStore wraps the skill CRUD operations with automatic tenant_id filtering. It does NOT replace the existing Store — it is a lightweight adapter that composes over *db.Pool and applies tenant scoping to every query it issues.

Construction:

ts := skill.NewTenantStore(pool, logger)

Usage from a handler (tenant_id comes from context):

skills, err := ts.ListSkills(ctx, ListOpts{Status: models.SkillStatusActive, Limit: 50})

func NewTenantStore

func NewTenantStore(pool *db.Pool, logger *zap.Logger) *TenantStore

NewTenantStore creates a new tenant-aware skill store. logger defaults to zap.NewNop() if nil, matching the existing Store convention.

func (*TenantStore) CreateSkill

func (ts *TenantStore) CreateSkill(ctx context.Context, skill *models.Skill) error

CreateSkill inserts a new skill into the database with the tenant_id from ctx. When no tenant is present, tenant_id is left NULL (backward compat). The existing ON CONFLICT (name) upsert semantics are preserved — but when a tenant is active, the conflict target is effectively scoped by the tenant_id column via the WHERE clause.

func (*TenantStore) DeleteSkill

func (ts *TenantStore) DeleteSkill(ctx context.Context, name string) error

DeleteSkill removes a skill by name, scoped to the tenant in ctx. Also removes associated dependencies, evidences, and resources via CASCADE on the foreign keys (migrations/001_initial.up.sql). Returns ErrSkillNotFound when no matching skill exists for the tenant.

func (*TenantStore) GetSkill

func (ts *TenantStore) GetSkill(ctx context.Context, name string) (*models.Skill, error)

GetSkill retrieves a single skill by name, scoped to the tenant in ctx. Returns ErrSkillNotFound when the skill does not exist or belongs to a different tenant.

func (*TenantStore) ListSkills

func (ts *TenantStore) ListSkills(ctx context.Context, opts ListOpts) ([]models.Skill, error)

ListSkills returns skills scoped to the tenant in ctx. When no tenant is present (single-tenant mode), returns all skills regardless of tenant_id.

func (*TenantStore) SearchSkills

func (ts *TenantStore) SearchSkills(ctx context.Context, query string, opts ListOpts) ([]models.Skill, error)

SearchSkills performs a keyword search over skills, scoped to the tenant in ctx. Uses pg_trgm similarity and ILIKE fallback, matching the existing Store.textSearch behaviour. When no tenant is present, searches across all tenants (backward compat).

func (*TenantStore) UpdateSkill

func (ts *TenantStore) UpdateSkill(ctx context.Context, name string, skill *models.Skill) error

UpdateSkill updates an existing skill identified by name, scoped to the tenant in ctx. Returns ErrSkillNotFound when no matching skill exists for the tenant.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL