Commit Graph
113 Commits
Author SHA1 Message Date
mathiasandClaude Opus 4.8 c93b433aaf feat(secrets): file-backed SecretStore for Stage-0
Implements ports.SecretStore over a 0600 JSON file as a stand-in for op/ESO so
the demo runs without live op. Put persists atomically (temp + rename) and
merges; Get returns ErrNotFound for unknown refs so a missing token fails loud.
Behind the port, so swapping to op/ESO later is wiring, not code (ADR-002).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:57:44 +02:00
mathiasandClaude Opus 4.8 c424d88c95 feat(config): typed env-driven configuration
Parse TAPIR_* env into a typed Config with homelab defaults (gateway URL,
summarizer model, token ref, redirect addr). Secrets (gateway key, OAuth
client secret) come from env only; the refresh token never lives here — it is
addressed by an opaque ref behind the SecretStore port. Per-command validation
(ValidateForAuth/ValidateForRun) so each command demands only what it needs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:57:07 +02:00
mathiasandClaude Opus 4.8 c40b46b661 fix(llm): send generous max_tokens on every request
The copied OpenAI-compatible client sent no max_tokens. Thinking models
(qwen3, deepseek-r1) spend their budget on the reasoning trace and return
EMPTY content when max_tokens is unset, which the summarizer treats as an
error. ADR-004 says change Tapir's copy rather than the hyperguild upstream,
so set a generous default (8192) leaving room for both reasoning and output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:55:49 +02:00
mathiasandClaude Opus 4.8 2695b5d91e feat(adapters): add Postgres store sink with durable dedup
CI / Lint / Test / Vet (push) Successful in 10s
CI / Build & Import (push) Failing after 1s
CI / Mirror to GitHub (push) Has been skipped
Implements ports.Sink over Postgres (pgx/v5 + pgxpool, DSN from env per
estate convention). This is the primary sink (ADR-003) and the source of
the engine's durable, cross-restart dedup — the in-engine processed map is
process-lifetime only.

- Migrations (golang-migrate, NNN_name.up/down.sql per estate convention,
  applied from an embedded FS on New): users, videos, transcripts,
  summaries, sink_deliveries. Every user-owned table carries user_id
  (Stage-0 per-user isolation promise, data-model.md). summaries has
  UNIQUE(user_id, video_id) — at most one summary per video; highlights /
  takeaways are jsonb.
- Deliver upserts the summary idempotently on (user_id, video_id)
  (ON CONFLICT DO UPDATE) inside one tx with its sink_delivery row. Re-
  delivering the same summary updates in place, never duplicates or errors.
- Dedup reads (store methods, not a new port): HasSummary(ctx,userID,
  videoID) and SeenVideoIDs(ctx,userID) — both user_id-scoped, so one
  user never sees another's videos.

summaries.video_id is intentionally not FK-constrained to videos at Stage 0:
the sink receives only a Summary, so the dedup key stands alone; video-row
persistence is the engine/source's concern, deferred.

Tested against a real in-process Postgres via embedded-postgres (real SQL:
constraints, ON CONFLICT, jsonb, user_id scoping) — no docker, no live
cluster, no creds, fully offline.

Deps: golang-migrate/migrate/v4 and jackc/pgx/v5 (runtime),
fergusstrange/embedded-postgres + stretchr/testify (test-only). go mod tidy
raised the go directive to 1.25.0 (minimum required by the dep graph;
estate elsewhere already runs 1.26.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:04:04 +02:00
mathias 0af2991d79 merge: captions-first YouTube VideoSource (Worker C, agent/youtube-source)
CI / Lint / Test / Vet (push) Successful in 12s
CI / Build & Import (push) Failing after 0s
CI / Mirror to GitHub (push) Has been skipped
2026-06-02 17:22:51 +02:00
mathiasandClaude Opus 4.8 9a7ba3a346 feat(adapters): add captions-first YouTube VideoSource
Implements ports.VideoSource against the YouTube Data API v3:
ListSubscriptions (paginated), NewVideos (recent per channel), and
captions-first FetchTranscript — an absent caption track yields
domain.SourceNone (not an error) per ADR-007, with no audio download
or speech-to-text.

OAuth is written fresh on golang.org/x/oauth2 (ADR-006, distinct from
ingestion's inbound MCP auth); the Google token endpoint is inlined to
avoid the heavy x/oauth2/google dep. The per-connection refresh token is
resolved through the SecretStore port from an opaque TokenSecretRef and
is never stored on the adapter or logged.

Unit-tested against an httptest server + fake SecretStore (no live
googleapis egress): subscriptions list/pagination, new-video detection,
captions present -> Source set, captions absent -> SourceNone no error,
and secret-ref resolution failure surfacing as an error.

oauth2 pinned to v0.30.0 to keep the go directive at 1.23.x (koala
runner), not the v0.36 line that requires a newer toolchain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:06:40 +02:00
mathiasandClaude Opus 4.8 6da9d61e63 feat(adapters): add Summarizer backed by local-first llm routing
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>
2026-06-02 17:04:29 +02:00
mathiasandClaude Opus 4.8 0aeb3aa99e feat(adapters): copy stdlib llm package (Client + Router) per ADR-004
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>
2026-06-02 17:01:56 +02:00
mathiasandClaude Opus 4.8 6421a1334a feat(usecase): add subscription-driven watch loop with per-video dedup
CI / Lint / Test / Vet (push) Successful in 2s
CI / Build & Import (push) Failing after 1s
CI / Mirror to GitHub (push) Has been skipped
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>
2026-06-02 16:25:52 +02:00
mathiasandClaude Opus 4.8 b4aa096ac3 feat(usecase): implement ProcessNewVideo core path
CI / Lint / Test / Vet (push) Successful in 3s
CI / Build & Import (push) Failing after 1s
CI / Mirror to GitHub (push) Has been skipped
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>
2026-06-02 16:24:18 +02:00
mathias 6fd31e197c feat: add use-case engine scaffold (RED)
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).
2026-06-02 11:05:12 +00:00
mathias c26e29ac4d feat: add ports (VideoSource, Summarizer, Sink, SecretStore)
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).
2026-06-02 11:05:01 +00:00
mathias 31251a41c3 feat: add domain entities (Clean Architecture core)
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).
2026-06-02 11:04:48 +00:00