Implement ports.Summarizer in internal/adapters/summarizer. It routes through a
local Primary endpoint first and an optional BYO Fallback, owning the routing
itself (not delegating to llm.Router) so it can record AIProvider, AIModel, and
FallbackUsed on domain.Summary. Prompt asks for JSON {summary, highlights,
takeaways}; the parser tolerates thinking-model fences/reasoning and rejects an
empty summary.
The summarizer is the single egress point for content toward an AI model, so it
enforces the local-first guarantee from ai_routing.feature: with no BYO
configured (nil fallback) there is no external endpoint, so content reaches the
local stack and nowhere else. Tests assert all four scenarios via a fake client.
Model alias is config (TAPIR_SUMMARIZER_MODEL, host/name) — not hardcoded;
docs/homelab-integration.md notes it stays `confirm` and that thinking models
need an explicit max_tokens or they return empty content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy hyperguild/ingestion/internal/llm into internal/adapters/llm and own it.
Tapir owes that repo nothing at the dependency level — no module dep added.
Router gives the local-Primary -> BYO-Fallback path needed for ai_routing.feature.
Copied tests rewritten from testify to stdlib testing to keep go.mod
dependency-free (repo has zero deps; acceptance tests are stdlib too).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ProcessNewVideos walks a user's subscriptions and processes each newly
seen video. Two remaining .feature scenarios are now covered: a channel
the user is not subscribed to is never surfaced (so never processed), and
a video already processed in this engine's lifetime is not summarized
twice. Dedup is in-memory and per-user; durable cross-restart dedup stays
the store's concern (no new port), per docs/data-model.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve transcript -> summarize -> deliver, or skip when no usable
transcript. Sinks fail independently: a failing sink does not abort the
others and successful deliveries are kept; per-sink errors are returned
joined. Makes the two scaffolded acceptance scenarios GREEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Engine wires the ports and exposes ProcessNewVideo, the core use case. Returns
ErrNotImplemented on purpose so the acceptance suite fails RED — implementing it
to make those tests pass is the first build task. Depends only on ports + domain
(dependencies point inward).
The hexagonal interfaces the engine depends on. Keeps the engine provider- and
sink-agnostic: YouTube/Vimeo implement VideoSource, the AI router implements
Summarizer, store/brain implement Sink, ESO implements SecretStore. This is what
makes standalone-vs-homelab a wiring choice (ADR-003).
Pure domain types matching docs/data-model.md: User, Subscription, Video,
Transcript, Summary, plus Provider and TranscriptSource enums. Stdlib-only,
no outward dependencies — the innermost layer. Video carries user_id per the
per-user-isolation decision (no global dedup).