driven

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrInventoryNotSupported = errors.New("inventory not supported by this connector")

ErrInventoryNotSupported is returned from Inventory by connectors whose upstream API natively signals deletions (so the orchestrator's snapshot-diff reconciliation is not needed and would be wasteful). OneDrive is the canonical example — its delta API emits @removed tombstones, so it declares no ReconciliationScopes and this sentinel guards against future callers invoking Inventory directly.

View Source
var ErrRESTUnsupported = errors.New("connector does not expose a REST client")

ErrRESTUnsupported is returned by RESTClient implementations attached to connectors that do not speak HTTP — the local filesystem connector is the canonical example. Callers should treat this as a hard signal that the connector is not a valid target for the operation and bail out rather than retry.

Functions

This section is empty.

Types

type AICredentials

type AICredentials struct {
	APIKey  string
	BaseURL string // Optional custom base URL
}

AICredentials holds AI provider API keys and base URLs from environment variables

type AIServiceFactory

type AIServiceFactory interface {
	// CreateEmbeddingService creates an embedding service from settings and credentials.
	// Credentials come from ConfigProvider (environment variables).
	// Returns error if settings are invalid or service creation fails.
	CreateEmbeddingService(settings *domain.EmbeddingSettings, credentials *AICredentials) (EmbeddingService, error)

	// CreateLLMService creates an LLM service from settings and credentials.
	// Credentials come from ConfigProvider (environment variables).
	// Returns error if settings are invalid or service creation fails.
	CreateLLMService(settings *domain.LLMSettings, credentials *AICredentials) (LLMService, error)
}

AIServiceFactory creates AI services based on configuration. Credentials are provided separately from domain settings.

type AuthAdapter

type AuthAdapter interface {
	// Password operations
	HashPassword(password string) (string, error)
	VerifyPassword(password, hash string) bool

	// Token operations
	GenerateToken(claims *domain.TokenClaims) (string, error)
	ParseToken(token string) (*domain.TokenClaims, error)
}

AuthAdapter handles authentication cryptographic operations. This does NOT handle storage - use SessionStore for session persistence.

type AuthorizationCodeStore added in v0.2.1

type AuthorizationCodeStore interface {
	// Save stores a new authorization code
	Save(ctx context.Context, code *domain.AuthorizationCode) error

	// GetAndMarkUsed atomically retrieves the code and marks it as used.
	// This ensures single-use semantics for authorization codes.
	// Returns domain.ErrNotFound if code doesn't exist.
	GetAndMarkUsed(ctx context.Context, code string) (*domain.AuthorizationCode, error)

	// Cleanup removes expired authorization codes.
	// Should be called periodically (e.g., via cron job).
	Cleanup(ctx context.Context) error
}

AuthorizationCodeStore manages short-lived authorization codes for the auth code flow

type Capabilities

type Capabilities struct {
	// OAuth providers that are configured via environment variables
	OAuthProviders []domain.PlatformType

	// AI providers available for embedding
	EmbeddingProviders []domain.AIProvider

	// AI providers available for LLM
	LLMProviders []domain.AIProvider

	// SearchEngineAvailable indicates if a search engine (e.g. OpenSearch) was initialized
	SearchEngineAvailable bool

	// VectorStoreAvailable indicates if a vector store (e.g. pgvector) was initialized
	VectorStoreAvailable bool

	// Operational boundaries from environment variables
	Limits OperationalLimits
}

Capabilities represents what features are available based on environment configuration

type CapabilityStore

type CapabilityStore interface {
	// GetPreferences returns the persisted preferences for a team. Empty
	// preferences (no rows) are not an error — the returned struct's
	// Toggles map is empty and consumers fall back to descriptor defaults.
	GetPreferences(ctx context.Context, teamID string) (*domain.CapabilityPreferences, error)

	// SetToggles applies a partial update: each entry in toggles is
	// upserted as one row keyed on (team_id, capability_type). Toggles
	// not present in the map are left unchanged in storage.
	//
	// The empty map is a valid input (no-op). Implementations MUST be
	// idempotent — repeating a SetToggles call with the same input is
	// safe and produces the same end state.
	SetToggles(ctx context.Context, teamID string, toggles map[domain.CapabilityType]bool) error
}

CapabilityStore persists per-team capability toggle preferences.

Storage is conceptually one row per (team, capability_type) toggle. Implementations expose this as a flat domain.CapabilityPreferences map keyed on CapabilityType, so service-side code stays generic — there are no typed columns per capability.

Capabilities absent from the persisted set fall back to the descriptor's DefaultEnabled at resolution time. Implementations MUST NOT manufacture rows for capabilities the operator has not explicitly toggled — the difference between "explicitly true", "explicitly false", and "default" is meaningful.

type ChunkOptions

type ChunkOptions struct {
	MaxChunkSize int // Maximum characters per chunk
	Overlap      int // Character overlap between chunks
}

ChunkOptions configures chunking behavior

func DefaultChunkOptions

func DefaultChunkOptions() ChunkOptions

DefaultChunkOptions returns sensible defaults

type ChunkResult

type ChunkResult struct {
	Content   string
	StartChar int
	EndChar   int
	Position  int
}

ChunkResult represents a chunk with position info

type Chunker

type Chunker interface {
	// Chunk splits content into chunks
	Chunk(content string, opts ChunkOptions) []ChunkResult
}

Chunker splits document content into searchable chunks

type CompositeDocumentDeleteObserver added in v0.4.0

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

CompositeDocumentDeleteObserver fans out delete events to multiple underlying observers. Each observer is invoked in registration order; observer errors are aggregated (not short-circuited) so a failure in one observer does not prevent later observers from running.

This composite is the canonical way to attach more than one cleanup concern to the same delete signal — e.g. permission tuple cleanup alongside entity-register cleanup. Wiring callers (main.go) construct the composite and pass it as the single DocumentDeleteObserver to SourceService and SyncOrchestrator.

Per Core's existing contract, observer errors are logged-and-ignored by the caller (sync/source service). This composite preserves that contract: it returns an aggregated error so the caller can log it, but the deletion itself is not affected by any observer failure.

func NewCompositeDocumentDeleteObserver added in v0.4.0

func NewCompositeDocumentDeleteObserver(observers ...DocumentDeleteObserver) *CompositeDocumentDeleteObserver

NewCompositeDocumentDeleteObserver constructs a fanout observer. Nil observers in the input are filtered out so callers can pass optionally-nil observers without manual nil checks.

func (*CompositeDocumentDeleteObserver) OnDocumentDeleted added in v0.4.0

func (c *CompositeDocumentDeleteObserver) OnDocumentDeleted(ctx context.Context, source *domain.Source, doc *domain.Document) error

OnDocumentDeleted invokes every underlying observer and aggregates errors. Observers run in the order they were provided to the constructor; later observers are not skipped when an earlier one returns an error.

func (*CompositeDocumentDeleteObserver) OnSourceDeleted added in v0.4.0

func (c *CompositeDocumentDeleteObserver) OnSourceDeleted(ctx context.Context, source *domain.Source) error

OnSourceDeleted is the source-cascade equivalent of OnDocumentDeleted. Same fan-out + aggregation semantics.

type ConfigProvider

type ConfigProvider interface {
	// GetOAuthCredentials returns OAuth client credentials for a platform.
	// Returns nil if the platform is not configured in environment variables.
	GetOAuthCredentials(platform domain.PlatformType) *OAuthCredentials

	// GetAICredentials returns AI provider credentials (API key, base URL).
	// Returns nil if the provider is not configured in environment variables.
	GetAICredentials(provider domain.AIProvider) *AICredentials

	// IsOAuthConfigured returns true if OAuth credentials are available for the platform.
	IsOAuthConfigured(platform domain.PlatformType) bool

	// IsAIConfigured returns true if AI credentials are available for the provider.
	IsAIConfigured(provider domain.AIProvider) bool

	// GetCapabilities returns information about what's available based on env configuration.
	// This is used for the /api/v1/capabilities endpoint.
	GetCapabilities() *Capabilities

	// GetBaseURL returns the application base URL for OAuth callbacks.
	GetBaseURL() string
}

ConfigProvider provides access to configuration from environment variables. This is a driven port that abstracts environment-based configuration. Implementation lives in internal/config/ (infrastructure layer).

type ConnectionStore

type ConnectionStore interface {
	// Save stores a new connection or updates an existing one.
	// Secrets are encrypted before storage.
	Save(ctx context.Context, conn *domain.Connection) error

	// Get retrieves a connection by ID with decrypted secrets.
	// Returns domain.ErrNotFound if the connection doesn't exist.
	Get(ctx context.Context, id string) (*domain.Connection, error)

	// List retrieves all connections as summaries (no secrets).
	List(ctx context.Context) ([]*domain.ConnectionSummary, error)

	// Delete removes a connection by ID.
	// Returns domain.ErrNotFound if the connection doesn't exist.
	Delete(ctx context.Context, id string) error

	// GetByPlatform retrieves connections for a platform type (no secrets).
	GetByPlatform(ctx context.Context, platform domain.PlatformType) ([]*domain.ConnectionSummary, error)

	// GetByAccountID retrieves a connection by platform type and account ID.
	// Returns nil if not found.
	GetByAccountID(ctx context.Context, platform domain.PlatformType, accountID string) (*domain.Connection, error)

	// GetByTenantID retrieves an app-only connection by platform type and tenant ID.
	// Returns nil if not found.
	GetByTenantID(ctx context.Context, platform domain.PlatformType, tenantID string) (*domain.Connection, error)

	// UpdateSecrets updates the encrypted secrets and OAuth metadata.
	// Used after token refresh.
	UpdateSecrets(ctx context.Context, id string, secrets *domain.ConnectionSecrets, expiry *time.Time) error

	// UpdateLastUsed updates the last_used_at timestamp.
	UpdateLastUsed(ctx context.Context, id string) error
}

ConnectionStore persists connector connections with encrypted secrets.

type Connector

type Connector interface {
	// Type returns the provider type.
	Type() domain.ProviderType

	// ValidateConfig validates source configuration.
	ValidateConfig(config domain.SourceConfig) error

	// FetchChanges fetches document changes since last sync.
	// Returns changes, next cursor, and error.
	// The cursor enables incremental sync - pass empty string for full sync.
	FetchChanges(ctx context.Context, source *domain.Source, cursor string) ([]*domain.Change, string, error)

	// FetchDocument fetches a single document by external ID.
	// Returns the document, content hash, and error.
	FetchDocument(ctx context.Context, source *domain.Source, externalID string) (*domain.Document, string, error)

	// TestConnection tests the connection to the source.
	TestConnection(ctx context.Context, source *domain.Source) error

	// ReconciliationScopes reports external-ID prefixes for which this
	// connector can produce a complete current-state Inventory. The
	// orchestrator uses the declared scopes to detect deletions: anything
	// stored under a scope's prefix that is absent from the latest Inventory
	// is treated as deleted upstream.
	//
	// Declare a scope only when the upstream API does NOT natively signal
	// deletes (GitHub issues/PRs, Notion pages and database entries).
	// Connectors whose delta API emits tombstones (OneDrive) should return
	// nil — reconciliation would be redundant and wasteful.
	ReconciliationScopes() []string

	// Inventory returns the complete set of canonical IDs currently present
	// upstream within the given scope prefix. The prefix is one of the
	// values from ReconciliationScopes.
	//
	// Contract:
	//   - The returned slice MUST be complete — every canonical ID the
	//     upstream source of truth holds under this prefix, right now.
	//   - On any partial-result condition (paginated fetch failure, rate
	//     limit exhausted mid-walk, etc.) Inventory MUST return an error
	//     rather than a partial slice. A partial inventory causes false
	//     positives in the orchestrator's delete diff and permanently
	//     erases real documents.
	//   - Archived / trashed items are treated as "no longer present" and
	//     MUST be omitted from the inventory (not included).
	//   - Connectors that do not support inventory (empty scopes) return
	//     ErrInventoryNotSupported.
	Inventory(ctx context.Context, source *domain.Source, scope string) ([]string, error)

	// RESTClient returns a RESTClient that reuses this connector's auth,
	// rate-limiting, and retry plumbing for callers that need to invoke
	// upstream REST endpoints not covered by the typed methods above.
	//
	// MUST always return a non-nil value. Connectors without a REST surface
	// (e.g. local filesystem) return a sentinel whose Do always returns
	// ErrRESTUnsupported — callers type-check the error rather than the
	// return value.
	RESTClient() RESTClient
}

Connector fetches documents from a source provider. Connectors are created by ConnectorBuilder with resolved credentials.

type ConnectorBuilder

type ConnectorBuilder interface {
	// Type returns the provider type this builder creates.
	Type() domain.ProviderType

	// Build creates a connector scoped to a specific container.
	// The containerID comes from source.SelectedContainers (one per sync job).
	// For providers that don't support container selection, containerID may be empty.
	// The TokenProvider handles credential retrieval and OAuth token refresh.
	Build(ctx context.Context, tokenProvider TokenProvider, containerID string) (Connector, error)

	// SupportsOAuth returns true if this connector uses OAuth authentication.
	SupportsOAuth() bool

	// OAuthConfig returns OAuth configuration for this provider.
	// Returns nil if the provider doesn't support OAuth.
	OAuthConfig() *OAuthConfig

	// SupportsContainerSelection returns true if this connector supports container picking.
	// If true, admins can select specific containers (repos, drives, spaces) to index.
	// If false, the connector indexes all accessible content.
	SupportsContainerSelection() bool
}

ConnectorBuilder creates connector instances for a specific provider type. Each provider has its own builder registered with the ConnectorFactory.

type ConnectorFactory

type ConnectorFactory interface {
	// Register registers a connector builder for a provider type.
	Register(builder ConnectorBuilder)

	// Create creates a connector for the given source, scoped to a container.
	// Called by SyncOrchestrator once per container in source.SelectedContainers.
	// For providers without container selection, containerID may be empty.
	Create(ctx context.Context, source *domain.Source, containerID string) (Connector, error)

	// SupportedTypes returns all registered provider types.
	SupportedTypes() []domain.ProviderType

	// GetBuilder returns the builder for a provider type.
	GetBuilder(providerType domain.ProviderType) (ConnectorBuilder, error)

	// SupportsOAuth returns true if the provider supports OAuth.
	SupportsOAuth(providerType domain.ProviderType) bool

	// GetOAuthConfig returns OAuth config for a provider.
	GetOAuthConfig(providerType domain.ProviderType) *OAuthConfig
}

ConnectorFactory manages connector builders and creates connectors.

type Container

type Container struct {
	// ID is the provider-specific identifier.
	// Format varies by provider:
	//   - GitHub: "owner/repo"
	//   - Google Drive: drive ID
	//   - Confluence: space key
	ID string `json:"id"`

	// Name is the human-readable display name.
	Name string `json:"name"`

	// Description is an optional description of the container.
	Description string `json:"description,omitempty"`

	// Type identifies the container type.
	// Examples: "repository", "shared_drive", "space", "project", "channel"
	Type string `json:"type"`

	// HasChildren indicates if this container has child containers (for folder navigation).
	HasChildren bool `json:"has_children,omitempty"`

	// Metadata contains provider-specific additional data.
	// Values may be any JSON-encodable type (booleans, ints, strings).
	// Examples:
	//   - GitHub: {"owner": "octocat", "private": true, "archived": false}
	//   - Google Drive: {"shared": true}
	Metadata map[string]any `json:"metadata,omitempty"`
}

Container represents a top-level container that can be selected for indexing. Examples:

  • GitHub: repository (owner/repo)
  • Google Drive: shared drive
  • Confluence: space
  • Jira: project
  • Notion: workspace
  • Slack: channel

type ContainerLister

type ContainerLister interface {
	// ListContainers lists containers accessible with the current credentials.
	// Pagination is handled via cursor - pass empty string for the first page.
	// parentID can be used to list children of a specific container (for folder navigation).
	// Pass empty string for root level containers.
	// Returns:
	//   - containers: list of available containers
	//   - nextCursor: cursor for the next page, empty if no more pages
	//   - err: any error that occurred
	ListContainers(ctx context.Context, cursor string, parentID string) ([]*Container, string, error)
}

ContainerLister lists available containers for a connector. Not all connectors support container selection - some index all accessible content.

The ContainerLister is typically retrieved from the ConnectorFactory for a specific provider type and installation.

type ContainerListerFactory

type ContainerListerFactory interface {
	// Create creates a ContainerLister for the given provider and installation.
	// Returns nil if the provider doesn't support container selection.
	Create(ctx context.Context, providerType domain.ProviderType, installationID string) (ContainerLister, error)

	// SupportsContainerSelection returns true if the provider supports container selection.
	// If false, the connector indexes all accessible content.
	SupportsContainerSelection(providerType domain.ProviderType) bool
}

ContainerListerFactory creates ContainerListers for different providers.

type ContentExtractor

type ContentExtractor interface {
	// Extract extracts text content from raw data
	Extract(ctx context.Context, data []byte, mimeType string) (string, error)

	// SupportedTypes returns supported MIME types
	SupportedTypes() []string
}

ContentExtractor extracts text content from various file formats

type ContentFilter added in v0.2.1

type ContentFilter interface {
	// ShouldFetchContent determines if content should be fetched for a file.
	// It combines path pattern matching and MIME type detection to make the decision.
	// Returns:
	//   - shouldFetch: true if content should be fetched and processed
	//   - mimeType: detected MIME type from the file path
	ShouldFetchContent(ctx context.Context, path string, settings *domain.SyncExclusionSettings) (shouldFetch bool, mimeType string)

	// GetMimeType returns the MIME type for a file path based on its extension or filename.
	// This provides a single source of truth for MIME type detection across the application.
	GetMimeType(path string) string
}

ContentFilter determines whether content should be fetched and processed based on file path patterns and MIME type exclusions. This port enables early filtering before content is fetched, reducing wasted resources.

type CredentialsStore

type CredentialsStore interface {
	// Save stores credentials (encrypts sensitive fields)
	Save(ctx context.Context, creds *domain.Credentials) error

	// Get retrieves credentials by ID
	Get(ctx context.Context, id string) (*domain.Credentials, error)

	// List retrieves all credentials
	List(ctx context.Context) ([]*domain.Credentials, error)

	// Delete deletes credentials
	Delete(ctx context.Context, id string) error

	// GetByProvider retrieves credentials for a provider type
	GetByProvider(ctx context.Context, providerType domain.ProviderType) ([]*domain.Credentials, error)
}

CredentialsStore handles credential persistence (PostgreSQL, encrypted)

type DBCredentialProvider added in v0.3.0

type DBCredentialProvider interface {
	// Resolve returns the full Postgres DSN
	// (e.g. "postgres://user:pass@host:port/db?sslmode=...").
	//
	// Implementations that perform network I/O (secrets managers, IAM)
	// should honour ctx for cancellation and timeouts. Implementations that
	// read purely from process state (env, file) may ignore ctx. A non-nil
	// error indicates the credential could not be resolved and the binary
	// must refuse to start.
	Resolve(ctx context.Context) (string, error)
}

DBCredentialProvider resolves the application's Postgres connection string at startup.

It is the seam between the binary and whatever source ultimately holds the DB credential. The default implementation reads DATABASE_URL from the process environment. Other implementations (HashiCorp Vault, AWS Secrets Manager, IAM auth tokens, etc.) are out of scope for this port's package but are expected to plug in here without changes to the core or to the postgres adapter.

The returned value is an opaque, libpq-compatible DSN. The core does not parse or interpret it; only the postgres adapter does. Resolve is called exactly once at boot, before sql.Open.

type DistributedLock

type DistributedLock interface {
	// Acquire attempts to acquire a named lock with the given TTL.
	// Returns true if the lock was successfully acquired, false if already held by another instance.
	// The lock will automatically expire after TTL (implementation dependent).
	Acquire(ctx context.Context, name string, ttl time.Duration) (acquired bool, err error)

	// Release releases a named lock.
	// This is best-effort; implementations with TTL will auto-expire anyway.
	// Safe to call even if the lock is not held or has expired.
	Release(ctx context.Context, name string) error

	// Extend extends the TTL of a currently held lock.
	// Returns error if the lock is not held by this instance.
	// Note: Not all implementations support TTL extension (e.g., PostgreSQL advisory locks).
	Extend(ctx context.Context, name string, ttl time.Duration) error

	// Ping checks if the lock backend is healthy.
	Ping(ctx context.Context) error
}

DistributedLock provides distributed locking for coordinating work across instances. This is used to prevent duplicate execution in multi-instance deployments.

type DocumentDeleteObserver added in v0.3.0

type DocumentDeleteObserver interface {
	OnDocumentDeleted(ctx context.Context, source *domain.Source, doc *domain.Document) error
	OnSourceDeleted(ctx context.Context, source *domain.Source) error
}

DocumentDeleteObserver is invoked when documents or sources are deleted. Implementations may perform arbitrary cleanup: side-index pruning, audit writes, tombstone emission, etc.

This is the deletion-side mirror of DocumentIngestObserver. Same posture: fire after the underlying delete succeeds, log-and-continue on observer errors so observer health does not affect deletion correctness.

Contract:

  • OnDocumentDeleted is called once per document, after that document has been removed from the document store. It fires for both upstream-driven deletions (SyncOrchestrator.processDelete) and admin-driven cascades (SourceService.Delete). Observers see every deletion regardless of origin.
  • OnSourceDeleted is called once after the source has been removed from the source store. Per-document events for that source's documents fire first (during the cascade), then OnSourceDeleted fires last.
  • The source/document values passed in are captured before the underlying delete and are safe to read inside the observer.
  • Returned errors are logged and ignored — observer failure does NOT fail the deletion. This matches the nil-guarded log-and-continue pattern used by DocumentIngestObserver.

type DocumentIDProvider added in v0.2.2

type DocumentIDProvider interface {
	// GetAllowedDocumentIDs returns the document IDs that the caller is authorised
	// to see, using a three-case contract:
	//
	//   - nil return (and nil error): the provider declines to filter. Downstream
	//     treats this as "no filter" and passes the query through unchanged.
	//
	//   - empty slice return ([]string{}, nil): authoritative deny-all. Downstream
	//     MUST return zero results. This is the critical case that distinguishes
	//     this contract from the previous fail-open design — implementations MUST
	//     return an empty slice (not nil) when asserting that the caller has access
	//     to zero documents.
	//
	//   - non-empty slice return: authoritative allow-list. Downstream filters
	//     results to exactly these IDs.
	//
	//   - non-nil error: propagated; the query is rejected. Do not return a nil
	//     slice with an error to mean "deny on uncertainty" — use the error path.
	//
	// The pipeline stage consuming this provider translates the return into a
	// *domain.DocumentIDFilter: nil → leave the filter unset; non-nil (including
	// empty) → &DocumentIDFilter{Apply: true, IDs: <return>}.
	GetAllowedDocumentIDs(ctx context.Context, query string, filters pipeline.SearchFilters) ([]string, error)
}

DocumentIDProvider provides document IDs for pre-filtering search results. Implementations can apply ACL, tenant isolation, or other authoritative filtering logic.

type DocumentIngestObserver added in v0.3.0

type DocumentIngestObserver interface {
	OnDocumentIngested(ctx context.Context, source *domain.Source, doc *domain.Document) error
}

DocumentIngestObserver is invoked once per document after successful ingestion. Implementations may perform arbitrary post-processing: secondary indexing, cache warming, audit writes, etc.

Contract:

  • Called once per successfully-saved document.
  • Not called when documentStore.Save fails.
  • Invoked asynchronously: the orchestrator dispatches the call from a bounded goroutine pool and returns from the per-doc processing path before the observer completes. Implementations MUST be goroutine-safe because the same observer may be called concurrently from multiple goroutines (one per in-flight document) and may run after the sync function has returned.
  • The ctx passed to OnDocumentIngested is detached from the orchestrator's request context and bounded by SyncOrchestratorConfig. OnDocumentIngestedTimeout (default 30s). Observers honouring the context will be cancelled on timeout; observers that ignore it run to completion without affecting sync correctness.
  • Returned errors are logged and ignored — observer failure does NOT fail the ingest. This matches the nil-guarded log-and-continue pattern used by other lifecycle observers.
  • Callers that need to wait for in-flight observers (tests, graceful shutdown) call SyncOrchestrator.WaitForObservers.

type DocumentResult

type DocumentResult struct {
	DocumentID string
	SourceID   string
	Title      string
	Content    string // Full document content (for snippet extraction)
	Score      float64
}

DocumentResult represents a document-level search result from BM25.

type DocumentRetrievedEvent added in v0.3.0

type DocumentRetrievedEvent struct {
	UserID     string
	DocumentID string
	DurationNs int64
	ClientType string
	ClientID   string
	ClientName string
}

DocumentRetrievedEvent carries the outcome of a single-document fetch.

ClientID and ClientName follow the same semantics as on SearchCompletedEvent: they identify the calling application and are optional — empty is valid whenever the concept doesn't apply.

type DocumentStore

type DocumentStore interface {
	// Save creates or updates a document
	Save(ctx context.Context, doc *domain.Document) error

	// SaveBatch saves multiple documents in a transaction
	SaveBatch(ctx context.Context, docs []*domain.Document) error

	// Get retrieves a document by ID
	Get(ctx context.Context, id string) (*domain.Document, error)

	// GetByIDs retrieves multiple documents by ID in a single round trip.
	// The returned map is keyed by document ID; missing IDs simply don't
	// appear (this is not an error). Used by the search service to avoid
	// N database queries when materialising ranked search results.
	GetByIDs(ctx context.Context, ids []string) (map[string]*domain.Document, error)

	// GetByExternalID retrieves a document by source and external ID
	GetByExternalID(ctx context.Context, sourceID, externalID string) (*domain.Document, error)

	// GetBySource retrieves all documents for a source with pagination
	GetBySource(ctx context.Context, sourceID string, limit, offset int) ([]*domain.Document, error)

	// Delete deletes a document
	Delete(ctx context.Context, id string) error

	// DeleteBySource deletes all documents for a source
	DeleteBySource(ctx context.Context, sourceID string) error

	// DeleteBySourceAndContainer deletes all documents for a specific container within a source
	DeleteBySourceAndContainer(ctx context.Context, sourceID, containerID string) error

	// DeleteBatch deletes multiple documents by ID
	DeleteBatch(ctx context.Context, ids []string) error

	// Count returns total document count
	Count(ctx context.Context) (int, error)

	// CountBySource returns document count for a source
	CountBySource(ctx context.Context, sourceID string) (int, error)

	// ListExternalIDs returns all external IDs for a source (for diff sync)
	ListExternalIDs(ctx context.Context, sourceID string) ([]string, error)
}

DocumentStore handles document persistence (PostgreSQL)

type EmbeddingService

type EmbeddingService interface {
	// Embed generates embeddings for multiple texts
	Embed(ctx context.Context, texts []string) ([][]float32, error)

	// EmbedQuery generates an embedding for a search query
	// May use different model/parameters optimized for queries
	EmbedQuery(ctx context.Context, query string) ([]float32, error)

	// Dimensions returns the embedding dimension size
	Dimensions() int

	// Model returns the model name being used
	Model() string

	// HealthCheck verifies the embedding service is available
	HealthCheck(ctx context.Context) error

	// Close releases resources held by the embedding service
	Close() error
}

EmbeddingService generates text embeddings

type EntityAnalysis added in v0.4.0

type EntityAnalysis struct {
	// DocumentID identifies the document this analysis belongs to.
	DocumentID string `json:"document_id"`

	// ContentSHA256 is the hex-encoded SHA-256 digest of the document content
	// at the time this analysis was produced. Together with AnalyzerVersion it
	// forms the cache validity key: a stored row is a hit only when both fields
	// match the incoming request.
	ContentSHA256 string `json:"content_sha256"`

	// AnalyzerVersion is an opaque token that identifies the detector
	// configuration used to produce this analysis. Changing the detector or its
	// configuration should produce a new AnalyzerVersion so that stale cached
	// results are not served.
	AnalyzerVersion string `json:"analyzer_version"`

	// RulesetVersion records the taxonomy version in effect when this analysis
	// was produced. Reserved for future taxonomy-evolution invalidation; the
	// entity-extractor stage does not currently use it to invalidate cached rows.
	RulesetVersion int `json:"ruleset_version"`

	// Spans contains the entity spans detected in the document content.
	Spans []pipeline.EntitySpan `json:"spans"`

	// AnalyzedAt is the Unix timestamp (seconds) at which this analysis was
	// produced and stored.
	AnalyzedAt int64 `json:"analyzed_at"`
}

EntityAnalysis is the cached result of running entity detection over a specific version of a document's content. It carries enough information to decide whether a stored result is still valid for the current document and detector configuration.

type EntityRegister added in v0.4.0

type EntityRegister interface {
	// Get retrieves a cached EntityAnalysis for the given document, content
	// digest, and analyzer version.
	//
	// It applies a three-case contract on the return:
	//
	//   - hit (*EntityAnalysis, true, nil): the store contains a row whose
	//     doc_id, content_sha256, AND analyzer_version all match the request.
	//     The returned analysis may be used directly; the consuming stage must
	//     not re-run detection.
	//
	//   - miss (nil, false, nil): the store either has no row for doc_id, or
	//     has a row whose content_sha256 or analyzer_version does not match.
	//     Both cases are misses, not errors. The consuming stage should fall
	//     through to the detector. Implementations must not return a non-nil
	//     error to signal a mismatch.
	//
	//   - storage error (nil, false, non-nil error): a problem with the
	//     underlying store prevented the read. The consuming stage treats this
	//     the same as a miss — it falls through to the detector — but it logs
	//     the error at warn level so operators can observe store degradation.
	//
	// The content_sha256 and analyzer_version arguments are the values computed
	// by the consuming stage for the current request; implementations must not
	// derive them independently.
	Get(ctx context.Context, docID, contentSHA256, analyzerVersion string) (*EntityAnalysis, bool, error)

	// Put stores an EntityAnalysis, keyed on (doc_id, analyzer_version).
	//
	// Put is an upsert: if a row with the same (doc_id, analyzer_version) key
	// already exists it is overwritten. This allows a content change (new
	// content_sha256) to invalidate the previous result for the same document
	// and analyzer version.
	//
	// Callers treat Put as best-effort: the consuming stage logs a warn on
	// error but continues to attach the freshly-detected spans to the candidate
	// regardless of whether caching succeeded.
	Put(ctx context.Context, analysis *EntityAnalysis) error

	// DeleteByDocument removes every cached analysis row for the given
	// document, across all content_sha256 / analyzer_version combinations.
	// Called from the document-delete observer chain so cache rows do not
	// outlive the documents they describe.
	//
	// Removing a non-existent document is not an error — the operation is
	// idempotent. Implementations MUST return any storage-level error
	// unchanged so the caller can log and continue (observers are
	// log-and-continue: a delete-cleanup failure must not prevent the
	// document deletion itself from succeeding).
	DeleteByDocument(ctx context.Context, docID string) error
}

EntityRegister is a cache for entity analysis results, keyed on a combination of document ID, content digest, and analyzer version. It allows the entity-extractor stage to skip re-detection when the document content and detector configuration have not changed since the last analysis.

type ExtractionContextStore added in v0.4.0

type ExtractionContextStore interface {
	Get(ctx context.Context) (string, error)
	Set(ctx context.Context, context string) error
}

ExtractionContextStore exposes a single admin-editable string that the entity-extractor stage appends to its LLM prompt as additional domain context (e.g. "this corpus contains insurance case files; CL-NNNN-NNNN patterns are claim numbers"). The store backs a singleton row — the taxonomy of categories is governed elsewhere; this is purely a free-form instructional bias for the model.

Get returns the current context string. An empty string is the default-installed value and means "no additional context"; callers must be tolerant of empty strings.

Set replaces the singleton row's content. Implementations must be idempotent and safe under concurrent calls (last writer wins).

type LLMService

type LLMService interface {
	// Complete sends a completion request to the LLM and returns the response.
	// This is the primary method for all LLM interactions.
	Complete(ctx context.Context, req domain.CompletionRequest) (domain.CompletionResponse, error)

	// Model returns the model name being used (e.g., "gpt-4o", "claude-3-5-sonnet")
	Model() string

	// Ping verifies the LLM service is reachable and properly configured
	Ping(ctx context.Context) error

	// Close releases resources held by the LLM service
	Close() error
}

LLMService provides a generic completion interface for Large Language Models. This is a pure provider interface - task-specific logic (query expansion, summarization, etc.) is implemented in pipeline stages.

type Limiter added in v0.4.0

type Limiter interface {
	Wait(ctx context.Context, weight int64) error
	Update(remaining int64, resetAt time.Time)
}

Limiter is a generic, provider-agnostic rate-limit budget. It serves two roles to the consumer:

  1. Pre-flight back-pressure: callers ask Wait(ctx, weight) before making a rate-limited call. Wait blocks until the budget can service the weighted request, or returns ctx.Err() if the context is cancelled while waiting. Concurrent Waits must serialise through the underlying bucket so multiple goroutines share one budget.

  2. Post-response reconciliation: after a successful response from the remote service, the consumer calls Update with the authoritative remaining-budget and reset-time signalled by the service (typically read from x-ratelimit-* response headers). The Limiter adjusts its internal accounting so subsequent Waits reflect the server's view.

The "weight" is provider-defined and opaque to the Limiter. For token-priced APIs it is the estimated token count of the request; for request-priced APIs it is 1; for cost-budget Limiters it would be the estimated dollar cost. The Limiter does not interpret the units.

Implementations must be safe for concurrent use. Update is fire-and- forget — implementations may not return errors, must not block on I/O, and must serialise correctly with in-flight Waits.

type Normaliser

type Normaliser interface {
	// Normalise transforms raw content into normalized text.
	// The mimeType helps determine the appropriate processing.
	Normalise(content string, mimeType string) string

	// SupportedTypes returns MIME types this normaliser handles.
	// Can include wildcards like "text/*" or specific types like "text/markdown".
	SupportedTypes() []string

	// Priority returns the normaliser priority (higher = more specific).
	// Priority ranges:
	//   90-100: Connector-specific (e.g., Gmail email normaliser)
	//   50-89:  Format-specific (PDF, Markdown, HTML)
	//   10-49:  Generic (basic text processing)
	//   1-9:    Fallback (raw text extraction)
	Priority() int
}

Normaliser normalizes raw document content for indexing. It transforms provider-specific document formats into normalized text.

type NormaliserRegistry

type NormaliserRegistry interface {
	// Get retrieves the best-matching normaliser for a MIME type.
	// Returns nil if no normaliser is registered for the type.
	// When multiple match, the highest priority normaliser is returned.
	Get(mimeType string) Normaliser

	// GetAll retrieves all normalisers that match a MIME type, sorted by priority (highest first).
	GetAll(mimeType string) []Normaliser

	// Register registers a normaliser.
	Register(normaliser Normaliser)

	// List returns all registered MIME types.
	List() []string
}

NormaliserRegistry manages content normalisers. When multiple normalisers match a MIME type, the highest priority one is used.

type OAuthClientStore added in v0.2.1

type OAuthClientStore interface {
	// Save stores a new client or updates an existing one
	Save(ctx context.Context, client *domain.OAuthClient) error

	// Get retrieves a client by client_id
	// Returns domain.ErrNotFound if the client doesn't exist
	Get(ctx context.Context, clientID string) (*domain.OAuthClient, error)

	// Delete removes a client by client_id
	// Returns domain.ErrNotFound if the client doesn't exist
	Delete(ctx context.Context, clientID string) error

	// List retrieves all registered clients
	List(ctx context.Context) ([]*domain.OAuthClient, error)
}

OAuthClientStore manages OAuth 2.0 client registrations

type OAuthConfig

type OAuthConfig struct {
	// AuthURL is the authorization endpoint
	AuthURL string

	// TokenURL is the token exchange endpoint
	TokenURL string

	// Scopes are the required OAuth scopes
	Scopes []string

	// UserInfoURL is the endpoint to fetch user info (optional)
	UserInfoURL string
}

OAuthConfig contains OAuth settings for a provider.

type OAuthCredentials

type OAuthCredentials struct {
	ClientID     string
	ClientSecret string
}

OAuthCredentials holds OAuth client credentials from environment variables

type OAuthHandler

type OAuthHandler interface {
	// DefaultConfig returns the default OAuth configuration (auth URL, token URL, scopes)
	DefaultConfig() OAuthConfig

	// BuildAuthURL constructs the OAuth authorization URL with PKCE
	BuildAuthURL(clientID, redirectURI, state, codeChallenge string, scopes []string) string

	// ExchangeCode exchanges an authorization code for access tokens
	ExchangeCode(ctx context.Context, clientID, clientSecret, code, redirectURI, codeVerifier string) (*OAuthToken, error)

	// GetUserInfo retrieves user information using an access token
	GetUserInfo(ctx context.Context, accessToken string) (*OAuthUserInfo, error)

	// RefreshToken refreshes an expired access token
	RefreshToken(ctx context.Context, refreshToken string) (*OAuthToken, error)
}

OAuthHandler handles OAuth flow for a provider.

type OAuthHandlerFactory

type OAuthHandlerFactory interface {
	// GetOAuthHandler returns an OAuth handler for the given platform.
	// Returns nil if the platform doesn't support OAuth or isn't implemented.
	GetOAuthHandler(platform domain.PlatformType) OAuthHandler
}

OAuthHandlerFactory creates OAuth handlers for specific platforms. This abstracts the connectors.Factory dependency.

type OAuthState

type OAuthState struct {
	// State is a cryptographically random string used for CSRF protection.
	State string

	// ProviderType is the OAuth provider (github, google_drive, etc.)
	ProviderType string

	// CodeVerifier is the PKCE code verifier (plain text, not hashed).
	// This is used to generate code_challenge for the auth request
	// and sent as code_verifier during token exchange.
	CodeVerifier string

	// RedirectURI is the callback URL where the provider will redirect.
	RedirectURI string

	// ReturnContext indicates where to redirect after OAuth completes.
	// Values: "setup", "admin-sources", or empty for default behavior.
	ReturnContext string

	// CreatedAt is when the state was created.
	CreatedAt time.Time

	// ExpiresAt is when the state expires (typically 10 minutes).
	ExpiresAt time.Time
}

OAuthState represents a pending OAuth authorization flow state. Used for CSRF protection and PKCE code verifier storage.

type OAuthStateStore

type OAuthStateStore interface {
	// Save stores a new OAuth state.
	// The state typically expires in 10 minutes.
	Save(ctx context.Context, state *OAuthState) error

	// GetAndDelete atomically retrieves and deletes the state.
	// This ensures single-use semantics.
	// Returns nil, nil if the state doesn't exist or has expired.
	GetAndDelete(ctx context.Context, state string) (*OAuthState, error)

	// Cleanup removes expired states.
	// Should be called periodically (e.g., every hour) to clean up orphaned states.
	Cleanup(ctx context.Context) error
}

OAuthStateStore manages OAuth flow state for CSRF protection. States are single-use and expire after a short period.

type OAuthToken

type OAuthToken struct {
	AccessToken  string
	RefreshToken string
	ExpiresIn    int    // Seconds until expiry
	TokenType    string // Usually "Bearer"
	Scope        string // Space-separated scopes
}

OAuthToken represents OAuth tokens from a provider.

type OAuthTokenProvider

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

OAuthTokenProvider implements TokenProvider for OAuth credentials. It automatically refreshes tokens when they expire.

func NewOAuthTokenProvider

func NewOAuthTokenProvider(
	creds *domain.Credentials,
	refresher TokenRefresher,
	store CredentialsStore,
) *OAuthTokenProvider

NewOAuthTokenProvider creates a token provider for OAuth credentials.

func (*OAuthTokenProvider) AuthMethod

func (p *OAuthTokenProvider) AuthMethod() domain.AuthMethod

AuthMethod returns OAuth2.

func (*OAuthTokenProvider) GetAccessToken

func (p *OAuthTokenProvider) GetAccessToken(ctx context.Context) (string, error)

GetAccessToken returns a valid access token, refreshing if needed.

func (*OAuthTokenProvider) GetCredentials

func (p *OAuthTokenProvider) GetCredentials(ctx context.Context) (*domain.Credentials, error)

GetCredentials returns the full credentials.

func (*OAuthTokenProvider) IsValid

func (p *OAuthTokenProvider) IsValid(ctx context.Context) bool

IsValid checks if credentials are valid (not expired or can be refreshed).

type OAuthTokenStore added in v0.2.1

type OAuthTokenStore interface {
	// SaveAccessToken stores a new access token
	SaveAccessToken(ctx context.Context, token *domain.OAuthAccessToken) error

	// GetAccessToken retrieves an access token by its ID (jti claim)
	// Returns domain.ErrNotFound if the token doesn't exist
	GetAccessToken(ctx context.Context, tokenID string) (*domain.OAuthAccessToken, error)

	// RevokeAccessToken marks an access token as revoked
	// Returns domain.ErrNotFound if the token doesn't exist
	RevokeAccessToken(ctx context.Context, tokenID string) error

	// SaveRefreshToken stores a new refresh token
	SaveRefreshToken(ctx context.Context, token *domain.OAuthRefreshToken) error

	// GetRefreshToken retrieves a refresh token by its ID
	// Returns domain.ErrNotFound if the token doesn't exist
	GetRefreshToken(ctx context.Context, tokenID string) (*domain.OAuthRefreshToken, error)

	// RevokeRefreshToken marks a refresh token as revoked
	// Returns domain.ErrNotFound if the token doesn't exist
	RevokeRefreshToken(ctx context.Context, tokenID string) error

	// RotateRefreshToken marks the old token as rotated and returns the new token ID.
	// This implements refresh token rotation for enhanced security.
	// Returns domain.ErrNotFound if the old token doesn't exist.
	RotateRefreshToken(ctx context.Context, oldTokenID string, newTokenID string) error

	// RevokeAllForClient revokes all tokens (access and refresh) for a given client.
	// Used when a client is deleted or compromised.
	RevokeAllForClient(ctx context.Context, clientID string) error

	// ListUsersForClient returns the distinct user IDs that currently hold at
	// least one non-revoked, non-expired access token for the given clientID.
	// Returns a non-nil empty slice when no users are connected.
	ListUsersForClient(ctx context.Context, clientID string) ([]string, error)

	// ListClientsForUser returns the distinct client IDs the given user
	// currently holds at least one non-revoked, non-expired access token for.
	// Returns a non-nil empty slice when the user has no active connections.
	ListClientsForUser(ctx context.Context, userID string) ([]string, error)

	// Cleanup removes expired tokens.
	// Should be called periodically (e.g., via cron job).
	Cleanup(ctx context.Context) error
}

OAuthTokenStore manages access tokens and refresh tokens with revocation support

type OAuthUserInfo

type OAuthUserInfo struct {
	ID       string // Provider-specific user ID
	Email    string
	Name     string
	ImageURL string
}

OAuthUserInfo represents user info from OAuth provider.

type OperationalLimits

type OperationalLimits struct {
	SyncMinInterval   int // Minutes floor (default: 5)
	SyncMaxInterval   int // Minutes ceiling (default: 1440)
	MaxWorkers        int // Worker ceiling (default: 10)
	MaxResultsPerPage int // Results ceiling (default: 100)
}

OperationalLimits defines guardrails from environment configuration

type QueueStats

type QueueStats struct {
	// PendingCount is the number of tasks waiting to be processed
	PendingCount int64 `json:"pending_count"`

	// ProcessingCount is the number of tasks currently being processed
	ProcessingCount int64 `json:"processing_count"`

	// CompletedCount is the number of successfully completed tasks
	CompletedCount int64 `json:"completed_count"`

	// FailedCount is the number of tasks that failed after all retries
	FailedCount int64 `json:"failed_count"`

	// OldestPendingAge is the age of the oldest pending task in seconds
	OldestPendingAge int64 `json:"oldest_pending_age"`
}

QueueStats contains queue statistics

type RESTClient added in v0.3.0

type RESTClient interface {
	// Do executes an authenticated, rate-limited, retried request against
	// path (a connector-relative path; the adapter applies its own base URL)
	// and decodes the JSON response into result. body, if non-nil, is
	// JSON-encoded as the request body.
	//
	// Callers MUST NOT pass absolute URLs in path.
	Do(ctx context.Context, method, path string, body, result any) error
}

RESTClient is the per-connector seam for callers that need to invoke adapter-side REST endpoints not covered by the typed methods on a Connector. Use it when you want to extend a connector with an additional upstream API call (e.g. an admin or metadata endpoint) without reimplementing the connector's auth, rate-limiting, and retry plumbing.

Implementations MUST:

  • apply the connector's existing auth (token from the wired TokenProvider), rate-limiting, and retry/backoff semantics — Do is a thin wrapper over the adapter's existing HTTP plumbing, not a parallel code path that bypasses it.
  • JSON-encode the body if non-nil, JSON-decode the response into result if non-nil. Implementations that cannot do this for a particular endpoint (e.g. binary downloads) should expose a typed method on the concrete adapter instead of routing through Do.
  • return a typed error for known recoverable conditions (e.g. cursor expiry); raw network errors are returned wrapped.

Connectors with no REST surface return a sentinel implementation whose Do always returns ErrRESTUnsupported, so callers can rely on Connector.RESTClient() being non-nil.

type RetrievalObserver added in v0.3.0

type RetrievalObserver interface {
	OnSearchCompleted(ctx context.Context, event SearchCompletedEvent) error
	OnDocumentRetrieved(ctx context.Context, event DocumentRetrievedEvent) error
}

RetrievalObserver is invoked when a retrieval operation (search, document fetch) completes successfully. Implementations may perform arbitrary post-processing: metrics emission, structured logging, cache updates, audit writes, etc.

Contract:

  • Called after the response has been built, on a successful path only. Not called for 4xx/5xx responses.
  • Invoked asynchronously on a separate goroutine. Observers must not assume the originating request context is still live; the ctx passed in is detached from the request.
  • Returned errors are logged and ignored — observer failure does NOT affect the response returned to the caller.

This is the retrieval-side mirror of DocumentIngestObserver. The key difference is that retrieval observers run asynchronously: they sit on the user-facing request path, so observer latency must not add to response time. Ingest observers run synchronously because they sit on a background sync path where latency is not user-visible.

type RetryBackoff added in v0.4.0

type RetryBackoff struct {
	Base        time.Duration
	Max         time.Duration
	MaxAttempts int
}

RetryBackoff describes the per-source retry policy. The store applies the schedule deterministically so two callers with the same policy produce the same next_retry_after.

For attempt N (1-indexed), the next-retry delay is:

delay = min(base * 2^(N-1), max)

Once N exceeds MaxAttempts the row is marked terminal — no further retry pickups, but the row stays for operator visibility.

type SchedulerStore

type SchedulerStore interface {
	// GetScheduledTask retrieves a scheduled task by ID
	GetScheduledTask(ctx context.Context, id string) (*domain.ScheduledTask, error)

	// ListScheduledTasks retrieves all scheduled tasks for a team
	ListScheduledTasks(ctx context.Context, teamID string) ([]*domain.ScheduledTask, error)

	// SaveScheduledTask creates or updates a scheduled task
	SaveScheduledTask(ctx context.Context, task *domain.ScheduledTask) error

	// DeleteScheduledTask removes a scheduled task
	DeleteScheduledTask(ctx context.Context, id string) error

	// GetDueScheduledTasks retrieves scheduled tasks that are due to run
	GetDueScheduledTasks(ctx context.Context) ([]*domain.ScheduledTask, error)

	// UpdateLastRun updates the last run time and next run time
	UpdateLastRun(ctx context.Context, id string, lastError string) error
}

SchedulerStore handles persistence for scheduled tasks. This is separate from TaskQueue because scheduled tasks are configuration, not transient queue items.

type SearchCompletedEvent added in v0.3.0

type SearchCompletedEvent struct {
	UserID      string
	Query       string
	DocumentIDs []string
	ResultCount int
	DurationNs  int64
	ClientType  string // "http" | "mcp" | etc.
	ClientID    string // OAuth client_id for MCP; empty when not applicable.
	ClientName  string // Human-readable client name; empty when not known.

	// DocumentOutcomes carries per-document policy decisions made during
	// retrieval (e.g. content rewriting or drop). Position-aligned with
	// DocumentIDs. Populated only by transports whose pipeline runs a
	// content-policy stage. Transports without one leave it nil.
	//
	// The element shape is intentionally a loose map so this port does
	// not depend on policy-implementation types. Each entry SHOULD carry:
	//
	//   "document_id"        string
	//   "position"           int
	//   "result"             string  ("allowed" | "masked" | "blocked")
	//   "masked_categories"  []string
	//   "before_hash"        string  (hex SHA-256, optional)
	//   "after_hash"         string  (hex SHA-256, optional)
	//
	// Observers that don't understand the shape may ignore the field.
	DocumentOutcomes []map[string]any
}

SearchCompletedEvent carries the outcome of a search request.

ClientID and ClientName identify the calling application (e.g. an OAuth client such as Claude Desktop or Cursor), not the end user. They are optional: adapters where application-level identity doesn't apply (such as session-based HTTP auth) leave both empty. UserID still identifies the human; ClientType still identifies the transport.

type SearchEngine

type SearchEngine interface {
	// IndexDocument indexes a full document for BM25 text search.
	// Uses document_id as the OpenSearch document ID (upsert semantics).
	IndexDocument(ctx context.Context, doc *domain.DocumentContent) error

	// SearchDocuments performs a BM25 text search returning document-level results.
	SearchDocuments(ctx context.Context, query string, opts domain.SearchOptions) ([]DocumentResult, int, error)

	// DeleteByDocument deletes all indexed data for a document
	DeleteByDocument(ctx context.Context, documentID string) error

	// DeleteByDocuments deletes all indexed data for multiple documents in a single operation
	DeleteByDocuments(ctx context.Context, documentIDs []string) error

	// DeleteBySource deletes all indexed data for a source
	DeleteBySource(ctx context.Context, sourceID string) error

	// DeleteBySourceAndContainer deletes all indexed data for a specific container within a source
	DeleteBySourceAndContainer(ctx context.Context, sourceID, containerID string) error

	// HealthCheck verifies the search engine is available
	HealthCheck(ctx context.Context) error

	// Count returns the total number of indexed documents
	Count(ctx context.Context) (int64, error)

	// GetDocument retrieves a document's full indexed content by document ID.
	// Returns domain.ErrNotFound if the document is not in the search index.
	GetDocument(ctx context.Context, documentID string) (*domain.DocumentContent, error)
}

SearchEngine handles search indexing and querying.

type SearchQueryRepository

type SearchQueryRepository interface {
	// Save logs a search query for analytics tracking
	Save(ctx context.Context, query *domain.SearchQuery) error

	// GetSearchHistory retrieves recent search queries
	// Returns up to limit most recent searches, ordered by created_at desc
	GetSearchHistory(ctx context.Context, teamID string, limit int) ([]*domain.SearchQuery, error)

	// GetSearchAnalytics computes aggregated search analytics for a time period
	GetSearchAnalytics(ctx context.Context, teamID string, period domain.AnalyticsPeriod) (*domain.SearchAnalytics, error)

	// GetSearchMetrics computes performance metrics for a time period
	GetSearchMetrics(ctx context.Context, teamID string, period domain.AnalyticsPeriod) (*domain.SearchMetrics, error)
}

SearchQueryRepository handles search query logging and analytics

type SessionStore

type SessionStore interface {
	// Save stores a session with TTL based on ExpiresAt
	Save(ctx context.Context, session *domain.Session) error

	// Get retrieves a session by ID
	Get(ctx context.Context, id string) (*domain.Session, error)

	// GetByToken retrieves a session by token value
	GetByToken(ctx context.Context, token string) (*domain.Session, error)

	// GetByRefreshToken retrieves a session by refresh token value
	GetByRefreshToken(ctx context.Context, refreshToken string) (*domain.Session, error)

	// Delete deletes a session
	Delete(ctx context.Context, id string) error

	// DeleteByToken deletes a session by token
	DeleteByToken(ctx context.Context, token string) error

	// DeleteByUser deletes all sessions for a user (logout everywhere)
	DeleteByUser(ctx context.Context, userID string) error

	// ListByUser lists all active sessions for a user
	ListByUser(ctx context.Context, userID string) ([]*domain.Session, error)
}

SessionStore handles session persistence (Redis)

type SettingsStore

type SettingsStore interface {
	// GetSettings retrieves settings for a team
	GetSettings(ctx context.Context, teamID string) (*domain.Settings, error)

	// SaveSettings persists team settings
	SaveSettings(ctx context.Context, settings *domain.Settings) error

	// GetAISettings retrieves AI-specific settings for a team
	GetAISettings(ctx context.Context, teamID string) (*domain.AISettings, error)

	// SaveAISettings persists AI-specific settings
	SaveAISettings(ctx context.Context, teamID string, settings *domain.AISettings) error
}

SettingsStore persists team and AI settings

type SourceStore

type SourceStore interface {
	// Save creates or updates a source
	Save(ctx context.Context, source *domain.Source) error

	// Get retrieves a source by ID
	Get(ctx context.Context, id string) (*domain.Source, error)

	// GetByName retrieves a source by name
	GetByName(ctx context.Context, name string) (*domain.Source, error)

	// List retrieves all sources
	List(ctx context.Context) ([]*domain.Source, error)

	// ListEnabled retrieves all enabled sources
	ListEnabled(ctx context.Context) ([]*domain.Source, error)

	// Delete deletes a source
	Delete(ctx context.Context, id string) error

	// SetEnabled updates the enabled status
	SetEnabled(ctx context.Context, id string, enabled bool) error

	// CountByConnection returns the number of sources using a connection
	CountByConnection(ctx context.Context, connectionID string) (int, error)

	// ListByConnection returns sources using a connection
	ListByConnection(ctx context.Context, connectionID string) ([]*domain.Source, error)

	// UpdateContainers updates the containers for a source
	UpdateContainers(ctx context.Context, id string, containers []domain.Container) error
}

SourceStore handles source persistence (PostgreSQL)

type StaticTokenProvider

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

StaticTokenProvider implements TokenProvider for non-OAuth credentials. Used for API keys, PATs, and basic auth.

func NewStaticTokenProvider

func NewStaticTokenProvider(creds *domain.Credentials) *StaticTokenProvider

NewStaticTokenProvider creates a token provider for static credentials.

func (*StaticTokenProvider) AuthMethod

func (p *StaticTokenProvider) AuthMethod() domain.AuthMethod

AuthMethod returns the authentication method.

func (*StaticTokenProvider) GetAccessToken

func (p *StaticTokenProvider) GetAccessToken(ctx context.Context) (string, error)

GetAccessToken returns the API key or PAT.

func (*StaticTokenProvider) GetCredentials

func (p *StaticTokenProvider) GetCredentials(ctx context.Context) (*domain.Credentials, error)

GetCredentials returns the full credentials.

func (*StaticTokenProvider) IsValid

func (p *StaticTokenProvider) IsValid(ctx context.Context) bool

IsValid returns true - static credentials don't expire.

type SyncEventRepository added in v0.3.0

type SyncEventRepository interface {
	// Save logs a sync event for audit tracking
	Save(ctx context.Context, event *domain.SyncEvent) error

	// List retrieves recent sync events for a team
	// Returns up to limit most recent events, ordered by created_at desc
	List(ctx context.Context, teamID string, limit int) ([]*domain.SyncEvent, error)

	// ListBySource retrieves recent sync events for a specific source
	// Returns up to limit most recent events, ordered by created_at desc
	ListBySource(ctx context.Context, sourceID string, limit int) ([]*domain.SyncEvent, error)
}

SyncEventRepository handles sync event logging and audit trails

type SyncFailedDocRecord added in v0.4.0

type SyncFailedDocRecord struct {
	SourceID   string
	ExternalID string
	// Err is the error returned by the per-doc processing path.
	// Stringified and truncated by the store; pass it through as-is.
	Err error
	// Now is the wall-clock time the failure happened — passed in so
	// tests can use a fake clock.
	Now time.Time
	// Backoff is the schedule the store applies when computing
	// next_retry_after and the terminal flag. Required.
	Backoff RetryBackoff
}

SyncFailedDocRecord is the input to Record. The implementation derives the resulting attempt_count, next_retry_after, and terminal flag from the supplied policy plus any prior row's state.

type SyncFailedDocStore added in v0.4.0

type SyncFailedDocStore interface {
	// Record creates or updates the skip-list row for (source_id,
	// external_id). The implementation is responsible for computing the
	// next attempt count, the next_retry_after timestamp, and whether the
	// row should be marked terminal — the caller passes the failure
	// detail and a backoff policy and the implementation does the math.
	//
	// Callers MUST pass a non-empty external_id and a non-nil failure.
	// last_error is truncated to a sane length by the implementation.
	Record(ctx context.Context, failure SyncFailedDocRecord) error

	// MarkSucceeded removes the skip-list row for (source_id, external_id)
	// after a successful re-ingest. No-op when no row exists.
	MarkSucceeded(ctx context.Context, sourceID, externalID string) error

	// ListReadyForRetry returns the skip-list rows that are due for retry
	// for the given source. "Due" means non-terminal AND
	// next_retry_after <= now. Results are bounded by limit so a source
	// with a huge backlog doesn't dominate a single sync run.
	//
	// Ordered by next_retry_after ASC so the oldest-due rows retry first.
	ListReadyForRetry(ctx context.Context, sourceID string, now time.Time, limit int) ([]domain.SyncFailedDoc, error)

	// ListBySource returns every skip-list row for a source, regardless
	// of retry status. Used by the admin endpoint that surfaces failing
	// docs for operator triage. Ordered by last_attempted_at DESC.
	ListBySource(ctx context.Context, sourceID string, limit int) ([]domain.SyncFailedDoc, error)

	// CountBySource returns the number of skip-list rows for a source.
	// Used to drive small UI badges ("12 failing docs") without paging.
	CountBySource(ctx context.Context, sourceID string) (int, error)
}

SyncFailedDocStore persists per-document failure state for the sync orchestrator's skip-list / retry mechanism. The orchestrator no longer stalls cursor-advance on per-doc errors; instead it records failures here and retries them on subsequent runs with exponential backoff.

All methods are tenant-isolated by source_id (sources are themselves scoped to a team).

type SyncStateStore

type SyncStateStore interface {
	// Save creates or updates sync state
	Save(ctx context.Context, state *domain.SyncState) error

	// Get retrieves sync state for a source
	Get(ctx context.Context, sourceID string) (*domain.SyncState, error)

	// List retrieves sync states for all sources
	List(ctx context.Context) ([]*domain.SyncState, error)

	// Delete deletes sync state for a source
	Delete(ctx context.Context, sourceID string) error

	// UpdateStatus updates only the status field
	UpdateStatus(ctx context.Context, sourceID string, status domain.SyncStatus) error

	// UpdateCursor updates the sync cursor
	UpdateCursor(ctx context.Context, sourceID string, cursor string) error
}

SyncStateStore handles sync state persistence (PostgreSQL)

type TaskFilter

type TaskFilter struct {
	// TeamID filters by team (required)
	TeamID string

	// Status filters by task status (optional, empty means all)
	Status domain.TaskStatus

	// Type filters by task type (optional, empty means all)
	Type domain.TaskType

	// Limit is the maximum number of tasks to return
	Limit int

	// Offset is the number of tasks to skip (for pagination)
	Offset int
}

TaskFilter specifies criteria for listing tasks

type TaskQueue

type TaskQueue interface {
	// Enqueue adds a task to the queue for processing.
	// The task will be picked up by a worker based on priority and scheduled time.
	Enqueue(ctx context.Context, task *domain.Task) error

	// EnqueueBatch adds multiple tasks to the queue atomically.
	// If any task fails to enqueue, all tasks are rolled back.
	EnqueueBatch(ctx context.Context, tasks []*domain.Task) error

	// Dequeue retrieves the next available task for processing.
	// This should block until a task is available or context is cancelled.
	// The task is marked as processing and will not be returned to other workers.
	// Returns nil, nil if no tasks are available (for non-blocking implementations).
	Dequeue(ctx context.Context) (*domain.Task, error)

	// DequeueWithTimeout retrieves the next available task, waiting up to timeout.
	// Returns nil, nil if timeout is reached with no tasks available.
	DequeueWithTimeout(ctx context.Context, timeout int) (*domain.Task, error)

	// Ack acknowledges successful completion of a task.
	// The task is removed from the queue.
	Ack(ctx context.Context, taskID string) error

	// Nack indicates task processing failed and should be retried.
	// The task is returned to the queue with updated retry count.
	// If max retries exceeded, task is moved to failed state.
	Nack(ctx context.Context, taskID string, reason string) error

	// GetTask retrieves a task by ID (for status checking).
	GetTask(ctx context.Context, taskID string) (*domain.Task, error)

	// ListTasks retrieves tasks matching the filter criteria.
	ListTasks(ctx context.Context, filter TaskFilter) ([]*domain.Task, error)

	// CancelTask marks a pending task as cancelled.
	// Returns error if task is already processing or completed.
	CancelTask(ctx context.Context, taskID string) error

	// PurgeTasks removes completed/failed tasks older than the specified age.
	// This is used for cleanup.
	PurgeTasks(ctx context.Context, olderThan int) (int, error)

	// Stats returns queue statistics.
	Stats(ctx context.Context) (*QueueStats, error)

	// Ping checks if the queue backend is healthy.
	Ping(ctx context.Context) error

	// GetJobStats computes aggregated job statistics for a time period
	// This is used by the admin dashboard to show job execution metrics
	GetJobStats(ctx context.Context, teamID string, period domain.AnalyticsPeriod) (*domain.JobStats, error)

	// CountTasks returns the total number of tasks matching the filter
	// This is used for pagination to determine if there are more results
	CountTasks(ctx context.Context, filter TaskFilter) (int64, error)

	// Close cleans up resources.
	Close() error
}

TaskQueue handles background task queuing and processing. Implementations can use Redis (preferred) or Postgres (fallback).

type TokenProvider

type TokenProvider interface {
	// GetAccessToken returns a valid access token.
	// For OAuth, this automatically refreshes expired tokens.
	// For API keys, this returns the stored key.
	GetAccessToken(ctx context.Context) (string, error)

	// GetCredentials returns the full credentials.
	// Use GetAccessToken for most operations - this is for special cases.
	GetCredentials(ctx context.Context) (*domain.Credentials, error)

	// AuthMethod returns the authentication method.
	AuthMethod() domain.AuthMethod

	// IsValid checks if the credentials are still valid.
	IsValid(ctx context.Context) bool
}

TokenProvider provides access tokens for API authentication. Implementations cover multiple auth flows: OAuth (delegated, with automatic token refresh), static credentials (API keys, PATs, basic auth), and app-only flows (client_credentials grant, no per-user context, short-lived tokens re-fetched on expiry rather than refreshed).

type TokenProviderFactory

type TokenProviderFactory interface {
	// Create creates a TokenProvider for a connection.
	// It looks up the connection by ID, decrypts credentials, and creates
	// an appropriate TokenProvider based on the connection's AuthMethod.
	// AuthMethodOAuth2 returns a provider that refreshes delegated tokens
	// automatically. AuthMethodAppOnly returns a provider that fetches a
	// short-lived token via the client_credentials grant and re-fetches on
	// expiry without a refresh-token step. Other methods (APIKey, PAT, etc.)
	// return a static provider.
	Create(ctx context.Context, connectionID string) (TokenProvider, error)

	// CreateFromConnection creates a TokenProvider from a connection directly.
	// Use this when you already have the connection loaded. The returned
	// provider type depends on the connection's AuthMethod, including
	// AuthMethodAppOnly for app-only (client_credentials) connections.
	CreateFromConnection(ctx context.Context, conn *domain.Connection) (TokenProvider, error)
}

TokenProviderFactory creates TokenProviders from connection IDs. It resolves connection credentials and wraps them in appropriate TokenProviders.

type TokenRefresher

type TokenRefresher interface {
	// Refresh refreshes the OAuth tokens.
	// Returns the new tokens and updates the credentials in the store.
	Refresh(ctx context.Context, creds *domain.Credentials) (*domain.Credentials, error)
}

TokenRefresher handles OAuth token refresh operations. This is used internally by OAuth TokenProviders.

type UserCreateObserver added in v0.3.0

type UserCreateObserver interface {
	OnUserCreated(ctx context.Context, user *domain.User) error
}

UserCreateObserver is invoked once after a user has been successfully persisted by UserService.Create. Implementations may perform arbitrary post-write work: cache invalidation, telemetry, derived-state updates, reconciliation tied to a user's identity, etc.

Contract:

  • Called synchronously after userStore.Save succeeds.
  • Not called when userStore.Save fails.
  • Returned errors are logged and ignored — observer failure does NOT fail the user create. This matches the nil-guarded log-and-continue pattern used by DocumentIngestObserver and DocumentDeleteObserver.

type UserDeleteObserver added in v0.3.0

type UserDeleteObserver interface {
	OnUserDeleted(ctx context.Context, user *domain.User) error
}

UserDeleteObserver is invoked once after a user has been successfully removed by UserService.Delete. Implementations may perform arbitrary cleanup: cache invalidation, audit writes, derived-state pruning, reconciliation tied to a user's identity, etc.

Contract:

  • Called synchronously after userStore.Delete succeeds.
  • Not called when userStore.Delete fails.
  • The user value passed in is captured before the underlying delete and is safe to read inside the observer.
  • Returned errors are logged and ignored — observer failure does NOT fail the user delete. This matches the nil-guarded log-and-continue pattern used by DocumentIngestObserver and DocumentDeleteObserver.

type UserStore

type UserStore interface {
	// Save creates or updates a user
	Save(ctx context.Context, user *domain.User) error

	// Get retrieves a user by ID
	Get(ctx context.Context, id string) (*domain.User, error)

	// GetByEmail retrieves a user by email
	GetByEmail(ctx context.Context, email string) (*domain.User, error)

	// List retrieves all users for a team
	List(ctx context.Context, teamID string) ([]*domain.User, error)

	// Delete deletes a user
	Delete(ctx context.Context, id string) error

	// UpdateLastLogin updates the last login timestamp
	UpdateLastLogin(ctx context.Context, id string) error
}

UserStore handles user persistence (PostgreSQL)

type VectorIndex

type VectorIndex interface {
	// Index adds a single embedding to the index
	Index(ctx context.Context, id string, documentID string, embedding []float32) error

	// IndexBatch adds multiple embeddings with their chunk content
	IndexBatch(ctx context.Context, ids []string, documentIDs []string, sourceIDs []string, contents []string, embeddings [][]float32) error

	// Search finds similar vectors, returns chunk IDs and distances
	Search(ctx context.Context, embedding []float32, k int) ([]string, []float64, error)

	// SearchWithContent finds similar vectors and returns chunk content alongside IDs/distances.
	// sourceIDs optionally filters results to specific sources (nil or empty = no filter).
	// documentFilter applies the three-case document-ID contract (see domain.DocumentIDFilter godoc):
	// nil = no document filter; Apply && empty IDs = deny-all (must return zero results);
	// Apply && non-empty IDs = allow-list.
	SearchWithContent(ctx context.Context, embedding []float32, k int, sourceIDs []string, documentFilter *domain.DocumentIDFilter) ([]VectorSearchResult, error)

	// Delete removes a single embedding by chunk ID
	Delete(ctx context.Context, id string) error

	// DeleteBatch removes multiple embeddings by chunk IDs
	DeleteBatch(ctx context.Context, ids []string) error

	// DeleteByDocument removes all embeddings for a document
	DeleteByDocument(ctx context.Context, documentID string) error

	// DeleteByDocuments removes all embeddings for multiple documents in a single operation
	DeleteByDocuments(ctx context.Context, documentIDs []string) error

	// DeleteBySourceAndContainer removes all embeddings for a specific container within a source
	DeleteBySourceAndContainer(ctx context.Context, sourceID, containerID string) error

	// HealthCheck verifies the vector store is available
	HealthCheck(ctx context.Context) error
}

VectorIndex handles vector similarity search using a standalone embeddings table. This interface allows for dedicated vector store implementations (e.g., pgvector).

type VectorSearchResult

type VectorSearchResult struct {
	ChunkID    string
	DocumentID string
	Content    string
	Distance   float64
}

VectorSearchResult represents a chunk-level vector search result with content.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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