Documentation
¶
Index ¶
- 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) 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) GetByPath(ctx context.Context, tenantIDs []uuid.UUID, home uuid.UUID, category 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
- 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 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 NearDuplicatePair
- type OverrideEvent
- type OverrideLogRepository
- type RelatedResult
- type RetentionRepository
- type SearchParams
- type SearchResult
- type SectionRepository
- func (r *SectionRepository) CreateBatch(ctx context.Context, sections []models.Section) error
- func (r *SectionRepository) DeleteByDocumentID(ctx context.Context, docID uuid.UUID) error
- func (r *SectionRepository) FindSimilarDocuments(ctx context.Context, tenantID uuid.UUID, newEmbeddings []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) 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 ¶
This section is empty.
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) 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) 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) 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.
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 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 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 RetentionRepository ¶
type RetentionRepository struct {
// contains filtered or unexported fields
}
RetentionRepository runs the retention sweep SQL: archive expired docs and hard-delete docs past the archive grace window. No LLM calls.
func NewRetentionRepository ¶
func NewRetentionRepository(db *gorm.DB) *RetentionRepository
func (*RetentionRepository) ArchiveExpired ¶
func (r *RetentionRepository) ArchiveExpired(ctx context.Context, tenantID uuid.UUID, cutoffs map[string]time.Time) (int64, error)
ArchiveExpired sets archived_at on docs whose freshest section is older than the doc_type cutoff. "Freshest" = latest of verified_at/updated_at/created_at, so editing a section (bumps updated_at, not verified_at) keeps a doc alive, not just mark_verified. One UPDATE per doc_type.
Zero-section fallback: a doc with NO sections satisfies NOT EXISTS trivially and would otherwise be archived on its very next sweep regardless of its own age. The extra guard makes the zero-section case fall back to the document's own created_at/updated_at, so a freshly-created empty doc is not archived until it is itself older than the cutoff. Docs that have sections are unaffected.
func (*RetentionRepository) DeleteArchived ¶
func (r *RetentionRepository) DeleteArchived(ctx context.Context, tenantID uuid.UUID, before time.Time) (int64, error)
DeleteArchived hard-deletes docs archived before `before` in one atomic statement: writes a deletion_events audit row per victim, purges cleanup_queue rows referencing a victim, prunes the victims' relation_tuples, then deletes the docs (sections cascade). Returns count deleted.
type SearchParams ¶
type SearchParams struct {
TenantIDs []uuid.UUID
Embedding pgvector.Vector
Query string
Category *string
Subcategory *string
Limit 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 — calibrated from Score
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
}
type SectionRepository ¶
type SectionRepository struct {
// contains filtered or unexported fields
}
func NewSectionRepository ¶
func NewSectionRepository(db *gorm.DB) *SectionRepository
func (*SectionRepository) CreateBatch ¶
func (*SectionRepository) DeleteByDocumentID ¶
func (*SectionRepository) FindSimilarDocuments ¶
func (r *SectionRepository) FindSimilarDocuments( ctx context.Context, tenantID uuid.UUID, newEmbeddings []pgvector.Vector, threshold float64, limit int, excludeCategory string, excludeSubcategory *string, excludeSlug string, ) ([]SimilarityCandidate, error)
FindSimilarDocuments scans the WRITE tenant's OWN existing sections and returns documents whose best section match against any of newEmbeddings meets the threshold. It deliberately excludes the common (bootstrap) pool: this backs the write-time duplicate guard, and a caller can't edit or merge common-pool docs, so flagging a common-pool near-duplicate would block a legitimate write against an un-editable doc. Pass excludeCategory/Subcategory/Slug so updating an existing doc doesn't self-flag. Returns up to `limit`, ordered by similarity desc. Runs as one query via a CROSS JOIN over a Postgres vector array literal.
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.
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"`
MaxSimilarity float64 `json:"similarity"`
}
SimilarityCandidate is an existing document that may collide with a new save. MaxSimilarity is the cosine (0..1) of the best-matching section pair between the new embeddings and the candidate's existing sections.
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.