repository

package
v1.1.2 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Index

Constants

View Source
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

func NormalizePair(a, b uuid.UUID) (uuid.UUID, uuid.UUID)

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

func ReadTenants(tenantID uuid.UUID) []uuid.UUID

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) Create

func (r *APIKeyRepository) Create(ctx context.Context, key *models.APIKey) error

func (*APIKeyRepository) Delete

func (r *APIKeyRepository) Delete(ctx context.Context, id uuid.UUID) error

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

func (r *APIKeyRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.APIKey, error)

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 (r *APIKeyRepository) ListByTenant(ctx context.Context, tenantID uuid.UUID) ([]models.APIKey, error)

func (*APIKeyRepository) PurgeDeadBefore

func (r *APIKeyRepository) PurgeDeadBefore(ctx context.Context, cutoff time.Time) (int64, error)

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.

func (*APIKeyRepository) Revoke

func (r *APIKeyRepository) Revoke(ctx context.Context, id uuid.UUID) error

Revoke sets revoked_at on the key, effectively disabling it.

func (*APIKeyRepository) SetExpiry

func (r *APIKeyRepository) SetExpiry(ctx context.Context, id uuid.UUID, t *time.Time) error

SetExpiry sets expires_at on a key (used by rotation's grace window to let a predecessor stay valid until it lapses). A nil t clears any expiry.

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

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) Create

func (r *DocumentRepository) Create(ctx context.Context, doc *models.Document) error

func (*DocumentRepository) Delete

func (r *DocumentRepository) Delete(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) error

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

func (r *DocumentRepository) TouchAccessed(ctx context.Context, docIDs []uuid.UUID) error

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

func (r *EdgeRepository) Create(ctx context.Context, e *models.Edge) (*models.Edge, bool, error)

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

func (r *EdgeRepository) Delete(ctx context.Context, id uuid.UUID) (int64, error)

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) GetByID added in v1.1.1

func (r *EdgeRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Edge, error)

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

func (r *ImportJobRepository) ClaimNext(ctx context.Context) (*models.ImportJob, error)

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

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

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

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 OverrideEvent struct {
	TenantID     uuid.UUID
	Tool         string
	TargetID     *uuid.UUID
	OverrideType string
	Reason       string
	APIKeyID     *uuid.UUID
}

type OverrideLogRepository

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

func NewOverrideLogRepository

func NewOverrideLogRepository(db *gorm.DB) *OverrideLogRepository

func (*OverrideLogRepository) Log

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

func (r *SectionRepository) CountByDocumentID(ctx context.Context, docID uuid.UUID) (int64, error)

CountByDocumentID returns how many sections a document currently has.

func (*SectionRepository) CreateBatch

func (r *SectionRepository) CreateBatch(ctx context.Context, sections []models.Section) error

func (*SectionRepository) Delete added in v1.1.1

func (r *SectionRepository) Delete(ctx context.Context, id uuid.UUID) (int64, error)

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 (r *SectionRepository) DeleteByDocumentID(ctx context.Context, docID uuid.UUID) error

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) GetByID

func (r *SectionRepository) GetByID(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) (*models.Section, error)

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.

func (*SectionRepository) Update

func (r *SectionRepository) Update(ctx context.Context, section *models.Section) error

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) Create

func (r *TenantRepository) Create(ctx context.Context, tenant *models.Tenant) error

func (*TenantRepository) Delete

func (r *TenantRepository) Delete(ctx context.Context, id uuid.UUID) error

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) GetByID

func (r *TenantRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Tenant, error)

func (*TenantRepository) GetByIDs

func (r *TenantRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]models.Tenant, error)

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.

func (*TenantRepository) List

func (r *TenantRepository) List(ctx context.Context) ([]models.Tenant, error)

func (*TenantRepository) Update

func (r *TenantRepository) Update(ctx context.Context, tenant *models.Tenant) error

Jump to

Keyboard shortcuts

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