Code sync, security fixes
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user