Documentation
¶
Index ¶
- Constants
- func NormalizePair(a, b uuid.UUID) (uuid.UUID, uuid.UUID)
- func ReadTenants(tenantID uuid.UUID) []uuid.UUID
- type APIKeyRepository
- func (r *APIKeyRepository) Create(ctx context.Context, key *models.APIKey) error
- func (r *APIKeyRepository) Delete(ctx context.Context, id uuid.UUID) error
- func (r *APIKeyRepository) FindBySubjectID(ctx context.Context, subjectID string) ([]models.APIKey, error)
- func (r *APIKeyRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.APIKey, error)
- func (r *APIKeyRepository) ListByTenant(ctx context.Context, tenantID uuid.UUID) ([]models.APIKey, error)
- func (r *APIKeyRepository) PurgeDeadBefore(ctx context.Context, cutoff time.Time) (int64, error)
- func (r *APIKeyRepository) Revoke(ctx context.Context, id uuid.UUID) error
- func (r *APIKeyRepository) SetExpiry(ctx context.Context, id uuid.UUID, t *time.Time) error
- type CleanupQueueRepository
- func (r *CleanupQueueRepository) CountPending(ctx context.Context, tenantID uuid.UUID) (int64, error)
- func (r *CleanupQueueRepository) GetByID(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) (*models.CleanupQueue, error)
- func (r *CleanupQueueRepository) ListAll(ctx context.Context, tenantID uuid.UUID, limit int) ([]models.CleanupQueue, error)
- func (r *CleanupQueueRepository) ListPending(ctx context.Context, tenantID uuid.UUID, limit int) ([]models.CleanupQueue, error)
- func (r *CleanupQueueRepository) Resolve(ctx context.Context, tenantID uuid.UUID, id uuid.UUID, resolution string, ...) error
- func (r *CleanupQueueRepository) Upsert(ctx context.Context, entry *models.CleanupQueue) (bool, error)
- type DocumentRepository
- func (r *DocumentRepository) ArchiveByID(ctx context.Context, id uuid.UUID, reason string) (int64, error)
- func (r *DocumentRepository) Create(ctx context.Context, doc *models.Document) error
- func (r *DocumentRepository) Delete(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) error
- func (r *DocumentRepository) GenerateIndex(ctx context.Context, tenantIDs []uuid.UUID, depth IndexDepth, category *string) ([]IndexEntry, error)
- func (r *DocumentRepository) GetByID(ctx context.Context, tenantIDs []uuid.UUID, id uuid.UUID) (*models.Document, error)
- func (r *DocumentRepository) GetByIDIncludingArchived(ctx context.Context, tenantIDs []uuid.UUID, id uuid.UUID) (*models.Document, error)
- func (r *DocumentRepository) GetByPath(ctx context.Context, tenantIDs []uuid.UUID, home uuid.UUID, category string, ...) (*models.Document, error)
- func (r *DocumentRepository) LatestHandoff(ctx context.Context, tenantIDs []uuid.UUID, subcategory *string, ...) (*models.Document, error)
- func (r *DocumentRepository) List(ctx context.Context, tenantIDs []uuid.UUID, category *string, ...) ([]models.Document, error)
- func (r *DocumentRepository) Save(ctx context.Context, tenantID uuid.UUID, doc *models.Document) error
- func (r *DocumentRepository) TouchAccessed(ctx context.Context, docIDs []uuid.UUID) error
- type EdgeListItem
- type EdgeRepository
- func (r *EdgeRepository) Create(ctx context.Context, e *models.Edge) (*models.Edge, bool, error)
- func (r *EdgeRepository) Delete(ctx context.Context, id uuid.UUID) (int64, error)
- func (r *EdgeRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Edge, error)
- func (r *EdgeRepository) ListByDocument(ctx context.Context, docID uuid.UUID, readTenants []uuid.UUID) ([]EdgeListItem, error)
- type ImportJobRepository
- func (r *ImportJobRepository) ClaimNext(ctx context.Context) (*models.ImportJob, error)
- func (r *ImportJobRepository) Create(ctx context.Context, job *models.ImportJob) error
- func (r *ImportJobRepository) Finish(ctx context.Context, id uuid.UUID, status, errMsg string, ...) error
- func (r *ImportJobRepository) GetByID(ctx context.Context, id, tenantID uuid.UUID) (*models.ImportJob, error)
- func (r *ImportJobRepository) GetStatusByID(ctx context.Context, id, tenantID uuid.UUID) (*models.ImportJob, error)
- func (r *ImportJobRepository) SweepRunningToFailed(ctx context.Context) (int64, error)
- func (r *ImportJobRepository) UpdateProgress(ctx context.Context, id uuid.UUID, total, imported, skipped, failed int) error
- type IndexDepth
- type IndexEntry
- type InstanceConfigRepository
- type LintFinding
- type LintRepository
- func (r *LintRepository) CheckEmptyCategories(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)
- func (r *LintRepository) CheckNearDuplicates(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)
- func (r *LintRepository) CheckSparse(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)
- func (r *LintRepository) CheckStale(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)
- func (r *LintRepository) FindNearDuplicatePairs(ctx context.Context, tenantID uuid.UUID, threshold float64) ([]NearDuplicatePair, error)
- type LintSeverity
- type LintThresholds
- type MutationEvent
- type MutationHistoryRepository
- func (r *MutationHistoryRepository) ListByDocument(ctx context.Context, docID uuid.UUID, limit int) ([]models.MutationHistory, error)
- func (r *MutationHistoryRepository) Log(ctx context.Context, ev MutationEvent) error
- func (r *MutationHistoryRepository) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)
- type NearDuplicatePair
- type OverrideEvent
- type OverrideLogRepository
- type RelatedResult
- type SearchParams
- type SearchResult
- type SectionRepository
- func (r *SectionRepository) CountByDocumentID(ctx context.Context, docID uuid.UUID) (int64, error)
- func (r *SectionRepository) CreateBatch(ctx context.Context, sections []models.Section) error
- func (r *SectionRepository) Delete(ctx context.Context, id uuid.UUID) (int64, error)
- func (r *SectionRepository) DeleteByDocumentID(ctx context.Context, docID uuid.UUID) error
- func (r *SectionRepository) FindByContentHash(ctx context.Context, tenantID uuid.UUID, hash string, excludeCategory string, ...) (*SimilarityCandidate, error)
- func (r *SectionRepository) FindSimilarDocuments(ctx context.Context, tenantID uuid.UUID, newCentroid pgvector.Vector, ...) ([]SimilarityCandidate, error)
- func (r *SectionRepository) GetByID(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) (*models.Section, error)
- func (r *SectionRepository) GetRelated(ctx context.Context, tenantIDs []uuid.UUID, documentID uuid.UUID, limit int) ([]RelatedResult, error)
- func (r *SectionRepository) HybridSearch(ctx context.Context, p SearchParams) ([]SearchResult, error)
- func (r *SectionRepository) MarkVerified(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) error
- func (r *SectionRepository) Snippets(ctx context.Context, tenantIDs []uuid.UUID, query string, ...) (map[uuid.UUID]string, error)
- func (r *SectionRepository) Update(ctx context.Context, section *models.Section) error
- type SimilarityCandidate
- type TenantRepository
- func (r *TenantRepository) Create(ctx context.Context, tenant *models.Tenant) error
- func (r *TenantRepository) Delete(ctx context.Context, id uuid.UUID) error
- func (r *TenantRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Tenant, error)
- func (r *TenantRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]models.Tenant, error)
- func (r *TenantRepository) List(ctx context.Context) ([]models.Tenant, error)
- func (r *TenantRepository) Update(ctx context.Context, tenant *models.Tenant) error
Constants ¶
const ( SnippetStartSel = "" SnippetStopSel = "" )
Snippet highlight sentinels: PUA runes ts_headline wraps around matched terms. Their presence signals a centered match; absence signals the leading-text fallback. Shared with the service-side strip/detect helpers — single source.
Variables ¶
This section is empty.
Functions ¶
func NormalizePair ¶
NormalizePair returns (lo, hi) with lo.String() <= hi.String(), so cleanup_queue always stores the smaller UUID in doc_a_id — preventing two rows per unordered pair.
func ReadTenants ¶
ReadTenants is the exported form of readTenants for service-layer WRITE-path callers of the set-based read methods (GetByPath/GetByID/List): it preserves the single-tenant + common-pool scope so writes and the guest-editor common-pool path keep their exact pre-aggregation behavior. READ callers pass the service-computed readable set instead.
Types ¶
type APIKeyRepository ¶
type APIKeyRepository struct {
// contains filtered or unexported fields
}
func NewAPIKeyRepository ¶
func NewAPIKeyRepository(db *gorm.DB) *APIKeyRepository
func (*APIKeyRepository) Delete ¶
Delete permanently removes an API key row. Unlike Revoke (which sets revoked_at and preserves the row/audit trail for normal key retirement), this hard-deletes — APIKey has no soft-delete column, so this is a real DELETE. Reserved for the break-glass reset, which must remove the admin key entirely (re-arming bootstrap), not merely disable it.
func (*APIKeyRepository) FindBySubjectID ¶
func (r *APIKeyRepository) FindBySubjectID(ctx context.Context, subjectID string) ([]models.APIKey, error)
FindBySubjectID returns every API key whose authz subject (authzseed.APIKeySubjectID: explicit subject_id, else the tenant service principal "svc:<tenant_id>") equals subjectID. Mirrors that resolution in SQL so callers can reverse-map an authz subject back to the key row(s) that mint it — used by the break-glass reset to find the admin key(s) behind a system:memory#admin tuple's subject.
func (*APIKeyRepository) GetByID ¶
GetByID returns a single key by id (including revoked/expired rows — callers like rotation need the metadata regardless of current validity).
func (*APIKeyRepository) ListByTenant ¶
func (*APIKeyRepository) PurgeDeadBefore ¶
PurgeDeadBefore hard-deletes keys that went dead — revoked or expired — strictly before cutoff, so long-retired keys stop cluttering listings. Returns the number of rows removed. Used by the scheduled dead-key sweep.
type CleanupQueueRepository ¶
type CleanupQueueRepository struct {
// contains filtered or unexported fields
}
func NewCleanupQueueRepository ¶
func NewCleanupQueueRepository(db *gorm.DB) *CleanupQueueRepository
func (*CleanupQueueRepository) CountPending ¶
func (r *CleanupQueueRepository) CountPending(ctx context.Context, tenantID uuid.UUID) (int64, error)
CountPending returns the number of unresolved entries for the tenant.
func (*CleanupQueueRepository) GetByID ¶
func (r *CleanupQueueRepository) GetByID(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) (*models.CleanupQueue, error)
GetByID returns a queue entry by id, scoped to the caller's tenants (own + common pool). ErrNotFound when out of scope. Used to resolve the entry's referenced document before an authorization Check.
func (*CleanupQueueRepository) ListAll ¶
func (r *CleanupQueueRepository) ListAll(ctx context.Context, tenantID uuid.UUID, limit int) ([]models.CleanupQueue, error)
ListAll returns queue entries for the tenant, most recent first.
func (*CleanupQueueRepository) ListPending ¶
func (r *CleanupQueueRepository) ListPending(ctx context.Context, tenantID uuid.UUID, limit int) ([]models.CleanupQueue, error)
ListPending returns unresolved queue entries, newest first.
func (*CleanupQueueRepository) Resolve ¶
func (r *CleanupQueueRepository) Resolve(ctx context.Context, tenantID uuid.UUID, id uuid.UUID, resolution string, note string, mergedInto *uuid.UUID) error
Resolve marks a queue entry as resolved with the given resolution/note. If resolution is "merged", set mergedInto to the surviving doc ID.
func (*CleanupQueueRepository) Upsert ¶
func (r *CleanupQueueRepository) Upsert(ctx context.Context, entry *models.CleanupQueue) (bool, error)
Upsert inserts a queue entry for the (tenant, doc_a, doc_b) pair only if no unresolved row exists for the same unordered pair. Returns true if inserted.
type DocumentRepository ¶
type DocumentRepository struct {
// contains filtered or unexported fields
}
func NewDocumentRepository ¶
func NewDocumentRepository(db *gorm.DB) *DocumentRepository
func (*DocumentRepository) ArchiveByID ¶ added in v1.1.1
func (r *DocumentRepository) ArchiveByID(ctx context.Context, id uuid.UUID, reason string) (int64, error)
ArchiveByID archives one document by id. The AND archived_at IS NULL guard makes it idempotent: superseding an already-archived target is a 0-row no-op.
func (*DocumentRepository) GenerateIndex ¶
func (r *DocumentRepository) GenerateIndex(ctx context.Context, tenantIDs []uuid.UUID, depth IndexDepth, category *string) ([]IndexEntry, error)
GenerateIndex produces a tiered catalog of documents over a tenant-id set (the caller's readable scope), filtering tenant_id IN (?).
- summary: one row per (tenant, category, subcategory) with COUNT and aggregated titles
- category: one row per document (DocCount=1, Topics=title), filtered by category
- full: one row per document (DocCount=1, Topics=title), all categories
func (*DocumentRepository) GetByID ¶
func (r *DocumentRepository) GetByID(ctx context.Context, tenantIDs []uuid.UUID, id uuid.UUID) (*models.Document, error)
GetByID resolves a document by primary key, scoped to the given tenant-id set.
func (*DocumentRepository) GetByIDIncludingArchived ¶ added in v1.1.1
func (r *DocumentRepository) GetByIDIncludingArchived(ctx context.Context, tenantIDs []uuid.UUID, id uuid.UUID) (*models.Document, error)
GetByIDIncludingArchived resolves a document by id across the tenant-id set WITHOUT the archived_at filter, so an edge can point at (or supersede) a doc that is already archived. Sections are not preloaded.
func (*DocumentRepository) GetByPath ¶
func (r *DocumentRepository) GetByPath(ctx context.Context, tenantIDs []uuid.UUID, home uuid.UUID, category string, subcategory *string, slug string) (*models.Document, error)
GetByPath resolves a document by path across the given tenant-id set. `home` is the requesting tenant used only for ordering: a doc owned by `home` is preferred over one in the common pool or another readable tenant when the same path exists in several.
func (*DocumentRepository) LatestHandoff ¶ added in v1.1.1
func (r *DocumentRepository) LatestHandoff(ctx context.Context, tenantIDs []uuid.UUID, subcategory *string, anyProject bool, excludeID *uuid.UUID) (*models.Document, error)
LatestHandoff returns the newest non-archived handoff in the tenant set (nil,nil if none), excluding excludeID. anyProject omits the project filter; else matches subcategory exactly (nil ⇒ IS NULL) — the exact-project chain key auto-chain needs.
func (*DocumentRepository) List ¶
func (r *DocumentRepository) List(ctx context.Context, tenantIDs []uuid.UUID, category *string, subcategory *string, limit, offset int) ([]models.Document, error)
List returns documents across the given tenant-id set, optionally filtered. A positive limit paginates via LIMIT/OFFSET; limit <= 0 returns the full list. The order carries an id tiebreak because (category, subcategory, slug) is not unique across the aggregated tenant set, so offset paging is total — no page skips or duplicates a row (design D6).
func (*DocumentRepository) Save ¶
func (r *DocumentRepository) Save(ctx context.Context, tenantID uuid.UUID, doc *models.Document) error
Save persists doc (and, via GORM associations, its sections) scoped to tenantID. gorm's db.Save is a PK-keyed UPDATE with no tenant_id predicate, so before saving we verify the row actually exists under tenantID: a cross-tenant id is un-writable (returns ErrNotFound) rather than silently overwriting another tenant's document. The mismatch guard stays for callers that pass a doc whose TenantID differs from the write tenant.
func (*DocumentRepository) TouchAccessed ¶ added in v1.1.1
TouchAccessed day-granular bumps last_accessed_at=now() for the given docs, skipping any already touched today so repeat same-day serves cost <=1 write (D2). Empty input is a no-op. Plain []uuid.UUID + GORM IN ? matches the column.
type EdgeListItem ¶ added in v1.1.1
type EdgeListItem struct {
EdgeID uuid.UUID `json:"edge_id"`
EdgeType string `json:"edge_type"`
Direction string `json:"direction"`
OtherDocumentID uuid.UUID `json:"other_document_id"`
OtherDocumentPath string `json:"other_document_path"`
OtherDocumentTitle string `json:"other_document_title"`
OtherDocumentArchived bool `json:"other_document_archived"`
ActorSubject string `json:"actor_subject"`
CreatedAt time.Time `json:"created_at"`
}
EdgeListItem is one row of ListByDocument: the edge plus the OTHER endpoint's identity and archived flag. Direction is relative to the queried document.
type EdgeRepository ¶ added in v1.1.1
type EdgeRepository struct {
// contains filtered or unexported fields
}
func NewEdgeRepository ¶ added in v1.1.1
func NewEdgeRepository(db *gorm.DB) *EdgeRepository
func (*EdgeRepository) Create ¶ added in v1.1.1
Create inserts an edge. On a unique-triple conflict it returns the EXISTING edge with created=false, so the caller runs no second side effect (idempotent).
func (*EdgeRepository) Delete ¶ added in v1.1.1
Delete removes one edge by id, returning rows-affected so the caller maps 0 -> ErrNotFound. Deleting a supersedes edge does NOT un-archive its target.
func (*EdgeRepository) ListByDocument ¶ added in v1.1.1
func (r *EdgeRepository) ListByDocument(ctx context.Context, docID uuid.UUID, readTenants []uuid.UUID) ([]EdgeListItem, error)
ListByDocument returns docID's edges (both directions) with the other endpoint's identity, but only when that endpoint's tenant is in readTenants — an out-of-scope sibling is omitted, not leaked. No archived_at filter: in-scope archived endpoints show.
type ImportJobRepository ¶
type ImportJobRepository struct {
// contains filtered or unexported fields
}
ImportJobRepository persists the async document-import queue (design D7). Rows carry the uploaded archive as bytea plus progress counters a worker updates as it drains the queue.
func NewImportJobRepository ¶
func NewImportJobRepository(db *gorm.DB) *ImportJobRepository
func (*ImportJobRepository) ClaimNext ¶
ClaimNext atomically claims the oldest queued job and flips it to running, using SELECT ... FOR UPDATE SKIP LOCKED so multiple worker replicas cooperate without ever double-processing a row (design: Risks — multi-replica worker). Returns (nil, nil) when the queue holds no claimable job.
func (*ImportJobRepository) Create ¶
Create inserts a job (typically status=queued) with its archive bytes.
func (*ImportJobRepository) Finish ¶
func (r *ImportJobRepository) Finish(ctx context.Context, id uuid.UUID, status, errMsg string, total, imported, skipped, failed int) error
Finish writes the terminal status (succeeded|failed), final counters, and the error string (empty on success). The write is guarded by WHERE status = running so a terminal row is never overwritten: once a startup sweep marks an orphaned job failed, a slower worker's Finish(succeeded) matches no row, is a no-op, and returns ErrNotFound. This makes the terminal state deterministic (failed wins), matching the interrupted->failed->retry semantics (design D9).
func (*ImportJobRepository) GetByID ¶
func (r *ImportJobRepository) GetByID(ctx context.Context, id, tenantID uuid.UUID) (*models.ImportJob, error)
GetByID returns a job scoped to its owning tenant. ErrNotFound when the id is unknown or belongs to a different tenant — a job is visible only to its owner.
func (*ImportJobRepository) GetStatusByID ¶
func (r *ImportJobRepository) GetStatusByID(ctx context.Context, id, tenantID uuid.UUID) (*models.ImportJob, error)
GetStatusByID returns a job WITHOUT its Archive blob — for the status/poll path, which never needs the (up to ~32MiB) archive. ClaimNext/GetByID keep loading the full row for the worker. Same tenant scoping and not-found mapping as GetByID: a job is visible only to its owning tenant.
func (*ImportJobRepository) SweepRunningToFailed ¶
func (r *ImportJobRepository) SweepRunningToFailed(ctx context.Context) (int64, error)
SweepRunningToFailed reclaims only jobs stuck in `running` past staleRunningThreshold (a crashed process left the row orphaned). Called on worker start. It deliberately does NOT touch a job a live peer replica is actively processing: ClaimNext bumps updated_at at claim time (gorm auto-manages UpdatedAt), so a live import's row stays fresh and is skipped by the age guard — the operator sees a clean failure to retry only for genuinely orphaned rows, not for imports another replica is still running (D9, F3).
func (*ImportJobRepository) UpdateProgress ¶
func (r *ImportJobRepository) UpdateProgress(ctx context.Context, id uuid.UUID, total, imported, skipped, failed int) error
UpdateProgress writes the counters of an in-flight job (e.g. seeding total at the start of processing) without changing its status. The write is guarded by WHERE status = running so it only mutates a job that is still running: if a peer replica's startup sweep already reclaimed the row (running->failed), the update is a no-op and ErrNotFound is returned, letting the caller tell "job no longer running" from success (design D9).
type IndexDepth ¶
type IndexDepth string
IndexDepth controls the aggregation level of GenerateIndex output.
const ( IndexDepthSummary IndexDepth = "summary" IndexDepthCategory IndexDepth = "category" IndexDepthFull IndexDepth = "full" )
type IndexEntry ¶
type IndexEntry struct {
TenantID uuid.UUID `json:"tenant_id"`
TenantName string `json:"tenant_name,omitempty"`
Category string `json:"category"`
Subcategory *string `json:"subcategory,omitempty"`
DocCount int `json:"doc_count"`
Topics string `json:"topics"`
}
IndexEntry is one row in the catalog produced by GenerateIndex.
type InstanceConfigRepository ¶ added in v1.1.1
type InstanceConfigRepository struct {
// contains filtered or unexported fields
}
InstanceConfigRepository reads and writes the singleton instance_config row.
func NewInstanceConfigRepository ¶ added in v1.1.1
func NewInstanceConfigRepository(db *gorm.DB) *InstanceConfigRepository
func (*InstanceConfigRepository) Get ¶ added in v1.1.1
func (r *InstanceConfigRepository) Get(ctx context.Context) (*models.InstanceConfig, error)
Get returns the singleton config row, creating it (defaults) if absent.
func (*InstanceConfigRepository) SetHistoryEnabled ¶ added in v1.1.1
func (r *InstanceConfigRepository) SetHistoryEnabled(ctx context.Context, enabled bool) error
SetHistoryEnabled flips the global mutation-history toggle, upserting the singleton row so it exists even on a pre-seed database.
func (*InstanceConfigRepository) Update ¶ added in v1.1.1
func (r *InstanceConfigRepository) Update(ctx context.Context, patch models.InstanceConfigPatch) error
Update applies a partial update to the singleton, writing only the supplied (non-nil) columns plus updated_at. A patch with no fields set is a no-op.
type LintFinding ¶
type LintFinding struct {
Check string `json:"check"`
Severity LintSeverity `json:"severity"`
DocumentPath string `json:"document_path"`
Message string `json:"message"`
}
type LintRepository ¶
type LintRepository struct {
// contains filtered or unexported fields
}
func NewLintRepository ¶
func NewLintRepository(db *gorm.DB) *LintRepository
func (*LintRepository) CheckEmptyCategories ¶
func (r *LintRepository) CheckEmptyCategories(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)
CheckEmptyCategories finds subcategories with fewer documents than the minimum threshold.
func (*LintRepository) CheckNearDuplicates ¶
func (r *LintRepository) CheckNearDuplicates(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)
CheckNearDuplicates finds doc pairs sharing at least one near-duplicate section. Metric: MAX section-pair cosine per pair, above thresholds.DuplicateSimilarity.
func (*LintRepository) CheckSparse ¶
func (r *LintRepository) CheckSparse(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)
CheckSparse finds documents with too few sections or insufficient content length.
func (*LintRepository) CheckStale ¶
func (r *LintRepository) CheckStale(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)
CheckStale finds documents that have not been updated within the stale_days threshold.
func (*LintRepository) FindNearDuplicatePairs ¶
func (r *LintRepository) FindNearDuplicatePairs(ctx context.Context, tenantID uuid.UUID, threshold float64) ([]NearDuplicatePair, error)
FindNearDuplicatePairs returns doc-ID pairs whose section-level cosine reaches the threshold. Metric: MAX(1 - cosine) over section pairs — "best matching section across the pair", more discriminating than doc-AVG. Feeds the cleanup queue.
Cost control (audit #14): scoped to the tenant's OWN docs only — the shared common pool is excluded, so cross-tenant override pairs (never auto-mergeable) aren't enqueued. Bounded via per-section HNSW k-NN, capped outer scan and result set, under a statement_timeout (runBoundedScan).
type LintSeverity ¶
type LintSeverity string
const ( LintSeverityWarning LintSeverity = "warning" LintSeverityInfo LintSeverity = "info" )
type LintThresholds ¶
type LintThresholds struct {
StaleDays int `json:"stale_days"`
SparseMinSections int `json:"sparse_min_sections"`
SparseMinContentLen int `json:"sparse_min_content_len"`
DuplicateSimilarity float64 `json:"duplicate_similarity"`
EmptyCategoryMinDocs int `json:"empty_category_min_docs"`
// Bounds on the near-duplicate section self-join (audit #10). Zero means
// "use the package default" so existing callers stay unaffected.
DuplicateMaxSections int `json:"duplicate_max_sections,omitempty"`
DuplicateNeighbors int `json:"duplicate_neighbors,omitempty"`
DuplicateMaxPairs int `json:"duplicate_max_pairs,omitempty"`
}
func DefaultLintThresholds ¶
func DefaultLintThresholds() LintThresholds
type MutationEvent ¶ added in v1.1.1
type MutationEvent struct {
TenantID uuid.UUID
DocumentID uuid.UUID
SectionID *uuid.UUID
DocumentPath string
OpType string
ActorSubject string
ActorEmail *string
APIKeyID *uuid.UUID
Before *string
}
MutationEvent carries the fields logged for one document mutation.
type MutationHistoryRepository ¶ added in v1.1.1
type MutationHistoryRepository struct {
// contains filtered or unexported fields
}
MutationHistoryRepository appends and reads the mutation_history audit table.
func NewMutationHistoryRepository ¶ added in v1.1.1
func NewMutationHistoryRepository(db *gorm.DB) *MutationHistoryRepository
func (*MutationHistoryRepository) ListByDocument ¶ added in v1.1.1
func (r *MutationHistoryRepository) ListByDocument(ctx context.Context, docID uuid.UUID, limit int) ([]models.MutationHistory, error)
ListByDocument returns a document's history newest-first, capped at limit (<=0 = uncapped).
func (*MutationHistoryRepository) Log ¶ added in v1.1.1
func (r *MutationHistoryRepository) Log(ctx context.Context, ev MutationEvent) error
Log appends one mutation history row. Best-effort — the caller decides whether a logging error is fatal (it never is; a dropped audit row beats a failed write).
func (*MutationHistoryRepository) PruneOlderThan ¶ added in v1.1.1
func (r *MutationHistoryRepository) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)
PruneOlderThan hard-deletes history rows created before cutoff, returning the count.
type NearDuplicatePair ¶
type NearDuplicatePair struct {
DocAID uuid.UUID `gorm:"column:doc_a_id"`
DocBID uuid.UUID `gorm:"column:doc_b_id"`
Similarity float64 `gorm:"column:similarity"`
}
NearDuplicatePair is a raw doc pair from the near-duplicate scan. The document IDs let the cleanup scanner upsert into the cleanup_queue table.
type OverrideEvent ¶
type OverrideLogRepository ¶
type OverrideLogRepository struct {
// contains filtered or unexported fields
}
func NewOverrideLogRepository ¶
func NewOverrideLogRepository(db *gorm.DB) *OverrideLogRepository
func (*OverrideLogRepository) Log ¶
func (r *OverrideLogRepository) Log(ctx context.Context, ev OverrideEvent) error
Log records a single override event. Best-effort — caller decides whether to fail on logging errors (usually not; a dropped audit row beats a dropped request).
type RelatedResult ¶
type RelatedResult struct {
DocumentID uuid.UUID `json:"document_id"`
Category string `json:"category"`
Subcategory *string `json:"subcategory,omitempty"`
Slug string `json:"slug"`
DocTitle string `json:"doc_title"`
Similarity float64 `json:"similarity"`
// Owning-tenant label (cross-tenant reads). TenantID comes from SQL; Name and
// Type are resolved by the service layer for the distinct result tenants.
TenantID uuid.UUID `json:"tenant_id"`
TenantName string `json:"tenant_name,omitempty"`
TenantType string `json:"tenant_type,omitempty"`
}
type SearchParams ¶
type SearchParams struct {
TenantIDs []uuid.UUID
Embedding pgvector.Vector
Query string
Category *string
Subcategory *string
DocType *string
Limit int
// CandidatePool is the per-list SQL LIMIT (semantic + lexical) before fusion;
// <= 0 falls back to defaultCandidatePool so direct callers stay sane.
CandidatePool int
// MMRLambda gates optional MMR diversity re-ranking; nil (default) leaves
// HybridSearch byte-identical to the plain fused, score-sorted path.
MMRLambda *float64
// StalenessPenalty (weight in [0,1]; 0=off) down-weights candidates verified
// past their doc_type threshold, applied post-fusion/pre-MMR. StalenessThresholds
// maps doc_type -> day threshold; a missing entry or nil VerifiedAt = no penalty.
StalenessPenalty float64
StalenessThresholds map[string]int
}
SearchParams groups the inputs for hybrid search.
type SearchResult ¶
type SearchResult struct {
SectionID uuid.UUID `json:"section_id"`
DocumentID uuid.UUID `json:"document_id"`
Heading *string `json:"heading,omitempty"`
Content string `json:"content,omitempty"`
Score float64 `json:"score"`
Tier string `json:"relevance,omitempty"` // high | standard | low — from match structure
Category string `json:"category"`
Subcategory *string `json:"subcategory,omitempty"`
Slug string `json:"slug"`
DocTitle string `json:"doc_title"`
DocType string `json:"doc_type,omitempty"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
SectionCreated time.Time `json:"-"`
// Owning-tenant label (cross-tenant reads). TenantID comes from SQL; Name and
// Type are resolved by the service layer for the distinct result tenants.
TenantID uuid.UUID `json:"tenant_id"`
TenantName string `json:"tenant_name,omitempty"`
TenantType string `json:"tenant_type,omitempty"`
// Staleness overlay (set by service layer after fetch, not by SQL).
Status string `json:"status,omitempty"` // "needs_verification" when guarded
Preview string `json:"preview,omitempty"` // short preview of withheld content
VerifyHints []string `json:"verify_hints,omitempty"` // code paths to check
StaleDays int `json:"age_days,omitempty"` // age of verified_at in days
ThresholdDays int `json:"threshold_days,omitempty"` // threshold for this doc_type
// SnippetCentered is set only when search ran in snippet mode: true if the
// window landed on a real lexical match, false for the leading-text fallback
// (purely-semantic hit). Nil (omitted) when snippet mode is off.
SnippetCentered *bool `json:"snippet_centered,omitempty"`
}
type SectionRepository ¶
type SectionRepository struct {
// contains filtered or unexported fields
}
func NewSectionRepository ¶
func NewSectionRepository(db *gorm.DB) *SectionRepository
func (*SectionRepository) CountByDocumentID ¶ added in v1.1.1
CountByDocumentID returns how many sections a document currently has.
func (*SectionRepository) CreateBatch ¶
func (*SectionRepository) Delete ¶ added in v1.1.1
Delete removes one section by id, returning rows-affected so the caller can map 0 -> ErrNotFound. Scope is gated upstream by GetByID.
func (*SectionRepository) DeleteByDocumentID ¶
func (*SectionRepository) FindByContentHash ¶ added in v1.1.1
func (r *SectionRepository) FindByContentHash( ctx context.Context, tenantID uuid.UUID, hash string, excludeCategory string, excludeSubcategory *string, excludeSlug string, ) (*SimilarityCandidate, error)
FindByContentHash returns the write tenant's own non-excluded document with an identical content_hash, or (nil, nil) when none. Backs the exact-dup short-circuit; common pool excluded (tenant-scoped), target path self-excluded.
func (*SectionRepository) FindSimilarDocuments ¶
func (r *SectionRepository) FindSimilarDocuments( ctx context.Context, tenantID uuid.UUID, newCentroid pgvector.Vector, threshold float64, limit int, excludeCategory string, excludeSubcategory *string, excludeSlug string, ) ([]SimilarityCandidate, error)
FindSimilarDocuments returns the write tenant's own documents whose centroid (AVG of section embeddings) meets threshold vs newCentroid — document-level, no section-count bias. Common pool excluded (un-editable), target path self-excluded.
func (*SectionRepository) GetRelated ¶
func (r *SectionRepository) GetRelated(ctx context.Context, tenantIDs []uuid.UUID, documentID uuid.UUID, limit int) ([]RelatedResult, error)
GetRelated returns documents semantically related to documentID, restricted to the caller's readable tenant set (tenant_id IN tenantIDs) so no result can leak a tenant outside that set. The service layer computes tenantIDs via readScope/readableTenants and resolves the per-result tenant labels.
func (*SectionRepository) HybridSearch ¶
func (r *SectionRepository) HybridSearch(ctx context.Context, p SearchParams) ([]SearchResult, error)
HybridSearch gathers vector and lexical candidates (scope-filtered, capped) via a FULL OUTER JOIN so lexical-only matches are recalled, then fuses them in Go. Scope filters run inside both CTEs, before ranking.
func (*SectionRepository) MarkVerified ¶
func (r *SectionRepository) MarkVerified(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) error
MarkVerified sets verified_at = NOW() if the section belongs to one of the caller's accessible tenants. ErrNotFound if not in scope.
func (*SectionRepository) Snippets ¶ added in v1.1.1
func (r *SectionRepository) Snippets(ctx context.Context, tenantIDs []uuid.UUID, query string, sectionIDs []uuid.UUID, snippetChars int) (map[uuid.UUID]string, error)
Snippets returns per-section ts_headline windows for the given section IDs, keyed by id. snippetChars sets the word budget (MaxWords ~= chars/6, floor 8); the d.tenant_id filter is defense-in-depth (IDs already came from a scoped result). Empty sectionIDs -> empty map, no query. Values still contain the PUA sentinels so the caller can detect centering before stripping.
type SimilarityCandidate ¶
type SimilarityCandidate struct {
DocumentID uuid.UUID `json:"document_id"`
Category string `json:"category"`
Subcategory *string `json:"subcategory,omitempty"`
Slug string `json:"slug"`
Title string `json:"title"`
Similarity float64 `json:"similarity"`
}
SimilarityCandidate is an existing document that may collide with a new save. Similarity is the cosine (0..1) between the new document's centroid and this candidate's centroid (mean of its sections); 1.0 on an exact-hash hit.
func (SimilarityCandidate) Path ¶
func (c SimilarityCandidate) Path() string
Path returns the candidate's hierarchical path string.
type TenantRepository ¶
type TenantRepository struct {
// contains filtered or unexported fields
}
func NewTenantRepository ¶
func NewTenantRepository(db *gorm.DB) *TenantRepository
func (*TenantRepository) Delete ¶
Delete removes a tenant and everything scoped to it in one transaction: the authz relation tuples (document, tenant, and service-principal), its documents (with their sections), API keys, and the per-tenant bookkeeping tables (import_jobs, cleanup_queue, override_log, deletion_events). tenant_users cascade via their FK. Leaving any of these behind orphans rows and — for the tuples — leaves live authorization grants pointing at a tenant that no longer exists.
func (*TenantRepository) GetByIDs ¶
GetByIDs fetches the tenants for the given ids in one query. Used by the read path to label cross-tenant results with their owning tenant's name/type without an N+1. Missing ids are simply absent from the result.