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
- Variables
- func TenantFromContext(ctx context.Context) (uuid.UUID, bool)
- type ListOpts
- type Store
- func (s *Store) AddDependency(ctx context.Context, skillID, dependsOn uuid.UUID, ...) error
- func (s *Store) AddEvidence(ctx context.Context, evidence *models.Evidence) error
- func (s *Store) AddResource(ctx context.Context, resource *models.Resource) error
- func (s *Store) BulkAddEvidence(ctx context.Context, skillID uuid.UUID, evidences []models.Evidence) error
- func (s *Store) BulkAddResources(ctx context.Context, skillID uuid.UUID, resources []models.Resource) error
- func (s *Store) Create(ctx context.Context, skill *models.Skill) error
- func (s *Store) CreateFromTOML(ctx context.Context, wrapper *models.TOMLSkillWrapper) (*models.Skill, error)
- func (s *Store) DeleteEvidence(ctx context.Context, evidenceID uuid.UUID) error
- func (s *Store) DeleteResource(ctx context.Context, resourceID uuid.UUID) error
- func (s *Store) ExportToTOML(ctx context.Context, skillName string) ([]byte, error)
- func (s *Store) GetAllDependencies(ctx context.Context, skillID uuid.UUID) ([]models.Skill, error)
- func (s *Store) GetByName(ctx context.Context, name string) (*models.Skill, error)
- func (s *Store) GetCoverage(ctx context.Context, domain string) (map[string]interface{}, error)
- func (s *Store) GetDependencyTree(ctx context.Context, rootName string, maxDepth int) (*models.SkillTreeNode, error)
- func (s *Store) GetDependents(ctx context.Context, skillID uuid.UUID) ([]models.Skill, error)
- func (s *Store) GetEvidence(ctx context.Context, skillID uuid.UUID) ([]models.Evidence, error)
- func (s *Store) GetEvidenceByID(ctx context.Context, evidenceID uuid.UUID) (*models.Evidence, error)
- func (s *Store) GetEvidenceByLanguage(ctx context.Context, language string) ([]models.Evidence, error)
- func (s *Store) GetEvidenceByPattern(ctx context.Context, pattern string) ([]models.Evidence, error)
- func (s *Store) GetEvidenceByProject(ctx context.Context, project string) ([]models.Evidence, error)
- func (s *Store) GetMissingSkills(ctx context.Context, domain string) ([]models.SkillRegistryEntry, error)
- func (s *Store) GetResourceByID(ctx context.Context, resourceID uuid.UUID) (*models.Resource, error)
- func (s *Store) GetResources(ctx context.Context, skillID uuid.UUID) ([]models.Resource, error)
- func (s *Store) GetResourcesNeedingValidation(ctx context.Context, olderThan time.Duration) ([]models.Resource, error)
- func (s *Store) GetTree(ctx context.Context, name string, maxDepth int) (*models.SkillTreeNode, error)
- func (s *Store) ImportFromTOML(ctx context.Context, tomlData []byte) (*models.Skill, error)
- func (s *Store) InvalidateEvidence(ctx context.Context, evidenceID uuid.UUID) error
- func (s *Store) InvalidateResourceCache(ctx context.Context, resourceID uuid.UUID) error
- func (s *Store) ListSkills(ctx context.Context, status models.SkillStatus, limit, offset int) ([]models.Skill, error)
- func (s *Store) Pool() *db.Pool
- func (s *Store) RemoveDependency(ctx context.Context, skillID, dependsOn uuid.UUID) error
- func (s *Store) Search(ctx context.Context, query string, limit int) ([]models.SearchResult, error)
- func (s *Store) SubmitLearningJob(ctx context.Context, projectPath string, languages []string) (*models.LearningJob, error)
- func (s *Store) TouchSkillUpdatedAt(ctx context.Context, skillID uuid.UUID) error
- func (s *Store) UpdateRegistryAfterChange(ctx context.Context, skillID uuid.UUID) error
- func (s *Store) UpdateResourceHash(ctx context.Context, resourceID uuid.UUID, hash string) error
- func (s *Store) UpdateStatus(ctx context.Context, skillID uuid.UUID, newStatus models.SkillStatus) error
- func (s *Store) ValidateEvidence(ctx context.Context, evidenceID uuid.UUID) error
- func (s *Store) VectorSearch(ctx context.Context, embedding []float32, limit int) ([]models.SearchResult, error)
- func (s *Store) WithEmbedder(e db.Embedder) *Store
- func (s *Store) WithLogger(logger *zap.Logger) *Store
- type TenantStore
- func (ts *TenantStore) CreateSkill(ctx context.Context, skill *models.Skill) error
- func (ts *TenantStore) DeleteSkill(ctx context.Context, name string) error
- func (ts *TenantStore) GetSkill(ctx context.Context, name string) (*models.Skill, error)
- func (ts *TenantStore) ListSkills(ctx context.Context, opts ListOpts) ([]models.Skill, error)
- func (ts *TenantStore) SearchSkills(ctx context.Context, query string, opts ListOpts) ([]models.Skill, error)
- func (ts *TenantStore) UpdateSkill(ctx context.Context, name string, skill *models.Skill) error
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
AddEvidence attaches a new evidence record to a skill.
func (*Store) AddResource ¶
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) 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 ¶
DeleteEvidence removes an evidence record.
func (*Store) DeleteResource ¶
DeleteResource removes a resource from a skill.
func (*Store) ExportToTOML ¶
ExportToTOML exports a skill and its dependencies and resources as TOML.
func (*Store) GetAllDependencies ¶
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) GetCoverage ¶
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 ¶
GetDependents returns all skills that directly depend on the given skill.
func (*Store) GetEvidence ¶
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 ¶
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 ¶
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 ¶
InvalidateEvidence marks an evidence record as not validated.
func (*Store) InvalidateResourceCache ¶
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 ¶
Pool returns the underlying database pool for operations that need direct database access (e.g., audit logging from other packages).
func (*Store) RemoveDependency ¶
RemoveDependency removes a directed edge between two skills.
func (*Store) Search ¶
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 ¶
TouchSkillUpdatedAt updates the updated_at timestamp for a skill.
func (*Store) UpdateRegistryAfterChange ¶
UpdateRegistryAfterChange recalculates registry state for affected skills. Should be called after any skill or dependency mutation.
func (*Store) UpdateResourceHash ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
UpdateSkill updates an existing skill identified by name, scoped to the tenant in ctx. Returns ErrSkillNotFound when no matching skill exists for the tenant.