Reshape the dead per-user transcripts table (PK videos.id, user_id, RLS-FORCEd — never read or written by app code) into the shared public caption store ADR-021 specifies: keyed by (provider, provider_video_id), no user_id, NOT RLS-scoped. Migration 015 (reversible). Add ports.TranscriptStore + Store.GetTranscript/SaveTranscript via the raw pool (no withUser): public content, shared across users by construction. SaveTranscript persists only terminal outcomes (captions/none) and refuses SourceRateLimited so a transient 429 can never be stored as a false permanent absence (ADR-014). Flip the isolation proof: transcripts leaves the RLS-scoped set; TestTranscriptsTableIsSharedNotRLS asserts it is the SINGLE non-RLS surface (writable/readable with no user scope, no user_id column, RLS off on it alone, still on every user-owned table) — the proof the public-content classification was applied exactly here and leaked nowhere. appPool made idempotent so two tests can build it. Adjust the 010/011/014 up-down migration tests for the new HEAD. account.go: user deletion no longer strips shared transcripts. Reconcile data-model.md + CLAUDE.md. Wiring the engine to read-stored-first is the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
3.0 KiB
Go
63 lines
3.0 KiB
Go
// Package ports defines the interfaces (hexagonal "ports") that the use-case
|
|
// engine depends on. Concrete adapters live in internal/adapters. The engine
|
|
// imports this package; it never imports a concrete provider, store, or model.
|
|
// See docs/architecture/architecture.md.
|
|
package ports
|
|
|
|
import (
|
|
"context"
|
|
|
|
"gitea.d-ma.be/mathias/tapir/internal/domain"
|
|
)
|
|
|
|
// VideoSource is a video platform Tapir watches (YouTube, Vimeo).
|
|
type VideoSource interface {
|
|
// ListSubscriptions returns the channels the user subscribes to.
|
|
ListSubscriptions(ctx context.Context, userID string) ([]domain.Subscription, error)
|
|
// NewVideos returns videos newly seen for the given subscription.
|
|
NewVideos(ctx context.Context, sub domain.Subscription) ([]domain.Video, error)
|
|
// FetchTranscript resolves a transcript, captions-first. Returns a
|
|
// Transcript with Source == SourceNone when none is available (not an error).
|
|
FetchTranscript(ctx context.Context, v domain.Video) (domain.Transcript, error)
|
|
}
|
|
|
|
// Summarizer turns a transcript into a summary. Backed by the AI router
|
|
// (local Primary -> BYO Fallback). See docs/use-cases/ai_routing.feature.
|
|
type Summarizer interface {
|
|
Summarize(ctx context.Context, v domain.Video, t domain.Transcript) (domain.Summary, error)
|
|
}
|
|
|
|
// TranscriptStore persists transcripts as shared, video-keyed public content
|
|
// (ADR-021). It is keyed by the cross-user dedup key (provider, providerVideoID)
|
|
// — the video's public identity, NOT Tapir's per-user videos.id — and holds only
|
|
// public caption content, so it is deliberately NOT user-scoped: two users who
|
|
// share a video share the one row. The engine reads it before any caption fetch
|
|
// so re-analysis never re-touches YouTube (ADR-010/014).
|
|
type TranscriptStore interface {
|
|
// GetTranscript returns the stored transcript for a video and whether one
|
|
// exists. A stored Source == SourceNone (captions permanently absent) is a
|
|
// real hit: ok is true and HasText() is false, so callers skip without
|
|
// re-fetching. A transient rate-limit is never stored, so it never appears
|
|
// here as a false absence.
|
|
GetTranscript(ctx context.Context, provider, providerVideoID string) (t domain.Transcript, ok bool, err error)
|
|
// SaveTranscript upserts the transcript for (provider, providerVideoID). Only
|
|
// terminal outcomes are persisted: SourceCaptions (with text) or SourceNone.
|
|
// SourceRateLimited must NOT be passed — it is a per-user retry (ADR-014), not
|
|
// a shared terminal state.
|
|
SaveTranscript(ctx context.Context, provider, providerVideoID string, t domain.Transcript) error
|
|
}
|
|
|
|
// Sink delivers a summary to a destination (user store, brain, ...).
|
|
// Implementations fail independently of one another.
|
|
type Sink interface {
|
|
// Name identifies the sink for delivery records ("store", "brain").
|
|
Name() string
|
|
Deliver(ctx context.Context, s domain.Summary) error
|
|
}
|
|
|
|
// SecretStore resolves opaque references to secret material (OAuth tokens,
|
|
// BYO keys). Backed by ESO/1Password. Tables store only the reference.
|
|
type SecretStore interface {
|
|
Get(ctx context.Context, ref string) (string, error)
|
|
}
|