Files
relata/internal/domain/ports.go
T
2026-08-11 10:40:10 +01:00

390 lines
16 KiB
Go

package domain
import (
"context"
"time"
)
// ---------------------------------------------------------------------------
// AI provider ports
//
// These exist so that Ollama can be replaced by an OpenAI-compatible API or any
// other provider without touching the service layer. Adapters live in
// internal/embeddings and internal/analysis.
// ---------------------------------------------------------------------------
// Embedder turns text into vectors.
type Embedder interface {
// Embed returns one vector per input text, in the same order. Implementations
// must honour ctx cancellation and must not retry indefinitely.
Embed(ctx context.Context, texts []string) ([][]float32, error)
// ModelID identifies the exact model, for stamping onto derived records.
ModelID() string
// Dimensions is the vector width this model produces.
Dimensions() int
}
// ChatMessage is one turn of a model conversation.
type ChatMessage struct {
Role string // "system" | "user" | "assistant"
Content string
}
// CompletionRequest asks the model for a bounded response.
type CompletionRequest struct {
Messages []ChatMessage
// JSONSchema, when non-empty, requests structured output conforming to it.
JSONSchema []byte
// MaxTokens bounds the response so a runaway generation cannot exhaust
// memory or time.
MaxTokens int
Temperature float64
}
// CompletionResponse is a model reply plus what it cost.
type CompletionResponse struct {
Content string
Model string
PromptTokens int
OutputTokens int
Duration time.Duration
}
// Analyser performs model-assisted structured tasks. Its output is always
// treated as untrusted and validated against a schema before use.
type Analyser interface {
Complete(ctx context.Context, req CompletionRequest) (*CompletionResponse, error)
ModelID() string
}
// ProviderHealth describes whether an AI provider is reachable and configured.
type ProviderHealth struct {
Name string `json:"name"`
Endpoint string `json:"endpoint"`
Model string `json:"model"`
Available bool `json:"available"`
Detail string `json:"detail,omitempty"`
Models []string `json:"models,omitempty"`
}
// HealthChecker is implemented by providers that can report reachability.
type HealthChecker interface {
Health(ctx context.Context) ProviderHealth
}
// ---------------------------------------------------------------------------
// Vector store port
// ---------------------------------------------------------------------------
// VectorPoint is a stored embedding plus the minimal retrieval metadata needed
// to resolve it back to MongoDB. Payloads deliberately exclude document text.
type VectorPoint struct {
ChunkID string
WorkspaceID string
ItemID string
SourceType SourceType
CreatedAt time.Time
EmbeddingModel string
Vector []float32
}
// VectorMatch is a similarity search hit.
type VectorMatch struct {
ChunkID string
ItemID string
WorkspaceID string
// Score is cosine similarity in roughly -1..1; higher is more similar.
Score float64
SourceType SourceType
CreatedAt time.Time
EmbeddingModel string
}
// VectorFilter narrows a similarity search. All fields are optional and are
// combined with AND. WorkspaceID is always applied by the caller.
type VectorFilter struct {
WorkspaceID string
ItemIDs []string
ExcludeItemIDs []string
SourceTypes []SourceType
EmbeddingModel string
CreatedFrom *time.Time
CreatedTo *time.Time
}
// VectorStore persists and searches embeddings.
type VectorStore interface {
// EnsureCollection creates the collection if absent, sized for dim.
EnsureCollection(ctx context.Context, dim int) error
Upsert(ctx context.Context, points []VectorPoint) error
Search(ctx context.Context, vector []float32, limit int, f VectorFilter) ([]VectorMatch, error)
DeleteByItem(ctx context.Context, workspaceID, itemID string) error
DeleteByChunks(ctx context.Context, workspaceID string, chunkIDs []string) error
// CountByWorkspace reports how many vectors exist, for reconciliation.
CountByWorkspace(ctx context.Context, workspaceID string) (int64, error)
Health(ctx context.Context) error
}
// ---------------------------------------------------------------------------
// Repository ports
//
// Every method that touches user content takes a workspace ID and must include
// it in the underlying query. There is intentionally no "get by ID" that skips
// the workspace filter.
// ---------------------------------------------------------------------------
// Page describes a bounded slice of results.
type Page struct {
Limit int
Offset int
}
// Normalise clamps a page request to safe bounds.
func (p Page) Normalise(defLimit, maxLimit int) Page {
if p.Limit <= 0 {
p.Limit = defLimit
}
if p.Limit > maxLimit {
p.Limit = maxLimit
}
if p.Offset < 0 {
p.Offset = 0
}
return p
}
// ItemFilter selects items using typed values only. Client-supplied MongoDB
// operators are never accepted anywhere in this struct.
type ItemFilter struct {
SourceTypes []SourceType
MIMETypes []string
Statuses []ProcessingStatus
CreatedFrom *time.Time
CreatedTo *time.Time
ContentHash string
TitleContains string
IncludeDeleted bool
}
// ItemRepository stores source items.
type ItemRepository interface {
Create(ctx context.Context, it *Item) error
Get(ctx context.Context, workspaceID, id string) (*Item, error)
GetMany(ctx context.Context, workspaceID string, ids []string) ([]*Item, error)
Update(ctx context.Context, it *Item) error
SetStatus(ctx context.Context, workspaceID, id string, st ProcessingStatus, errMsg string) error
AddWarning(ctx context.Context, workspaceID, id string, w ParseWarning) error
List(ctx context.Context, workspaceID string, f ItemFilter, p Page) ([]*Item, int64, error)
FindByContentHash(ctx context.Context, workspaceID, hash string) (*Item, error)
// TextSearch is the lexical half of hybrid retrieval.
TextSearch(ctx context.Context, workspaceID, query string, f ItemFilter, limit int) ([]LexicalHit, error)
Tombstone(ctx context.Context, workspaceID, id string) error
HardDelete(ctx context.Context, workspaceID, id string) error
Count(ctx context.Context, workspaceID string) (int64, error)
CountByStatus(ctx context.Context, workspaceID string) (map[ProcessingStatus]int64, error)
}
// LexicalHit is a keyword-search result with its raw text score.
type LexicalHit struct {
ItemID string
Score float64
}
// ChunkRepository stores item chunks.
type ChunkRepository interface {
ReplaceForItem(ctx context.Context, workspaceID, itemID string, chunks []*Chunk) error
ListByItem(ctx context.Context, workspaceID, itemID string) ([]*Chunk, error)
GetMany(ctx context.Context, workspaceID string, ids []string) ([]*Chunk, error)
MarkEmbedded(ctx context.Context, workspaceID string, ids []string, model string) error
ListUnembedded(ctx context.Context, workspaceID, model string, limit int) ([]*Chunk, error)
DeleteByItem(ctx context.Context, workspaceID, itemID string) error
Count(ctx context.Context, workspaceID string) (int64, error)
// TextSearch finds chunks lexically, used to produce excerpts.
TextSearch(ctx context.Context, workspaceID, query string, limit int) ([]ChunkHit, error)
}
// ChunkHit is a lexical chunk match.
type ChunkHit struct {
ChunkID string
ItemID string
Score float64
Text string
}
// EntityFilter selects entities by typed values.
type EntityFilter struct {
Types []EntityType
NameContains string
NormalisedKey string
ItemID string
MinMentions int
}
// EntityRepository stores extracted entities.
type EntityRepository interface {
// UpsertByKey merges a newly observed entity into an existing one with the
// same (workspace, type, normalised key), accumulating references. It never
// merges entities whose keys merely look similar.
UpsertByKey(ctx context.Context, e *Entity) (*Entity, error)
Get(ctx context.Context, workspaceID, id string) (*Entity, error)
GetMany(ctx context.Context, workspaceID string, ids []string) ([]*Entity, error)
FindByNormalised(ctx context.Context, workspaceID string, keys []string) ([]*Entity, error)
List(ctx context.Context, workspaceID string, f EntityFilter, p Page) ([]*Entity, int64, error)
ListByItem(ctx context.Context, workspaceID, itemID string) ([]*Entity, error)
// ItemsSharingEntities returns item IDs that reference any of the given
// entities, excluding excludeItem. This is the indexed path that keeps
// candidate discovery away from all-pairs comparison.
ItemsSharingEntities(ctx context.Context, workspaceID string, entityIDs []string, excludeItem string, limit int) ([]string, error)
RemoveItemRefs(ctx context.Context, workspaceID, itemID string) (orphaned []string, err error)
DeleteMany(ctx context.Context, workspaceID string, ids []string) error
Count(ctx context.Context, workspaceID string) (int64, error)
}
// AliasRepository stores candidate and confirmed entity aliases.
type AliasRepository interface {
Upsert(ctx context.Context, a *EntityAlias) error
ListByEntity(ctx context.Context, workspaceID, entityID string) ([]*EntityAlias, error)
List(ctx context.Context, workspaceID string, status AliasStatus, p Page) ([]*EntityAlias, int64, error)
SetStatus(ctx context.Context, workspaceID, id string, st AliasStatus) error
DeleteByEntity(ctx context.Context, workspaceID, entityID string) error
}
// ClaimRepository stores extracted claims.
type ClaimRepository interface {
ReplaceForItem(ctx context.Context, workspaceID, itemID string, claims []*Claim) error
ListByItem(ctx context.Context, workspaceID, itemID string) ([]*Claim, error)
// FindBySubjects locates claims about the same coarse subject, which is how
// support/contradiction candidates are found cheaply.
FindBySubjects(ctx context.Context, workspaceID string, subjects []string, excludeItem string, limit int) ([]*Claim, error)
GetMany(ctx context.Context, workspaceID string, ids []string) ([]*Claim, error)
DeleteByItem(ctx context.Context, workspaceID, itemID string) error
Count(ctx context.Context, workspaceID string) (int64, error)
}
// RelationshipFilter selects relationships by typed values.
type RelationshipFilter struct {
Types []RelationshipType
Provenances []Provenance
ReviewStatus []ReviewStatus
MinScore float64
NodeID string
NodeKind NodeKind
Direction string // "out" | "in" | "both"
ConfirmedOnly bool
}
// RelationshipRepository stores relationships and their review history.
type RelationshipRepository interface {
// UpsertByDedupeKey inserts or refreshes a relationship without creating a
// duplicate on reprocessing. It never overwrites a human review decision.
UpsertByDedupeKey(ctx context.Context, r *Relationship) (*Relationship, error)
// FindByDedupeKey returns the relationship with this dedupe key, or
// ErrNotFound. It exists so callers can locate an existing link without
// paging through a node's relationships — a well-connected item can have
// more than one page of them, and a paged scan would miss the match and
// then collide with the unique index.
FindByDedupeKey(ctx context.Context, workspaceID, dedupeKey string) (*Relationship, error)
Create(ctx context.Context, r *Relationship) error
Get(ctx context.Context, workspaceID, id string) (*Relationship, error)
List(ctx context.Context, workspaceID string, f RelationshipFilter, p Page) ([]*Relationship, int64, error)
ListForNode(ctx context.Context, workspaceID string, kind NodeKind, id string, f RelationshipFilter) (out, in []*Relationship, err error)
// ListForItems fetches relationships touching any of the given items, used
// for relationship expansion during search.
ListForItems(ctx context.Context, workspaceID string, itemIDs []string, minScore float64) ([]*Relationship, error)
Update(ctx context.Context, r *Relationship) error
AppendReview(ctx context.Context, workspaceID, id string, ev ReviewEvent, newStatus ReviewStatus, newType RelationshipType, newProv Provenance) (*Relationship, error)
TombstoneForItem(ctx context.Context, workspaceID, itemID string) (int64, error)
DeleteForItem(ctx context.Context, workspaceID, itemID string) (int64, error)
Count(ctx context.Context, workspaceID string) (int64, error)
CountPendingReview(ctx context.Context, workspaceID string) (int64, error)
}
// JobRepository is the persistent queue.
type JobRepository interface {
Enqueue(ctx context.Context, j *Job) error
// Claim atomically leases the next runnable job for owner.
Claim(ctx context.Context, owner string, types []JobType, lease time.Duration) (*Job, error)
// Heartbeat extends a lease held by owner; it fails if the lease was lost.
Heartbeat(ctx context.Context, id, owner string, lease time.Duration) error
Complete(ctx context.Context, id, owner string, att JobAttempt) error
Fail(ctx context.Context, id, owner string, att JobAttempt, retryAfter *time.Time) error
Get(ctx context.Context, id string) (*Job, error)
List(ctx context.Context, workspaceID string, statuses []JobStatus, types []JobType, p Page) ([]*Job, int64, error)
Retry(ctx context.Context, workspaceID, id string) (*Job, error)
CountByStatus(ctx context.Context, workspaceID string) (map[JobStatus]int64, error)
// ReleaseExpired returns jobs whose lease elapsed to the pending pool.
ReleaseExpired(ctx context.Context, now time.Time) (int64, error)
DeleteForItem(ctx context.Context, workspaceID, itemID string) (int64, error)
}
// AuditRepository is append-only.
type AuditRepository interface {
Record(ctx context.Context, ev *AuditEvent) error
List(ctx context.Context, workspaceID string, p Page) ([]*AuditEvent, int64, error)
}
// RunRepository stores processing run history.
type RunRepository interface {
Create(ctx context.Context, r *ProcessingRun) error
Finish(ctx context.Context, id, outcome, errMsg string, stats map[string]int) error
ListByItem(ctx context.Context, workspaceID, itemID string, limit int) ([]*ProcessingRun, error)
}
// ModelVersionRepository tracks which models have produced stored data.
type ModelVersionRepository interface {
Observe(ctx context.Context, workspaceID, kind, name string, dims int) error
List(ctx context.Context, workspaceID string) ([]*ModelVersion, error)
}
// UserRepository stores local accounts.
type UserRepository interface {
Create(ctx context.Context, u *User) error
GetByUsername(ctx context.Context, username string) (*User, error)
GetByID(ctx context.Context, id string) (*User, error)
Update(ctx context.Context, u *User) error
Count(ctx context.Context) (int64, error)
// GetByOAuthSubject finds a federated account by provider and subject, the
// only pair a provider promises not to reassign. Returns ErrNotFound when
// the person has not signed in before.
GetByOAuthSubject(ctx context.Context, provider, subject string) (*User, error)
}
// APITokenRepository stores non-interactive API credentials. Only hashes are
// held; there is deliberately no method that returns a token's secret.
type APITokenRepository interface {
Create(ctx context.Context, t *APIToken) error
// GetByHash finds a token by the SHA-256 of its secret. The lookup is by
// the full hash, so a partial match cannot narrow the search space.
GetByHash(ctx context.Context, hash string) (*APIToken, error)
// ListForUser returns a person's tokens, newest first, including revoked
// and expired ones so the history stays visible.
ListForUser(ctx context.Context, workspaceID, userID string) ([]*APIToken, error)
Get(ctx context.Context, workspaceID, id string) (*APIToken, error)
Revoke(ctx context.Context, workspaceID, id, revokedBy string, at time.Time) error
// TouchLastUsed records use. Callers throttle this so a busy token does not
// turn every read into a write.
TouchLastUsed(ctx context.Context, id string, at time.Time) error
// DeleteForUser removes a user's tokens, used when an account is removed.
DeleteForUser(ctx context.Context, workspaceID, userID string) (int64, error)
}
// SessionRepository stores browser sessions.
type SessionRepository interface {
Create(ctx context.Context, s *Session) error
GetByTokenHash(ctx context.Context, hash string) (*Session, error)
Touch(ctx context.Context, id string, lastSeen, expires time.Time) error
Revoke(ctx context.Context, id string) error
RevokeAllForUser(ctx context.Context, userID string) error
DeleteExpired(ctx context.Context, before time.Time) (int64, error)
}
// WorkspaceRepository stores workspaces.
type WorkspaceRepository interface {
Create(ctx context.Context, w *Workspace) error
Get(ctx context.Context, id string) (*Workspace, error)
GetBySlug(ctx context.Context, slug string) (*Workspace, error)
List(ctx context.Context) ([]*Workspace, error)
}